From 84bb15e5dbeddce8f6a6b496d667723772fb29c5 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 7 Nov 2022 20:53:35 +0000 Subject: [PATCH 01/71] porting profile to reducer --- .../kotlin/fake/FakeChatEngine.kt | 6 +- .../main/kotlin/app/dapk/st/core/JobBag.kt | 4 + features/directory/build.gradle | 2 - .../kotlin/app/dapk/st/home/HomeModule.kt | 6 +- .../kotlin/app/dapk/st/home/HomeViewModel.kt | 9 +- .../kotlin/app/dapk/st/home/MainActivity.kt | 2 +- features/profile/build.gradle | 11 +- .../app/dapk/st/profile/ProfileModule.kt | 9 +- .../app/dapk/st/profile/ProfileScreen.kt | 28 ++-- .../app/dapk/st/profile/ProfileViewModel.kt | 142 ------------------ .../dapk/st/profile/state/ProfileAction.kt | 17 +++ .../dapk/st/profile/state/ProfileReducer.kt | 87 +++++++++++ .../st/profile/{ => state}/ProfileState.kt | 15 +- .../dapk/st/profile/state/ProfileUseCase.kt | 30 ++++ .../st/profile/state/ProfileReducerTest.kt | 121 +++++++++++++++ 15 files changed, 307 insertions(+), 182 deletions(-) delete mode 100644 features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt create mode 100644 features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileAction.kt create mode 100644 features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileReducer.kt rename features/profile/src/main/kotlin/app/dapk/st/profile/{ => state}/ProfileState.kt (71%) create mode 100644 features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt create mode 100644 features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt diff --git a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt index bf5ff6f..9452a2f 100644 --- a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt +++ b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt @@ -12,13 +12,9 @@ import java.io.InputStream class FakeChatEngine : ChatEngine by mockk() { fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn() - fun givenDirectory() = every { directory() }.delegateReturn() - fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn() - fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit() - fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit() - + fun givenInvites() = every { invites() }.delegateEmit() } \ No newline at end of file diff --git a/core/src/main/kotlin/app/dapk/st/core/JobBag.kt b/core/src/main/kotlin/app/dapk/st/core/JobBag.kt index 1e22518..d74b214 100644 --- a/core/src/main/kotlin/app/dapk/st/core/JobBag.kt +++ b/core/src/main/kotlin/app/dapk/st/core/JobBag.kt @@ -25,4 +25,8 @@ class JobBag { jobs.remove(key.java.canonicalName)?.cancel() } + fun cancelAll() { + jobs.values.forEach { it.cancel() } + } + } \ No newline at end of file diff --git a/features/directory/build.gradle b/features/directory/build.gradle index d1d22ef..a3d7490 100644 --- a/features/directory/build.gradle +++ b/features/directory/build.gradle @@ -3,7 +3,6 @@ applyAndroidComposeLibraryModule(project) dependencies { implementation project(":chat-engine") implementation project(":domains:android:compose-core") - implementation project(":domains:android:viewmodel") implementation project(":domains:state") implementation project(":features:messenger") implementation project(":core") @@ -16,7 +15,6 @@ dependencies { androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":domains:state")) androidImportFixturesWorkaround(project, project(":domains:store")) - androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":chat-engine")) } \ No newline at end of file diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt index 12bfd88..9133a05 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt @@ -5,7 +5,7 @@ import app.dapk.st.directory.state.DirectoryState import app.dapk.st.domain.StoreModule import app.dapk.st.engine.ChatEngine import app.dapk.st.login.LoginViewModel -import app.dapk.st.profile.ProfileViewModel +import app.dapk.st.profile.state.ProfileState class HomeModule( private val chatEngine: ChatEngine, @@ -13,13 +13,13 @@ class HomeModule( val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase, ) : ProvidableModule { - internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel { + internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profile: ProfileState): HomeViewModel { return HomeViewModel( chatEngine, storeModule.credentialsStore(), directory, login, - profileViewModel, + profile, storeModule.cacheCleaner(), betaVersionUpgradeUseCase, ) diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index 7aa055f..f4545d7 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -10,7 +10,8 @@ import app.dapk.st.home.HomeScreenState.* import app.dapk.st.login.LoginViewModel import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.isSignedIn -import app.dapk.st.profile.ProfileViewModel +import app.dapk.st.profile.state.ProfileAction +import app.dapk.st.profile.state.ProfileState import app.dapk.st.viewmodel.DapkViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -24,7 +25,7 @@ internal class HomeViewModel( private val credentialsProvider: CredentialsStore, private val directoryState: DirectoryState, private val loginViewModel: LoginViewModel, - private val profileViewModel: ProfileViewModel, + private val profileState: ProfileState, private val cacheCleaner: StoreCleaner, private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase, ) : DapkViewModel( @@ -35,7 +36,7 @@ internal class HomeViewModel( fun directory() = directoryState fun login() = loginViewModel - fun profile() = profileViewModel + fun profile() = profileState fun start() { viewModelScope.launch { @@ -125,7 +126,7 @@ internal class HomeViewModel( Page.Profile -> { directoryState.dispatch(ComponentLifecycle.OnGone) - profileViewModel.reset() + profileState.dispatch(ProfileAction.Reset) } } } diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index f8d86a3..7ac8bc8 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -25,7 +25,7 @@ class MainActivity : DapkActivity() { private val directoryViewModel by state { module().directoryState() } private val loginViewModel by viewModel { module().loginViewModel() } - private val profileViewModel by viewModel { module().profileViewModel() } + private val profileViewModel by state { module().profileState() } private val homeViewModel by viewModel { module().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/features/profile/build.gradle b/features/profile/build.gradle index 89e50f1..f3697f1 100644 --- a/features/profile/build.gradle +++ b/features/profile/build.gradle @@ -4,8 +4,17 @@ dependencies { implementation project(":chat-engine") implementation project(":features:settings") implementation project(':domains:store') + implementation project(':domains:state') implementation project(":domains:android:compose-core") - implementation project(":domains:android:viewmodel") implementation project(":design-library") implementation project(":core") + + kotlinTest(it) + + androidImportFixturesWorkaround(project, project(":matrix:common")) + androidImportFixturesWorkaround(project, project(":core")) + androidImportFixturesWorkaround(project, project(":domains:state")) + androidImportFixturesWorkaround(project, project(":domains:store")) + androidImportFixturesWorkaround(project, project(":domains:android:stub")) + androidImportFixturesWorkaround(project, project(":chat-engine")) } \ No newline at end of file diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt index 4766144..2b0037f 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt @@ -1,16 +1,21 @@ package app.dapk.st.profile +import app.dapk.st.core.JobBag import app.dapk.st.core.ProvidableModule +import app.dapk.st.core.createStateViewModel import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.engine.ChatEngine +import app.dapk.st.profile.state.ProfileState +import app.dapk.st.profile.state.ProfileUseCase +import app.dapk.st.profile.state.profileReducer class ProfileModule( private val chatEngine: ChatEngine, private val errorTracker: ErrorTracker, ) : ProvidableModule { - fun profileViewModel(): ProfileViewModel { - return ProfileViewModel(chatEngine, errorTracker) + fun profileState(): ProfileState { + return createStateViewModel { profileReducer(chatEngine, errorTracker, ProfileUseCase(chatEngine, errorTracker), JobBag()) } } } \ No newline at end of file diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt index d08d2b4..6b2fbdc 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt @@ -20,18 +20,22 @@ import androidx.compose.ui.unit.dp import app.dapk.st.core.Lce import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.components.CenteredLoading +import app.dapk.st.core.page.PageAction import app.dapk.st.design.components.* import app.dapk.st.engine.RoomInvite import app.dapk.st.engine.RoomInvite.InviteMeta +import app.dapk.st.profile.state.Page +import app.dapk.st.profile.state.ProfileAction +import app.dapk.st.profile.state.ProfileState import app.dapk.st.settings.SettingsActivity @Composable -fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) { +fun ProfileScreen(viewModel: ProfileState, onTopLevelBack: () -> Unit) { viewModel.ObserveEvents() LifecycleEffect( - onStart = { viewModel.start() }, - onStop = { viewModel.stop() } + onStart = { viewModel.dispatch(ProfileAction.ComponentLifecycle.Visible) }, + onStop = { viewModel.dispatch(ProfileAction.ComponentLifecycle.Gone) } ) val context = LocalContext.current @@ -39,11 +43,11 @@ fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) { val onNavigate: (SpiderPage?) -> Unit = { when (it) { null -> onTopLevelBack() - else -> viewModel.goTo(it) + else -> viewModel.dispatch(PageAction.GoTo(it)) } } - Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) { + Spider(currentPage = viewModel.current.state1.page, onNavigate = onNavigate) { item(Page.Routes.profile) { ProfilePage(context, viewModel, it) } @@ -54,7 +58,7 @@ fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) { } @Composable -private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: Page.Profile) { +private fun ProfilePage(context: Context, viewModel: ProfileState, profile: Page.Profile) { Box( modifier = Modifier .fillMaxWidth() @@ -67,7 +71,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: when (val state = profile.content) { is Lce.Loading -> CenteredLoading() - is Lce.Error -> GenericError { viewModel.start() } + is Lce.Error -> GenericError { viewModel.dispatch(ProfileAction.ComponentLifecycle.Visible) } is Lce.Content -> { val configuration = LocalConfiguration.current val content = state.value @@ -111,7 +115,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: TextRow( title = "Invitations", content = "${content.invitationsCount} pending", - onClick = { viewModel.goToInvitations() } + onClick = { viewModel.dispatch(ProfileAction.GoToInvitations) } ) } } @@ -119,7 +123,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: } @Composable -private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) { +private fun SpiderItemScope.Invitations(viewModel: ProfileState, invitations: Page.Invitations) { when (val state = invitations.content) { is Lce.Loading -> CenteredLoading() is Lce.Content -> { @@ -133,11 +137,11 @@ private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations TextRow(title = text, includeDivider = false) { Spacer(modifier = Modifier.height(4.dp)) Row { - Button(modifier = Modifier.weight(1f), onClick = { viewModel.rejectRoomInvite(it.roomId) }) { + Button(modifier = Modifier.weight(1f), onClick = { viewModel.dispatch(ProfileAction.RejectRoomInvite(it.roomId)) }) { Text("Reject".uppercase()) } Spacer(modifier = Modifier.fillMaxWidth(0.1f)) - Button(modifier = Modifier.weight(1f), onClick = { viewModel.acceptRoomInvite(it.roomId) }) { + Button(modifier = Modifier.weight(1f), onClick = { viewModel.dispatch(ProfileAction.AcceptRoomInvite(it.roomId)) }) { Text("Accept".uppercase()) } } @@ -154,7 +158,7 @@ private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value @Composable -private fun ProfileViewModel.ObserveEvents() { +private fun ProfileState.ObserveEvents() { // StartObserving { // this@ObserveEvents.events.launch { // when (it) { diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt deleted file mode 100644 index ac5806c..0000000 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt +++ /dev/null @@ -1,142 +0,0 @@ -package app.dapk.st.profile - -import android.util.Log -import androidx.lifecycle.viewModelScope -import app.dapk.st.core.Lce -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.design.components.SpiderPage -import app.dapk.st.engine.ChatEngine -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.viewmodel.DapkViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch - -class ProfileViewModel( - private val chatEngine: ChatEngine, - private val errorTracker: ErrorTracker, -) : DapkViewModel( - ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false)) -) { - - private var currentPageJob: Job? = null - - fun start() { - goToProfile() - } - - private fun goToProfile() { - combine( - flow { - val result = runCatching { chatEngine.me(forceRefresh = true) } - .onFailure { errorTracker.track(it, "Loading profile") } - emit(result) - }, - chatEngine.invites(), - transform = { me, invites -> me to invites } - ) - .onEach { (me, invites) -> - updatePageState { - when (me.isSuccess) { - true -> copy(content = Lce.Content(Page.Profile.Content(me.getOrThrow(), invites.size))) - false -> copy(content = Lce.Error(me.exceptionOrNull()!!)) - } - } - } - .launchPageJob() - } - - - fun goToInvitations() { - updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) } - - chatEngine.invites() - .onEach { - updatePageState { - copy(content = Lce.Content(it)) - } - } - .launchPageJob() - } - - fun goTo(page: SpiderPage) { - currentPageJob?.cancel() - updateState { copy(page = page) } - when (page.state) { - is Page.Invitations -> goToInvitations() - is Page.Profile -> goToProfile() - } - } - - private fun Flow.launchPageJob() { - currentPageJob?.cancel() - currentPageJob = this.launchIn(viewModelScope) - } - - fun updateDisplayName() { - // TODO - } - - fun updateAvatar() { - // TODO - } - - fun acceptRoomInvite(roomId: RoomId) { - launchCatching { chatEngine.joinRoom(roomId) }.fold( - onError = {} - ) - } - - fun rejectRoomInvite(roomId: RoomId) { - launchCatching { chatEngine.rejectJoinRoom(roomId) }.fold( - onError = { - Log.e("!!!", it.message, it) - } - ) - } - - @Suppress("UNCHECKED_CAST") - private inline fun updatePageState(crossinline block: S.() -> S) { - val page = state.page - val currentState = page.state - require(currentState is S) - updateState { copy(page = (page as SpiderPage).copy(state = block(page.state))) } - } - - fun reset() { - when (state.page.state) { - is Page.Invitations -> updateState { - ProfileScreenState( - SpiderPage( - Page.Routes.profile, - "Profile", - null, - Page.Profile(Lce.Loading()), - hasToolbar = false - ) - ) - } - - is Page.Profile -> { - // do nothing - } - } - } - - fun stop() { - currentPageJob?.cancel() - } - -} - -fun DapkViewModel.launchCatching(block: suspend () -> T): LaunchCatching { - return object : LaunchCatching { - override fun fold(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) { - viewModelScope.launch { runCatching { block() }.fold(onSuccess, onError) } - } - } -} - -interface LaunchCatching { - fun fold(onSuccess: (T) -> Unit = {}, onError: (Throwable) -> Unit = {}) -} \ No newline at end of file diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileAction.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileAction.kt new file mode 100644 index 0000000..898b235 --- /dev/null +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileAction.kt @@ -0,0 +1,17 @@ +package app.dapk.st.profile.state + +import app.dapk.st.matrix.common.RoomId +import app.dapk.state.Action + +sealed interface ProfileAction : Action { + + sealed interface ComponentLifecycle : ProfileAction { + object Visible : ComponentLifecycle + object Gone : ComponentLifecycle + } + + object GoToInvitations : ProfileAction + data class AcceptRoomInvite(val roomId: RoomId) : ProfileAction + data class RejectRoomInvite(val roomId: RoomId) : ProfileAction + object Reset : ProfileAction +} \ No newline at end of file diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileReducer.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileReducer.kt new file mode 100644 index 0000000..8d01ee7 --- /dev/null +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileReducer.kt @@ -0,0 +1,87 @@ +package app.dapk.st.profile.state + +import app.dapk.st.core.JobBag +import app.dapk.st.core.Lce +import app.dapk.st.core.extensions.ErrorTracker +import app.dapk.st.core.page.PageAction +import app.dapk.st.core.page.PageStateChange +import app.dapk.st.core.page.createPageReducer +import app.dapk.st.core.page.withPageContext +import app.dapk.st.design.components.SpiderPage +import app.dapk.st.engine.ChatEngine +import app.dapk.state.async +import app.dapk.state.createReducer +import app.dapk.state.sideEffect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +fun profileReducer( + chatEngine: ChatEngine, + errorTracker: ErrorTracker, + profileUseCase: ProfileUseCase, + jobBag: JobBag, +) = createPageReducer( + initialPage = SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false), + factory = { + createReducer( + initialState = Unit, + + async(ProfileAction.ComponentLifecycle::class) { + when (it) { + ProfileAction.ComponentLifecycle.Visible -> { + jobBag.replace(Page.Profile::class, profileUseCase.content().onEach { content -> + withPageContext { + pageDispatch(PageStateChange.UpdatePage(it.copy(content = content))) + } + }.launchIn(coroutineScope)) + } + + ProfileAction.ComponentLifecycle.Gone -> jobBag.cancelAll() + } + }, + + async(ProfileAction.GoToInvitations::class) { + dispatch(PageAction.GoTo(SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading())))) + + jobBag.replace(Page.Invitations::class, chatEngine.invites() + .onEach { invitations -> + withPageContext { + pageDispatch(PageStateChange.UpdatePage(it.copy(content = Lce.Content(invitations)))) + } + } + .launchIn(coroutineScope)) + + }, + + async(ProfileAction.Reset::class) { + when (rawPage().state) { + is Page.Invitations -> { + dispatch(PageAction.GoTo(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))) + } + + is Page.Profile -> { + // do nothing + } + } + }, + + sideEffect(ProfileAction.AcceptRoomInvite::class) { action, _ -> + kotlin.runCatching { chatEngine.joinRoom(action.roomId) }.fold( + onFailure = { errorTracker.track(it) }, + onSuccess = {} + ) + }, + + sideEffect(ProfileAction.RejectRoomInvite::class) { action, _ -> + kotlin.runCatching { chatEngine.rejectJoinRoom(action.roomId) }.fold( + onFailure = { errorTracker.track(it) }, + onSuccess = {} + ) + }, + + sideEffect(PageStateChange.ChangePage::class) { action, _ -> + jobBag.cancel(action.previous.state::class) + }, + ) + } +) diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileState.kt similarity index 71% rename from features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt rename to features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileState.kt index b7754df..7490b49 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileState.kt @@ -1,14 +1,14 @@ -package app.dapk.st.profile +package app.dapk.st.profile.state import app.dapk.st.core.Lce +import app.dapk.st.core.State +import app.dapk.st.core.page.PageContainer import app.dapk.st.design.components.Route -import app.dapk.st.design.components.SpiderPage import app.dapk.st.engine.Me import app.dapk.st.engine.RoomInvite +import app.dapk.state.Combined2 -data class ProfileScreenState( - val page: SpiderPage, -) +typealias ProfileState = State, Unit>, Unit> sealed interface Page { data class Profile(val content: Lce) : Page { @@ -25,8 +25,3 @@ sealed interface Page { val invitation = Route("Invitations") } } - -sealed interface ProfileEvent { - -} - diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt new file mode 100644 index 0000000..001ef15 --- /dev/null +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt @@ -0,0 +1,30 @@ +package app.dapk.st.profile.state + +import app.dapk.st.core.Lce +import app.dapk.st.core.extensions.ErrorTracker +import app.dapk.st.engine.ChatEngine +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map + +class ProfileUseCase( + private val chatEngine: ChatEngine, + private val errorTracker: ErrorTracker, +) { + + fun content(): Flow> { + val flow = flow { + val result = runCatching { chatEngine.me(forceRefresh = true) } + .onFailure { errorTracker.track(it, "Loading profile") } + emit(result) + } + val combine = combine(flow, chatEngine.invites(), transform = { me, invites -> me to invites }) + return combine.map { (me, invites) -> + when (me.isSuccess) { + true -> Lce.Content(Page.Profile.Content(me.getOrThrow(), invites.size)) + false -> Lce.Error(me.exceptionOrNull()!!) + } + } + } +} \ No newline at end of file diff --git a/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt b/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt new file mode 100644 index 0000000..68fb60c --- /dev/null +++ b/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt @@ -0,0 +1,121 @@ +package app.dapk.st.profile.state + +import app.dapk.st.core.Lce +import app.dapk.st.core.page.PageAction +import app.dapk.st.core.page.PageContainer +import app.dapk.st.core.page.PageStateChange +import app.dapk.st.design.components.SpiderPage +import app.dapk.st.engine.Me +import app.dapk.st.matrix.common.HomeServerUrl +import app.dapk.state.Combined2 +import fake.FakeChatEngine +import fake.FakeErrorTracker +import fake.FakeJobBag +import fixture.aRoomId +import fixture.aRoomInvite +import fixture.aUserId +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import test.assertOnlyDispatches +import test.delegateEmit +import test.testReducer + +private val INITIAL_PROFILE_PAGE = SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false) +private val INITIAL_INVITATION_PAGE = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()), hasToolbar = true) +private val A_ROOM_ID = aRoomId() +private val A_PROFILE_CONTENT = Page.Profile.Content(Me(aUserId(), null, null, HomeServerUrl("ignored")), invitationsCount = 4) + +class ProfileReducerTest { + + private val fakeChatEngine = FakeChatEngine() + private val fakeErrorTracker = FakeErrorTracker() + private val fakeProfileUseCase = FakeProfileUseCase() + private val fakeJobBag = FakeJobBag() + + private val runReducerTest = testReducer { _: (Unit) -> Unit -> + profileReducer( + fakeChatEngine, + fakeErrorTracker, + fakeProfileUseCase.instance, + fakeJobBag.instance, + ) + } + + @Test + fun `initial state is empty loading`() = runReducerTest { + assertInitialState(pageState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))) + } + + @Test + fun `given on Profile page, when Reset, then does nothing`() = runReducerTest { + reduce(ProfileAction.Reset) + + assertNoChanges() + } + + @Test + fun `when Visible, then updates Profile page content`() = runReducerTest { + fakeJobBag.instance.expect { it.replace(Page.Profile::class, any()) } + fakeProfileUseCase.givenContent().emits(Lce.Content(A_PROFILE_CONTENT)) + + reduce(ProfileAction.ComponentLifecycle.Visible) + + assertOnlyDispatches( + PageStateChange.UpdatePage(INITIAL_PROFILE_PAGE.state.copy(Lce.Content(A_PROFILE_CONTENT))), + ) + } + + @Test + fun `when GoToInvitations, then goes to Invitations page and updates content`() = runReducerTest { + fakeJobBag.instance.expect { it.replace(Page.Invitations::class, any()) } + val goToInvitations = PageAction.GoTo(INITIAL_INVITATION_PAGE) + actionSideEffect(goToInvitations) { pageState(goToInvitations.page) } + val content = listOf(aRoomInvite()) + fakeChatEngine.givenInvites().emits(content) + + reduce(ProfileAction.GoToInvitations) + + assertOnlyDispatches( + PageAction.GoTo(INITIAL_INVITATION_PAGE), + PageStateChange.UpdatePage(INITIAL_INVITATION_PAGE.state.copy(Lce.Content(content))), + ) + } + + @Test + fun `given on Invitation page, when Reset, then goes to Profile page`() = runReducerTest { + setState(pageState(INITIAL_INVITATION_PAGE)) + + reduce(ProfileAction.Reset) + + assertOnlyDispatches(PageAction.GoTo(INITIAL_PROFILE_PAGE)) + } + + @Test + fun `when RejectRoomInvite, then rejects room`() = runReducerTest { + fakeChatEngine.expect { it.rejectJoinRoom(A_ROOM_ID) } + + reduce(ProfileAction.RejectRoomInvite(A_ROOM_ID)) + } + + @Test + fun `when AcceptRoomInvite, then joins room`() = runReducerTest { + fakeChatEngine.expect { it.joinRoom(A_ROOM_ID) } + + reduce(ProfileAction.AcceptRoomInvite(A_ROOM_ID)) + } + + @Test + fun `when ChangePage, then cancels any previous page jobs`() = runReducerTest { + fakeJobBag.instance.expect { it.cancel(Page.Invitations::class) } + + reduce(PageStateChange.ChangePage(INITIAL_INVITATION_PAGE, INITIAL_PROFILE_PAGE)) + } +} + +private fun

pageState(page: SpiderPage) = Combined2(PageContainer(page), Unit) + +class FakeProfileUseCase { + val instance = mockk() + fun givenContent() = every { instance.content() }.delegateEmit() +} From aa19346aed09de97388548079c4ea7487434ae4a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 7 Nov 2022 21:13:52 +0000 Subject: [PATCH 02/71] create tests for profile use case --- .../kotlin/fake/FakeChatEngine.kt | 1 + .../dapk/st/profile/state/ProfileUseCase.kt | 19 +++++--- .../st/profile/state/ProfileUseCaseTest.kt | 45 +++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileUseCaseTest.kt diff --git a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt index 9452a2f..9486318 100644 --- a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt +++ b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt @@ -17,4 +17,5 @@ class FakeChatEngine : ChatEngine by mockk() { fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit() fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit() fun givenInvites() = every { invites() }.delegateEmit() + fun givenMe(forceRefresh: Boolean) = coEvery { me(forceRefresh) }.delegateReturn() } \ No newline at end of file diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt index 001ef15..ba533ed 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt @@ -3,6 +3,7 @@ package app.dapk.st.profile.state import app.dapk.st.core.Lce import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.Me import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow @@ -13,18 +14,22 @@ class ProfileUseCase( private val errorTracker: ErrorTracker, ) { + private var meCache: Me? = null + fun content(): Flow> { - val flow = flow { - val result = runCatching { chatEngine.me(forceRefresh = true) } - .onFailure { errorTracker.track(it, "Loading profile") } - emit(result) - } - val combine = combine(flow, chatEngine.invites(), transform = { me, invites -> me to invites }) - return combine.map { (me, invites) -> + return combine(fetchMe(), chatEngine.invites(), transform = { me, invites -> me to invites }).map { (me, invites) -> when (me.isSuccess) { true -> Lce.Content(Page.Profile.Content(me.getOrThrow(), invites.size)) false -> Lce.Error(me.exceptionOrNull()!!) } } } + + private fun fetchMe() = flow { + meCache?.let { emit(Result.success(it)) } + val result = runCatching { chatEngine.me(forceRefresh = true) } + .onFailure { errorTracker.track(it, "Loading profile") } + .onSuccess { meCache = it } + emit(result) + } } \ No newline at end of file diff --git a/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileUseCaseTest.kt b/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileUseCaseTest.kt new file mode 100644 index 0000000..5b583ba --- /dev/null +++ b/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileUseCaseTest.kt @@ -0,0 +1,45 @@ +package app.dapk.st.profile.state + +import app.dapk.st.core.Lce +import app.dapk.st.engine.Me +import app.dapk.st.matrix.common.HomeServerUrl +import fake.FakeChatEngine +import fake.FakeErrorTracker +import fixture.aRoomInvite +import fixture.aUserId +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +private val A_ME = Me(aUserId(), null, null, HomeServerUrl("ignored")) +private val AN_INVITES_LIST = listOf(aRoomInvite(), aRoomInvite(), aRoomInvite(), aRoomInvite()) +private val AN_ERROR = RuntimeException() + +class ProfileUseCaseTest { + + private val fakeChatEngine = FakeChatEngine() + private val fakeErrorTracker = FakeErrorTracker() + + private val useCase = ProfileUseCase(fakeChatEngine, fakeErrorTracker) + + @Test + fun `given me and invites, when fetching content, then emits content`() = runTest { + fakeChatEngine.givenMe(forceRefresh = true).returns(A_ME) + fakeChatEngine.givenInvites().emits(AN_INVITES_LIST) + + val result = useCase.content().first() + + result shouldBeEqualTo Lce.Content(Page.Profile.Content(A_ME, invitationsCount = AN_INVITES_LIST.size)) + } + + @Test + fun `given me fails, when fetching content, then emits error`() = runTest { + fakeChatEngine.givenMe(forceRefresh = true).throws(AN_ERROR) + fakeChatEngine.givenInvites().emits(emptyList()) + + val result = useCase.content().first() + + result shouldBeEqualTo Lce.Error(AN_ERROR) + } +} \ No newline at end of file From b8f60d804a242cdaecc6c98b5c72fa79dcd80bfc Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 10 Nov 2022 22:37:08 +0000 Subject: [PATCH 03/71] tracking and logging locally sync errors --- .../dapk/st/matrix/sync/internal/DefaultSyncService.kt | 2 +- .../app/dapk/st/matrix/sync/internal/FlowIterator.kt | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt index 13a9076..1d92479 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt @@ -76,7 +76,7 @@ internal class DefaultSyncService( ) SyncUseCase( overviewStore, - SideEffectFlowIterator(logger), + SideEffectFlowIterator(logger, errorTracker), SyncSideEffects(keySharer, verificationHandler, deviceNotifier, messageDecrypter, json, oneTimeKeyProducer, logger), httpClient, syncStore, diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt index 16a6a3b..71e5208 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt @@ -1,12 +1,13 @@ package app.dapk.st.matrix.sync.internal +import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.matrix.common.MatrixLogTag.SYNC import app.dapk.st.matrix.common.MatrixLogger import app.dapk.st.matrix.common.matrixLog import kotlinx.coroutines.* -internal class SideEffectFlowIterator(private val logger: MatrixLogger) { - suspend fun loop(initial: T?, onPost: suspend () -> Unit, onIteration: suspend (T?) -> T?) { +internal class SideEffectFlowIterator(private val logger: MatrixLogger, private val errorTracker: ErrorTracker) { + suspend fun loop(initial: T?, onPost: suspend (Throwable?) -> Unit, onIteration: suspend (T?) -> T?) { var previousState = initial while (currentCoroutineContext().isActive) { @@ -15,11 +16,12 @@ internal class SideEffectFlowIterator(private val logger: MatrixLogger) { previousState = withContext(NonCancellable) { onIteration(previousState) } - onPost() + onPost(null) } catch (error: Throwable) { logger.matrixLog(SYNC, "on loop error: ${error.message}") - error.printStackTrace() + errorTracker.track(error, "sync loop error") delay(10000L) + onPost(error) } } logger.matrixLog(SYNC, "isActive: ${currentCoroutineContext().isActive}") From d66f0c0ab9ab633f514f44321446e45c2ceaadd5 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 10 Nov 2022 22:40:42 +0000 Subject: [PATCH 04/71] allowing the messages view to fail with nothing to see here - means the user can go into the settings and enable logging to find out what's going wrong --- .../src/main/kotlin/app/dapk/st/home/HomeViewModel.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index f4545d7..fd8de22 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -15,7 +15,6 @@ import app.dapk.st.profile.state.ProfileState import app.dapk.st.viewmodel.DapkViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -58,11 +57,10 @@ internal class HomeViewModel( private suspend fun initialHomeContent(): SignedIn { val me = chatEngine.me(forceRefresh = false) - val initialInvites = chatEngine.invites().first().size return when (val current = state) { - Loading -> SignedIn(Page.Directory, me, invites = initialInvites) - is SignedIn -> current.copy(me = me, invites = initialInvites) - SignedOut -> SignedIn(Page.Directory, me, invites = initialInvites) + Loading -> SignedIn(Page.Directory, me, invites = 0) + is SignedIn -> current.copy(me = me, invites = current.invites) + SignedOut -> SignedIn(Page.Directory, me, invites = 0) } } From 0020c29426d248caf98c64a14e02cc02146dda3f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 10 Nov 2022 23:13:09 +0000 Subject: [PATCH 05/71] logging inner error cause to the log output - also fixes log lines introducing blank lines due to a missing line height --- .../app/dapk/st/tracking/CrashTrackerLogger.kt | 13 +++++++++++++ .../dapk/st/settings/eventlogger/EventLogScreen.kt | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashTrackerLogger.kt b/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashTrackerLogger.kt index cd2a99d..b5eaea4 100644 --- a/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashTrackerLogger.kt +++ b/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashTrackerLogger.kt @@ -10,6 +10,19 @@ class CrashTrackerLogger : ErrorTracker { override fun track(throwable: Throwable, extra: String) { Log.e("ST", throwable.message, throwable) log(AppLogTag.ERROR_NON_FATAL, "${throwable.message ?: "N/A"} extra=$extra") + + throwable.findCauseMessage()?.let { + if (throwable.message != it) { + log(AppLogTag.ERROR_NON_FATAL, it) + } + } + } +} + +private fun Throwable.findCauseMessage(): String? { + return when (val inner = this.cause) { + null -> this.message ?: "" + else -> inner.findCauseMessage() } } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogScreen.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogScreen.kt index 50cbfee..51cbcff 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogScreen.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogScreen.kt @@ -107,10 +107,10 @@ private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSel null -> "${it.time}: ${it.tag}: ${it.content}" else -> "${it.time}: ${it.content}" } - Text( text = text, - modifier = Modifier.padding(horizontal = 4.dp), + lineHeight = 14.sp, + modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth(), fontSize = 10.sp, ) } From f498840c17a26c1b0433464cb20dad37c0865a1b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 10 Nov 2022 23:14:02 +0000 Subject: [PATCH 06/71] reverting unused error emissions --- .../kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt index 71e5208..e10368d 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt @@ -7,7 +7,7 @@ import app.dapk.st.matrix.common.matrixLog import kotlinx.coroutines.* internal class SideEffectFlowIterator(private val logger: MatrixLogger, private val errorTracker: ErrorTracker) { - suspend fun loop(initial: T?, onPost: suspend (Throwable?) -> Unit, onIteration: suspend (T?) -> T?) { + suspend fun loop(initial: T?, onPost: suspend () -> Unit, onIteration: suspend (T?) -> T?) { var previousState = initial while (currentCoroutineContext().isActive) { @@ -16,12 +16,11 @@ internal class SideEffectFlowIterator(private val logger: MatrixLogger, private previousState = withContext(NonCancellable) { onIteration(previousState) } - onPost(null) + onPost() } catch (error: Throwable) { logger.matrixLog(SYNC, "on loop error: ${error.message}") errorTracker.track(error, "sync loop error") delay(10000L) - onPost(error) } } logger.matrixLog(SYNC, "isActive: ${currentCoroutineContext().isActive}") From 4119bcc83080e04a8dcbbcaa12f8223ace27d841 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 05:09:56 +0000 Subject: [PATCH 07/71] Bump leakcanary-android from 2.9.1 to 2.10 Bumps [leakcanary-android](https://github.com/square/leakcanary) from 2.9.1 to 2.10. - [Release notes](https://github.com/square/leakcanary/releases) - [Changelog](https://github.com/square/leakcanary/blob/main/docs/changelog.md) - [Commits](https://github.com/square/leakcanary/compare/v2.9.1...v2.10) --- updated-dependencies: - dependency-name: com.squareup.leakcanary:leakcanary-android dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 2623c1c..ee41b5b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -132,7 +132,7 @@ ext.Dependencies.with { sqldelightAndroid = "com.squareup.sqldelight:android-driver:${sqldelightVer}" sqldelightInMemory = "com.squareup.sqldelight:sqlite-driver:${sqldelightVer}" - leakCanary = 'com.squareup.leakcanary:leakcanary-android:2.9.1' + leakCanary = 'com.squareup.leakcanary:leakcanary-android:2.10' ktorAndroid = "io.ktor:ktor-client-android:${ktorVer}" ktorCore = "io.ktor:ktor-client-core:${ktorVer}" From 89b5a724dccf5d28e773e9189119773179f64daa Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 15 Nov 2022 21:14:32 +0000 Subject: [PATCH 08/71] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d45604f..27ae60c 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,16 @@ - Importing of E2E room keys from Element clients - [UnifiedPush](https://unifiedpush.org/) - FOSS variant +- Minimal HTML formatting +- Invitations +- Image attachments ### Planned - Device verification (technically supported but has no UI) -- Invitations (technically supported but has no UI) - Room history -- Message media - Cross signing - Google drive backups -- Markdown subset (bold, italic, blocks) - Changing user name/avatar - Room settings and information - Exporting E2E room keys From a466f1a9b33eb4ddea8d5da6281081e8992341fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Nov 2022 05:04:18 +0000 Subject: [PATCH 09/71] Bump accompanist-systemuicontroller from 0.27.0 to 0.27.1 Bumps [accompanist-systemuicontroller](https://github.com/google/accompanist) from 0.27.0 to 0.27.1. - [Release notes](https://github.com/google/accompanist/releases) - [Commits](https://github.com/google/accompanist/compare/v0.27.0...v0.27.1) --- updated-dependencies: - dependency-name: com.google.accompanist:accompanist-systemuicontroller dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index ee41b5b..fdd095b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -143,7 +143,7 @@ ext.Dependencies.with { ktorContentNegotiation = "io.ktor:ktor-client-content-negotiation:${ktorVer}" coil = "io.coil-kt:coil-compose:2.2.2" - accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.27.0" + accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.27.1" junit = "junit:junit:4.13.2" kluent = "org.amshove.kluent:kluent:1.72" From bb7e8603dc88115ccad52e5fadf774a1b4281df7 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 28 Nov 2022 18:57:50 +0000 Subject: [PATCH 10/71] Make use of extract state modules --- build.gradle | 7 +- .../app/dapk/st/design/components/Spider.kt | 64 ------ domains/android/compose-core/build.gradle | 1 - .../app/dapk/st/core/ActivityExtensions.kt | 50 ----- .../kotlin/app/dapk/st/core/StateViewModel.kt | 48 ----- .../app/dapk/st/core/page/PageReducer.kt | 95 --------- domains/state/build.gradle | 14 -- .../src/main/kotlin/app/dapk/state/State.kt | 194 ------------------ .../kotlin/fake/FakeEventSource.kt | 20 -- .../testFixtures/kotlin/test/ReducerTest.kt | 153 -------------- features/directory/build.gradle | 4 +- .../app/dapk/st/directory/DirectoryModule.kt | 2 +- .../dapk/st/directory/state/DirectoryState.kt | 2 +- features/home/build.gradle | 2 +- .../kotlin/app/dapk/st/home/MainActivity.kt | 2 +- features/messenger/build.gradle | 4 +- .../dapk/st/messenger/MessengerActivity.kt | 12 +- .../app/dapk/st/messenger/MessengerModule.kt | 2 +- .../messenger/gallery/ImageGalleryActivity.kt | 9 +- .../messenger/gallery/ImageGalleryModule.kt | 2 +- .../messenger/gallery/ImageGalleryScreen.kt | 7 +- .../gallery/state/ImageGalleryReducer.kt | 10 +- .../gallery/state/ImageGalleryState.kt | 6 +- .../dapk/st/messenger/state/MessengerState.kt | 2 +- .../gallery/state/ImageGalleryReducerTest.kt | 9 +- features/profile/build.gradle | 4 +- .../app/dapk/st/profile/ProfileModule.kt | 2 +- .../app/dapk/st/profile/ProfileScreen.kt | 5 +- .../dapk/st/profile/state/ProfileReducer.kt | 10 +- .../app/dapk/st/profile/state/ProfileState.kt | 6 +- .../st/profile/state/ProfileReducerTest.kt | 8 +- features/settings/build.gradle | 3 +- .../app/dapk/st/settings/SettingsActivity.kt | 8 +- .../app/dapk/st/settings/SettingsModule.kt | 1 + .../app/dapk/st/settings/SettingsScreen.kt | 5 +- .../app/dapk/st/settings/SettingsState.kt | 10 +- .../dapk/st/settings/state/SettingsReducer.kt | 10 +- .../dapk/st/settings/SettingsReducerTest.kt | 8 +- .../kotlin/internalfixture/PageFixture.kt | 2 +- settings.gradle | 3 + 40 files changed, 91 insertions(+), 715 deletions(-) delete mode 100644 design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt delete mode 100644 domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt delete mode 100644 domains/android/compose-core/src/main/kotlin/app/dapk/st/core/page/PageReducer.kt delete mode 100644 domains/state/build.gradle delete mode 100644 domains/state/src/main/kotlin/app/dapk/state/State.kt delete mode 100644 domains/state/src/testFixtures/kotlin/fake/FakeEventSource.kt delete mode 100644 domains/state/src/testFixtures/kotlin/test/ReducerTest.kt diff --git a/build.gradle b/build.gradle index 997f289..e1c1e9b 100644 --- a/build.gradle +++ b/build.gradle @@ -151,6 +151,12 @@ ext.androidImportFixturesWorkaround = { project, fixtures -> project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}.jar") } +ext.androidImportCompositeFixturesWorkaround = { project, fixtures -> + project.dependencies.testImplementation(project.dependencies.testFixtures(fixtures)) + project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}-test-fixtures.jar") + project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}.jar") +} + ext.isFoss = { return rootProject.hasProperty("foss") } @@ -163,7 +169,6 @@ ext.firebase = { dependencies, name -> } } - if (launchTask.contains("codeCoverageReport".toLowerCase())) { apply from: 'tools/coverage.gradle' } diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt deleted file mode 100644 index 8d28d8c..0000000 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt +++ /dev/null @@ -1,64 +0,0 @@ -package app.dapk.st.design.components - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember - -@Composable -fun Spider(currentPage: SpiderPage, onNavigate: (SpiderPage?) -> Unit, graph: SpiderScope.() -> Unit) { - val pageCache = remember { mutableMapOf, SpiderPage>() } - pageCache[currentPage.route] = currentPage - - val navigateAndPopStack = { - pageCache.remove(currentPage.route) - onNavigate(pageCache[currentPage.parent]) - } - val itemScope = object : SpiderItemScope { - override fun goBack() { - navigateAndPopStack() - } - } - - val computedWeb = remember(true) { - mutableMapOf, @Composable (T) -> Unit>().also { computedWeb -> - val scope = object : SpiderScope { - override fun item(route: Route, content: @Composable SpiderItemScope.(T) -> Unit) { - computedWeb[route] = { content(itemScope, it as T) } - } - } - graph.invoke(scope) - } - } - - Column { - if (currentPage.hasToolbar) { - Toolbar( - onNavigate = navigateAndPopStack, - title = currentPage.label - ) - } - BackHandler(onBack = navigateAndPopStack) - computedWeb[currentPage.route]!!.invoke(currentPage.state) - } -} - - -interface SpiderScope { - fun item(route: Route, content: @Composable SpiderItemScope.(T) -> Unit) -} - -interface SpiderItemScope { - fun goBack() -} - -data class SpiderPage( - val route: Route, - val label: String, - val parent: Route<*>?, - val state: T, - val hasToolbar: Boolean = true, -) - -@JvmInline -value class Route(val value: String) \ No newline at end of file diff --git a/domains/android/compose-core/build.gradle b/domains/android/compose-core/build.gradle index 5de977e..e04cf9f 100644 --- a/domains/android/compose-core/build.gradle +++ b/domains/android/compose-core/build.gradle @@ -5,5 +5,4 @@ dependencies { implementation project(":features:navigator") implementation project(":design-library") api project(":domains:android:core") - api project(":domains:state") } diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt index d935abf..9c1adec 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt @@ -21,53 +21,3 @@ inline fun ComponentActivity.viewModel( } return ViewModelLazy(VM::class, { viewModelStore }, { factoryPromise }) } - - -inline fun ComponentActivity.state( - noinline factory: () -> State -): Lazy> { - val factoryPromise = object : Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return when(modelClass) { - StateViewModel::class.java -> factory() as T - else -> throw Error() - } - } - } - return KeyedViewModelLazy( - key = S::class.java.canonicalName!!, - StateViewModel::class, - { viewModelStore }, - { factoryPromise } - ) as Lazy> -} - -class KeyedViewModelLazy @JvmOverloads constructor( - private val key: String, - private val viewModelClass: KClass, - private val storeProducer: () -> ViewModelStore, - private val factoryProducer: () -> ViewModelProvider.Factory, -) : Lazy { - private var cached: VM? = null - - override val value: VM - get() { - val viewModel = cached - return if (viewModel == null) { - val factory = factoryProducer() - val store = storeProducer() - ViewModelProvider( - store, - factory, - CreationExtras.Empty - ).get(key, viewModelClass.java).also { - cached = it - } - } else { - viewModel - } - } - - override fun isInitialized(): Boolean = cached != null -} \ No newline at end of file diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt deleted file mode 100644 index a584b8a..0000000 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt +++ /dev/null @@ -1,48 +0,0 @@ -package app.dapk.st.core - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.dapk.state.Action -import app.dapk.state.ReducerFactory -import app.dapk.state.Store -import app.dapk.state.createStore -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow - -class StateViewModel( - reducerFactory: ReducerFactory, - eventSource: MutableSharedFlow, -) : ViewModel(), State { - - private val store: Store = createStore(reducerFactory, viewModelScope) - override val events: SharedFlow = eventSource - override val current - get() = _state!! - private var _state: S by mutableStateOf(store.getState()) - - init { - _state = store.getState() - store.subscribe { - _state = it - } - } - - override fun dispatch(action: Action) { - store.dispatch(action) - } -} - -fun createStateViewModel(block: (suspend (E) -> Unit) -> ReducerFactory): StateViewModel { - val eventSource = MutableSharedFlow(extraBufferCapacity = 1) - val reducer = block { eventSource.emit(it) } - return StateViewModel(reducer, eventSource) -} - -interface State { - fun dispatch(action: Action) - val events: SharedFlow - val current: S -} diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/page/PageReducer.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/page/PageReducer.kt deleted file mode 100644 index f84b00a..0000000 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/page/PageReducer.kt +++ /dev/null @@ -1,95 +0,0 @@ -package app.dapk.st.core.page - -import app.dapk.st.design.components.SpiderPage -import app.dapk.state.* -import kotlin.reflect.KClass - -sealed interface PageAction : Action { - data class GoTo

(val page: SpiderPage

) : PageAction

-} - -sealed interface PageStateChange : Action { - data class ChangePage

(val previous: SpiderPage, val newPage: SpiderPage) : PageAction

- data class UpdatePage

(val pageContent: P) : PageAction

-} - -data class PageContainer

( - val page: SpiderPage -) - -interface PageReducerScope

{ - fun withPageContent(page: KClass, block: PageDispatchScope.() -> Unit) - fun rawPage(): SpiderPage -} - -interface PageDispatchScope { - fun ReducerScope<*>.pageDispatch(action: PageAction) - fun getPageState(): PC? -} - -fun

createPageReducer( - initialPage: SpiderPage, - factory: PageReducerScope

.() -> ReducerFactory, -): ReducerFactory, S>> = shareState { - combineReducers(createPageReducer(initialPage), factory(pageReducerScope())) -} - -private fun

SharedStateScope, S>>.pageReducerScope() = object : PageReducerScope

{ - override fun withPageContent(page: KClass, block: PageDispatchScope.() -> Unit) { - val currentPage = getSharedState().state1.page.state - if (currentPage::class == page) { - val pageDispatchScope = object : PageDispatchScope { - override fun ReducerScope<*>.pageDispatch(action: PageAction) { - val currentPageGuard = getSharedState().state1.page.state - if (currentPageGuard::class == page) { - dispatch(action) - } - } - - override fun getPageState() = getSharedState().state1.page.state as? PC - } - block(pageDispatchScope) - } - } - - override fun rawPage() = getSharedState().state1.page -} - -@Suppress("UNCHECKED_CAST") -private fun

createPageReducer( - initialPage: SpiderPage -): ReducerFactory> { - return createReducer( - initialState = PageContainer( - page = initialPage - ), - - async(PageAction.GoTo::class) { action -> - val state = getState() - if (state.page.state::class != action.page.state::class) { - dispatch(PageStateChange.ChangePage(previous = state.page, newPage = action.page)) - } else { - dispatch(PageStateChange.UpdatePage(action.page.state)) - } - }, - - change(PageStateChange.ChangePage::class) { action, state -> - state.copy(page = action.newPage as SpiderPage) - }, - - change(PageStateChange.UpdatePage::class) { action, state -> - val isSamePage = state.page.state::class == action.pageContent::class - if (isSamePage) { - val updatedPageContent = (state.page as SpiderPage).copy(state = action.pageContent) - state.copy(page = updatedPageContent as SpiderPage) - } else { - state - } - }, - ) -} - -inline fun PageReducerScope<*>.withPageContext(crossinline block: PageDispatchScope.(PC) -> Unit) { - withPageContent(PC::class) { getPageState()?.let { block(it) } } -} - diff --git a/domains/state/build.gradle b/domains/state/build.gradle deleted file mode 100644 index 9db4d7f..0000000 --- a/domains/state/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id 'kotlin' - id 'java-test-fixtures' -} - -dependencies { - implementation Dependencies.mavenCentral.kotlinCoroutinesCore - - testFixturesImplementation testFixtures(project(":core")) - testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore - testFixturesImplementation Dependencies.mavenCentral.kluent - testFixturesImplementation Dependencies.mavenCentral.mockk - testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest -} \ No newline at end of file diff --git a/domains/state/src/main/kotlin/app/dapk/state/State.kt b/domains/state/src/main/kotlin/app/dapk/state/State.kt deleted file mode 100644 index 252043e..0000000 --- a/domains/state/src/main/kotlin/app/dapk/state/State.kt +++ /dev/null @@ -1,194 +0,0 @@ -package app.dapk.state - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlin.reflect.KClass - -fun createStore(reducerFactory: ReducerFactory, coroutineScope: CoroutineScope): Store { - val subscribers = mutableListOf<(S) -> Unit>() - var state: S = reducerFactory.initialState() - return object : Store { - private val scope = createScope(coroutineScope, this) - private val reducer = reducerFactory.create(scope) - - override fun dispatch(action: Action) { - coroutineScope.launch { - state = reducer.reduce(action).also { nextState -> - if (nextState != state) { - subscribers.forEach { it.invoke(nextState) } - } - } - } - } - - override fun getState() = state - - override fun subscribe(subscriber: (S) -> Unit) { - subscribers.add(subscriber) - } - } -} - -interface ReducerFactory { - fun create(scope: ReducerScope): Reducer - fun initialState(): S -} - -fun interface Reducer { - fun reduce(action: Action): S -} - -private fun createScope(coroutineScope: CoroutineScope, store: Store) = object : ReducerScope { - override val coroutineScope = coroutineScope - override fun dispatch(action: Action) = store.dispatch(action) - override fun getState(): S = store.getState() -} - -interface Store { - fun dispatch(action: Action) - fun getState(): S - fun subscribe(subscriber: (S) -> Unit) -} - -interface ReducerScope { - val coroutineScope: CoroutineScope - fun dispatch(action: Action) - fun getState(): S -} - -sealed interface ActionHandler { - val key: KClass - - class Async(override val key: KClass, val handler: suspend ReducerScope.(Action) -> Unit) : ActionHandler - class Sync(override val key: KClass, val handler: (Action, S) -> S) : ActionHandler - class Delegate(override val key: KClass, val handler: ReducerScope.(Action) -> ActionHandler) : ActionHandler -} - -data class Combined2(val state1: S1, val state2: S2) - -fun interface SharedStateScope { - fun getSharedState(): C -} - -fun shareState(block: SharedStateScope.() -> ReducerFactory): ReducerFactory { - var internalScope: ReducerScope? = null - val scope = SharedStateScope { internalScope!!.getState() } - val combinedFactory = block(scope) - return object : ReducerFactory { - override fun create(scope: ReducerScope) = combinedFactory.create(scope).also { internalScope = scope } - override fun initialState() = combinedFactory.initialState() - } -} - -fun combineReducers(r1: ReducerFactory, r2: ReducerFactory): ReducerFactory> { - return object : ReducerFactory> { - override fun create(scope: ReducerScope>): Reducer> { - val r1Scope = createReducerScope(scope) { scope.getState().state1 } - val r2Scope = createReducerScope(scope) { scope.getState().state2 } - - val r1Reducer = r1.create(r1Scope) - val r2Reducer = r2.create(r2Scope) - return Reducer { - Combined2(r1Reducer.reduce(it), r2Reducer.reduce(it)) - } - } - - override fun initialState(): Combined2 = Combined2(r1.initialState(), r2.initialState()) - } -} - -private fun createReducerScope(scope: ReducerScope<*>, state: () -> S) = object : ReducerScope { - override val coroutineScope: CoroutineScope = scope.coroutineScope - override fun dispatch(action: Action) = scope.dispatch(action) - override fun getState() = state.invoke() -} - -fun createReducer( - initialState: S, - vararg reducers: (ReducerScope) -> ActionHandler, -): ReducerFactory { - return object : ReducerFactory { - override fun create(scope: ReducerScope): Reducer { - val reducersMap = reducers - .map { it.invoke(scope) } - .groupBy { it.key } - - return Reducer { action -> - val result = reducersMap.keys - .filter { it.java.isAssignableFrom(action::class.java) } - .fold(scope.getState()) { acc, key -> - val actionHandlers = reducersMap[key]!! - actionHandlers.fold(acc) { acc, handler -> - when (handler) { - is ActionHandler.Async -> { - scope.coroutineScope.launch { - handler.handler.invoke(scope, action) - } - acc - } - - is ActionHandler.Sync -> handler.handler.invoke(action, acc) - is ActionHandler.Delegate -> when (val next = handler.handler.invoke(scope, action)) { - is ActionHandler.Async -> { - scope.coroutineScope.launch { - next.handler.invoke(scope, action) - } - acc - } - - is ActionHandler.Sync -> next.handler.invoke(action, acc) - is ActionHandler.Delegate -> error("is not possible") - } - } - } - } - result - } - } - - override fun initialState(): S = initialState - - } -} - -fun sideEffect(klass: KClass, block: suspend (A, S) -> Unit): (ReducerScope) -> ActionHandler { - return { - ActionHandler.Async(key = klass as KClass) { action -> block(action as A, getState()) } - } -} - -fun change(klass: KClass, block: (A, S) -> S): (ReducerScope) -> ActionHandler { - return { - ActionHandler.Sync(key = klass as KClass, block as (Action, S) -> S) - } -} - -fun async(klass: KClass, block: suspend ReducerScope.(A) -> Unit): (ReducerScope) -> ActionHandler { - return { - ActionHandler.Async(key = klass as KClass, block as suspend ReducerScope.(Action) -> Unit) - } -} - -fun multi(klass: KClass, block: Multi.(A) -> (ReducerScope) -> ActionHandler): (ReducerScope) -> ActionHandler { - val multiScope = object : Multi { - override fun sideEffect(block: suspend (S) -> Unit): (ReducerScope) -> ActionHandler = sideEffect(klass) { _, state -> block(state) } - override fun change(block: (A, S) -> S): (ReducerScope) -> ActionHandler = change(klass, block) - override fun async(block: suspend ReducerScope.(A) -> Unit): (ReducerScope) -> ActionHandler = async(klass, block) - override fun nothing() = sideEffect { } - } - - return { - ActionHandler.Delegate(key = klass as KClass) { action -> - block(multiScope, action as A).invoke(this) - } - } -} - -interface Multi { - fun sideEffect(block: suspend (S) -> Unit): (ReducerScope) -> ActionHandler - fun nothing(): (ReducerScope) -> ActionHandler - fun change(block: (A, S) -> S): (ReducerScope) -> ActionHandler - fun async(block: suspend ReducerScope.(A) -> Unit): (ReducerScope) -> ActionHandler -} - -interface Action \ No newline at end of file diff --git a/domains/state/src/testFixtures/kotlin/fake/FakeEventSource.kt b/domains/state/src/testFixtures/kotlin/fake/FakeEventSource.kt deleted file mode 100644 index 9a2d816..0000000 --- a/domains/state/src/testFixtures/kotlin/fake/FakeEventSource.kt +++ /dev/null @@ -1,20 +0,0 @@ -package fake - -import org.amshove.kluent.internal.assertEquals - -class FakeEventSource : (E) -> Unit { - - private val captures = mutableListOf() - - override fun invoke(event: E) { - captures.add(event) - } - - fun assertEvents(expected: List) { - assertEquals(expected, captures) - } - - fun assertNoEvents() { - assertEquals(emptyList(), captures) - } -} \ No newline at end of file diff --git a/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt b/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt deleted file mode 100644 index b50846b..0000000 --- a/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt +++ /dev/null @@ -1,153 +0,0 @@ -package test - -import app.dapk.state.Action -import app.dapk.state.Reducer -import app.dapk.state.ReducerFactory -import app.dapk.state.ReducerScope -import fake.FakeEventSource -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.internal.assertEquals -import org.amshove.kluent.shouldBeEqualTo - -interface ReducerTest { - operator fun invoke(block: suspend ReducerTestScope.() -> Unit) -} - -fun testReducer(block: ((E) -> Unit) -> ReducerFactory): ReducerTest { - val fakeEventSource = FakeEventSource() - val reducerFactory = block(fakeEventSource) - return object : ReducerTest { - override fun invoke(block: suspend ReducerTestScope.() -> Unit) { - runReducerTest(reducerFactory, fakeEventSource, block) - } - } -} - -fun runReducerTest(reducerFactory: ReducerFactory, fakeEventSource: FakeEventSource, block: suspend ReducerTestScope.() -> Unit) { - runTest { - val expectTestScope = ExpectTest(coroutineContext) - block(ReducerTestScope(reducerFactory, fakeEventSource, expectTestScope)) - expectTestScope.verifyExpects() - } -} - -class ReducerTestScope( - private val reducerFactory: ReducerFactory, - private val fakeEventSource: FakeEventSource, - private val expectTestScope: ExpectTestScope -) : ExpectTestScope by expectTestScope, Reducer { - - private var invalidateCapturedState: Boolean = false - private val actionSideEffects = mutableMapOf S>() - private var manualState: S? = null - private var capturedResult: S? = null - - private val actionCaptures = mutableListOf() - private val reducerScope = object : ReducerScope { - override val coroutineScope = CoroutineScope(UnconfinedTestDispatcher()) - override fun dispatch(action: Action) { - actionCaptures.add(action) - - if (actionSideEffects.containsKey(action)) { - setState(actionSideEffects.getValue(action).invoke(), invalidateCapturedState = true) - } - } - - override fun getState() = manualState ?: reducerFactory.initialState() - } - private val reducer: Reducer = reducerFactory.create(reducerScope) - - override fun reduce(action: Action) = reducer.reduce(action).also { - capturedResult = if (invalidateCapturedState) manualState else it - } - - fun actionSideEffect(action: Action, handler: () -> S) { - actionSideEffects[action] = handler - } - - fun setState(state: S, invalidateCapturedState: Boolean = false) { - manualState = state - this.invalidateCapturedState = invalidateCapturedState - } - - fun setState(block: (S) -> S) { - setState(block(reducerScope.getState())) - } - - fun assertInitialState(expected: S) { - reducerFactory.initialState() shouldBeEqualTo expected - } - - fun assertEvents(events: List) { - fakeEventSource.assertEvents(events) - } - - fun assertOnlyStateChange(expected: S) { - assertStateChange(expected) - assertNoDispatches() - fakeEventSource.assertNoEvents() - } - - fun assertOnlyStateChange(block: (S) -> S) { - val expected = block(reducerScope.getState()) - assertStateChange(expected) - assertNoDispatches() - fakeEventSource.assertNoEvents() - } - - fun assertStateChange(expected: S) { - capturedResult shouldBeEqualTo expected - } - - fun assertDispatches(expected: List) { - assertEquals(expected, actionCaptures) - } - - fun assertNoDispatches() { - assertEquals(emptyList(), actionCaptures) - } - - fun assertNoStateChange() { - assertEquals(reducerScope.getState(), capturedResult) - } - - fun assertNoEvents() { - fakeEventSource.assertNoEvents() - } - - fun assertOnlyDispatches(expected: List) { - assertDispatches(expected) - fakeEventSource.assertNoEvents() - assertNoStateChange() - } - - fun assertOnlyEvents(events: List) { - fakeEventSource.assertEvents(events) - assertNoDispatches() - assertNoStateChange() - } - - fun assertNoChanges() { - assertNoStateChange() - assertNoEvents() - assertNoDispatches() - } -} - -fun ReducerTestScope.assertOnlyDispatches(vararg action: Action) { - this.assertOnlyDispatches(action.toList()) -} - -fun ReducerTestScope.assertDispatches(vararg action: Action) { - this.assertDispatches(action.toList()) -} - -fun ReducerTestScope.assertEvents(vararg event: E) { - this.assertEvents(event.toList()) -} - -fun ReducerTestScope.assertOnlyEvents(vararg event: E) { - this.assertOnlyEvents(event.toList()) -} \ No newline at end of file diff --git a/features/directory/build.gradle b/features/directory/build.gradle index a3d7490..26115b8 100644 --- a/features/directory/build.gradle +++ b/features/directory/build.gradle @@ -3,7 +3,7 @@ applyAndroidComposeLibraryModule(project) dependencies { implementation project(":chat-engine") implementation project(":domains:android:compose-core") - implementation project(":domains:state") + implementation 'screen-state:screen-android' implementation project(":features:messenger") implementation project(":core") implementation project(":design-library") @@ -11,9 +11,9 @@ dependencies { kotlinTest(it) + testImplementation 'screen-state:state-test' androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":domains:state")) androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":chat-engine")) diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt index 58a95ee..8ef9027 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt @@ -2,7 +2,7 @@ package app.dapk.st.directory import android.content.Context import app.dapk.st.core.ProvidableModule -import app.dapk.st.core.createStateViewModel +import app.dapk.st.state.createStateViewModel import app.dapk.st.core.JobBag import app.dapk.st.directory.state.DirectoryState import app.dapk.st.directory.state.directoryReducer diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryState.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryState.kt index 3f20567..87a032a 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryState.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryState.kt @@ -1,6 +1,6 @@ package app.dapk.st.directory.state -import app.dapk.st.core.State +import app.dapk.st.state.State import app.dapk.st.engine.DirectoryState typealias DirectoryState = State diff --git a/features/home/build.gradle b/features/home/build.gradle index 0237cb0..e508c14 100644 --- a/features/home/build.gradle +++ b/features/home/build.gradle @@ -9,7 +9,7 @@ dependencies { implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") implementation project(':domains:store') - implementation project(':domains:state') + implementation 'screen-state:screen-android' implementation project(":core") implementation project(":design-library") implementation Dependencies.mavenCentral.coil diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index 7ac8bc8..7bcb3e9 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -13,11 +13,11 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import app.dapk.st.core.DapkActivity import app.dapk.st.core.module -import app.dapk.st.core.state import app.dapk.st.core.viewModel import app.dapk.st.directory.DirectoryModule import app.dapk.st.login.LoginModule import app.dapk.st.profile.ProfileModule +import app.dapk.st.state.state import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach diff --git a/features/messenger/build.gradle b/features/messenger/build.gradle index 59d3ed8..1e7aa46 100644 --- a/features/messenger/build.gradle +++ b/features/messenger/build.gradle @@ -6,7 +6,7 @@ dependencies { implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") implementation project(":domains:store") - implementation project(":domains:state") + implementation 'screen-state:screen-android' implementation project(":core") implementation project(":features:navigator") implementation project(":design-library") @@ -14,10 +14,10 @@ dependencies { kotlinTest(it) + testImplementation 'screen-state:state-test' androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":domains:store")) - androidImportFixturesWorkaround(project, project(":domains:state")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":chat-engine")) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt index 69f8730..439fdfc 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt @@ -10,15 +10,17 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier -import app.dapk.st.core.* +import app.dapk.st.core.AndroidUri +import app.dapk.st.core.DapkActivity +import app.dapk.st.core.MimeType import app.dapk.st.core.extensions.unsafeLazy +import app.dapk.st.core.module import app.dapk.st.matrix.common.RoomId import app.dapk.st.messenger.gallery.GetImageFromGallery import app.dapk.st.messenger.state.ComposerStateChange -import app.dapk.st.messenger.state.MessengerEvent -import app.dapk.st.messenger.state.MessengerScreenState import app.dapk.st.messenger.state.MessengerState import app.dapk.st.navigator.MessageAttachment +import app.dapk.st.state.state import coil.request.ImageRequest import kotlinx.parcelize.Parcelize @@ -27,7 +29,7 @@ val LocalImageRequestFactory = staticCompositionLocalOf { class MessengerActivity : DapkActivity() { private val module by unsafeLazy { module() } - private val state by state { module.messengerState(readPayload()) } + private val state: MessengerState by state { module.messengerState(readPayload()) } companion object { @@ -87,4 +89,4 @@ data class MessagerActivityPayload( fun Activity.readPayload(): T = intent.getParcelableExtra("key") ?: intent.getStringExtra("shortcut_key")!!.let { MessagerActivityPayload(it) as T -} \ No newline at end of file +} diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt index 50f12ab..d2001c5 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt @@ -5,7 +5,7 @@ import android.content.Context import app.dapk.st.core.DeviceMeta import app.dapk.st.core.JobBag import app.dapk.st.core.ProvidableModule -import app.dapk.st.core.createStateViewModel +import app.dapk.st.state.createStateViewModel import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.engine.ChatEngine import app.dapk.st.matrix.common.RoomId diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt index 599d104..c6379e1 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt @@ -15,16 +15,17 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.lifecycleScope import app.dapk.st.core.* -import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.design.components.GenericError +import app.dapk.st.messenger.gallery.state.ImageGalleryState +import app.dapk.st.state.state import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize class ImageGalleryActivity : DapkActivity() { - private val module by unsafeLazy { module() } - private val imageGalleryState by state { + private val imageGalleryState: ImageGalleryState by state { val payload = intent.getParcelableExtra("key") as? ImageGalleryActivityPayload + val module = module() module.imageGalleryState(payload!!.roomName) } @@ -94,4 +95,4 @@ class GetImageFromGallery : ActivityResultContract Unit, onI } } - Spider(currentPage = state.current.state1.page, onNavigate = onNavigate) { + Spider(currentPage = state.current.state1.page, onNavigate = onNavigate, toolbar = { navigate, title -> Toolbar(navigate, title) }) { item(ImageGalleryPage.Routes.folders) { ImageGalleryFolders( it, diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryReducer.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryReducer.kt index db75035..3bd33aa 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryReducer.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryReducer.kt @@ -2,15 +2,15 @@ package app.dapk.st.messenger.gallery.state import app.dapk.st.core.JobBag import app.dapk.st.core.Lce -import app.dapk.st.core.page.PageAction -import app.dapk.st.core.page.PageStateChange -import app.dapk.st.core.page.createPageReducer -import app.dapk.st.core.page.withPageContext -import app.dapk.st.design.components.SpiderPage import app.dapk.st.messenger.gallery.FetchMediaFoldersUseCase import app.dapk.st.messenger.gallery.FetchMediaUseCase +import app.dapk.state.SpiderPage import app.dapk.state.async import app.dapk.state.createReducer +import app.dapk.state.page.PageAction +import app.dapk.state.page.PageStateChange +import app.dapk.state.page.createPageReducer +import app.dapk.state.page.withPageContext import app.dapk.state.sideEffect import kotlinx.coroutines.launch diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryState.kt index 3e07c31..4f95a3f 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryState.kt @@ -1,12 +1,12 @@ package app.dapk.st.messenger.gallery.state import app.dapk.st.core.Lce -import app.dapk.st.core.State -import app.dapk.st.design.components.Route +import app.dapk.st.state.State import app.dapk.st.messenger.gallery.Folder import app.dapk.st.messenger.gallery.Media -import app.dapk.st.core.page.PageContainer import app.dapk.state.Combined2 +import app.dapk.state.Route +import app.dapk.state.page.PageContainer typealias ImageGalleryState = State, Unit>, Unit> diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerState.kt index 0fa7a5c..385767c 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerState.kt @@ -1,7 +1,7 @@ package app.dapk.st.messenger.state import app.dapk.st.core.Lce -import app.dapk.st.core.State +import app.dapk.st.state.State import app.dapk.st.design.components.BubbleModel import app.dapk.st.engine.MessengerPageState import app.dapk.st.engine.RoomEvent diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryReducerTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryReducerTest.kt index bb684b1..d4decf4 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryReducerTest.kt +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/gallery/state/ImageGalleryReducerTest.kt @@ -2,15 +2,15 @@ package app.dapk.st.messenger.gallery.state import android.net.Uri import app.dapk.st.core.Lce -import app.dapk.st.core.page.PageAction -import app.dapk.st.core.page.PageContainer -import app.dapk.st.core.page.PageStateChange -import app.dapk.st.design.components.SpiderPage import app.dapk.st.messenger.gallery.FetchMediaFoldersUseCase import app.dapk.st.messenger.gallery.FetchMediaUseCase import app.dapk.st.messenger.gallery.Folder import app.dapk.st.messenger.gallery.Media import app.dapk.state.Combined2 +import app.dapk.state.SpiderPage +import app.dapk.state.page.PageAction +import app.dapk.state.page.PageContainer +import app.dapk.state.page.PageStateChange import fake.FakeJobBag import fake.FakeUri import io.mockk.coEvery @@ -18,7 +18,6 @@ import io.mockk.mockk import org.junit.Test import test.assertOnlyDispatches import test.delegateReturn -import test.expect import test.testReducer private const val A_ROOM_NAME = "a room name" diff --git a/features/profile/build.gradle b/features/profile/build.gradle index f3697f1..f3bf95b 100644 --- a/features/profile/build.gradle +++ b/features/profile/build.gradle @@ -4,16 +4,16 @@ dependencies { implementation project(":chat-engine") implementation project(":features:settings") implementation project(':domains:store') - implementation project(':domains:state') + implementation 'screen-state:screen-android' implementation project(":domains:android:compose-core") implementation project(":design-library") implementation project(":core") kotlinTest(it) + testImplementation 'screen-state:state-test' androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":domains:state")) androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":chat-engine")) diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt index 2b0037f..165f993 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt @@ -2,7 +2,7 @@ package app.dapk.st.profile import app.dapk.st.core.JobBag import app.dapk.st.core.ProvidableModule -import app.dapk.st.core.createStateViewModel +import app.dapk.st.state.createStateViewModel import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.engine.ChatEngine import app.dapk.st.profile.state.ProfileState diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt index 6b2fbdc..b740780 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp import app.dapk.st.core.Lce import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.components.CenteredLoading -import app.dapk.st.core.page.PageAction import app.dapk.st.design.components.* import app.dapk.st.engine.RoomInvite import app.dapk.st.engine.RoomInvite.InviteMeta @@ -28,6 +27,8 @@ import app.dapk.st.profile.state.Page import app.dapk.st.profile.state.ProfileAction import app.dapk.st.profile.state.ProfileState import app.dapk.st.settings.SettingsActivity +import app.dapk.state.SpiderPage +import app.dapk.state.page.PageAction @Composable fun ProfileScreen(viewModel: ProfileState, onTopLevelBack: () -> Unit) { @@ -47,7 +48,7 @@ fun ProfileScreen(viewModel: ProfileState, onTopLevelBack: () -> Unit) { } } - Spider(currentPage = viewModel.current.state1.page, onNavigate = onNavigate) { + Spider(currentPage = viewModel.current.state1.page, onNavigate = onNavigate, toolbar = { navigate, title -> Toolbar(navigate, title) }) { item(Page.Routes.profile) { ProfilePage(context, viewModel, it) } diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileReducer.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileReducer.kt index 8d01ee7..1badceb 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileReducer.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileReducer.kt @@ -3,14 +3,14 @@ package app.dapk.st.profile.state import app.dapk.st.core.JobBag import app.dapk.st.core.Lce import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.core.page.PageAction -import app.dapk.st.core.page.PageStateChange -import app.dapk.st.core.page.createPageReducer -import app.dapk.st.core.page.withPageContext -import app.dapk.st.design.components.SpiderPage import app.dapk.st.engine.ChatEngine +import app.dapk.state.SpiderPage import app.dapk.state.async import app.dapk.state.createReducer +import app.dapk.state.page.PageAction +import app.dapk.state.page.PageStateChange +import app.dapk.state.page.createPageReducer +import app.dapk.state.page.withPageContext import app.dapk.state.sideEffect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileState.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileState.kt index 7490b49..3869bb8 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileState.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileState.kt @@ -1,12 +1,12 @@ package app.dapk.st.profile.state import app.dapk.st.core.Lce -import app.dapk.st.core.State -import app.dapk.st.core.page.PageContainer -import app.dapk.st.design.components.Route +import app.dapk.st.state.State import app.dapk.st.engine.Me import app.dapk.st.engine.RoomInvite import app.dapk.state.Combined2 +import app.dapk.state.Route +import app.dapk.state.page.PageContainer typealias ProfileState = State, Unit>, Unit> diff --git a/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt b/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt index 68fb60c..e5849e5 100644 --- a/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt +++ b/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt @@ -1,13 +1,13 @@ package app.dapk.st.profile.state import app.dapk.st.core.Lce -import app.dapk.st.core.page.PageAction -import app.dapk.st.core.page.PageContainer -import app.dapk.st.core.page.PageStateChange -import app.dapk.st.design.components.SpiderPage import app.dapk.st.engine.Me import app.dapk.st.matrix.common.HomeServerUrl import app.dapk.state.Combined2 +import app.dapk.state.SpiderPage +import app.dapk.state.page.PageAction +import app.dapk.state.page.PageContainer +import app.dapk.state.page.PageStateChange import fake.FakeChatEngine import fake.FakeErrorTracker import fake.FakeJobBag diff --git a/features/settings/build.gradle b/features/settings/build.gradle index d88337d..d646822 100644 --- a/features/settings/build.gradle +++ b/features/settings/build.gradle @@ -7,15 +7,16 @@ dependencies { implementation project(':domains:android:push') implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") + implementation 'screen-state:screen-android' implementation project(":design-library") implementation project(":core") kotlinTest(it) + testImplementation 'screen-state:state-test' androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":domains:store")) - androidImportFixturesWorkaround(project, project(":domains:state")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":chat-engine")) diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt index 1cc26df..216617f 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt @@ -4,11 +4,15 @@ import android.os.Bundle import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier -import app.dapk.st.core.* +import app.dapk.st.core.DapkActivity +import app.dapk.st.core.module +import app.dapk.st.core.resetModules +import app.dapk.st.settings.state.SettingsState +import app.dapk.st.state.state class SettingsActivity : DapkActivity() { - private val settingsState by state { module().settingsState() } + private val settingsState: SettingsState by state { module().settingsState() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt index a3271e3..61d4a9a 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt @@ -10,6 +10,7 @@ import app.dapk.st.push.PushModule import app.dapk.st.settings.eventlogger.EventLoggerViewModel import app.dapk.st.settings.state.SettingsState import app.dapk.st.settings.state.settingsReducer +import app.dapk.st.state.createStateViewModel class SettingsModule( private val chatEngine: ChatEngine, diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt index 6e73893..f7038c3 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt @@ -41,7 +41,6 @@ import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.components.Header import app.dapk.st.core.extensions.takeAs import app.dapk.st.core.getActivity -import app.dapk.st.core.page.PageAction import app.dapk.st.design.components.* import app.dapk.st.engine.ImportResult import app.dapk.st.navigator.Navigator @@ -51,6 +50,8 @@ import app.dapk.st.settings.state.ComponentLifecycle import app.dapk.st.settings.state.RootActions import app.dapk.st.settings.state.ScreenAction import app.dapk.st.settings.state.SettingsState +import app.dapk.state.SpiderPage +import app.dapk.state.page.PageAction @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @Composable @@ -66,7 +67,7 @@ internal fun SettingsScreen(settingsState: SettingsState, onSignOut: () -> Unit, else -> settingsState.dispatch(PageAction.GoTo(it)) } } - Spider(currentPage = settingsState.current.state1.page, onNavigate = onNavigate) { + Spider(currentPage = settingsState.current.state1.page, onNavigate = onNavigate, toolbar = { navigate, title -> Toolbar(navigate, title) }) { item(Page.Routes.root) { RootSettings( it, diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt index ff796c7..bb71728 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt @@ -2,10 +2,10 @@ package app.dapk.st.settings import android.net.Uri import app.dapk.st.core.Lce -import app.dapk.st.design.components.Route -import app.dapk.st.design.components.SpiderPage import app.dapk.st.engine.ImportResult import app.dapk.st.push.Registrar +import app.dapk.state.Route +import app.dapk.state.SpiderPage internal data class SettingsScreenState( val page: SpiderPage, @@ -26,9 +26,9 @@ internal sealed interface Page { object Routes { val root = Route("Settings") - val encryption = Route("Encryption") - val pushProviders = Route("PushProviders") - val importRoomKeys = Route("ImportRoomKey") + val encryption = Route("Encryption") + val pushProviders = Route("PushProviders") + val importRoomKeys = Route("ImportRoomKey") } } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsReducer.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsReducer.kt index a17dd55..075661c 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsReducer.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsReducer.kt @@ -3,10 +3,8 @@ package app.dapk.st.settings.state import android.content.ContentResolver import app.dapk.st.core.JobBag import app.dapk.st.core.Lce -import app.dapk.st.core.State +import app.dapk.st.state.State import app.dapk.st.core.ThemeStore -import app.dapk.st.core.page.* -import app.dapk.st.design.components.SpiderPage import app.dapk.st.domain.StoreCleaner import app.dapk.st.domain.application.eventlog.LoggingStore import app.dapk.st.domain.application.message.MessageOptionsStore @@ -16,10 +14,8 @@ import app.dapk.st.push.PushTokenRegistrars import app.dapk.st.settings.* import app.dapk.st.settings.SettingItem.Id.* import app.dapk.st.settings.SettingsEvent.* -import app.dapk.state.Combined2 -import app.dapk.state.async -import app.dapk.state.createReducer -import app.dapk.state.multi +import app.dapk.state.* +import app.dapk.state.page.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch diff --git a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsReducerTest.kt b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsReducerTest.kt index 4b6e2ec..837a5ad 100644 --- a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsReducerTest.kt +++ b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsReducerTest.kt @@ -1,10 +1,6 @@ package app.dapk.st.settings import app.dapk.st.core.Lce -import app.dapk.st.core.page.PageAction -import app.dapk.st.core.page.PageContainer -import app.dapk.st.core.page.PageStateChange -import app.dapk.st.design.components.SpiderPage import app.dapk.st.engine.ImportResult import app.dapk.st.push.Registrar import app.dapk.st.settings.state.ComponentLifecycle @@ -12,6 +8,10 @@ import app.dapk.st.settings.state.RootActions import app.dapk.st.settings.state.ScreenAction import app.dapk.st.settings.state.settingsReducer import app.dapk.state.Combined2 +import app.dapk.state.SpiderPage +import app.dapk.state.page.PageAction +import app.dapk.state.page.PageContainer +import app.dapk.state.page.PageStateChange import fake.* import fixture.aRoomId import internalfake.FakeSettingsItemFactory diff --git a/features/settings/src/test/kotlin/internalfixture/PageFixture.kt b/features/settings/src/test/kotlin/internalfixture/PageFixture.kt index d42834f..61ae7a9 100644 --- a/features/settings/src/test/kotlin/internalfixture/PageFixture.kt +++ b/features/settings/src/test/kotlin/internalfixture/PageFixture.kt @@ -1,7 +1,7 @@ package internalfixture -import app.dapk.st.design.components.SpiderPage import app.dapk.st.settings.Page +import app.dapk.state.SpiderPage internal fun aImportRoomKeysPage( state: Page.ImportRoomKey = Page.ImportRoomKey() diff --git a/settings.gradle b/settings.gradle index bd5242f..f677ed5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,9 @@ dependencyResolutionManagement { } } rootProject.name = "SmallTalk" + +includeBuild '../screen-state' + include ':app' include ':design-library' From 37ad97c76123d468e89da6570da05445f9bd6037 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 28 Nov 2022 19:03:43 +0000 Subject: [PATCH 11/71] using screen-state submodule --- .gitmodules | 3 +++ build.gradle | 6 ------ screen-state | 1 + settings.gradle | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 .gitmodules create mode 160000 screen-state diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..74b800c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "screen-state"] + path = screen-state + url = git@github.com:ouchadam/screen-state.git diff --git a/build.gradle b/build.gradle index e1c1e9b..944d6f1 100644 --- a/build.gradle +++ b/build.gradle @@ -151,12 +151,6 @@ ext.androidImportFixturesWorkaround = { project, fixtures -> project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}.jar") } -ext.androidImportCompositeFixturesWorkaround = { project, fixtures -> - project.dependencies.testImplementation(project.dependencies.testFixtures(fixtures)) - project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}-test-fixtures.jar") - project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}.jar") -} - ext.isFoss = { return rootProject.hasProperty("foss") } diff --git a/screen-state b/screen-state new file mode 160000 index 0000000..4e92f14 --- /dev/null +++ b/screen-state @@ -0,0 +1 @@ +Subproject commit 4e92f14031cc8be907cba09b3bfc1d9dbd380072 diff --git a/settings.gradle b/settings.gradle index f677ed5..d24e38e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,7 +7,7 @@ dependencyResolutionManagement { } rootProject.name = "SmallTalk" -includeBuild '../screen-state' +includeBuild 'screen-state' include ':app' From 99c9556fb3cb07c639530e680112e1b184d0c2e7 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 28 Nov 2022 19:09:53 +0000 Subject: [PATCH 12/71] use recursive submodule checkout --- .github/workflows/assemble.yml | 3 +++ .github/workflows/check_size.yml | 3 +++ .github/workflows/release-candidate.yml | 3 +++ .github/workflows/release-train.yml | 2 ++ .github/workflows/test.yml | 2 ++ 5 files changed, 13 insertions(+) diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml index 26935f8..3db296c 100644 --- a/.github/workflows/assemble.yml +++ b/.github/workflows/assemble.yml @@ -17,6 +17,9 @@ jobs: steps: - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - uses: actions/setup-java@v3 with: distribution: 'adopt' diff --git a/.github/workflows/check_size.yml b/.github/workflows/check_size.yml index 1a855e0..4a40c71 100644 --- a/.github/workflows/check_size.yml +++ b/.github/workflows/check_size.yml @@ -14,6 +14,9 @@ jobs: steps: - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - uses: actions/setup-java@v2 with: distribution: 'adopt' diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index c07920d..50b3a25 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -16,6 +16,9 @@ jobs: steps: - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - uses: actions/setup-java@v2 with: distribution: 'adopt' diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml index f9f2a85..0ec1e2d 100644 --- a/.github/workflows/release-train.yml +++ b/.github/workflows/release-train.yml @@ -16,6 +16,8 @@ jobs: steps: - uses: actions/checkout@v2 + with: + submodules: 'recursive' - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd29ec1..31224d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,8 @@ jobs: steps: - uses: actions/checkout@v2 + with: + submodules: 'recursive' - uses: actions/setup-java@v2 with: distribution: 'adopt' From eb896017f3683b4c1bb5599a077156c1a2c30c1f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 5 Dec 2022 22:25:06 +0000 Subject: [PATCH 13/71] First pass at extracting the engine module --- app/build.gradle | 19 +- .../app/dapk/st/SmallTalkApplication.kt | 15 +- .../kotlin/app/dapk/st/graph/AppModule.kt | 119 +++--- .../dapk/st/{graph => impl}/AndroidBase64.kt | 6 +- .../dapk/st/impl/AndroidImageContentReader.kt | 40 ++ .../dapk/st/{graph => impl}/AppTaskRunner.kt | 2 +- .../{graph => impl}/BackgroundWorkAdapter.kt | 4 +- .../{graph => impl}/DefaultDatabaseDropper.kt | 2 +- .../{ => impl}/SharedPreferencesDelegate.kt | 2 +- .../st/impl/SmallTalkDeviceNameGenerator.kt | 10 + .../st/{graph => impl}/TaskRunnerAdapter.kt | 2 +- chat-engine/build.gradle | 13 - .../kotlin/app/dapk/st/engine/ChatEngine.kt | 84 ---- .../main/kotlin/app/dapk/st/engine/Models.kt | 233 ----------- .../kotlin/fake/FakeChatEngine.kt | 21 - .../testFixtures/kotlin/fixture/Fixtures.kt | 77 ---- .../fixture/NotificationDiffFixtures.kt | 16 - .../main/kotlin/app/dapk/st/core/Base64.kt | 6 - domains/android/push/build.gradle | 4 +- .../kotlin/app/dapk/st/push/PushHandler.kt | 12 +- domains/firebase/messaging-noop/build.gradle | 2 +- domains/firebase/messaging/build.gradle | 2 +- domains/olm-stub/build.gradle | 7 - .../main/java/org/matrix/olm/OlmAccount.java | 85 ---- .../java/org/matrix/olm/OlmException.java | 70 ---- .../matrix/olm/OlmInboundGroupSession.java | 59 --- .../main/java/org/matrix/olm/OlmManager.java | 14 - .../main/java/org/matrix/olm/OlmMessage.java | 12 - .../matrix/olm/OlmOutboundGroupSession.java | 43 -- .../src/main/java/org/matrix/olm/OlmSAS.java | 32 -- .../main/java/org/matrix/olm/OlmSession.java | 63 --- .../main/java/org/matrix/olm/OlmUtility.java | 41 -- domains/olm/build.gradle | 15 - .../app/dapk/st/olm/DefaultSasSession.kt | 53 --- .../app/dapk/st/olm/DeviceKeyFactory.kt | 39 -- .../kotlin/app/dapk/st/olm/OlmExtensions.kt | 14 - .../app/dapk/st/olm/OlmPersistenceWrapper.kt | 74 ---- .../main/kotlin/app/dapk/st/olm/OlmStore.kt | 22 - .../main/kotlin/app/dapk/st/olm/OlmWrapper.kt | 389 ----------------- domains/store/build.gradle | 11 +- .../dapk/st/domain/CredentialsPreferences.kt | 25 -- .../app/dapk/st/domain/DevicePersistence.kt | 99 ----- .../app/dapk/st/domain/FilterPreferences.kt | 17 - .../app/dapk/st/domain/MemberPersistence.kt | 46 -- .../app/dapk/st/domain/OlmPersistence.kt | 119 ------ .../kotlin/app/dapk/st/domain/StoreModule.kt | 48 +-- .../dapk/st/domain/SyncTokenPreferences.kt | 25 -- .../eventlog/EventLogPersistence.kt | 4 +- .../domain/localecho/LocalEchoPersistence.kt | 120 ------ .../st/domain/profile/ProfilePersistence.kt | 51 --- .../dapk/st/domain/room/MutedRoomsStore.kt | 41 -- .../st/domain/sync/OverviewPersistence.kt | 99 ----- .../dapk/st/domain/sync/RoomPersistence.kt | 162 -------- .../sqldelight/app/dapk/db/model/Crypto.sq | 61 --- .../sqldelight/app/dapk/db/model/Device.sq | 47 --- .../app/dapk/db/model/InviteState.sq | 17 - .../sqldelight/app/dapk/db/model/LocalEcho.sq | 18 - .../sqldelight/app/dapk/db/model/MutedRoom.sq | 16 - .../app/dapk/db/model/OverviewState.sq | 25 -- .../sqldelight/app/dapk/db/model/RoomEvent.sq | 53 --- .../app/dapk/db/model/RoomMember.sq | 21 - .../app/dapk/db/model/UnreadEvent.sq | 18 - features/directory/build.gradle | 8 +- features/home/build.gradle | 2 +- .../kotlin/app/dapk/st/home/HomeModule.kt | 1 - .../kotlin/app/dapk/st/home/HomeViewModel.kt | 6 +- features/login/build.gradle | 2 +- features/messenger/build.gradle | 8 +- features/navigator/build.gradle | 2 +- features/notifications/build.gradle | 8 +- features/profile/build.gradle | 10 +- features/settings/build.gradle | 9 +- features/share-entry/build.gradle | 4 +- features/verification/build.gradle | 2 +- .../st/verification/VerificationModule.kt | 6 +- .../st/verification/VerificationViewModel.kt | 14 +- matrix-chat-engine/build.gradle | 34 -- .../DirectoryMergeWithLocalEchosUseCase.kt | 53 --- .../app/dapk/st/engine/DirectoryUseCase.kt | 43 -- .../app/dapk/st/engine/InviteUseCase.kt | 18 - .../app/dapk/st/engine/LocalEchoMapper.kt | 69 --- .../app/dapk/st/engine/LocalIdFactory.kt | 7 - .../app/dapk/st/engine/MappingExtensions.kt | 118 ------ .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 224 ---------- .../app/dapk/st/engine/MatrixFactory.kt | 258 ------------ .../app/dapk/st/engine/MatrixPushHandler.kt | 85 ---- .../st/engine/MergeWithLocalEchosUseCase.kt | 44 -- .../ObserveInviteNotificationsUseCase.kt | 44 -- .../ObserveUnreadNotificationsUseCaseImpl.kt | 99 ----- .../app/dapk/st/engine/ReadMarkingTimeline.kt | 55 --- .../app/dapk/st/engine/SendMessageUseCase.kt | 61 --- .../app/dapk/st/engine/TimelineUseCase.kt | 53 --- .../dapk/st/engine/DirectoryUseCaseTest.kt | 115 ----- .../app/dapk/st/engine/InviteUseCaseTest.kt | 39 -- .../app/dapk/st/engine/LocalEchoMapperTest.kt | 69 --- .../engine/MergeWithLocalEchosUseCaseTest.kt | 61 --- .../app/dapk/st/engine/MetaMapperTest.kt | 59 --- ...rveUnreadRenderNotificationsUseCaseTest.kt | 123 ------ .../dapk/st/engine/ReadMarkingTimelineTest.kt | 66 --- .../dapk/st/engine/SendMessageUseCaseTest.kt | 139 ------- .../app/dapk/st/engine/TimelineUseCaseTest.kt | 142 ------- .../test/kotlin/fake/FakeDirectoryUseCase.kt | 11 - .../test/kotlin/fake/FakeLocalEventMapper.kt | 14 - .../test/kotlin/fake/FakeLocalIdFactory.kt | 11 - .../src/test/kotlin/fake/FakeMetaMapper.kt | 12 - .../FakeObserveInviteNotificationsUseCase.kt | 10 - .../FakeObserveUnreadNotificationsUseCase.kt | 10 - matrix/common/build.gradle | 13 - .../dapk/st/matrix/common/AlgorithmName.kt | 7 - .../app/dapk/st/matrix/common/AvatarUrl.kt | 7 - .../app/dapk/st/matrix/common/CipherText.kt | 7 - .../dapk/st/matrix/common/CredentialsStore.kt | 10 - .../app/dapk/st/matrix/common/Curve25519.kt | 7 - .../dapk/st/matrix/common/DecryptionResult.kt | 6 - .../app/dapk/st/matrix/common/DeviceId.kt | 7 - .../app/dapk/st/matrix/common/Ed25519.kt | 7 - .../matrix/common/EncryptedMessageContent.kt | 22 - .../app/dapk/st/matrix/common/EventId.kt | 7 - .../app/dapk/st/matrix/common/EventType.kt | 19 - .../dapk/st/matrix/common/HomeServerUrl.kt | 7 - .../st/matrix/common/JsonCanonicalizer.kt | 28 -- .../app/dapk/st/matrix/common/JsonString.kt | 4 - .../app/dapk/st/matrix/common/MatrixLogger.kt | 53 --- .../kotlin/app/dapk/st/matrix/common/MxUrl.kt | 19 - .../app/dapk/st/matrix/common/RichText.kt | 43 -- .../app/dapk/st/matrix/common/RoomId.kt | 7 - .../app/dapk/st/matrix/common/RoomMember.kt | 11 - .../dapk/st/matrix/common/ServerKeyCount.kt | 7 - .../app/dapk/st/matrix/common/SessionId.kt | 7 - .../dapk/st/matrix/common/SharedRoomKey.kt | 9 - .../app/dapk/st/matrix/common/SignedJson.kt | 4 - .../app/dapk/st/matrix/common/SyncToken.kt | 7 - .../dapk/st/matrix/common/UserCredentials.kt | 25 -- .../app/dapk/st/matrix/common/UserId.kt | 7 - .../common/extensions/JsonStringExtensions.kt | 19 - .../st/matrix/common/JsonCanonicalizerTest.kt | 108 ----- .../kotlin/fake/FakeCredentialsStore.kt | 10 - .../kotlin/fake/FakeMatrixLogger.kt | 9 - .../kotlin/fixture/DecryptionResultFixture.kt | 10 - .../fixture/DeviceCredentialsFixture.kt | 13 - .../kotlin/fixture/EncryptedMessageFixture.kt | 21 - .../kotlin/fixture/ModelFixtures.kt | 15 - .../kotlin/fixture/RoomMemberFixture.kt | 11 - .../kotlin/fixture/SharedRoomKeyFixture.kt | 14 - .../kotlin/fixture/UserCredentialsFixture.kt | 13 - matrix/matrix-http-ktor/build.gradle | 14 - .../http/ktor/KtorMatrixHttpClientFactory.kt | 33 -- .../ktor/internal/KtorMatrixHttpClient.kt | 107 ----- matrix/matrix-http/build.gradle | 11 - .../app/dapk/st/matrix/http/HttpExtensions.kt | 9 - .../app/dapk/st/matrix/http/JsonExtensions.kt | 51 --- .../dapk/st/matrix/http/MatrixHttpClient.kt | 61 --- .../NullableJsonTransformingSerializer.kt | 26 -- .../dapk/st/matrix/http/RequestExtensions.kt | 7 - .../kotlin/fake/FakeHttpResponse.kt | 16 - .../kotlin/fake/FakeMatrixHttpClient.kt | 16 - .../testFixtures/kotlin/fixture/HttpError.kt | 14 - matrix/matrix/build.gradle | 9 - .../kotlin/app/dapk/st/matrix/MatrixClient.kt | 72 ---- .../app/dapk/st/matrix/ServiceInstaller.kt | 76 ---- matrix/services/auth/build.gradle | 1 - .../app/dapk/st/matrix/auth/AuthService.kt | 42 -- .../st/matrix/auth/internal/AuthRequest.kt | 106 ----- .../auth/internal/DefaultAuthService.kt | 39 -- .../internal/FetchWellKnownUseCaseImpl.kt | 51 --- .../LoginWithUserPasswordServerUseCase.kt | 35 -- .../internal/LoginWithUserPasswordUseCase.kt | 73 ---- .../matrix/auth/internal/RegisterUseCase.kt | 74 ---- matrix/services/crypto/build.gradle | 17 - .../dapk/st/matrix/crypto/CryptoService.kt | 193 --------- .../st/matrix/crypto/MatrixMediaDecrypter.kt | 50 --- .../kotlin/app/dapk/st/matrix/crypto/Olm.kt | 89 ---- .../crypto/internal/AccountCryptoUseCase.kt | 21 - .../crypto/internal/DefaultCryptoService.kt | 65 --- .../EncryptMessageWithMegolmUseCase.kt | 33 -- .../internal/FetchMegolmSessionUseCase.kt | 48 --- .../matrix/crypto/internal/MediaEncrypter.kt | 89 ---- .../crypto/internal/MessageToEncrypt.kt | 6 - .../st/matrix/crypto/internal/OlmCrypto.kt | 45 -- .../internal/OneTimeKeyUploaderUseCase.kt | 54 --- .../internal/RegisterOlmSessionUseCase.kt | 55 --- .../matrix/crypto/internal/RoomKeyImporter.kt | 221 ---------- .../crypto/internal/ShareRoomKeyUseCase.kt | 65 --- .../UpdateKnownOlmSessionUseCaseImpl.kt | 29 -- .../crypto/internal/VerificationHandler.kt | 215 ---------- .../internal/EncryptMegolmUseCaseTest.kt | 59 --- .../internal/FetchAccountCryptoUseCaseTest.kt | 48 --- .../internal/FetchMegolmSessionUseCaseTest.kt | 63 --- ...beCreateAndUploadOneTimeKeysUseCaseTest.kt | 74 ---- .../matrix/crypto/internal/OlmCryptoTest.kt | 101 ----- .../internal/RegisterOlmSessionUseCaseTest.kt | 89 ---- .../internal/ShareRoomKeyUseCaseTest.kt | 85 ---- .../UpdateKnownOlmSessionUseCaseTest.kt | 51 --- .../FakeEncryptMessageWithMegolmUseCase.kt | 14 - .../FakeFetchAccountCryptoUseCase.kt | 10 - .../FakeFetchMegolmSessionUseCase.kt | 13 - ...eMaybeCreateAndUploadOneTimeKeysUseCase.kt | 6 - .../FakeRegisterOlmSessionUseCase.kt | 24 -- .../internalfake/FakeShareRoomKeyUseCase.kt | 23 - .../FakeUpdateKnownOlmSessionUseCase.kt | 6 - .../kotlin/fake/FakeCryptoService.kt | 11 - .../src/testFixtures/kotlin/fake/FakeOlm.kt | 74 ---- .../kotlin/fixture/CryptoSessionFixtures.kt | 32 -- .../kotlin/fixture/FakeRoomMembersProvider.kt | 11 - matrix/services/device/build.gradle | 9 - .../dapk/st/matrix/device/DeviceService.kt | 141 ------- .../st/matrix/device/internal/ApiMessage.kt | 26 -- .../device/internal/DefaultDeviceService.kt | 141 ------- .../device/internal/EncyptionRequests.kt | 88 ---- .../kotlin/fake/FakeDeviceService.kt | 26 -- .../fixture/ClaimKeysResponseFixture.kt | 11 - .../kotlin/fixture/DeviceKeysFixutre.kt | 15 - .../kotlin/fixture/KeyClaimFixture.kt | 12 - matrix/services/message/build.gradle | 10 - .../dapk/st/matrix/message/ApiSendResponse.kt | 16 - .../st/matrix/message/BackgroundScheduler.kt | 11 - .../dapk/st/matrix/message/MediaEncrypter.kt | 31 -- .../st/matrix/message/MessageEncrypter.kt | 25 -- .../dapk/st/matrix/message/MessageService.kt | 162 -------- .../app/dapk/st/matrix/message/Store.kt | 14 - .../st/matrix/message/internal/ApiMessage.kt | 92 ---- .../message/internal/DefaultMessageService.kt | 87 ---- .../message/internal/ImageContentReader.kt | 16 - .../internal/SendEventMessageUseCase.kt | 27 -- .../message/internal/SendMessageUseCase.kt | 206 --------- .../st/matrix/message/internal/SendRequest.kt | 84 ---- .../kotlin/fixture/LocalEchoFixture.kt | 10 - .../kotlin/fixture/MessageFixture.kt | 19 - matrix/services/profile/build.gradle | 14 - .../app/dapk/st/matrix/room/ProfileService.kt | 41 -- .../app/dapk/st/matrix/room/ProfileStore.kt | 8 - .../room/internal/DefaultProfileService.kt | 27 -- .../st/matrix/room/internal/FetchMeUseCase.kt | 53 --- .../room/internal/FetchMeUseCaseTest.kt | 78 ---- matrix/services/push/build.gradle | 1 - .../app/dapk/st/matrix/push/PushService.kt | 49 --- .../push/internal/DefaultPushService.kt | 20 - .../st/matrix/push/internal/PushRequest.kt | 12 - .../push/internal/RegisterPushUseCase.kt | 39 -- matrix/services/room/build.gradle | 5 - .../app/dapk/st/matrix/room/RoomService.kt | 68 --- .../room/internal/DefaultRoomService.kt | 179 -------- .../matrix/room/internal/RoomInviteRemover.kt | 7 - .../st/matrix/room/internal/RoomMembers.kt | 61 --- matrix/services/sync/build.gradle | 14 - .../app/dapk/st/matrix/sync/OverviewState.kt | 48 --- .../app/dapk/st/matrix/sync/RoomState.kt | 146 ------- .../kotlin/app/dapk/st/matrix/sync/Store.kt | 71 ---- .../app/dapk/st/matrix/sync/SyncService.kt | 150 ------- .../sync/internal/DefaultSyncService.kt | 137 ------ .../st/matrix/sync/internal/FlowIterator.kt | 28 -- .../sync/internal/filter/FilterRequest.kt | 14 - .../sync/internal/filter/FilterUseCase.kt | 24 -- .../overview/ReducedSyncFilterUseCase.kt | 39 -- .../sync/internal/request/ApiAccountEvent.kt | 33 -- .../internal/request/ApiEncryptedContent.kt | 35 -- .../internal/request/ApiFilterResponse.kt | 41 -- .../sync/internal/request/ApiStrippedEvent.kt | 41 -- .../sync/internal/request/ApiSyncResponse.kt | 113 ----- .../sync/internal/request/ApiTimelineEvent.kt | 223 ---------- .../ApiTimelineMessageContentDeserializer.kt | 32 -- .../sync/internal/request/ApiToDeviceEvent.kt | 151 ------- .../request/EncryptedContentDeserializer.kt | 29 -- .../sync/internal/request/SyncRequest.kt | 19 - .../sync/internal/room/RoomEventsDecrypter.kt | 81 ---- .../sync/internal/room/RoomStateReducer.kt | 28 -- .../sync/internal/room/SyncEventDecrypter.kt | 54 --- .../sync/internal/room/SyncSideEffects.kt | 85 ---- .../internal/sync/EphemeralEventsUseCase.kt | 22 - .../sync/internal/sync/EventLookupUseCase.kt | 30 -- .../matrix/sync/internal/sync/LookupResult.kt | 22 - .../sync/internal/sync/RoomDataSource.kt | 62 --- .../sync/internal/sync/RoomEventCreator.kt | 192 --------- .../sync/internal/sync/RoomEventFactory.kt | 56 --- .../internal/sync/RoomOverviewProcessor.kt | 92 ---- .../sync/internal/sync/RoomProcessor.kt | 83 ---- .../sync/internal/sync/RoomRefresher.kt | 32 -- .../sync/internal/sync/RoomToProcess.kt | 14 - .../matrix/sync/internal/sync/SyncReducer.kt | 107 ----- .../matrix/sync/internal/sync/SyncUseCase.kt | 87 ---- .../internal/sync/TimelineEventsProcessor.kt | 61 --- .../internal/sync/UnreadEventsProcessor.kt | 58 --- .../AccumulatingRichTextContentParser.kt | 85 ---- .../sync/message/RichTextMessageParser.kt | 34 -- .../sync/message/RichTextPartBuilder.kt | 76 ---- .../sync/message/html/HtmlProcessor.kt | 27 -- .../sync/message/html/ListAccumulator.kt | 23 - .../message/html/RichTextHtmlTagParser.kt | 95 ----- .../internal/sync/message/html/TagCaptor.kt | 78 ---- .../internal/sync/message/url/UrlParser.kt | 53 --- .../sync/internal/filter/FilterUseCaseTest.kt | 44 -- .../internal/room/RoomEventsDecrypterTest.kt | 98 ----- .../sync/EphemeralEventsUseCaseTest.kt | 74 ---- .../internal/sync/EventLookupUseCaseTest.kt | 75 ---- .../sync/RichTextMessageParserTest.kt | 269 ------------ .../internal/sync/RoomEventCreatorTest.kt | 338 --------------- .../sync/internal/sync/RoomRefresherTest.kt | 67 --- .../sync/TimelineEventsProcessorTest.kt | 104 ----- .../sync/UnreadEventsProcessorTest.kt | 68 --- .../kotlin/internalfake/FakeEventLookup.kt | 17 - .../internalfake/FakeRoomEventCreator.kt | 41 -- .../internalfake/FakeRoomEventsDecrypter.kt | 15 - .../internalfake/FakeSyncEventDecrypter.kt | 14 - .../internalfixture/ApiSyncRoomFixture.kt | 119 ------ .../kotlin/fake/FakeFilterStore.kt | 13 - .../kotlin/fake/FakeMessageDecrypter.kt | 12 - .../kotlin/fake/FakeRoomDataSource.kt | 28 -- .../kotlin/fake/FakeRoomMembersService.kt | 19 - .../testFixtures/kotlin/fake/FakeRoomStore.kt | 47 --- .../kotlin/fake/FakeSyncService.kt | 16 - .../kotlin/fixture/RoomEventFixture.kt | 51 --- .../kotlin/fixture/RoomOverviewFixture.kt | 25 -- .../kotlin/fixture/RoomStateFixture.kt | 10 - .../kotlin/fixture/SyncServiceFixtures.kt | 2 - settings.gradle | 24 +- test-harness/build.gradle | 35 -- test-harness/src/test/kotlin/SmokeTest.kt | 218 ---------- .../io/ktor/client/engine/java/AJava.kt | 45 -- test-harness/src/test/kotlin/test/Test.kt | 232 ----------- .../src/test/kotlin/test/TestExtensions.kt | 14 - .../src/test/kotlin/test/TestMatrix.kt | 392 ------------------ .../src/test/kotlin/test/TestPersistence.kt | 72 ---- .../test/kotlin/test/impl/InMemoryDatabase.kt | 29 -- .../kotlin/test/impl/InMemoryPreferences.kt | 23 - .../test/kotlin/test/impl/InstantScheduler.kt | 21 - .../kotlin/test/impl/PrintingErrorTracking.kt | 10 - .../src/test/resources/element-keys.txt | 176 -------- .../src/test/resources/test-image.png | Bin 54591 -> 0 bytes .../src/test/resources/test-image2.png | Bin 59109 -> 0 bytes 329 files changed, 183 insertions(+), 16366 deletions(-) rename app/src/main/kotlin/app/dapk/st/{graph => impl}/AndroidBase64.kt (73%) create mode 100644 app/src/main/kotlin/app/dapk/st/impl/AndroidImageContentReader.kt rename app/src/main/kotlin/app/dapk/st/{graph => impl}/AppTaskRunner.kt (98%) rename app/src/main/kotlin/app/dapk/st/{graph => impl}/BackgroundWorkAdapter.kt (83%) rename app/src/main/kotlin/app/dapk/st/{graph => impl}/DefaultDatabaseDropper.kt (97%) rename app/src/main/kotlin/app/dapk/st/{ => impl}/SharedPreferencesDelegate.kt (97%) create mode 100644 app/src/main/kotlin/app/dapk/st/impl/SmallTalkDeviceNameGenerator.kt rename app/src/main/kotlin/app/dapk/st/{graph => impl}/TaskRunnerAdapter.kt (97%) delete mode 100644 chat-engine/build.gradle delete mode 100644 chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt delete mode 100644 chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt delete mode 100644 chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt delete mode 100644 chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt delete mode 100644 chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt delete mode 100644 core/src/main/kotlin/app/dapk/st/core/Base64.kt delete mode 100644 domains/olm-stub/build.gradle delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmAccount.java delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmException.java delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmInboundGroupSession.java delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmManager.java delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmMessage.java delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmOutboundGroupSession.java delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmSAS.java delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmSession.java delete mode 100644 domains/olm-stub/src/main/java/org/matrix/olm/OlmUtility.java delete mode 100644 domains/olm/build.gradle delete mode 100644 domains/olm/src/main/kotlin/app/dapk/st/olm/DefaultSasSession.kt delete mode 100644 domains/olm/src/main/kotlin/app/dapk/st/olm/DeviceKeyFactory.kt delete mode 100644 domains/olm/src/main/kotlin/app/dapk/st/olm/OlmExtensions.kt delete mode 100644 domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt delete mode 100644 domains/olm/src/main/kotlin/app/dapk/st/olm/OlmStore.kt delete mode 100644 domains/olm/src/main/kotlin/app/dapk/st/olm/OlmWrapper.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/CredentialsPreferences.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/DevicePersistence.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/FilterPreferences.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/OlmPersistence.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/profile/ProfilePersistence.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/room/MutedRoomsStore.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt delete mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/Crypto.sq delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/Device.sq delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/LocalEcho.sq delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/MutedRoom.sq delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/RoomMember.sq delete mode 100644 domains/store/src/main/sqldelight/app/dapk/db/model/UnreadEvent.sq delete mode 100644 matrix-chat-engine/build.gradle delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryMergeWithLocalEchosUseCase.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixFactory.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/SendMessageUseCase.kt delete mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/DirectoryUseCaseTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/InviteUseCaseTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MetaMapperTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ReadMarkingTimelineTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/SendMessageUseCaseTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/fake/FakeDirectoryUseCase.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/fake/FakeLocalEventMapper.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/fake/FakeLocalIdFactory.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/fake/FakeMetaMapper.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt delete mode 100644 matrix-chat-engine/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt delete mode 100644 matrix/common/build.gradle delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/AlgorithmName.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/AvatarUrl.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/CipherText.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/CredentialsStore.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/Curve25519.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/DecryptionResult.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/DeviceId.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/Ed25519.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EncryptedMessageContent.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventId.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventType.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/HomeServerUrl.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/JsonCanonicalizer.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/JsonString.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/MatrixLogger.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/MxUrl.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RichText.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RoomId.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RoomMember.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/ServerKeyCount.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SessionId.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SharedRoomKey.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SignedJson.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SyncToken.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/UserCredentials.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/UserId.kt delete mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/extensions/JsonStringExtensions.kt delete mode 100644 matrix/common/src/test/kotlin/app/dapk/st/matrix/common/JsonCanonicalizerTest.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fake/FakeCredentialsStore.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fake/FakeMatrixLogger.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fixture/DecryptionResultFixture.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fixture/DeviceCredentialsFixture.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fixture/EncryptedMessageFixture.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fixture/ModelFixtures.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fixture/RoomMemberFixture.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fixture/SharedRoomKeyFixture.kt delete mode 100644 matrix/common/src/testFixtures/kotlin/fixture/UserCredentialsFixture.kt delete mode 100644 matrix/matrix-http-ktor/build.gradle delete mode 100644 matrix/matrix-http-ktor/src/main/kotlin/app/dapk/st/matrix/http/ktor/KtorMatrixHttpClientFactory.kt delete mode 100644 matrix/matrix-http-ktor/src/main/kotlin/app/dapk/st/matrix/http/ktor/internal/KtorMatrixHttpClient.kt delete mode 100644 matrix/matrix-http/build.gradle delete mode 100644 matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/HttpExtensions.kt delete mode 100644 matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/JsonExtensions.kt delete mode 100644 matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt delete mode 100644 matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/NullableJsonTransformingSerializer.kt delete mode 100644 matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/RequestExtensions.kt delete mode 100644 matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt delete mode 100644 matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt delete mode 100644 matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt delete mode 100644 matrix/matrix/build.gradle delete mode 100644 matrix/matrix/src/main/kotlin/app/dapk/st/matrix/MatrixClient.kt delete mode 100644 matrix/matrix/src/main/kotlin/app/dapk/st/matrix/ServiceInstaller.kt delete mode 100644 matrix/services/auth/build.gradle delete mode 100644 matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/AuthService.kt delete mode 100644 matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/AuthRequest.kt delete mode 100644 matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/DefaultAuthService.kt delete mode 100644 matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/FetchWellKnownUseCaseImpl.kt delete mode 100644 matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordServerUseCase.kt delete mode 100644 matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordUseCase.kt delete mode 100644 matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/RegisterUseCase.kt delete mode 100644 matrix/services/crypto/build.gradle delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MatrixMediaDecrypter.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/Olm.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/AccountCryptoUseCase.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/EncryptMessageWithMegolmUseCase.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/FetchMegolmSessionUseCase.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MessageToEncrypt.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/OlmCrypto.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/OneTimeKeyUploaderUseCase.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RegisterOlmSessionUseCase.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/ShareRoomKeyUseCase.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/UpdateKnownOlmSessionUseCaseImpl.kt delete mode 100644 matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/VerificationHandler.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/EncryptMegolmUseCaseTest.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/FetchAccountCryptoUseCaseTest.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/FetchMegolmSessionUseCaseTest.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/MaybeCreateAndUploadOneTimeKeysUseCaseTest.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/OlmCryptoTest.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/RegisterOlmSessionUseCaseTest.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/ShareRoomKeyUseCaseTest.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/UpdateKnownOlmSessionUseCaseTest.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/internalfake/FakeEncryptMessageWithMegolmUseCase.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/internalfake/FakeFetchAccountCryptoUseCase.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/internalfake/FakeFetchMegolmSessionUseCase.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/internalfake/FakeMaybeCreateAndUploadOneTimeKeysUseCase.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/internalfake/FakeRegisterOlmSessionUseCase.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/internalfake/FakeShareRoomKeyUseCase.kt delete mode 100644 matrix/services/crypto/src/test/kotlin/internalfake/FakeUpdateKnownOlmSessionUseCase.kt delete mode 100644 matrix/services/crypto/src/testFixtures/kotlin/fake/FakeCryptoService.kt delete mode 100644 matrix/services/crypto/src/testFixtures/kotlin/fake/FakeOlm.kt delete mode 100644 matrix/services/crypto/src/testFixtures/kotlin/fixture/CryptoSessionFixtures.kt delete mode 100644 matrix/services/crypto/src/testFixtures/kotlin/fixture/FakeRoomMembersProvider.kt delete mode 100644 matrix/services/device/build.gradle delete mode 100644 matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/DeviceService.kt delete mode 100644 matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/ApiMessage.kt delete mode 100644 matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/DefaultDeviceService.kt delete mode 100644 matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/EncyptionRequests.kt delete mode 100644 matrix/services/device/src/testFixtures/kotlin/fake/FakeDeviceService.kt delete mode 100644 matrix/services/device/src/testFixtures/kotlin/fixture/ClaimKeysResponseFixture.kt delete mode 100644 matrix/services/device/src/testFixtures/kotlin/fixture/DeviceKeysFixutre.kt delete mode 100644 matrix/services/device/src/testFixtures/kotlin/fixture/KeyClaimFixture.kt delete mode 100644 matrix/services/message/build.gradle delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/ApiSendResponse.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/BackgroundScheduler.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/Store.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendEventMessageUseCase.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt delete mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt delete mode 100644 matrix/services/message/src/testFixtures/kotlin/fixture/LocalEchoFixture.kt delete mode 100644 matrix/services/message/src/testFixtures/kotlin/fixture/MessageFixture.kt delete mode 100644 matrix/services/profile/build.gradle delete mode 100644 matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt delete mode 100644 matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileStore.kt delete mode 100644 matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt delete mode 100644 matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt delete mode 100644 matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt delete mode 100644 matrix/services/push/build.gradle delete mode 100644 matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/PushService.kt delete mode 100644 matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/DefaultPushService.kt delete mode 100644 matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/PushRequest.kt delete mode 100644 matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/RegisterPushUseCase.kt delete mode 100644 matrix/services/room/build.gradle delete mode 100644 matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt delete mode 100644 matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt delete mode 100644 matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomInviteRemover.kt delete mode 100644 matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt delete mode 100644 matrix/services/sync/build.gradle delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterRequest.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterUseCase.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/overview/ReducedSyncFilterUseCase.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiAccountEvent.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiEncryptedContent.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiFilterResponse.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiStrippedEvent.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineMessageContentDeserializer.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiToDeviceEvent.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/EncryptedContentDeserializer.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/SyncRequest.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomStateReducer.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncEventDecrypter.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/EphemeralEventsUseCase.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCase.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/LookupResult.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresher.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomToProcess.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessor.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessor.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/AccumulatingRichTextContentParser.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/RichTextMessageParser.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/RichTextPartBuilder.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/HtmlProcessor.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/ListAccumulator.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/RichTextHtmlTagParser.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/TagCaptor.kt delete mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/url/UrlParser.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterUseCaseTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EphemeralEventsUseCaseTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichTextMessageParserTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessorTest.kt delete mode 100644 matrix/services/sync/src/test/kotlin/internalfake/FakeEventLookup.kt delete mode 100644 matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventCreator.kt delete mode 100644 matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventsDecrypter.kt delete mode 100644 matrix/services/sync/src/test/kotlin/internalfake/FakeSyncEventDecrypter.kt delete mode 100644 matrix/services/sync/src/test/kotlin/internalfixture/ApiSyncRoomFixture.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fake/FakeFilterStore.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fake/FakeMessageDecrypter.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomDataSource.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomMembersService.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomStore.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt delete mode 100644 matrix/services/sync/src/testFixtures/kotlin/fixture/SyncServiceFixtures.kt delete mode 100644 test-harness/build.gradle delete mode 100644 test-harness/src/test/kotlin/SmokeTest.kt delete mode 100644 test-harness/src/test/kotlin/io/ktor/client/engine/java/AJava.kt delete mode 100644 test-harness/src/test/kotlin/test/Test.kt delete mode 100644 test-harness/src/test/kotlin/test/TestExtensions.kt delete mode 100644 test-harness/src/test/kotlin/test/TestMatrix.kt delete mode 100644 test-harness/src/test/kotlin/test/TestPersistence.kt delete mode 100644 test-harness/src/test/kotlin/test/impl/InMemoryDatabase.kt delete mode 100644 test-harness/src/test/kotlin/test/impl/InMemoryPreferences.kt delete mode 100644 test-harness/src/test/kotlin/test/impl/InstantScheduler.kt delete mode 100644 test-harness/src/test/kotlin/test/impl/PrintingErrorTracking.kt delete mode 100644 test-harness/src/test/resources/element-keys.txt delete mode 100644 test-harness/src/test/resources/test-image.png delete mode 100644 test-harness/src/test/resources/test-image2.png diff --git a/app/build.gradle b/app/build.gradle index 0b091ca..419e3ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,32 +82,21 @@ dependencies { implementation project(":features:navigator") implementation project(":features:share-entry") - implementation project(':domains:store') implementation project(":domains:android:compose-core") implementation project(":domains:android:core") implementation project(":domains:android:tracking") implementation project(":domains:android:push") implementation project(":domains:android:work") implementation project(":domains:android:imageloader") - implementation project(":domains:olm") + implementation project(":domains:store") firebase(it, "messaging") - implementation project(":matrix:matrix") - implementation project(":matrix:matrix-http-ktor") - implementation project(":matrix:services:auth") - implementation project(":matrix:services:sync") - implementation project(":matrix:services:room") - implementation project(":matrix:services:push") - implementation project(":matrix:services:message") - implementation project(":matrix:services:device") - implementation project(":matrix:services:crypto") - implementation project(":matrix:services:profile") - implementation project(":core") - implementation project(":chat-engine") - implementation project(":matrix-chat-engine") + implementation "chat-engine:chat-engine" + implementation "chat-engine:matrix-chat-engine" + implementation "chat-engine.matrix:store" implementation Dependencies.google.androidxComposeUi implementation Dependencies.mavenCentral.ktorAndroid diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index 90a3a50..8a078b7 100644 --- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt +++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt @@ -9,7 +9,6 @@ import app.dapk.st.core.attachAppLogger import app.dapk.st.core.extensions.ResettableUnsafeLazy import app.dapk.st.core.extensions.Scope import app.dapk.st.directory.DirectoryModule -import app.dapk.st.domain.StoreModule import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.graph.AppModule import app.dapk.st.home.HomeModule @@ -55,17 +54,14 @@ class SmallTalkApplication : Application(), ModuleProvider { attachAppLogger(logger) _appLogger = logger - onApplicationLaunch(notificationsModule, storeModule) + onApplicationLaunch(notificationsModule) } - private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) { + private fun onApplicationLaunch(notificationsModule: NotificationsModule) { applicationScope.launch { featureModules.homeModule.betaVersionUpgradeUseCase.waitUnitReady() - - storeModule.credentialsStore().credentials()?.let { - featureModules.pushModule.pushTokenRegistrar().registerCurrentToken() - } - runCatching { storeModule.localEchoStore.preload() } + featureModules.chatEngineModule.engine.preload() + featureModules.pushModule.pushTokenRegistrar().registerCurrentToken() val notificationsUseCase = notificationsModule.notificationsUseCase() notificationsUseCase.listenForNotificationChanges(this) } @@ -99,7 +95,6 @@ class SmallTalkApplication : Application(), ModuleProvider { lazyFeatureModules.reset() val notificationsModule = featureModules.notificationsModule - val storeModule = appModule.storeModule.value - onApplicationLaunch(notificationsModule, storeModule) + onApplicationLaunch(notificationsModule) } } diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index 940c2f1..afe3097 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -2,42 +2,38 @@ package app.dapk.st.graph import android.app.Application import android.app.PendingIntent -import android.content.ContentResolver import android.content.Context import android.content.Intent -import android.graphics.BitmapFactory -import android.media.ExifInterface -import android.net.Uri import android.os.Build -import android.provider.OpenableColumns import app.dapk.db.DapkDb +import app.dapk.db.app.StDb +import app.dapk.engine.core.Base64 import app.dapk.st.BuildConfig -import app.dapk.st.SharedPreferencesDelegate import app.dapk.st.core.* import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.directory.DirectoryModule +import app.dapk.st.domain.MatrixStoreModule import app.dapk.st.domain.StoreModule +import app.dapk.st.engine.ImageContentReader import app.dapk.st.engine.MatrixEngine import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.home.BetaVersionUpgradeUseCase import app.dapk.st.home.HomeModule import app.dapk.st.home.MainActivity import app.dapk.st.imageloader.ImageLoaderModule +import app.dapk.st.impl.* import app.dapk.st.login.LoginModule -import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.JsonString import app.dapk.st.matrix.common.MatrixLogger import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerModule import app.dapk.st.messenger.gallery.ImageGalleryModule import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.MessageAttachment import app.dapk.st.notifications.NotificationsModule -import app.dapk.st.olm.OlmPersistenceWrapper import app.dapk.st.profile.ProfileModule import app.dapk.st.push.PushHandler import app.dapk.st.push.PushModule @@ -51,7 +47,6 @@ import app.dapk.st.work.WorkModule import com.squareup.sqldelight.android.AndroidSqliteDriver import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.Json -import java.io.InputStream internal class AppModule(context: Application, logger: MatrixLogger) { @@ -64,26 +59,48 @@ internal class AppModule(context: Application, logger: MatrixLogger) { } private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db") - private val database = DapkDb(driver) + private val stDriver = AndroidSqliteDriver(DapkDb.Schema, context, "stdb.db") + private val engineDatabase = DapkDb(driver) + private val stDatabase = StDb(stDriver) val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO) private val base64 = AndroidBase64() val storeModule = unsafeLazy { StoreModule( - database = database, + database = stDatabase, preferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", coroutineDispatchers), - errorTracker = trackingModule.errorTracker, credentialPreferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-credentials-preferences", coroutineDispatchers), databaseDropper = DefaultDatabaseDropper(coroutineDispatchers, driver), coroutineDispatchers = coroutineDispatchers ) } + + private val workModule = WorkModule(context) private val imageLoaderModule = ImageLoaderModule(context) private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) } - private val chatEngineModule = - ChatEngineModule(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta) + private val chatEngineModule = ChatEngineModule( + unsafeLazy { matrixStoreModule() }, + trackingModule, + workModule, + logger, + coroutineDispatchers, + imageContentReader, + base64, + buildMeta + ) + + private fun matrixStoreModule(): MatrixStoreModule { + val value = storeModule.value + return MatrixStoreModule( + engineDatabase, + value.preferences.engine(), + value.credentialPreferences.engine(), + trackingModule.errorTracker.engine(), + coroutineDispatchers.engine(), + ) + } val domainModules = DomainModules(chatEngineModule, trackingModule.errorTracker, context, coroutineDispatchers) @@ -133,7 +150,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) { internal class FeatureModules internal constructor( private val storeModule: Lazy, - private val chatEngineModule: ChatEngineModule, + val chatEngineModule: ChatEngineModule, private val domainModules: DomainModules, private val trackingModule: TrackingModule, private val coreAndroidModule: CoreAndroidModule, @@ -220,7 +237,7 @@ internal class FeatureModules internal constructor( } internal class ChatEngineModule( - private val storeModule: Lazy, + private val matrixStoreModule: Lazy, private val trackingModule: TrackingModule, private val workModule: WorkModule, private val logger: MatrixLogger, @@ -231,26 +248,22 @@ internal class ChatEngineModule( ) { val engine by unsafeLazy { - val store = storeModule.value + val matrixCoroutineDispatchers = app.dapk.engine.core.CoroutineDispatchers( + coroutineDispatchers.io, + coroutineDispatchers.main, + coroutineDispatchers.global + ) + val matrixStore = matrixStoreModule.value MatrixEngine.Factory().create( base64, - buildMeta, logger, SmallTalkDeviceNameGenerator(), - coroutineDispatchers, - trackingModule.errorTracker, + matrixCoroutineDispatchers, + trackingModule.errorTracker.engine(), imageContentReader, BackgroundWorkAdapter(workModule.workScheduler()), - store.memberStore(), - store.roomStore(), - store.profileStore(), - store.syncStore(), - store.overviewStore(), - store.filterStore(), - store.localEchoStore, - store.credentialsStore(), - store.knownDevicesStore(), - OlmPersistenceWrapper(store.olmStore(), base64), + matrixStore, + includeLogging = buildMeta.isDebug, ) } @@ -289,43 +302,23 @@ internal class DomainModules( val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(chatEngineModule.engine, AppTaskRunner(chatEngineModule.engine))) } - } -internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader { - override fun meta(uri: String): ImageContentReader.ImageContent { - val androidUri = Uri.parse(uri) - val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri") +private fun CoroutineDispatchers.engine() = app.dapk.engine.core.CoroutineDispatchers(this.io, this.main, this.global) - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeStream(fileStream, null, options) - - val fileSize = contentResolver.query(androidUri, null, null, null, null)?.use { cursor -> - cursor.moveToFirst() - val columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - cursor.getLong(columnIndex) - } ?: throw IllegalArgumentException("Could not process $uri") - - val shouldSwapSizes = ExifInterface(contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")).let { - val orientation = it.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) - orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270 - } - - return ImageContentReader.ImageContent( - height = if (shouldSwapSizes) options.outWidth else options.outHeight, - width = if (shouldSwapSizes) options.outHeight else options.outWidth, - size = fileSize, - mimeType = options.outMimeType, - fileName = androidUri.lastPathSegment ?: "file", - ) +private fun ErrorTracker.engine(): app.dapk.engine.core.extensions.ErrorTracker { + val tracker = this + return object : app.dapk.engine.core.extensions.ErrorTracker { + override fun track(throwable: Throwable, extra: String) = tracker.track(throwable, extra) } - - override fun inputStream(uri: String): InputStream = contentResolver.openInputStream(Uri.parse(uri))!! } -internal class SmallTalkDeviceNameGenerator : DeviceDisplayNameGenerator { - override fun generate(): String { - val randomIdentifier = (('A'..'Z') + ('a'..'z') + ('0'..'9')).shuffled().take(4).joinToString("") - return "SmallTalk Android ($randomIdentifier)" +private fun Preferences.engine(): app.dapk.engine.core.Preferences { + val prefs = this + return object : app.dapk.engine.core.Preferences { + override suspend fun store(key: String, value: String) = prefs.store(key, value) + override suspend fun readString(key: String) = prefs.readString(key) + override suspend fun clear() = prefs.clear() + override suspend fun remove(key: String) = prefs.remove(key) } } \ No newline at end of file diff --git a/app/src/main/kotlin/app/dapk/st/graph/AndroidBase64.kt b/app/src/main/kotlin/app/dapk/st/impl/AndroidBase64.kt similarity index 73% rename from app/src/main/kotlin/app/dapk/st/graph/AndroidBase64.kt rename to app/src/main/kotlin/app/dapk/st/impl/AndroidBase64.kt index dad547f..936a666 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AndroidBase64.kt +++ b/app/src/main/kotlin/app/dapk/st/impl/AndroidBase64.kt @@ -1,8 +1,8 @@ -package app.dapk.st.graph +package app.dapk.st.impl -import app.dapk.st.core.Base64 +import app.dapk.engine.core.Base64 -class AndroidBase64 : Base64 { +internal class AndroidBase64 : Base64 { override fun encode(input: ByteArray): String { return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT) } diff --git a/app/src/main/kotlin/app/dapk/st/impl/AndroidImageContentReader.kt b/app/src/main/kotlin/app/dapk/st/impl/AndroidImageContentReader.kt new file mode 100644 index 0000000..e555455 --- /dev/null +++ b/app/src/main/kotlin/app/dapk/st/impl/AndroidImageContentReader.kt @@ -0,0 +1,40 @@ +package app.dapk.st.impl + +import android.content.ContentResolver +import android.graphics.BitmapFactory +import android.media.ExifInterface +import android.net.Uri +import android.provider.OpenableColumns +import app.dapk.st.engine.ImageContentReader +import java.io.InputStream + +internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader { + override fun meta(uri: String): ImageContentReader.ImageContent { + val androidUri = Uri.parse(uri) + val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri") + + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeStream(fileStream, null, options) + + val fileSize = contentResolver.query(androidUri, null, null, null, null)?.use { cursor -> + cursor.moveToFirst() + val columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.getLong(columnIndex) + } ?: throw IllegalArgumentException("Could not process $uri") + + val shouldSwapSizes = ExifInterface(contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")).let { + val orientation = it.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) + orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270 + } + + return ImageContentReader.ImageContent( + height = if (shouldSwapSizes) options.outWidth else options.outHeight, + width = if (shouldSwapSizes) options.outHeight else options.outWidth, + size = fileSize, + mimeType = options.outMimeType, + fileName = androidUri.lastPathSegment ?: "file", + ) + } + + override fun inputStream(uri: String): InputStream = contentResolver.openInputStream(Uri.parse(uri))!! +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt b/app/src/main/kotlin/app/dapk/st/impl/AppTaskRunner.kt similarity index 98% rename from app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt rename to app/src/main/kotlin/app/dapk/st/impl/AppTaskRunner.kt index c2c6890..eb2b757 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt +++ b/app/src/main/kotlin/app/dapk/st/impl/AppTaskRunner.kt @@ -1,4 +1,4 @@ -package app.dapk.st.graph +package app.dapk.st.impl import app.dapk.st.engine.ChatEngine import app.dapk.st.push.PushTokenPayload diff --git a/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt b/app/src/main/kotlin/app/dapk/st/impl/BackgroundWorkAdapter.kt similarity index 83% rename from app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt rename to app/src/main/kotlin/app/dapk/st/impl/BackgroundWorkAdapter.kt index 4e9de87..c0aeee3 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt +++ b/app/src/main/kotlin/app/dapk/st/impl/BackgroundWorkAdapter.kt @@ -1,6 +1,6 @@ -package app.dapk.st.graph +package app.dapk.st.impl -import app.dapk.st.matrix.message.BackgroundScheduler +import app.dapk.st.engine.BackgroundScheduler import app.dapk.st.work.WorkScheduler class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : BackgroundScheduler { diff --git a/app/src/main/kotlin/app/dapk/st/graph/DefaultDatabaseDropper.kt b/app/src/main/kotlin/app/dapk/st/impl/DefaultDatabaseDropper.kt similarity index 97% rename from app/src/main/kotlin/app/dapk/st/graph/DefaultDatabaseDropper.kt rename to app/src/main/kotlin/app/dapk/st/impl/DefaultDatabaseDropper.kt index 6966bfe..fb59081 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/DefaultDatabaseDropper.kt +++ b/app/src/main/kotlin/app/dapk/st/impl/DefaultDatabaseDropper.kt @@ -1,4 +1,4 @@ -package app.dapk.st.graph +package app.dapk.st.impl import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.withIoContext diff --git a/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt b/app/src/main/kotlin/app/dapk/st/impl/SharedPreferencesDelegate.kt similarity index 97% rename from app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt rename to app/src/main/kotlin/app/dapk/st/impl/SharedPreferencesDelegate.kt index 2fb5fe4..fecf33e 100644 --- a/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt +++ b/app/src/main/kotlin/app/dapk/st/impl/SharedPreferencesDelegate.kt @@ -1,4 +1,4 @@ -package app.dapk.st +package app.dapk.st.impl import android.content.Context import app.dapk.st.core.CoroutineDispatchers diff --git a/app/src/main/kotlin/app/dapk/st/impl/SmallTalkDeviceNameGenerator.kt b/app/src/main/kotlin/app/dapk/st/impl/SmallTalkDeviceNameGenerator.kt new file mode 100644 index 0000000..245782f --- /dev/null +++ b/app/src/main/kotlin/app/dapk/st/impl/SmallTalkDeviceNameGenerator.kt @@ -0,0 +1,10 @@ +package app.dapk.st.impl + +import app.dapk.st.engine.DeviceDisplayNameGenerator + +internal class SmallTalkDeviceNameGenerator : DeviceDisplayNameGenerator { + override fun generate(): String { + val randomIdentifier = (('A'..'Z') + ('a'..'z') + ('0'..'9')).shuffled().take(4).joinToString("") + return "SmallTalk Android ($randomIdentifier)" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt b/app/src/main/kotlin/app/dapk/st/impl/TaskRunnerAdapter.kt similarity index 97% rename from app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt rename to app/src/main/kotlin/app/dapk/st/impl/TaskRunnerAdapter.kt index e914fb4..21c8f0b 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt +++ b/app/src/main/kotlin/app/dapk/st/impl/TaskRunnerAdapter.kt @@ -1,4 +1,4 @@ -package app.dapk.st.graph +package app.dapk.st.impl import app.dapk.st.engine.ChatEngine import app.dapk.st.engine.ChatEngineTask diff --git a/chat-engine/build.gradle b/chat-engine/build.gradle deleted file mode 100644 index beab4c8..0000000 --- a/chat-engine/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'kotlin' - id 'java-test-fixtures' -} - -dependencies { - api Dependencies.mavenCentral.kotlinCoroutinesCore - api project(":matrix:common") - - kotlinFixtures(it) - testFixturesImplementation(testFixtures(project(":matrix:common"))) - testFixturesImplementation(testFixtures(project(":core"))) -} \ No newline at end of file diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt deleted file mode 100644 index 564560d..0000000 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ /dev/null @@ -1,84 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.JsonString -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import kotlinx.coroutines.flow.Flow -import java.io.InputStream - -interface ChatEngine : TaskRunner { - - fun directory(): Flow - fun invites(): Flow - fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow - - fun notificationsMessages(): Flow - fun notificationsInvites(): Flow - - suspend fun login(request: LoginRequest): LoginResult - - suspend fun me(forceRefresh: Boolean): Me - - suspend fun InputStream.importRoomKeys(password: String): Flow - - suspend fun send(message: SendMessage, room: RoomOverview) - - suspend fun registerPushToken(token: String, gatewayUrl: String) - - suspend fun joinRoom(roomId: RoomId) - - suspend fun rejectJoinRoom(roomId: RoomId) - - suspend fun findMembersSummary(roomId: RoomId): List - - fun mediaDecrypter(): MediaDecrypter - - fun pushHandler(): PushHandler - - suspend fun muteRoom(roomId: RoomId) - suspend fun unmuteRoom(roomId: RoomId) -} - -interface TaskRunner { - - suspend fun runTask(task: ChatEngineTask): TaskResult - - sealed interface TaskResult { - object Success : TaskResult - data class Failure(val canRetry: Boolean) : TaskResult - } - -} - - -data class ChatEngineTask(val type: String, val jsonPayload: String) - -interface MediaDecrypter { - - fun decrypt(input: InputStream, k: String, iv: String): Collector - - fun interface Collector { - fun collect(partial: (ByteArray) -> Unit) - } - -} - -interface PushHandler { - fun onNewToken(payload: JsonString) - fun onMessageReceived(eventId: EventId?, roomId: RoomId?) -} - -typealias UnreadNotifications = Pair>, NotificationDiff> - -data class NotificationDiff( - val unchanged: Map>, - val changedOrNew: Map>, - val removed: Map>, - val newRooms: Set -) - -data class InviteNotification( - val content: String, - val roomId: RoomId -) \ No newline at end of file diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt deleted file mode 100644 index d253b33..0000000 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt +++ /dev/null @@ -1,233 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.* -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -typealias DirectoryState = List -typealias OverviewState = List -typealias InviteState = List - -data class DirectoryItem( - val overview: RoomOverview, - val unreadCount: UnreadCount, - val typing: Typing?, - val isMuted: Boolean, -) - -data class RoomOverview( - val roomId: RoomId, - val roomCreationUtc: Long, - val roomName: String?, - val roomAvatarUrl: AvatarUrl?, - val lastMessage: LastMessage?, - val isGroup: Boolean, - val readMarker: EventId?, - val isEncrypted: Boolean, -) { - - data class LastMessage( - val content: String, - val utcTimestamp: Long, - val author: RoomMember, - ) - -} - -data class RoomInvite( - val from: RoomMember, - val roomId: RoomId, - val inviteMeta: InviteMeta, -) { - sealed class InviteMeta { - object DirectMessage : InviteMeta() - data class Room(val roomName: String? = null) : InviteMeta() - } - -} - -@JvmInline -value class UnreadCount(val value: Int) - -data class Typing(val roomId: RoomId, val members: List) - -data class LoginRequest(val userName: String, val password: String, val serverUrl: String?) - -sealed interface LoginResult { - data class Success(val userCredentials: UserCredentials) : LoginResult - object MissingWellKnown : LoginResult - data class Error(val cause: Throwable) : LoginResult -} - -data class Me( - val userId: UserId, - val displayName: String?, - val avatarUrl: AvatarUrl?, - val homeServerUrl: HomeServerUrl, -) - -sealed interface ImportResult { - data class Success(val roomIds: Set, val totalImportedKeysCount: Long) : ImportResult - data class Error(val cause: Type) : ImportResult { - - sealed interface Type { - data class Unknown(val cause: Throwable) : Type - object NoKeysFound : Type - object UnexpectedDecryptionOutput : Type - object UnableToOpenFile : Type - object InvalidFile : Type - } - - } - - data class Update(val importedKeysCount: Long) : ImportResult -} - -data class MessengerPageState( - val self: UserId, - val roomState: RoomState, - val typing: Typing?, - val isMuted: Boolean, -) - -data class RoomState( - val roomOverview: RoomOverview, - val events: List, -) - -internal val DEFAULT_ZONE = ZoneId.systemDefault() -internal val MESSAGE_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm") - -sealed class RoomEvent { - - abstract val eventId: EventId - abstract val utcTimestamp: Long - abstract val author: RoomMember - abstract val meta: MessageMeta - abstract val edited: Boolean - - val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } - - data class Encrypted( - override val eventId: EventId, - override val utcTimestamp: Long, - override val author: RoomMember, - override val meta: MessageMeta, - ) : RoomEvent() { - - override val edited: Boolean = false - - } - - data class Redacted( - override val eventId: EventId, - override val utcTimestamp: Long, - override val author: RoomMember, - ) : RoomEvent() { - override val edited: Boolean = false - override val meta: MessageMeta = MessageMeta.FromServer - } - - data class Message( - override val eventId: EventId, - override val utcTimestamp: Long, - val content: RichText, - override val author: RoomMember, - override val meta: MessageMeta, - override val edited: Boolean = false, - ) : RoomEvent() - - data class Reply( - val message: RoomEvent, - val replyingTo: RoomEvent, - ) : RoomEvent() { - - override val eventId: EventId = message.eventId - override val utcTimestamp: Long = message.utcTimestamp - override val author: RoomMember = message.author - override val meta: MessageMeta = message.meta - override val edited: Boolean = message.edited - - val replyingToSelf = replyingTo.author == message.author - } - - data class Image( - override val eventId: EventId, - override val utcTimestamp: Long, - val imageMeta: ImageMeta, - override val author: RoomMember, - override val meta: MessageMeta, - override val edited: Boolean = false, - ) : RoomEvent() { - - data class ImageMeta( - val width: Int?, - val height: Int?, - val url: String, - val keys: Keys?, - ) { - - data class Keys( - val k: String, - val iv: String, - val v: String, - val hashes: Map, - ) - - } - } - -} - -sealed class MessageMeta { - - object FromServer : MessageMeta() - - data class LocalEcho( - val echoId: String, - val state: State - ) : MessageMeta() { - - sealed class State { - object Sending : State() - - object Sent : State() - - data class Error( - val message: String, - val type: Type, - ) : State() { - - enum class Type { - UNKNOWN - } - } - } - } -} - -sealed interface SendMessage { - - data class TextMessage( - val content: String, - val reply: Reply? = null, - ) : SendMessage { - - data class Reply( - val author: RoomMember, - val originalMessage: String, - val eventId: EventId, - val timestampUtc: Long, - ) - } - - data class ImageMessage( - val uri: String, - ) : SendMessage - -} \ No newline at end of file diff --git a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt deleted file mode 100644 index 9486318..0000000 --- a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt +++ /dev/null @@ -1,21 +0,0 @@ -package fake - -import app.dapk.st.engine.ChatEngine -import app.dapk.st.matrix.common.RoomId -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import test.delegateEmit -import test.delegateReturn -import java.io.InputStream - -class FakeChatEngine : ChatEngine by mockk() { - - fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn() - fun givenDirectory() = every { directory() }.delegateReturn() - fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn() - fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit() - fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit() - fun givenInvites() = every { invites() }.delegateEmit() - fun givenMe(forceRefresh: Boolean) = coEvery { me(forceRefresh) }.delegateReturn() -} \ No newline at end of file diff --git a/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt b/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt deleted file mode 100644 index 5bf9444..0000000 --- a/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt +++ /dev/null @@ -1,77 +0,0 @@ -package fixture - -import app.dapk.st.engine.* -import app.dapk.st.matrix.common.* - -fun aMessengerState( - self: UserId = aUserId(), - roomState: RoomState, - typing: Typing? = null, - isMuted: Boolean = false, -) = MessengerPageState(self, roomState, typing, isMuted) - -fun aRoomOverview( - roomId: RoomId = aRoomId(), - roomCreationUtc: Long = 0L, - roomName: String? = null, - roomAvatarUrl: AvatarUrl? = null, - lastMessage: RoomOverview.LastMessage? = null, - isGroup: Boolean = false, - readMarker: EventId? = null, - isEncrypted: Boolean = false, -) = RoomOverview(roomId, roomCreationUtc, roomName, roomAvatarUrl, lastMessage, isGroup, readMarker, isEncrypted) - -fun anEncryptedRoomMessageEvent( - eventId: EventId = anEventId(), - utcTimestamp: Long = 0L, - content: RichText = RichText.of("encrypted-content"), - author: RoomMember = aRoomMember(), - meta: MessageMeta = MessageMeta.FromServer, - edited: Boolean = false, -) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited) - -fun aRoomImageMessageEvent( - eventId: EventId = anEventId(), - utcTimestamp: Long = 0L, - content: RoomEvent.Image.ImageMeta = anImageMeta(), - author: RoomMember = aRoomMember(), - meta: MessageMeta = MessageMeta.FromServer, - edited: Boolean = false, -) = RoomEvent.Image(eventId, utcTimestamp, content, author, meta, edited) - -fun aRoomReplyMessageEvent( - message: RoomEvent = aRoomMessageEvent(), - replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")), -) = RoomEvent.Reply(message, replyingTo) - -fun aRoomMessageEvent( - eventId: EventId = anEventId(), - utcTimestamp: Long = 0L, - content: RichText = RichText.of("message-content"), - author: RoomMember = aRoomMember(), - meta: MessageMeta = MessageMeta.FromServer, - edited: Boolean = false, -) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited) - -fun anImageMeta( - width: Int? = 100, - height: Int? = 100, - url: String = "https://a-url.com", - keys: RoomEvent.Image.ImageMeta.Keys? = null -) = RoomEvent.Image.ImageMeta(width, height, url, keys) - -fun aRoomState( - roomOverview: RoomOverview = aRoomOverview(), - events: List = listOf(aRoomMessageEvent()), -) = RoomState(roomOverview, events) - -fun aRoomInvite( - from: RoomMember = aRoomMember(), - roomId: RoomId = aRoomId(), - inviteMeta: RoomInvite.InviteMeta = RoomInvite.InviteMeta.DirectMessage, -) = RoomInvite(from, roomId, inviteMeta) - -fun aTypingEvent( - roomId: RoomId = aRoomId(), - members: List = listOf(aRoomMember()) -) = Typing(roomId, members) \ No newline at end of file diff --git a/chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt b/chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt deleted file mode 100644 index bd50723..0000000 --- a/chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt +++ /dev/null @@ -1,16 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.engine.NotificationDiff - -object NotificationDiffFixtures { - - fun aNotificationDiff( - unchanged: Map> = emptyMap(), - changedOrNew: Map> = emptyMap(), - removed: Map> = emptyMap(), - newRooms: Set = emptySet(), - ) = NotificationDiff(unchanged, changedOrNew, removed, newRooms) - -} \ No newline at end of file diff --git a/core/src/main/kotlin/app/dapk/st/core/Base64.kt b/core/src/main/kotlin/app/dapk/st/core/Base64.kt deleted file mode 100644 index 3ad71f3..0000000 --- a/core/src/main/kotlin/app/dapk/st/core/Base64.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.dapk.st.core - -interface Base64 { - fun encode(input: ByteArray): String - fun decode(input: String): ByteArray -} \ No newline at end of file diff --git a/domains/android/push/build.gradle b/domains/android/push/build.gradle index e057b0f..fe22a03 100644 --- a/domains/android/push/build.gradle +++ b/domains/android/push/build.gradle @@ -2,9 +2,10 @@ applyAndroidLibraryModule(project) apply plugin: "org.jetbrains.kotlin.plugin.serialization" dependencies { + implementation "chat-engine:chat-engine" implementation project(':core') - implementation project(':domains:android:core') implementation project(':domains:store') + implementation project(':domains:android:core') firebase(it, "messaging") @@ -13,6 +14,5 @@ dependencies { kotlinTest(it) androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) } diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushHandler.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushHandler.kt index 5b7fa75..455a880 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushHandler.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushHandler.kt @@ -5,13 +5,13 @@ import app.dapk.st.matrix.common.RoomId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -interface PushHandler { - fun onNewToken(payload: PushTokenPayload) - fun onMessageReceived(eventId: EventId?, roomId: RoomId?) -} - @Serializable data class PushTokenPayload( @SerialName("token") val token: String, @SerialName("gateway_url") val gatewayUrl: String, -) \ No newline at end of file +) + +interface PushHandler { + fun onNewToken(payload: PushTokenPayload) + fun onMessageReceived(eventId: EventId?, roomId: RoomId?) +} \ No newline at end of file diff --git a/domains/firebase/messaging-noop/build.gradle b/domains/firebase/messaging-noop/build.gradle index 10bf518..6ed2375 100644 --- a/domains/firebase/messaging-noop/build.gradle +++ b/domains/firebase/messaging-noop/build.gradle @@ -2,5 +2,5 @@ applyAndroidLibraryModule(project) dependencies { implementation project(':core') - implementation project(':matrix:common') + implementation "chat-engine:chat-engine" } diff --git a/domains/firebase/messaging/build.gradle b/domains/firebase/messaging/build.gradle index 6622273..f768b98 100644 --- a/domains/firebase/messaging/build.gradle +++ b/domains/firebase/messaging/build.gradle @@ -3,7 +3,7 @@ applyAndroidLibraryModule(project) dependencies { implementation project(':core') implementation project(':domains:android:core') - implementation project(':matrix:common') + implementation "chat-engine:chat-engine" implementation platform('com.google.firebase:firebase-bom:29.0.3') implementation 'com.google.firebase:firebase-messaging' } diff --git a/domains/olm-stub/build.gradle b/domains/olm-stub/build.gradle deleted file mode 100644 index d946f5a..0000000 --- a/domains/olm-stub/build.gradle +++ /dev/null @@ -1,7 +0,0 @@ -plugins { - id 'kotlin' -} - -dependencies { - compileOnly 'org.json:json:20220924' -} \ No newline at end of file diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmAccount.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmAccount.java deleted file mode 100644 index 66008bb..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmAccount.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.matrix.olm; - -import java.io.Serializable; -import java.util.Map; - -public class OlmAccount implements Serializable { - public static final String JSON_KEY_ONE_TIME_KEY = "curve25519"; - public static final String JSON_KEY_IDENTITY_KEY = "curve25519"; - public static final String JSON_KEY_FINGER_PRINT_KEY = "ed25519"; - - public OlmAccount() throws OlmException { - throw new RuntimeException("stub"); - } - - long getOlmAccountId() { - throw new RuntimeException("stub"); - } - - public void releaseAccount() { - throw new RuntimeException("stub"); - } - - public boolean isReleased() { - throw new RuntimeException("stub"); - } - - public Map identityKeys() throws OlmException { - throw new RuntimeException("stub"); - } - - public long maxOneTimeKeys() { - throw new RuntimeException("stub"); - - } - - public void generateOneTimeKeys(int aNumberOfKeys) throws OlmException { - throw new RuntimeException("stub"); - } - - public Map> oneTimeKeys() throws OlmException { - throw new RuntimeException("stub"); - } - - public void removeOneTimeKeys(OlmSession aSession) throws OlmException { - throw new RuntimeException("stub"); - } - - public void markOneTimeKeysAsPublished() throws OlmException { - throw new RuntimeException("stub"); - } - - public String signMessage(String aMessage) throws OlmException { - throw new RuntimeException("stub"); - } - - protected byte[] serialize(byte[] aKey, StringBuffer aErrorMsg) { - throw new RuntimeException("stub"); - } - - protected void deserialize(byte[] aSerializedData, byte[] aKey) throws Exception { - throw new RuntimeException("stub"); - } - - public byte[] pickle(byte[] aKey, StringBuffer aErrorMsg) { - throw new RuntimeException("stub"); - - } - - public void unpickle(byte[] aSerializedData, byte[] aKey) throws Exception { - throw new RuntimeException("stub"); - } - - public void generateFallbackKey() throws OlmException { - throw new RuntimeException("stub"); - } - - public Map> fallbackKey() throws OlmException { - throw new RuntimeException("stub"); - } - - public void forgetFallbackKey() throws OlmException { - throw new RuntimeException("stub"); - } - -} diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmException.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmException.java deleted file mode 100644 index 9b693a7..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmException.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.matrix.olm; - -import java.io.IOException; - -public class OlmException extends IOException { - public static final int EXCEPTION_CODE_INIT_ACCOUNT_CREATION = 10; - public static final int EXCEPTION_CODE_ACCOUNT_SERIALIZATION = 100; - public static final int EXCEPTION_CODE_ACCOUNT_DESERIALIZATION = 101; - public static final int EXCEPTION_CODE_ACCOUNT_IDENTITY_KEYS = 102; - public static final int EXCEPTION_CODE_ACCOUNT_GENERATE_ONE_TIME_KEYS = 103; - public static final int EXCEPTION_CODE_ACCOUNT_ONE_TIME_KEYS = 104; - public static final int EXCEPTION_CODE_ACCOUNT_REMOVE_ONE_TIME_KEYS = 105; - public static final int EXCEPTION_CODE_ACCOUNT_MARK_ONE_KEYS_AS_PUBLISHED = 106; - public static final int EXCEPTION_CODE_ACCOUNT_SIGN_MESSAGE = 107; - public static final int EXCEPTION_CODE_ACCOUNT_GENERATE_FALLBACK_KEY = 108; - public static final int EXCEPTION_CODE_ACCOUNT_FALLBACK_KEY = 109; - public static final int EXCEPTION_CODE_ACCOUNT_FORGET_FALLBACK_KEY = 110; - public static final int EXCEPTION_CODE_CREATE_INBOUND_GROUP_SESSION = 200; - public static final int EXCEPTION_CODE_INIT_INBOUND_GROUP_SESSION = 201; - public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_IDENTIFIER = 202; - public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_DECRYPT_SESSION = 203; - public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_FIRST_KNOWN_INDEX = 204; - public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_IS_VERIFIED = 205; - public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_EXPORT = 206; - public static final int EXCEPTION_CODE_CREATE_OUTBOUND_GROUP_SESSION = 300; - public static final int EXCEPTION_CODE_INIT_OUTBOUND_GROUP_SESSION = 301; - public static final int EXCEPTION_CODE_OUTBOUND_GROUP_SESSION_IDENTIFIER = 302; - public static final int EXCEPTION_CODE_OUTBOUND_GROUP_SESSION_KEY = 303; - public static final int EXCEPTION_CODE_OUTBOUND_GROUP_ENCRYPT_MESSAGE = 304; - public static final int EXCEPTION_CODE_INIT_SESSION_CREATION = 400; - public static final int EXCEPTION_CODE_SESSION_INIT_OUTBOUND_SESSION = 401; - public static final int EXCEPTION_CODE_SESSION_INIT_INBOUND_SESSION = 402; - public static final int EXCEPTION_CODE_SESSION_INIT_INBOUND_SESSION_FROM = 403; - public static final int EXCEPTION_CODE_SESSION_ENCRYPT_MESSAGE = 404; - public static final int EXCEPTION_CODE_SESSION_DECRYPT_MESSAGE = 405; - public static final int EXCEPTION_CODE_SESSION_SESSION_IDENTIFIER = 406; - public static final int EXCEPTION_CODE_UTILITY_CREATION = 500; - public static final int EXCEPTION_CODE_UTILITY_VERIFY_SIGNATURE = 501; - public static final int EXCEPTION_CODE_PK_ENCRYPTION_CREATION = 600; - public static final int EXCEPTION_CODE_PK_ENCRYPTION_SET_RECIPIENT_KEY = 601; - public static final int EXCEPTION_CODE_PK_ENCRYPTION_ENCRYPT = 602; - public static final int EXCEPTION_CODE_PK_DECRYPTION_CREATION = 700; - public static final int EXCEPTION_CODE_PK_DECRYPTION_GENERATE_KEY = 701; - public static final int EXCEPTION_CODE_PK_DECRYPTION_DECRYPT = 702; - public static final int EXCEPTION_CODE_PK_DECRYPTION_SET_PRIVATE_KEY = 703; - public static final int EXCEPTION_CODE_PK_DECRYPTION_PRIVATE_KEY = 704; - public static final int EXCEPTION_CODE_PK_SIGNING_CREATION = 800; - public static final int EXCEPTION_CODE_PK_SIGNING_GENERATE_SEED = 801; - public static final int EXCEPTION_CODE_PK_SIGNING_INIT_WITH_SEED = 802; - public static final int EXCEPTION_CODE_PK_SIGNING_SIGN = 803; - public static final int EXCEPTION_CODE_SAS_CREATION = 900; - public static final int EXCEPTION_CODE_SAS_ERROR = 901; - public static final int EXCEPTION_CODE_SAS_MISSING_THEIR_PKEY = 902; - public static final int EXCEPTION_CODE_SAS_GENERATE_SHORT_CODE = 903; - public static final String EXCEPTION_MSG_INVALID_PARAMS_DESERIALIZATION = "invalid de-serialized parameters"; - private final int mCode; - private final String mMessage; - - public OlmException(int aExceptionCode, String aExceptionMessage) { - throw new RuntimeException("stub"); - } - - public int getExceptionCode() { - throw new RuntimeException("stub"); - } - - public String getMessage() { - throw new RuntimeException("stub"); - } -} diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmInboundGroupSession.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmInboundGroupSession.java deleted file mode 100644 index fc1c969..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmInboundGroupSession.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.matrix.olm; - -import java.io.Serializable; - -public class OlmInboundGroupSession implements Serializable { - - public OlmInboundGroupSession(String aSessionKey) throws OlmException { - throw new RuntimeException("stub"); - } - - public static OlmInboundGroupSession importSession(String exported) throws OlmException { - throw new RuntimeException("stub"); - } - - public void releaseSession() { - throw new RuntimeException("stub"); - } - - public boolean isReleased() { - throw new RuntimeException("stub"); - } - - public String sessionIdentifier() throws OlmException { - throw new RuntimeException("stub"); - } - - public long getFirstKnownIndex() throws OlmException { - throw new RuntimeException("stub"); - } - - public boolean isVerified() throws OlmException { - throw new RuntimeException("stub"); - } - - public String export(long messageIndex) throws OlmException { - throw new RuntimeException("stub"); - } - - public OlmInboundGroupSession.DecryptMessageResult decryptMessage(String aEncryptedMsg) throws OlmException { - throw new RuntimeException("stub"); - } - - protected byte[] serialize(byte[] aKey, StringBuffer aErrorMsg) { - throw new RuntimeException("stub"); - } - - protected void deserialize(byte[] aSerializedData, byte[] aKey) throws Exception { - throw new RuntimeException("stub"); - } - - public static class DecryptMessageResult { - public String mDecryptedMessage; - public long mIndex; - - public DecryptMessageResult() { - throw new RuntimeException("stub"); - } - } -} diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmManager.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmManager.java deleted file mode 100644 index 30a3676..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmManager.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.matrix.olm; - -public class OlmManager { - public OlmManager() { - throw new RuntimeException("stub"); - } - - public String getOlmLibVersion() { - throw new RuntimeException("stub"); - } - - public native String getOlmLibVersionJni(); - -} diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmMessage.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmMessage.java deleted file mode 100644 index e95dc17..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmMessage.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.matrix.olm; - -public class OlmMessage { - public static final int MESSAGE_TYPE_PRE_KEY = 0; - public static final int MESSAGE_TYPE_MESSAGE = 1; - public String mCipherText; - public long mType; - - public OlmMessage() { - throw new RuntimeException("stub"); - } -} diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmOutboundGroupSession.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmOutboundGroupSession.java deleted file mode 100644 index 05e4986..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmOutboundGroupSession.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.matrix.olm; - -import java.io.Serializable; - -public class OlmOutboundGroupSession implements Serializable { - - public OlmOutboundGroupSession() throws OlmException { - throw new RuntimeException("stub"); - } - - public void releaseSession() { - throw new RuntimeException("stub"); - } - - public boolean isReleased() { - throw new RuntimeException("stub"); - } - - public String sessionIdentifier() throws OlmException { - throw new RuntimeException("stub"); - } - - public int messageIndex() { - throw new RuntimeException("stub"); - } - - public String sessionKey() throws OlmException { - throw new RuntimeException("stub"); - } - - public String encryptMessage(String aClearMsg) throws OlmException { - throw new RuntimeException("stub"); - } - - protected byte[] serialize(byte[] aKey, StringBuffer aErrorMsg) { - throw new RuntimeException("stub"); - } - - protected void deserialize(byte[] aSerializedData, byte[] aKey) throws Exception { - throw new RuntimeException("stub"); - } - -} diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmSAS.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmSAS.java deleted file mode 100644 index b2404e1..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmSAS.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.matrix.olm; - -public class OlmSAS { - - public OlmSAS() throws OlmException { - throw new RuntimeException("stub"); - } - - public String getPublicKey() throws OlmException { - throw new RuntimeException("stub"); - } - - public void setTheirPublicKey(String otherPkey) throws OlmException { - throw new RuntimeException("stub"); - } - - public byte[] generateShortCode(String info, int byteNumber) throws OlmException { - throw new RuntimeException("stub"); - } - - public String calculateMac(String message, String info) throws OlmException { - throw new RuntimeException("stub"); - } - - public String calculateMacLongKdf(String message, String info) throws OlmException { - throw new RuntimeException("stub"); - } - - public void releaseSas() { - throw new RuntimeException("stub"); - } -} diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmSession.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmSession.java deleted file mode 100644 index 4200e30..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmSession.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.matrix.olm; - -import java.io.Serializable; - -public class OlmSession implements Serializable { - - public OlmSession() throws OlmException { - throw new RuntimeException("stub"); - } - - long getOlmSessionId() { - throw new RuntimeException("stub"); - } - - public void releaseSession() { - throw new RuntimeException("stub"); - } - - public boolean isReleased() { - throw new RuntimeException("stub"); - } - - public void initOutboundSession(OlmAccount aAccount, String aTheirIdentityKey, String aTheirOneTimeKey) throws OlmException { - throw new RuntimeException("stub"); - } - - public void initInboundSession(OlmAccount aAccount, String aPreKeyMsg) throws OlmException { - throw new RuntimeException("stub"); - } - - public void initInboundSessionFrom(OlmAccount aAccount, String aTheirIdentityKey, String aPreKeyMsg) throws OlmException { - throw new RuntimeException("stub"); - } - - public String sessionIdentifier() throws OlmException { - throw new RuntimeException("stub"); - } - - public boolean matchesInboundSession(String aOneTimeKeyMsg) { - throw new RuntimeException("stub"); - } - - public boolean matchesInboundSessionFrom(String aTheirIdentityKey, String aOneTimeKeyMsg) { - throw new RuntimeException("stub"); - } - - public OlmMessage encryptMessage(String aClearMsg) throws OlmException { - throw new RuntimeException("stub"); - } - - public String decryptMessage(OlmMessage aEncryptedMsg) throws OlmException { - throw new RuntimeException("stub"); - } - - protected byte[] serialize(byte[] aKey, StringBuffer aErrorMsg) { - throw new RuntimeException("stub"); - - } - - protected void deserialize(byte[] aSerializedData, byte[] aKey) throws Exception { - throw new RuntimeException("stub"); - } -} diff --git a/domains/olm-stub/src/main/java/org/matrix/olm/OlmUtility.java b/domains/olm-stub/src/main/java/org/matrix/olm/OlmUtility.java deleted file mode 100644 index e15d2c7..0000000 --- a/domains/olm-stub/src/main/java/org/matrix/olm/OlmUtility.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.matrix.olm; - -import org.json.JSONObject; - -import java.util.Map; - -public class OlmUtility { - public static final int RANDOM_KEY_SIZE = 32; - - public OlmUtility() throws OlmException { - throw new RuntimeException("stub"); - } - - public void releaseUtility() { - throw new RuntimeException("stub"); - } - - public void verifyEd25519Signature(String aSignature, String aFingerprintKey, String aMessage) throws OlmException { - throw new RuntimeException("stub"); - } - - public String sha256(String aMessageToHash) { - throw new RuntimeException("stub"); - } - - public static byte[] getRandomKey() { - throw new RuntimeException("stub"); - } - - public boolean isReleased() { - throw new RuntimeException("stub"); - } - - public static Map toStringMap(JSONObject jsonObject) { - throw new RuntimeException("stub"); - } - - public static Map> toStringMapMap(JSONObject jsonObject) { - throw new RuntimeException("stub"); - } -} diff --git a/domains/olm/build.gradle b/domains/olm/build.gradle deleted file mode 100644 index ca4ba3e..0000000 --- a/domains/olm/build.gradle +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - id 'kotlin' - id 'org.jetbrains.kotlin.plugin.serialization' -} - -dependencies { - implementation Dependencies.mavenCentral.kotlinSerializationJson - implementation Dependencies.mavenCentral.kotlinCoroutinesCore - - implementation project(":core") - implementation project(":domains:store") - implementation project(":matrix:services:crypto") - implementation project(":matrix:services:device") - compileOnly project(":domains:olm-stub") -} \ No newline at end of file diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/DefaultSasSession.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/DefaultSasSession.kt deleted file mode 100644 index 8a90246..0000000 --- a/domains/olm/src/main/kotlin/app/dapk/st/olm/DefaultSasSession.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.dapk.st.olm - -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.Ed25519 -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.crypto.Olm -import org.matrix.olm.OlmSAS -import org.matrix.olm.OlmUtility - -internal class DefaultSasSession(private val selfFingerprint: Ed25519) : Olm.SasSession { - - private val olmSAS = OlmSAS() - - override fun publicKey(): String { - return olmSAS.publicKey - } - - override suspend fun generateCommitment(hash: String, startJsonString: String): String { - val utility = OlmUtility() - return utility.sha256(olmSAS.publicKey + startJsonString).also { - utility.releaseUtility() - } - } - - override suspend fun calculateMac( - selfUserId: UserId, - selfDeviceId: DeviceId, - otherUserId: UserId, - otherDeviceId: DeviceId, - transactionId: String - ): Olm.MacResult { - val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + - selfUserId.value + - selfDeviceId.value + - otherUserId.value + - otherDeviceId.value + - transactionId - val deviceKeyId = "ed25519:${selfDeviceId.value}" - val macMap = mapOf( - deviceKeyId to olmSAS.calculateMac(selfFingerprint.value, baseInfo + deviceKeyId) - ) - val keys = olmSAS.calculateMac(macMap.keys.sorted().joinToString(separator = ","), baseInfo + "KEY_IDS") - return Olm.MacResult(macMap, keys) - } - - override fun setTheirPublicKey(key: String) { - olmSAS.setTheirPublicKey(key) - } - - override fun release() { - olmSAS.releaseSas() - } -} \ No newline at end of file diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/DeviceKeyFactory.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/DeviceKeyFactory.kt deleted file mode 100644 index 202154e..0000000 --- a/domains/olm/src/main/kotlin/app/dapk/st/olm/DeviceKeyFactory.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.dapk.st.olm - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.common.extensions.toJsonString -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.device.internal.DeviceKeys -import org.matrix.olm.OlmAccount - -class DeviceKeyFactory( - private val jsonCanonicalizer: JsonCanonicalizer, -) { - - fun create(userId: UserId, deviceId: DeviceId, identityKey: Ed25519, senderKey: Curve25519, olmAccount: OlmAccount): DeviceKeys { - val signable = mapOf( - "device_id" to deviceId.value, - "user_id" to userId.value, - "algorithms" to listOf(Olm.ALGORITHM_MEGOLM.value, Olm.ALGORITHM_OLM.value), - "keys" to mapOf( - "curve25519:${deviceId.value}" to senderKey.value, - "ed25519:${deviceId.value}" to identityKey.value, - ) - ).toJsonString() - - return DeviceKeys( - userId, - deviceId, - algorithms = listOf(Olm.ALGORITHM_MEGOLM, Olm.ALGORITHM_OLM), - keys = mapOf( - "curve25519:${deviceId.value}" to senderKey.value, - "ed25519:${deviceId.value}" to identityKey.value, - ), - signatures = mapOf( - userId.value to mapOf( - "ed25519:${deviceId.value}" to olmAccount.signMessage(jsonCanonicalizer.canonicalize(signable)) - ) - ) - ) - } -} \ No newline at end of file diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmExtensions.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmExtensions.kt deleted file mode 100644 index 9a47f49..0000000 --- a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmExtensions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.dapk.st.olm - -import app.dapk.st.matrix.common.Curve25519 -import app.dapk.st.matrix.common.Ed25519 -import org.matrix.olm.OlmAccount - -fun OlmAccount.readIdentityKeys(): Pair { - val identityKeys = this.identityKeys() - return Ed25519(identityKeys["ed25519"]!!) to Curve25519(identityKeys["curve25519"]!!) -} - -fun OlmAccount.oneTimeCurveKeys(): List> { - return this.oneTimeKeys()["curve25519"]?.map { it.key to Curve25519(it.value) } ?: emptyList() -} \ No newline at end of file diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt deleted file mode 100644 index 1299244..0000000 --- a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt +++ /dev/null @@ -1,74 +0,0 @@ -package app.dapk.st.olm - -import app.dapk.st.core.Base64 -import app.dapk.st.domain.OlmPersistence -import app.dapk.st.domain.SerializedObject -import app.dapk.st.matrix.common.Curve25519 -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.SessionId -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmInboundGroupSession -import org.matrix.olm.OlmOutboundGroupSession -import org.matrix.olm.OlmSession -import java.io.* - -class OlmPersistenceWrapper( - private val olmPersistence: OlmPersistence, - private val base64: Base64, -) : OlmStore { - - override suspend fun read(): OlmAccount? { - return olmPersistence.read()?.deserialize() - } - - override suspend fun persist(olmAccount: OlmAccount) { - olmPersistence.persist(SerializedObject(olmAccount.serialize())) - } - - override suspend fun readOutbound(roomId: RoomId): Pair? { - return olmPersistence.readOutbound(roomId)?.let { - it.first to it.second.deserialize() - } - } - - override suspend fun persistOutbound(roomId: RoomId, creationTimestampUtc: Long, outboundGroupSession: OlmOutboundGroupSession) { - olmPersistence.persistOutbound(roomId, creationTimestampUtc, SerializedObject(outboundGroupSession.serialize())) - } - - override suspend fun persistSession(identity: Curve25519, sessionId: SessionId, olmSession: OlmSession) { - olmPersistence.persistSession(identity, sessionId, SerializedObject(olmSession.serialize())) - } - - override suspend fun readSessions(identities: List): List>? { - return olmPersistence.readSessions(identities)?.map { it.first to it.second.deserialize() } - } - - override suspend fun persist(sessionId: SessionId, inboundGroupSession: OlmInboundGroupSession) { - olmPersistence.persist(sessionId, SerializedObject(inboundGroupSession.serialize())) - } - - override suspend fun transaction(action: suspend () -> Unit) { - olmPersistence.startTransaction { action() } - } - - override suspend fun readInbound(sessionId: SessionId): OlmInboundGroupSession? { - return olmPersistence.readInbound(sessionId)?.value?.deserialize() - } - - private fun T.serialize(): String { - val baos = ByteArrayOutputStream() - ObjectOutputStream(baos).use { - it.writeObject(this) - } - return base64.encode(baos.toByteArray()) - } - - @Suppress("UNCHECKED_CAST") - private fun String.deserialize(): T { - val decoded = base64.decode(this) - val baos = ByteArrayInputStream(decoded) - return ObjectInputStream(baos).use { - it.readObject() as T - } - } -} diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmStore.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmStore.kt deleted file mode 100644 index 2f22237..0000000 --- a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmStore.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.dapk.st.olm - -import app.dapk.st.matrix.common.Curve25519 -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.SessionId -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmInboundGroupSession -import org.matrix.olm.OlmOutboundGroupSession -import org.matrix.olm.OlmSession - -interface OlmStore { - suspend fun read(): OlmAccount? - suspend fun persist(olmAccount: OlmAccount) - - suspend fun transaction(action: suspend () -> Unit) - suspend fun readOutbound(roomId: RoomId): Pair? - suspend fun persistOutbound(roomId: RoomId, creationTimestampUtc: Long, outboundGroupSession: OlmOutboundGroupSession) - suspend fun persistSession(identity: Curve25519, sessionId: SessionId, olmSession: OlmSession) - suspend fun readSessions(identities: List): List>? - suspend fun persist(sessionId: SessionId, inboundGroupSession: OlmInboundGroupSession) - suspend fun readInbound(sessionId: SessionId): OlmInboundGroupSession? -} \ No newline at end of file diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmWrapper.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmWrapper.kt deleted file mode 100644 index 50c6548..0000000 --- a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmWrapper.kt +++ /dev/null @@ -1,389 +0,0 @@ -package app.dapk.st.olm - -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.SingletonFlows -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.core.extensions.ifNull -import app.dapk.st.core.withIoContext -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.common.MatrixLogTag.CRYPTO -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.crypto.Olm.* -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.internal.DeviceKeys -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.matrix.olm.* -import java.time.Clock - -private const val SEVEN_DAYS_MILLIS = 604800000 -private const val MEGOLM_ROTATION_MESSAGE_COUNT = 100 -private const val INIT_OLM = "init-olm" - -class OlmWrapper( - private val olmStore: OlmStore, - private val singletonFlows: SingletonFlows, - private val jsonCanonicalizer: JsonCanonicalizer, - private val deviceKeyFactory: DeviceKeyFactory, - private val errorTracker: ErrorTracker, - private val logger: MatrixLogger, - private val clock: Clock, - coroutineDispatchers: CoroutineDispatchers -) : Olm { - - init { - coroutineDispatchers.global.launch { - coroutineDispatchers.withIoContext { - singletonFlows.getOrPut(INIT_OLM) { - OlmManager() - }.collect() - } - } - } - - override suspend fun import(keys: List) { - interactWithOlm() - - olmStore.transaction { - keys.forEach { - val inBound = when (it.isExported) { - true -> OlmInboundGroupSession.importSession(it.sessionKey) - false -> OlmInboundGroupSession(it.sessionKey) - } - olmStore.persist(it.sessionId, inBound) - } - } - } - - override suspend fun ensureAccountCrypto(deviceCredentials: DeviceCredentials, onCreate: suspend (AccountCryptoSession) -> Unit): AccountCryptoSession { - interactWithOlm() - return singletonFlows.getOrPut("account-crypto") { - accountCrypto(deviceCredentials) ?: createAccountCrypto(deviceCredentials, onCreate) - }.first() - } - - private suspend fun accountCrypto(deviceCredentials: DeviceCredentials): AccountCryptoSession? { - return olmStore.read()?.let { olmAccount -> - createAccountCryptoSession(deviceCredentials, olmAccount, isNew = false) - } - } - - override suspend fun AccountCryptoSession.generateOneTimeKeys( - count: Int, - credentials: DeviceCredentials, - publishKeys: suspend (DeviceService.OneTimeKeys) -> Unit - ) { - interactWithOlm() - val olmAccount = this.olmAccount as OlmAccount - olmAccount.generateOneTimeKeys(count) - - val oneTimeKeys = DeviceService.OneTimeKeys(olmAccount.oneTimeCurveKeys().map { (key, value) -> - DeviceService.OneTimeKeys.Key.SignedCurve( - keyId = key, - value = value.value, - signature = DeviceService.OneTimeKeys.Key.SignedCurve.Ed25519Signature( - value = value.value.toSignedJson(olmAccount), - deviceId = credentials.deviceId, - userId = credentials.userId, - ) - ) - }) - publishKeys(oneTimeKeys) - olmAccount.markOneTimeKeysAsPublished() - updateAccountInstance(olmAccount) - } - - private suspend fun createAccountCrypto(deviceCredentials: DeviceCredentials, action: suspend (AccountCryptoSession) -> Unit): AccountCryptoSession { - val olmAccount = OlmAccount() - return createAccountCryptoSession(deviceCredentials, olmAccount, isNew = true).also { - action(it) - olmStore.persist(olmAccount) - } - } - - private fun createAccountCryptoSession(credentials: DeviceCredentials, olmAccount: OlmAccount, isNew: Boolean): AccountCryptoSession { - val (identityKey, senderKey) = olmAccount.readIdentityKeys() - return AccountCryptoSession( - fingerprint = identityKey, - senderKey = senderKey, - deviceKeys = deviceKeyFactory.create(credentials.userId, credentials.deviceId, identityKey, senderKey, olmAccount), - olmAccount = olmAccount, - maxKeys = olmAccount.maxOneTimeKeys().toInt(), - hasKeys = !isNew, - ) - } - - override suspend fun ensureRoomCrypto( - roomId: RoomId, - accountSession: AccountCryptoSession, - ): RoomCryptoSession { - interactWithOlm() - return singletonFlows.getOrPut("room-${roomId.value}") { - roomCrypto(roomId, accountSession) ?: createRoomCrypto(roomId, accountSession) - } - .first() - .maybeRotateRoomSession(roomId, accountSession) - } - - private suspend fun RoomCryptoSession.maybeRotateRoomSession(roomId: RoomId, accountSession: AccountCryptoSession): RoomCryptoSession { - val now = clock.millis() - return when { - this.messageIndex > MEGOLM_ROTATION_MESSAGE_COUNT || (now - this.creationTimestampUtc) > SEVEN_DAYS_MILLIS -> { - logger.matrixLog(CRYPTO, "rotating megolm for room ${roomId.value}") - createRoomCrypto(roomId, accountSession).also { rotatedSession -> - singletonFlows.update("room-${roomId.value}", rotatedSession) - } - } - - else -> this - } - } - - private suspend fun roomCrypto(roomId: RoomId, accountCryptoSession: AccountCryptoSession): RoomCryptoSession? { - return olmStore.readOutbound(roomId)?.let { (timestampUtc, outBound) -> - RoomCryptoSession( - creationTimestampUtc = timestampUtc, - key = outBound.sessionKey(), - messageIndex = outBound.messageIndex(), - accountCryptoSession = accountCryptoSession, - id = SessionId(outBound.sessionIdentifier()), - outBound = outBound - ) - } - } - - private suspend fun createRoomCrypto(roomId: RoomId, accountCryptoSession: AccountCryptoSession): RoomCryptoSession { - val outBound = OlmOutboundGroupSession() - val roomCryptoSession = RoomCryptoSession( - creationTimestampUtc = clock.millis(), - key = outBound.sessionKey(), - messageIndex = outBound.messageIndex(), - accountCryptoSession = accountCryptoSession, - id = SessionId(outBound.sessionIdentifier()), - outBound = outBound - ) - olmStore.persistOutbound(roomId, roomCryptoSession.creationTimestampUtc, outBound) - - val inBound = OlmInboundGroupSession(roomCryptoSession.key) - olmStore.persist(roomCryptoSession.id, inBound) - - logger.crypto("Creating megolm: ${roomCryptoSession.id}") - - return roomCryptoSession - } - - override suspend fun ensureDeviceCrypto(input: OlmSessionInput, olmAccount: AccountCryptoSession): DeviceCryptoSession { - interactWithOlm() - return deviceCrypto(input) ?: createDeviceCrypto(olmAccount, input) - } - - private suspend fun deviceCrypto(input: OlmSessionInput): DeviceCryptoSession? { - return olmStore.readSessions(listOf(input.identity))?.let { - DeviceCryptoSession( - input.deviceId, input.userId, input.identity, input.fingerprint, it.map { it.second } - ) - } - } - - private suspend fun createDeviceCrypto(accountCryptoSession: AccountCryptoSession, input: OlmSessionInput): DeviceCryptoSession { - val olmSession = OlmSession() - olmSession.initOutboundSession(accountCryptoSession.olmAccount as OlmAccount, input.identity.value, input.oneTimeKey) - val sessionId = SessionId(olmSession.sessionIdentifier()) - logger.crypto("creating olm session: $sessionId ${input.identity} ${input.userId} ${input.deviceId}") - olmStore.persistSession(input.identity, sessionId, olmSession) - return DeviceCryptoSession(input.deviceId, input.userId, input.identity, input.fingerprint, listOf(olmSession)) - } - - @Suppress("UNCHECKED_CAST") - override suspend fun DeviceCryptoSession.encrypt(messageJson: JsonString): EncryptionResult { - interactWithOlm() - val olmSession = this.olmSession as List - - logger.crypto("encrypting with session(s) ${olmSession.size}") - - val (result, session) = olmSession.firstNotNullOf { - kotlin.runCatching { - it.encryptMessage(jsonCanonicalizer.canonicalize(messageJson)) to it - }.getOrNull() - } - - logger.crypto("encrypt flow identity: ${this.identity}") - olmStore.persistSession(this.identity, SessionId(session.sessionIdentifier()), session) - return EncryptionResult( - cipherText = CipherText(result.mCipherText), - type = result.mType, - ) - } - - override suspend fun RoomCryptoSession.encrypt(roomId: RoomId, messageJson: JsonString): CipherText { - interactWithOlm() - val messagePayloadString = jsonCanonicalizer.canonicalize(messageJson) - val outBound = this.outBound as OlmOutboundGroupSession - val encryptedMessage = CipherText(outBound.encryptMessage(messagePayloadString)) - singletonFlows.update( - "room-${roomId.value}", - this.copy(outBound = outBound, messageIndex = outBound.messageIndex()) - ) - - olmStore.persistOutbound(roomId, this.creationTimestampUtc, outBound) - return encryptedMessage - } - - private fun String.toSignedJson(olmAccount: OlmAccount): SignedJson { - val json = JsonString(Json.encodeToString(mapOf("key" to this))) - return SignedJson(olmAccount.signMessage(jsonCanonicalizer.canonicalize(json))) - } - - override suspend fun decryptOlm(olmAccount: AccountCryptoSession, senderKey: Curve25519, type: Int, body: CipherText): DecryptionResult { - interactWithOlm() - val olmMessage = OlmMessage().apply { - this.mType = type.toLong() - this.mCipherText = body.value - } - - val readSession = olmStore.readSessions(listOf(senderKey)).let { - if (it == null) { - logger.crypto("no olm session found for $senderKey, creating a new one") - listOf(senderKey to OlmSession()) - } else { - logger.crypto("found olm session(s) ${it.size}") - it.forEach { - logger.crypto("${it.first} ${it.second.sessionIdentifier()}") - } - it - } - } - val errors = mutableListOf() - - return readSession.firstNotNullOfOrNull { (_, session) -> - kotlin.runCatching { - when (type) { - OlmMessage.MESSAGE_TYPE_PRE_KEY -> { - if (session.matchesInboundSession(body.value)) { - logger.matrixLog(CRYPTO, "matched inbound session, attempting decrypt") - session.decryptMessage(olmMessage)?.let { JsonString(it) } - } else { - logger.matrixLog(CRYPTO, "prekey has no inbound session, doing alternative flow") - val account = olmAccount.olmAccount as OlmAccount - - val session = OlmSession() - session.initInboundSessionFrom(account, senderKey.value, body.value) - account.removeOneTimeKeys(session) - olmAccount.updateAccountInstance(account) - session.decryptMessage(olmMessage)?.let { JsonString(it) }?.also { - logger.crypto("alt flow identity: $senderKey : ${session.sessionIdentifier()}") - olmStore.persistSession(senderKey, SessionId(session.sessionIdentifier()), session) - }.also { - session.releaseSession() - } - } - } - - OlmMessage.MESSAGE_TYPE_MESSAGE -> { - logger.crypto("decrypting olm message type") - session.decryptMessage(olmMessage)?.let { JsonString(it) } - } - - else -> throw IllegalArgumentException("Unknown message type: $type") - } - }.onFailure { - errors.add(it) - logger.crypto("error code: ${(it as? OlmException)?.exceptionCode}") - errorTracker.track(it, "failed to decrypt olm") - }.getOrNull()?.let { DecryptionResult.Success(it, isVerified = false) } - }.ifNull { - logger.matrixLog(CRYPTO, "failed to decrypt olm session") - DecryptionResult.Failed(errors.joinToString { it.message ?: "N/A" }) - }.also { - readSession.forEach { it.second.releaseSession() } - } - } - - private suspend fun AccountCryptoSession.updateAccountInstance(olmAccount: OlmAccount) { - singletonFlows.update("account-crypto", this.copy(olmAccount = olmAccount, hasKeys = true)) - olmStore.persist(olmAccount) - } - - override suspend fun decryptMegOlm(sessionId: SessionId, cipherText: CipherText): DecryptionResult { - interactWithOlm() - return when (val megolmSession = olmStore.readInbound(sessionId)) { - null -> DecryptionResult.Failed("no megolm session found for id: $sessionId") - else -> { - runCatching { - JsonString(megolmSession.decryptMessage(cipherText.value).mDecryptedMessage).also { - olmStore.persist(sessionId, megolmSession) - } - }.fold( - onSuccess = { DecryptionResult.Success(it, isVerified = false) }, - onFailure = { - errorTracker.track(it) - DecryptionResult.Failed(it.message ?: "Unknown") - } - ).also { - megolmSession.releaseSession() - } - } - } - } - - override suspend fun verifyExternalUser(keys: Ed25519?, recipeientKeys: Ed25519?): Boolean { - return false - } - - private suspend fun interactWithOlm() = singletonFlows.get(INIT_OLM).first() - - override suspend fun olmSessions(devices: List, onMissing: suspend (List) -> List): List { - interactWithOlm() - - val inputByIdentity = devices.groupBy { it.keys().first } - val inputByKeys = devices.associateBy { it.keys() } - - val inputs = inputByKeys.map { (keys, deviceKeys) -> - val (identity, fingerprint) = keys - Olm.OlmSessionInput(oneTimeKey = "ignored", identity = identity, deviceKeys.deviceId, deviceKeys.userId, fingerprint) - } - - val requestedIdentities = inputs.map { it.identity } - val foundSessions = olmStore.readSessions(requestedIdentities) ?: emptyList() - val foundSessionsByIdentity = foundSessions.groupBy { it.first } - - val foundSessionIdentities = foundSessions.map { it.first } - val missingIdentities = requestedIdentities - foundSessionIdentities.toSet() - - val newOlmSessions = if (missingIdentities.isNotEmpty()) { - onMissing(missingIdentities.map { inputByIdentity[it]!! }.flatten()) - } else emptyList() - - return (inputs.filterNot { missingIdentities.contains(it.identity) }.map { - val olmSession = foundSessionsByIdentity[it.identity]!!.map { it.second } - - logger.crypto("found ${olmSession.size} olm session(s) for ${it.identity}") - olmSession.forEach { - logger.crypto(it.sessionIdentifier()) - } - - DeviceCryptoSession( - deviceId = it.deviceId, - userId = it.userId, - identity = it.identity, - fingerprint = it.fingerprint, - olmSession = olmSession - ) - }) + newOlmSessions - } - - override suspend fun sasSession(deviceCredentials: DeviceCredentials): SasSession { - val account = ensureAccountCrypto(deviceCredentials, onCreate = {}) - return DefaultSasSession(account.fingerprint) - } -} - - -private fun DeviceKeys.keys(): Pair { - val identity = Curve25519(this.keys.filter { it.key.startsWith("curve25519:") }.values.first()) - val fingerprint = Ed25519(this.keys.filter { it.key.startsWith("ed25519:") }.values.first()) - return identity to fingerprint -} diff --git a/domains/store/build.gradle b/domains/store/build.gradle index 90b79fa..8abe3e9 100644 --- a/domains/store/build.gradle +++ b/domains/store/build.gradle @@ -6,20 +6,15 @@ plugins { } sqldelight { - DapkDb { - packageName = "app.dapk.db" + StDb { + packageName = "app.dapk.db.app" } linkSqlite = true } dependencies { - api project(":matrix:common") - implementation project(":matrix:services:sync") - implementation project(":matrix:services:message") - implementation project(":matrix:services:profile") - implementation project(":matrix:services:device") - implementation project(":matrix:services:room") implementation project(":core") + implementation "chat-engine:chat-engine" implementation Dependencies.mavenCentral.kotlinSerializationJson implementation Dependencies.mavenCentral.kotlinCoroutinesCore implementation "com.squareup.sqldelight:coroutines-extensions:1.5.4" diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/CredentialsPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/CredentialsPreferences.kt deleted file mode 100644 index 3bab1e8..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/CredentialsPreferences.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.dapk.st.domain - -import app.dapk.st.core.Preferences -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.UserCredentials - -internal class CredentialsPreferences( - private val preferences: Preferences, -) : CredentialsStore { - - override suspend fun credentials(): UserCredentials? { - return preferences.readString("credentials")?.let { json -> - with(UserCredentials) { json.fromJson() } - } - } - - override suspend fun update(credentials: UserCredentials) { - val json = with(UserCredentials) { credentials.toJson() } - preferences.store("credentials", json) - } - - override suspend fun clear() { - preferences.clear() - } -} diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/DevicePersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/DevicePersistence.kt deleted file mode 100644 index 123f9b7..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/DevicePersistence.kt +++ /dev/null @@ -1,99 +0,0 @@ -package app.dapk.st.domain - -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.withIoContext -import app.dapk.db.DapkDb -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.SessionId -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.device.KnownDeviceStore -import app.dapk.st.matrix.device.internal.DeviceKeys -import kotlinx.serialization.json.Json - -class DevicePersistence( - private val database: DapkDb, - private val devicesCache: KnownDevicesCache, - private val dispatchers: CoroutineDispatchers, -) : KnownDeviceStore { - - override suspend fun associateSession(sessionId: SessionId, deviceIds: List) { - dispatchers.withIoContext { - database.deviceQueries.transaction { - deviceIds.forEach { - database.deviceQueries.insertDeviceToMegolmSession( - device_id = it.value, - session_id = sessionId.value - ) - } - } - } - } - - override suspend fun markOutdated(userIds: List) { - devicesCache.updateOutdated(userIds) - database.deviceQueries.markOutdated(userIds.map { it.value }) - } - - override suspend fun maybeConsumeOutdated(userIds: List): List { - return devicesCache.consumeOutdated(userIds).also { - database.deviceQueries.markIndate(userIds.map { it.value }) - } - } - - override suspend fun updateDevices(devices: Map>): List { - devicesCache.putAll(devices) - database.deviceQueries.transaction { - devices.forEach { (userId, innerMap) -> - innerMap.forEach { (deviceId, keys) -> - database.deviceQueries.insertDevice( - user_id = userId.value, - device_id = deviceId.value, - blob = Json.encodeToString(DeviceKeys.serializer(), keys), - ) - } - } - } - return devicesCache.devices() - } - - override suspend fun devicesMegolmSession(userIds: List, sessionId: SessionId): List { - return database.deviceQueries.selectUserDevicesWithSessions(userIds.map { it.value }, sessionId.value).executeAsList().map { - Json.decodeFromString(DeviceKeys.serializer(), it.blob) - } - } - - override suspend fun device(userId: UserId, deviceId: DeviceId): DeviceKeys? { - return devicesCache.device(userId, deviceId) ?: database.deviceQueries.selectDevice(deviceId.value).executeAsOneOrNull()?.let { - Json.decodeFromString(DeviceKeys.serializer(), it) - }?.also { devicesCache.putAll(mapOf(userId to mapOf(deviceId to it))) } - } -} - -class KnownDevicesCache( - private val devicesCache: Map> = mutableMapOf(), - private var outdatedUserIds: MutableSet = mutableSetOf() -) { - - fun consumeOutdated(userIds: List): List { - val outdatedToConsume = outdatedUserIds.filter { userIds.contains(it) } -// val unknownIds = userIds.filter { devicesCache[it] == null } - outdatedUserIds = (outdatedUserIds - outdatedToConsume.toSet()).toMutableSet() - return outdatedToConsume - } - - fun updateOutdated(userIds: List) { - outdatedUserIds.addAll(userIds) - } - - fun putAll(devices: Map>) { - devices.mapValues { it.value.toMutableMap() } - } - - fun devices(): List { - return devicesCache.values.map { it.values }.flatten() - } - - fun device(userId: UserId, deviceId: DeviceId): DeviceKeys? { - return devicesCache[userId]?.get(deviceId) - } -} \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/FilterPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/FilterPreferences.kt deleted file mode 100644 index ddbba29..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/FilterPreferences.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.dapk.st.domain - -import app.dapk.st.core.Preferences -import app.dapk.st.matrix.sync.FilterStore - -internal class FilterPreferences( - private val preferences: Preferences -) : FilterStore { - - override suspend fun store(key: String, filterId: String) { - preferences.store(key, filterId) - } - - override suspend fun read(key: String): String? { - return preferences.readString(key) - } -} \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt deleted file mode 100644 index 4b3b20e..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt +++ /dev/null @@ -1,46 +0,0 @@ -package app.dapk.st.domain - -import app.dapk.db.DapkDb -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.withIoContext -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.room.MemberStore -import kotlinx.serialization.json.Json - -class MemberPersistence( - private val database: DapkDb, - private val coroutineDispatchers: CoroutineDispatchers, -) : MemberStore { - - override suspend fun insert(roomId: RoomId, members: List) { - coroutineDispatchers.withIoContext { - database.roomMemberQueries.transaction { - members.forEach { - database.roomMemberQueries.insert( - user_id = it.id.value, - room_id = roomId.value, - blob = Json.encodeToString(RoomMember.serializer(), it), - ) - } - } - } - } - - override suspend fun query(roomId: RoomId, userIds: List): List { - return coroutineDispatchers.withIoContext { - database.roomMemberQueries.selectMembersByRoomAndId(roomId.value, userIds.map { it.value }) - .executeAsList() - .map { Json.decodeFromString(RoomMember.serializer(), it) } - } - } - - override suspend fun query(roomId: RoomId, limit: Int): List { - return coroutineDispatchers.withIoContext { - database.roomMemberQueries.selectMembersByRoom(roomId.value, limit.toLong()) - .executeAsList() - .map { Json.decodeFromString(RoomMember.serializer(), it) } - } - } -} \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/OlmPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/OlmPersistence.kt deleted file mode 100644 index df3a63c..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/OlmPersistence.kt +++ /dev/null @@ -1,119 +0,0 @@ -package app.dapk.st.domain - -import app.dapk.db.DapkDb -import app.dapk.db.model.DbCryptoAccount -import app.dapk.db.model.DbCryptoMegolmInbound -import app.dapk.db.model.DbCryptoMegolmOutbound -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.withIoContext -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.Curve25519 -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.SessionId -import com.squareup.sqldelight.TransactionWithoutReturn -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -class OlmPersistence( - private val database: DapkDb, - private val credentialsStore: CredentialsStore, - private val dispatchers: CoroutineDispatchers, -) { - - suspend fun read(): String? { - return dispatchers.withIoContext { - database.cryptoQueries - .selectAccount(credentialsStore.credentials()!!.userId.value) - .executeAsOneOrNull() - } - } - - suspend fun persist(olmAccount: SerializedObject) { - dispatchers.withIoContext { - database.cryptoQueries.insertAccount( - DbCryptoAccount( - user_id = credentialsStore.credentials()!!.userId.value, - blob = olmAccount.value - ) - ) - } - } - - suspend fun readOutbound(roomId: RoomId): Pair? { - return dispatchers.withIoContext { - database.cryptoQueries - .selectMegolmOutbound(roomId.value) - .executeAsOneOrNull()?.let { - it.utcEpochMillis to it.blob - } - } - } - - suspend fun persistOutbound(roomId: RoomId, creationTimestampUtc: Long, outboundGroupSession: SerializedObject) { - dispatchers.withIoContext { - database.cryptoQueries.insertMegolmOutbound( - DbCryptoMegolmOutbound( - room_id = roomId.value, - blob = outboundGroupSession.value, - utcEpochMillis = creationTimestampUtc, - ) - ) - } - } - - suspend fun persistSession(identity: Curve25519, sessionId: SessionId, olmSession: SerializedObject) { - withContext(dispatchers.io) { - database.cryptoQueries.insertOlmSession( - identity_key = identity.value, - session_id = sessionId.value, - blob = olmSession.value, - ) - } - } - - suspend fun readSessions(identities: List): List>? { - return withContext(dispatchers.io) { - database.cryptoQueries - .selectOlmSession(identities.map { it.value }) - .executeAsList() - .map { Curve25519(it.identity_key) to it.blob } - .takeIf { it.isNotEmpty() } - } - } - - suspend fun startTransaction(action: suspend TransactionWithoutReturn.() -> Unit) { - val transaction = suspendCoroutine { continuation -> - database.cryptoQueries.transaction { - continuation.resume(this) - } - } - action(transaction) - } - - suspend fun persist(sessionId: SessionId, inboundGroupSession: SerializedObject) { - withContext(dispatchers.io) { - database.cryptoQueries.insertMegolmInbound( - DbCryptoMegolmInbound( - session_id = sessionId.value, - blob = inboundGroupSession.value - ) - ) - } - } - - suspend fun readInbound(sessionId: SessionId): SerializedObject? { - return withContext(dispatchers.io) { - database.cryptoQueries - .selectMegolmInbound(sessionId.value) - .executeAsOneOrNull() - ?.let { SerializedObject((it)) } - } - } - -} - -@JvmInline -value class SerializedObject(val value: String) \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt index 076693b..4cc6a4d 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt @@ -1,56 +1,23 @@ package app.dapk.st.domain -import app.dapk.db.DapkDb +import app.dapk.db.app.StDb import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.Preferences -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.domain.application.eventlog.EventLogPersistence import app.dapk.st.domain.application.eventlog.LoggingStore import app.dapk.st.domain.application.message.MessageOptionsStore -import app.dapk.st.domain.localecho.LocalEchoPersistence import app.dapk.st.domain.preference.CachingPreferences import app.dapk.st.domain.preference.PropertyCache -import app.dapk.st.domain.profile.ProfilePersistence import app.dapk.st.domain.push.PushTokenRegistrarPreferences -import app.dapk.st.domain.room.MutedStorePersistence -import app.dapk.st.domain.sync.OverviewPersistence -import app.dapk.st.domain.sync.RoomPersistence -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.message.LocalEchoStore -import app.dapk.st.matrix.room.MemberStore -import app.dapk.st.matrix.room.ProfileStore -import app.dapk.st.matrix.sync.FilterStore -import app.dapk.st.matrix.sync.OverviewStore -import app.dapk.st.matrix.sync.RoomStore -import app.dapk.st.matrix.sync.SyncStore class StoreModule( - private val database: DapkDb, + private val database: StDb, private val databaseDropper: DatabaseDropper, val preferences: Preferences, - private val credentialPreferences: Preferences, - private val errorTracker: ErrorTracker, + val credentialPreferences: Preferences, private val coroutineDispatchers: CoroutineDispatchers, ) { - private val muteableStore by unsafeLazy { MutedStorePersistence(database, coroutineDispatchers) } - - fun overviewStore(): OverviewStore = OverviewPersistence(database, coroutineDispatchers) - fun roomStore(): RoomStore { - return RoomPersistence( - database = database, - overviewPersistence = OverviewPersistence(database, coroutineDispatchers), - coroutineDispatchers = coroutineDispatchers, - muteableStore = muteableStore, - ) - } - - fun credentialsStore(): CredentialsStore = CredentialsPreferences(credentialPreferences) - fun syncStore(): SyncStore = SyncTokenPreferences(preferences) - fun filterStore(): FilterStore = FilterPreferences(preferences) - val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) } - private val cache = PropertyCache() val cachingPreferences = CachingPreferences(cache, preferences) @@ -58,11 +25,6 @@ class StoreModule( fun applicationStore() = ApplicationPreferences(preferences) - fun olmStore() = OlmPersistence(database, credentialsStore(), coroutineDispatchers) - fun knownDevicesStore() = DevicePersistence(database, KnownDevicesCache(), coroutineDispatchers) - - fun profileStore(): ProfileStore = ProfilePersistence(preferences) - fun cacheCleaner() = StoreCleaner { cleanCredentials -> if (cleanCredentials) { credentialPreferences.clear() @@ -79,8 +41,4 @@ class StoreModule( fun messageStore(): MessageOptionsStore = MessageOptionsStore(cachingPreferences) - fun memberStore(): MemberStore { - return MemberPersistence(database, coroutineDispatchers) - } - } diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt deleted file mode 100644 index 474df67..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.dapk.st.domain - -import app.dapk.st.core.Preferences -import app.dapk.st.matrix.common.SyncToken -import app.dapk.st.matrix.sync.SyncStore -import app.dapk.st.matrix.sync.SyncStore.SyncKey - -internal class SyncTokenPreferences( - private val preferences: Preferences -) : SyncStore { - - override suspend fun store(key: SyncKey, syncToken: SyncToken) { - preferences.store(key.value, syncToken.value) - } - - override suspend fun read(key: SyncKey): SyncToken? { - return preferences.readString(key.value)?.let { - SyncToken(it) - } - } - - override suspend fun remove(key: SyncKey) { - preferences.remove(key.value) - } -} diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/EventLogPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/EventLogPersistence.kt index 718e34a..023e417 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/EventLogPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/EventLogPersistence.kt @@ -1,6 +1,6 @@ package app.dapk.st.domain.application.eventlog -import app.dapk.db.DapkDb +import app.dapk.db.app.StDb import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.withIoContext import com.squareup.sqldelight.runtime.coroutines.asFlow @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class EventLogPersistence( - private val database: DapkDb, + private val database: StDb, private val coroutineDispatchers: CoroutineDispatchers, ) { diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt deleted file mode 100644 index 3ae9a1b..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt +++ /dev/null @@ -1,120 +0,0 @@ -package app.dapk.st.domain.localecho - -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.core.extensions.Scope -import app.dapk.db.DapkDb -import app.dapk.db.model.DbLocalEcho -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.message.LocalEchoStore -import app.dapk.st.matrix.message.MessageService -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.newSingleThreadContext -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json - -private typealias LocalEchoCache = Map> - -class LocalEchoPersistence( - private val errorTracker: ErrorTracker, - private val database: DapkDb, -) : LocalEchoStore { - - private val inMemoryEchos = MutableStateFlow(emptyMap()) - private val mirrorScope = Scope(newSingleThreadContext("local-echo-thread")) - - override suspend fun preload() { - withContext(Dispatchers.IO) { - val echos = database.localEchoQueries.selectAll().executeAsList().map { - Json.decodeFromString(MessageService.LocalEcho.serializer(), it.blob) - } - inMemoryEchos.value = echos.groupBy { - when (val message = it.message) { - is MessageService.Message.TextMessage -> message.roomId - is MessageService.Message.ImageMessage -> message.roomId - } - }.mapValues { - it.value.associateBy { - when (val message = it.message) { - is MessageService.Message.TextMessage -> message.localId - is MessageService.Message.ImageMessage -> message.localId - } - } - } - } - } - - override fun markSending(message: MessageService.Message) { - emitUpdate(MessageService.LocalEcho(eventId = null, message, state = MessageService.LocalEcho.State.Sending)) - } - - override suspend fun messageTransaction(message: MessageService.Message, action: suspend () -> EventId) { - emitUpdate(MessageService.LocalEcho(eventId = null, message, state = MessageService.LocalEcho.State.Sending)) - try { - val eventId = action.invoke() - emitUpdate(MessageService.LocalEcho(eventId = eventId, message, state = MessageService.LocalEcho.State.Sent)) - database.transaction { - when (message) { - is MessageService.Message.TextMessage -> database.localEchoQueries.delete(message.localId) - is MessageService.Message.ImageMessage -> database.localEchoQueries.delete(message.localId) - } - } - } catch (error: Exception) { - emitUpdate( - MessageService.LocalEcho( - eventId = null, - message, - state = MessageService.LocalEcho.State.Error(error.message ?: "", MessageService.LocalEcho.State.Error.Type.UNKNOWN) - ) - ) - errorTracker.track(error) - throw error - } - } - - private fun emitUpdate(localEcho: MessageService.LocalEcho) { - val newValue = inMemoryEchos.value.addEcho(localEcho) - inMemoryEchos.tryEmit(newValue) - - mirrorScope.launch { - when (val message = localEcho.message) { - is MessageService.Message.TextMessage -> database.localEchoQueries.insert( - DbLocalEcho( - message.localId, - message.roomId.value, - Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho) - ) - ) - - is MessageService.Message.ImageMessage -> database.localEchoQueries.insert( - DbLocalEcho( - message.localId, - message.roomId.value, - Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho) - ) - ) - } - } - } - - override fun observeLocalEchos(roomId: RoomId) = inMemoryEchos.map { - it[roomId]?.values?.toList() ?: emptyList() - } - - override fun observeLocalEchos() = inMemoryEchos.map { - it.mapValues { it.value.values.toList() } - } -} - -private fun LocalEchoCache.addEcho(localEcho: MessageService.LocalEcho): MutableMap> { - val newValue = this.toMutableMap() - val roomEchos = newValue.getOrPut(localEcho.roomId) { emptyMap() } - newValue[localEcho.roomId] = roomEchos.toMutableMap().also { it.update(localEcho) } - return newValue -} - -private fun MutableMap.update(localEcho: MessageService.LocalEcho) { - this[localEcho.localId] = localEcho -} diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/profile/ProfilePersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/profile/ProfilePersistence.kt deleted file mode 100644 index 29cd267..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/profile/ProfilePersistence.kt +++ /dev/null @@ -1,51 +0,0 @@ -package app.dapk.st.domain.profile - -import app.dapk.st.core.Preferences -import app.dapk.st.matrix.common.AvatarUrl -import app.dapk.st.matrix.common.HomeServerUrl -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.room.ProfileService -import app.dapk.st.matrix.room.ProfileStore -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json - -internal class ProfilePersistence( - private val preferences: Preferences, -) : ProfileStore { - - override suspend fun storeMe(me: ProfileService.Me) { - preferences.store( - "me", Json.encodeToString( - StoreMe.serializer(), StoreMe( - userId = me.userId, - displayName = me.displayName, - avatarUrl = me.avatarUrl, - homeServer = me.homeServerUrl, - ) - ) - ) - } - - override suspend fun readMe(): ProfileService.Me? { - return preferences.readString("me")?.let { - Json.decodeFromString(StoreMe.serializer(), it).let { - ProfileService.Me( - userId = it.userId, - displayName = it.displayName, - avatarUrl = it.avatarUrl, - homeServerUrl = it.homeServer - ) - } - } - } - -} - -@Serializable -private class StoreMe( - @SerialName("user_id") val userId: UserId, - @SerialName("display_name") val displayName: String?, - @SerialName("avatar_url") val avatarUrl: AvatarUrl?, - @SerialName("homeserver") val homeServer: HomeServerUrl, -) \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/room/MutedRoomsStore.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/room/MutedRoomsStore.kt deleted file mode 100644 index 4a45012..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/room/MutedRoomsStore.kt +++ /dev/null @@ -1,41 +0,0 @@ -package app.dapk.st.domain.room - -import app.dapk.db.DapkDb -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.withIoContext -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.MuteableStore -import com.squareup.sqldelight.runtime.coroutines.asFlow -import com.squareup.sqldelight.runtime.coroutines.mapToList -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map - -internal class MutedStorePersistence( - private val database: DapkDb, - private val coroutineDispatchers: CoroutineDispatchers, -) : MuteableStore { - - private val allMutedFlow = MutableSharedFlow>(replay = 1) - - override suspend fun mute(roomId: RoomId) { - coroutineDispatchers.withIoContext { - database.mutedRoomQueries.insertMuted(roomId.value) - } - } - - override suspend fun unmute(roomId: RoomId) { - coroutineDispatchers.withIoContext { - database.mutedRoomQueries.removeMuted(roomId.value) - } - } - - override suspend fun isMuted(roomId: RoomId) = allMutedFlow.firstOrNull()?.contains(roomId) ?: false - - override fun observeMuted(): Flow> = database.mutedRoomQueries.select() - .asFlow() - .mapToList() - .map { it.map { RoomId(it) }.toSet() } - -} \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt deleted file mode 100644 index b9bb5f8..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt +++ /dev/null @@ -1,99 +0,0 @@ -package app.dapk.st.domain.sync - -import app.dapk.db.DapkDb -import app.dapk.db.model.OverviewStateQueries -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.withIoContext -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.OverviewState -import app.dapk.st.matrix.sync.OverviewStore -import app.dapk.st.matrix.sync.RoomInvite -import app.dapk.st.matrix.sync.RoomOverview -import com.squareup.sqldelight.runtime.coroutines.asFlow -import com.squareup.sqldelight.runtime.coroutines.mapToList -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.serialization.json.Json - -private val json = Json - -internal class OverviewPersistence( - private val database: DapkDb, - private val dispatchers: CoroutineDispatchers, -) : OverviewStore { - - override fun latest(): Flow { - return database.overviewStateQueries.selectAll() - .asFlow() - .mapToList() - .map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } } - } - - override suspend fun removeRooms(roomsToRemove: List) { - dispatchers.withIoContext { - database.transaction { - roomsToRemove.forEach { - database.inviteStateQueries.remove(it.value) - database.overviewStateQueries.remove(it.value) - } - } - } - } - - override suspend fun persistInvites(invites: List) { - dispatchers.withIoContext { - database.inviteStateQueries.transaction { - invites.forEach { - database.inviteStateQueries.insert(it.roomId.value, json.encodeToString(RoomInvite.serializer(), it)) - } - } - } - } - - override fun latestInvites(): Flow> { - return database.inviteStateQueries.selectAll() - .asFlow() - .mapToList() - .map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } } - } - - override suspend fun removeInvites(invites: List) { - dispatchers.withIoContext { - database.inviteStateQueries.transaction { - invites.forEach { database.inviteStateQueries.remove(it.value) } - } - } - } - - override suspend fun persist(overviewState: OverviewState) { - dispatchers.withIoContext { - database.transaction { - overviewState.forEach { - database.overviewStateQueries.insertStateOverview(it) - } - } - } - } - - override suspend fun retrieve(): OverviewState { - return dispatchers.withIoContext { - val overviews = database.overviewStateQueries.selectAll().executeAsList() - overviews.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } - } - } - - internal fun retrieve(roomId: RoomId): RoomOverview? { - return database.overviewStateQueries.selectRoom(roomId.value).executeAsOneOrNull()?.let { - json.decodeFromString(RoomOverview.serializer(), it) - } - } -} - -private fun OverviewStateQueries.insertStateOverview(roomOverview: RoomOverview) { - this.insert( - room_id = roomOverview.roomId.value, - latest_activity_timestamp_utc = roomOverview.lastMessage?.utcTimestamp ?: roomOverview.roomCreationUtc, - blob = json.encodeToString(RoomOverview.serializer(), roomOverview), - read_marker = roomOverview.readMarker?.value - ) -} diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt deleted file mode 100644 index 0e97c7d..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt +++ /dev/null @@ -1,162 +0,0 @@ -package app.dapk.st.domain.sync - -import app.dapk.db.DapkDb -import app.dapk.db.model.RoomEventQueries -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.withIoContext -import app.dapk.st.domain.room.MutedStorePersistence -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.* -import com.squareup.sqldelight.Query -import com.squareup.sqldelight.runtime.coroutines.asFlow -import com.squareup.sqldelight.runtime.coroutines.mapToList -import com.squareup.sqldelight.runtime.coroutines.mapToOneNotNull -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.serialization.json.Json - -private val json = Json - -internal class RoomPersistence( - private val database: DapkDb, - private val overviewPersistence: OverviewPersistence, - private val coroutineDispatchers: CoroutineDispatchers, - private val muteableStore: MutedStorePersistence, -) : RoomStore, MuteableStore by muteableStore { - - override suspend fun persist(roomId: RoomId, events: List) { - coroutineDispatchers.withIoContext { - database.transaction { - events.forEach { - database.roomEventQueries.insertRoomEvent(roomId, it) - } - } - } - } - - override suspend fun remove(rooms: List) { - coroutineDispatchers.withIoContext { - database.roomEventQueries.transaction { - rooms.forEach { database.roomEventQueries.remove(it.value) } - } - } - } - - override suspend fun remove(eventId: EventId) { - coroutineDispatchers.withIoContext { - database.roomEventQueries.removeEvent(eventId.value) - } - } - - override fun latest(roomId: RoomId): Flow { - val overviewFlow = database.overviewStateQueries.selectRoom(roomId.value).asFlow().mapToOneNotNull().map { - json.decodeFromString(RoomOverview.serializer(), it) - }.distinctUntilChanged() - - return database.roomEventQueries.selectRoom(roomId.value) - .distinctFlowList() - .map { it.map { json.decodeFromString(RoomEvent.serializer(), it) } } - .combine(overviewFlow) { events, overview -> - RoomState(overview, events) - } - } - - override suspend fun retrieve(roomId: RoomId): RoomState? { - return coroutineDispatchers.withIoContext { - overviewPersistence.retrieve(roomId)?.let { overview -> - val roomEvents = database.roomEventQueries.selectRoom(roomId.value).executeAsList().map { - json.decodeFromString(RoomEvent.serializer(), it) - } - RoomState(overview, roomEvents) - } - } - } - - override suspend fun insertUnread(roomId: RoomId, eventIds: List) { - coroutineDispatchers.withIoContext { - database.transaction { - eventIds.forEach { eventId -> - database.unreadEventQueries.insertUnread( - event_id = eventId.value, - room_id = roomId.value, - ) - } - } - } - } - - override fun observeUnread(): Flow>> { - return database.roomEventQueries.selectAllUnread() - .distinctFlowList() - .map { - it.groupBy { RoomId(it.room_id) } - .mapKeys { overviewPersistence.retrieve(it.key)!! } - .mapValues { - it.value.map { - json.decodeFromString(RoomEvent.serializer(), it.blob) - } - } - } - } - - override fun observeUnreadCountById(): Flow> { - return database.roomEventQueries.selectAllUnread() - .asFlow() - .mapToList() - .map { - it.groupBy { RoomId(it.room_id) } - .mapValues { it.value.size } - } - } - - override fun observeNotMutedUnread(): Flow>> { - return database.roomEventQueries.selectNotMutedUnread() - .distinctFlowList() - .map { - it.groupBy { RoomId(it.room_id) } - .mapKeys { overviewPersistence.retrieve(it.key)!! } - .mapValues { - it.value.map { - json.decodeFromString(RoomEvent.serializer(), it.blob) - } - } - } - } - - private fun Query.distinctFlowList() = this.asFlow().mapToList().distinctUntilChanged() - - override suspend fun markRead(roomId: RoomId) { - coroutineDispatchers.withIoContext { - database.unreadEventQueries.removeRead(room_id = roomId.value) - } - } - - override fun observeEvent(eventId: EventId): Flow { - return database.roomEventQueries.selectEvent(event_id = eventId.value) - .asFlow() - .mapToOneNotNull() - .map { EventId(it) } - } - - override suspend fun findEvent(eventId: EventId): RoomEvent? { - return coroutineDispatchers.withIoContext { - database.roomEventQueries.selectEventContent(event_id = eventId.value) - .executeAsOneOrNull() - ?.let { json.decodeFromString(RoomEvent.serializer(), it) } - } - } -} - -private fun RoomEventQueries.insertRoomEvent(roomId: RoomId, roomEvent: RoomEvent) { - this.insert( - app.dapk.db.model.DbRoomEvent( - event_id = roomEvent.eventId.value, - room_id = roomId.value, - timestamp_utc = roomEvent.utcTimestamp, - blob = json.encodeToString(RoomEvent.serializer(), roomEvent), - ) - ) -} diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/Crypto.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/Crypto.sq deleted file mode 100644 index cba4742..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/Crypto.sq +++ /dev/null @@ -1,61 +0,0 @@ -CREATE TABLE dbCryptoAccount ( - user_id TEXT NOT NULL, - blob TEXT NOT NULL, - PRIMARY KEY (user_id) -); - -CREATE TABLE dbCryptoOlmSession ( - identity_key TEXT NOT NULL, - session_id TEXT NOT NULL, - blob TEXT NOT NULL, - PRIMARY KEY (identity_key, session_id) -); - -CREATE TABLE dbCryptoMegolmInbound ( - session_id TEXT NOT NULL, - blob TEXT NOT NULL, - PRIMARY KEY (session_id) -); - -CREATE TABLE dbCryptoMegolmOutbound ( - room_id TEXT NOT NULL, - utcEpochMillis INTEGER NOT NULL, - blob TEXT NOT NULL, - PRIMARY KEY (room_id) -); - -selectAccount: -SELECT blob -FROM dbCryptoAccount -WHERE user_id = ?; - -insertAccount: -INSERT OR REPLACE INTO dbCryptoAccount(user_id, blob) -VALUES ?; - -selectOlmSession: -SELECT blob, identity_key -FROM dbCryptoOlmSession -WHERE identity_key IN ?; - -insertOlmSession: -INSERT OR REPLACE INTO dbCryptoOlmSession(identity_key, session_id, blob) -VALUES (?, ?, ?); - -selectMegolmInbound: -SELECT blob -FROM dbCryptoMegolmInbound -WHERE session_id = ?; - -insertMegolmInbound: -INSERT OR REPLACE INTO dbCryptoMegolmInbound(session_id, blob) -VALUES ?; - -selectMegolmOutbound: -SELECT blob, utcEpochMillis -FROM dbCryptoMegolmOutbound -WHERE room_id = ?; - -insertMegolmOutbound: -INSERT OR REPLACE INTO dbCryptoMegolmOutbound(room_id, utcEpochMillis, blob) -VALUES ?; \ No newline at end of file diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/Device.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/Device.sq deleted file mode 100644 index 99156f8..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/Device.sq +++ /dev/null @@ -1,47 +0,0 @@ -CREATE TABLE dbDeviceKey ( - user_id TEXT NOT NULL, - device_id TEXT NOT NULL, - blob TEXT NOT NULL, - outdated INTEGER AS Int NOT NULL, - PRIMARY KEY (user_id, device_id) -); - -CREATE TABLE dbDeviceKeyToMegolmSession ( - device_id TEXT NOT NULL, - session_id TEXT NOT NULL, - PRIMARY KEY (device_id, session_id) -); - -selectUserDevicesWithSessions: -SELECT user_id, dbDeviceKey.device_id, blob -FROM dbDeviceKey -JOIN dbDeviceKeyToMegolmSession ON dbDeviceKeyToMegolmSession.device_id = dbDeviceKey.device_id -WHERE user_id IN ? AND dbDeviceKeyToMegolmSession.session_id = ?; - -selectDevice: -SELECT blob -FROM dbDeviceKey -WHERE device_id = ?; - -selectOutdatedUsers: -SELECT user_id -FROM dbDeviceKey -WHERE outdated = 1; - -insertDevice: -INSERT OR REPLACE INTO dbDeviceKey(user_id, device_id, blob, outdated) -VALUES (?, ?, ?, 0); - -markOutdated: -UPDATE dbDeviceKey -SET outdated = 1 -WHERE user_id IN ?; - -markIndate: -UPDATE dbDeviceKey -SET outdated = 0 -WHERE user_id IN ?; - -insertDeviceToMegolmSession: -INSERT OR REPLACE INTO dbDeviceKeyToMegolmSession(device_id, session_id) -VALUES (?, ?); diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq deleted file mode 100644 index d30ddbe..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE dbInviteState ( - room_id TEXT NOT NULL, - blob TEXT NOT NULL, - PRIMARY KEY (room_id) -); - -selectAll: -SELECT room_id, blob -FROM dbInviteState; - -insert: -INSERT OR REPLACE INTO dbInviteState(room_id, blob) -VALUES (?, ?); - -remove: -DELETE FROM dbInviteState -WHERE room_id = ?; \ No newline at end of file diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/LocalEcho.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/LocalEcho.sq deleted file mode 100644 index 1ecf0e4..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/LocalEcho.sq +++ /dev/null @@ -1,18 +0,0 @@ -CREATE TABLE IF NOT EXISTS dbLocalEcho ( - local_id TEXT NOT NULL, - room_id TEXT NOT NULL, - blob TEXT NOT NULL, - PRIMARY KEY (local_id) -); - -selectAll: -SELECT * -FROM dbLocalEcho; - -insert: -INSERT OR REPLACE INTO dbLocalEcho(local_id, room_id, blob) -VALUES ?; - -delete: -DELETE FROM dbLocalEcho -WHERE local_id = ?; diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/MutedRoom.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/MutedRoom.sq deleted file mode 100644 index 2054a20..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/MutedRoom.sq +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS dbMutedRoom ( - room_id TEXT NOT NULL, - PRIMARY KEY (room_id) -); - -insertMuted: -INSERT OR REPLACE INTO dbMutedRoom(room_id) -VALUES (?); - -removeMuted: -DELETE FROM dbMutedRoom -WHERE room_id = ?; - -select: -SELECT room_id -FROM dbMutedRoom; diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq deleted file mode 100644 index 100d397..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq +++ /dev/null @@ -1,25 +0,0 @@ -CREATE TABLE dbOverviewState ( - room_id TEXT NOT NULL, - latest_activity_timestamp_utc INTEGER NOT NULL, - read_marker TEXT, - blob TEXT NOT NULL, - PRIMARY KEY (room_id) -); - -selectAll: -SELECT * -FROM dbOverviewState -ORDER BY latest_activity_timestamp_utc DESC; - -selectRoom: -SELECT blob -FROM dbOverviewState -WHERE room_id = ?; - -insert: -INSERT OR REPLACE INTO dbOverviewState(room_id, latest_activity_timestamp_utc, read_marker, blob) -VALUES (?, ?, ?, ?); - -remove: -DELETE FROM dbOverviewState -WHERE room_id = ?; \ No newline at end of file diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq deleted file mode 100644 index 9fb7553..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq +++ /dev/null @@ -1,53 +0,0 @@ -CREATE TABLE IF NOT EXISTS dbRoomEvent ( - event_id TEXT NOT NULL, - room_id TEXT NOT NULL, - timestamp_utc INTEGER NOT NULL, - blob TEXT NOT NULL, - PRIMARY KEY (event_id) -); - -selectRoom: -SELECT blob -FROM dbRoomEvent -WHERE room_id = ? -ORDER BY timestamp_utc DESC -LIMIT 100; - -insert: -INSERT OR REPLACE INTO dbRoomEvent(event_id, room_id, timestamp_utc, blob) -VALUES ?; - -selectEvent: -SELECT event_id -FROM dbRoomEvent -WHERE event_id = ?; - -selectEventContent: -SELECT blob -FROM dbRoomEvent -WHERE event_id = ?; - -selectAllUnread: -SELECT dbRoomEvent.blob, dbRoomEvent.room_id -FROM dbUnreadEvent -INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id -ORDER BY dbRoomEvent.timestamp_utc DESC -LIMIT 100; - -selectNotMutedUnread: -SELECT dbRoomEvent.blob, dbRoomEvent.room_id -FROM dbUnreadEvent -INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id -LEFT OUTER JOIN dbMutedRoom - ON dbUnreadEvent.room_id = dbMutedRoom.room_id - WHERE dbMutedRoom.room_id IS NULL -ORDER BY dbRoomEvent.timestamp_utc DESC -LIMIT 100; - -remove: -DELETE FROM dbRoomEvent -WHERE room_id = ?; - -removeEvent: -DELETE FROM dbRoomEvent -WHERE event_id = ?; \ No newline at end of file diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomMember.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/RoomMember.sq deleted file mode 100644 index 4e1de82..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomMember.sq +++ /dev/null @@ -1,21 +0,0 @@ -CREATE TABLE dbRoomMember ( - user_id TEXT NOT NULL, - room_id TEXT NOT NULL, - blob TEXT NOT NULL, - PRIMARY KEY (user_id, room_id) -); - -selectMembersByRoomAndId: -SELECT blob -FROM dbRoomMember -WHERE room_id = ? AND user_id IN ?; - -selectMembersByRoom: -SELECT blob -FROM dbRoomMember -WHERE room_id = ? -LIMIT ?; - -insert: -INSERT OR REPLACE INTO dbRoomMember(user_id, room_id, blob) -VALUES (?, ?, ?); \ No newline at end of file diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/UnreadEvent.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/UnreadEvent.sq deleted file mode 100644 index 6ec0b58..0000000 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/UnreadEvent.sq +++ /dev/null @@ -1,18 +0,0 @@ -CREATE TABLE IF NOT EXISTS dbUnreadEvent ( - event_id TEXT NOT NULL, - room_id TEXT NOT NULL, - PRIMARY KEY (event_id) -); - -insertUnread: -INSERT OR REPLACE INTO dbUnreadEvent(event_id, room_id) -VALUES (?, ?); - -removeRead: -DELETE FROM dbUnreadEvent -WHERE room_id = ?; - -selectUnreadByRoom: -SELECT event_id -FROM dbUnreadEvent -WHERE room_id = ?; diff --git a/features/directory/build.gradle b/features/directory/build.gradle index 26115b8..ddf2e65 100644 --- a/features/directory/build.gradle +++ b/features/directory/build.gradle @@ -1,8 +1,8 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":chat-engine") implementation project(":domains:android:compose-core") + implementation "chat-engine:chat-engine" implementation 'screen-state:screen-android' implementation project(":features:messenger") implementation project(":core") @@ -12,9 +12,9 @@ dependencies { kotlinTest(it) testImplementation 'screen-state:state-test' - androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) - androidImportFixturesWorkaround(project, project(":chat-engine")) +// androidImportFixturesWorkaround(project, project(":domains:store")) +// androidImportFixturesWorkaround(project, project(":chat-engine")) +// androidImportFixturesWorkaround(project, project(":matrix:common")) } \ No newline at end of file diff --git a/features/home/build.gradle b/features/home/build.gradle index e508c14..e45d07a 100644 --- a/features/home/build.gradle +++ b/features/home/build.gradle @@ -1,7 +1,7 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" implementation project(":features:directory") implementation project(":features:login") implementation project(":features:settings") diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt index 9133a05..6d31a40 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt @@ -16,7 +16,6 @@ class HomeModule( internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profile: ProfileState): HomeViewModel { return HomeViewModel( chatEngine, - storeModule.credentialsStore(), directory, login, profile, diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index fd8de22..aa3e247 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.launch internal class HomeViewModel( private val chatEngine: ChatEngine, - private val credentialsProvider: CredentialsStore, private val directoryState: DirectoryState, private val loginViewModel: LoginViewModel, private val profileState: ProfileState, @@ -39,7 +38,7 @@ internal class HomeViewModel( fun start() { viewModelScope.launch { - state = if (credentialsProvider.isSignedIn()) { + state = if (chatEngine.isSignedIn()) { _events.emit(HomeEvent.OnShowContent) initialHomeContent() } else { @@ -48,11 +47,10 @@ internal class HomeViewModel( } viewModelScope.launch { - if (credentialsProvider.isSignedIn()) { + if (chatEngine.isSignedIn()) { listenForInviteChanges() } } - } private suspend fun initialHomeContent(): SignedIn { diff --git a/features/login/build.gradle b/features/login/build.gradle index ac3340b..848ec0f 100644 --- a/features/login/build.gradle +++ b/features/login/build.gradle @@ -1,7 +1,7 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" implementation project(":domains:android:compose-core") implementation project(":domains:android:push") implementation project(":domains:android:viewmodel") diff --git a/features/messenger/build.gradle b/features/messenger/build.gradle index 1e7aa46..9700ddf 100644 --- a/features/messenger/build.gradle +++ b/features/messenger/build.gradle @@ -2,7 +2,7 @@ applyAndroidComposeLibraryModule(project) apply plugin: 'kotlin-parcelize' dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") implementation project(":domains:store") @@ -15,10 +15,10 @@ dependencies { kotlinTest(it) testImplementation 'screen-state:state-test' - androidImportFixturesWorkaround(project, project(":matrix:common")) +// testImplementation 'chat-engine:chat-engine-test' + androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) - androidImportFixturesWorkaround(project, project(":chat-engine")) + androidImportFixturesWorkaround(project, project(":domains:store")) } \ No newline at end of file diff --git a/features/navigator/build.gradle b/features/navigator/build.gradle index f3bacd7..1aee7ce 100644 --- a/features/navigator/build.gradle +++ b/features/navigator/build.gradle @@ -4,5 +4,5 @@ apply plugin: 'kotlin-parcelize' dependencies { compileOnly project(":domains:android:stub") implementation project(":core") - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" } \ No newline at end of file diff --git a/features/notifications/build.gradle b/features/notifications/build.gradle index b7d1171..b108ab6 100644 --- a/features/notifications/build.gradle +++ b/features/notifications/build.gradle @@ -1,8 +1,8 @@ applyAndroidLibraryModule(project) dependencies { - implementation project(":chat-engine") - implementation project(':domains:store') + implementation "chat-engine:chat-engine" +// implementation project(':domains:store') implementation project(":domains:android:work") implementation project(':domains:android:push') implementation project(":domains:android:core") @@ -17,7 +17,7 @@ dependencies { kotlinTest(it) androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":matrix:common")) - androidImportFixturesWorkaround(project, project(":chat-engine")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) +// androidImportFixturesWorkaround(project, project(":matrix:common")) +// androidImportFixturesWorkaround(project, project(":chat-engine")) } \ No newline at end of file diff --git a/features/profile/build.gradle b/features/profile/build.gradle index f3bf95b..c1460f0 100644 --- a/features/profile/build.gradle +++ b/features/profile/build.gradle @@ -1,9 +1,9 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" implementation project(":features:settings") - implementation project(':domains:store') +// implementation project(':domains:store') implementation 'screen-state:screen-android' implementation project(":domains:android:compose-core") implementation project(":design-library") @@ -12,9 +12,9 @@ dependencies { kotlinTest(it) testImplementation 'screen-state:state-test' - androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) - androidImportFixturesWorkaround(project, project(":chat-engine")) +// androidImportFixturesWorkaround(project, project(":chat-engine")) +// androidImportFixturesWorkaround(project, project(":matrix:common")) +// androidImportFixturesWorkaround(project, project(":domains:store")) } \ No newline at end of file diff --git a/features/settings/build.gradle b/features/settings/build.gradle index d646822..63d8c48 100644 --- a/features/settings/build.gradle +++ b/features/settings/build.gradle @@ -1,7 +1,7 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" implementation project(":features:navigator") implementation project(':domains:store') implementation project(':domains:android:push') @@ -14,10 +14,11 @@ dependencies { kotlinTest(it) testImplementation 'screen-state:state-test' - androidImportFixturesWorkaround(project, project(":matrix:common")) +// testImplementation 'chat-engine:chat-engine-test' androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) - androidImportFixturesWorkaround(project, project(":chat-engine")) +// androidImportFixturesWorkaround(project, project(":matrix:common")) +// androidImportFixturesWorkaround(project, project(":domains:store")) +// androidImportFixturesWorkaround(project, project(":chat-engine")) } \ No newline at end of file diff --git a/features/share-entry/build.gradle b/features/share-entry/build.gradle index 176afb2..235b2e9 100644 --- a/features/share-entry/build.gradle +++ b/features/share-entry/build.gradle @@ -1,10 +1,10 @@ applyAndroidComposeLibraryModule(project) dependencies { + implementation "chat-engine:chat-engine" implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") - implementation project(':domains:store') - implementation project(':chat-engine') +// implementation project(':domains:store') implementation project(":core") implementation project(":design-library") implementation project(":features:navigator") diff --git a/features/verification/build.gradle b/features/verification/build.gradle index 989463e..21d6274 100644 --- a/features/verification/build.gradle +++ b/features/verification/build.gradle @@ -1,7 +1,7 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":matrix:services:crypto") + implementation "chat-engine:chat-engine" implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") implementation project(":design-library") diff --git a/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationModule.kt b/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationModule.kt index 1e76f7e..8b63c70 100644 --- a/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationModule.kt +++ b/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationModule.kt @@ -1,14 +1,14 @@ package app.dapk.st.verification import app.dapk.st.core.ProvidableModule -import app.dapk.st.matrix.crypto.CryptoService +import app.dapk.st.engine.ChatEngine class VerificationModule( - private val cryptoService: CryptoService + private val chatEngine: ChatEngine, ) : ProvidableModule { fun verificationViewModel(): VerificationViewModel { - return VerificationViewModel(cryptoService) + return VerificationViewModel(chatEngine) } } \ No newline at end of file diff --git a/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationViewModel.kt b/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationViewModel.kt index b612235..e2c0e77 100644 --- a/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationViewModel.kt +++ b/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationViewModel.kt @@ -1,20 +1,18 @@ package app.dapk.st.verification -import androidx.lifecycle.viewModelScope -import app.dapk.st.matrix.crypto.CryptoService -import app.dapk.st.matrix.crypto.Verification +import app.dapk.st.engine.ChatEngine import app.dapk.st.viewmodel.DapkViewModel -import kotlinx.coroutines.launch class VerificationViewModel( - private val cryptoService: CryptoService, + private val chatEngine: ChatEngine, ) : DapkViewModel( initialState = VerificationScreenState(foo = "") ) { fun inSecureAccept() { - viewModelScope.launch { - cryptoService.verificationAction(Verification.Action.InsecureAccept) - } + // TODO verify via chat-engine +// viewModelScope.launch { +// cryptoService.verificationAction(Verification.Action.InsecureAccept) +// } } diff --git a/matrix-chat-engine/build.gradle b/matrix-chat-engine/build.gradle deleted file mode 100644 index 7d6b221..0000000 --- a/matrix-chat-engine/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -plugins { - id 'java-test-fixtures' - id 'kotlin' -} - -dependencies { - api Dependencies.mavenCentral.kotlinCoroutinesCore - - implementation project(":core") - implementation project(":chat-engine") - - implementation project(":domains:olm") - - implementation project(":matrix:matrix") - implementation project(":matrix:matrix-http-ktor") - implementation project(":matrix:services:auth") - implementation project(":matrix:services:sync") - implementation project(":matrix:services:room") - implementation project(":matrix:services:push") - implementation project(":matrix:services:message") - implementation project(":matrix:services:device") - implementation project(":matrix:services:crypto") - implementation project(":matrix:services:profile") - - kotlinTest(it) - kotlinFixtures(it) - - testImplementation(testFixtures(project(":matrix:services:sync"))) - testImplementation(testFixtures(project(":matrix:services:message"))) - testImplementation(testFixtures(project(":matrix:common"))) - testImplementation(testFixtures(project(":core"))) - testImplementation(testFixtures(project(":domains:store"))) - testImplementation(testFixtures(project(":chat-engine"))) -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryMergeWithLocalEchosUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryMergeWithLocalEchosUseCase.kt deleted file mode 100644 index 2f0d602..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryMergeWithLocalEchosUseCase.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.common.asString -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.room.RoomService - -internal typealias DirectoryMergeWithLocalEchosUseCase = suspend (OverviewState, UserId, Map>) -> OverviewState - -internal class DirectoryMergeWithLocalEchosUseCaseImpl( - private val roomService: RoomService, -) : DirectoryMergeWithLocalEchosUseCase { - - override suspend fun invoke(overview: OverviewState, selfId: UserId, echos: Map>): OverviewState { - return when { - echos.isEmpty() -> overview - else -> overview.map { - when (val roomEchos = echos[it.roomId]) { - null -> it - else -> it.mergeWithLocalEchos( - member = roomService.findMember(it.roomId, selfId) ?: RoomMember( - selfId, - null, - avatarUrl = null, - ), - echos = roomEchos, - ) - } - } - } - } - - private fun RoomOverview.mergeWithLocalEchos(member: RoomMember, echos: List): RoomOverview { - val latestEcho = echos.maxByOrNull { it.timestampUtc } - return if (latestEcho != null && latestEcho.timestampUtc > (this.lastMessage?.utcTimestamp ?: 0)) { - this.copy( - lastMessage = RoomOverview.LastMessage( - content = when (val message = latestEcho.message) { - is MessageService.Message.TextMessage -> message.content.body.asString() - is MessageService.Message.ImageMessage -> "\uD83D\uDCF7" - }, - utcTimestamp = latestEcho.timestampUtc, - author = member, - ) - ) - } else { - this - } - } - -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt deleted file mode 100644 index 27b8799..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt +++ /dev/null @@ -1,43 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.core.extensions.combine -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.sync.RoomStore -import app.dapk.st.matrix.sync.SyncService -import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapConcat -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map - -internal class DirectoryUseCase( - private val syncService: SyncService, - private val messageService: MessageService, - private val credentialsStore: CredentialsStore, - private val roomStore: RoomStore, - private val mergeLocalEchosUseCase: DirectoryMergeWithLocalEchosUseCase, -) { - - fun state(): Flow { - return flow { emit(credentialsStore.credentials()!!.userId) }.flatMapConcat { userId -> - combine( - syncService.startSyncing(), - syncService.overview().map { it.map { it.engine() } }, - messageService.localEchos(), - roomStore.observeUnreadCountById(), - syncService.events(), - roomStore.observeMuted(), - ) { _, overviewState, localEchos, unread, events, muted -> - mergeLocalEchosUseCase.invoke(overviewState, userId, localEchos).map { roomOverview -> - DirectoryItem( - overview = roomOverview, - unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0), - typing = events.filterIsInstance().firstOrNull { it.roomId == roomOverview.roomId }?.engine(), - isMuted = muted.contains(roomOverview.roomId), - ) - } - } - } - } -} diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt deleted file mode 100644 index e903473..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.sync.SyncService -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map - -class InviteUseCase( - private val syncService: SyncService -) { - - fun invites() = invitesDatasource() - - private fun invitesDatasource() = combine( - syncService.startSyncing(), - syncService.invites().map { it.map { it.engine() } } - ) { _, invites -> invites } - -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt deleted file mode 100644 index a415b78..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt +++ /dev/null @@ -1,69 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.message.MessageService - -internal class LocalEchoMapper(private val metaMapper: MetaMapper) { - - fun MessageService.LocalEcho.toMessage(member: RoomMember): RoomEvent { - return when (val message = this.message) { - is MessageService.Message.TextMessage -> { - val mappedMessage = RoomEvent.Message( - eventId = this.eventId ?: EventId(this.localId), - content = message.content.body, - author = member, - utcTimestamp = message.timestampUtc, - meta = metaMapper.toMeta(this) - ) - - when (val reply = message.reply) { - null -> mappedMessage - else -> RoomEvent.Reply( - mappedMessage, RoomEvent.Message( - eventId = reply.eventId, - content = reply.originalMessage, - author = reply.author, - utcTimestamp = reply.timestampUtc, - meta = MessageMeta.FromServer - ) - ) - } - } - - is MessageService.Message.ImageMessage -> { - RoomEvent.Image( - eventId = this.eventId ?: EventId(this.localId), - author = member, - utcTimestamp = message.timestampUtc, - meta = metaMapper.toMeta(this), - imageMeta = RoomEvent.Image.ImageMeta(message.content.meta.width, message.content.meta.height, message.content.uri, null), - ) - } - } - } - - fun RoomEvent.mergeWith(echo: MessageService.LocalEcho): RoomEvent = when (this) { - is RoomEvent.Message -> this.copy(meta = metaMapper.toMeta(echo)) - is RoomEvent.Reply -> this.copy(message = this.message.mergeWith(echo)) - is RoomEvent.Image -> this.copy(meta = metaMapper.toMeta(echo)) - is RoomEvent.Encrypted -> this.copy(meta = metaMapper.toMeta(echo)) - is RoomEvent.Redacted -> this - } -} - -internal class MetaMapper { - - fun toMeta(echo: MessageService.LocalEcho) = MessageMeta.LocalEcho( - echoId = echo.localId, - state = when (val localEchoState = echo.state) { - MessageService.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending - MessageService.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent - is MessageService.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error( - localEchoState.message, - type = MessageMeta.LocalEcho.State.Error.Type.UNKNOWN, - ) - } - ) - -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt deleted file mode 100644 index 57b37eb..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.engine - -import java.util.* - -internal class LocalIdFactory { - fun create() = "local.${UUID.randomUUID()}" -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt deleted file mode 100644 index 64bc368..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt +++ /dev/null @@ -1,118 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.auth.AuthService -import app.dapk.st.matrix.sync.InviteMeta -import app.dapk.st.matrix.auth.AuthService.LoginRequest as MatrixLoginRequest -import app.dapk.st.matrix.auth.AuthService.LoginResult as MatrixLoginResult -import app.dapk.st.matrix.crypto.ImportResult as MatrixImportResult -import app.dapk.st.matrix.room.ProfileService.Me as MatrixMe -import app.dapk.st.matrix.sync.LastMessage as MatrixLastMessage -import app.dapk.st.matrix.sync.MessageMeta as MatrixMessageMeta -import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent -import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite -import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview -import app.dapk.st.matrix.sync.RoomState as MatrixRoomState -import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing as MatrixTyping - -fun MatrixRoomOverview.engine() = RoomOverview( - this.roomId, - this.roomCreationUtc, - this.roomName, - this.roomAvatarUrl, - this.lastMessage?.engine(), - this.isGroup, - this.readMarker, - this.isEncrypted -) - -fun MatrixLastMessage.engine() = RoomOverview.LastMessage( - this.content, - this.utcTimestamp, - this.author, -) - -fun MatrixTyping.engine() = Typing( - this.roomId, - this.members, -) - -fun LoginRequest.engine() = MatrixLoginRequest( - this.userName, - this.password, - this.serverUrl -) - -fun MatrixLoginResult.engine() = when (this) { - is AuthService.LoginResult.Error -> LoginResult.Error(this.cause) - AuthService.LoginResult.MissingWellKnown -> LoginResult.MissingWellKnown - is AuthService.LoginResult.Success -> LoginResult.Success(this.userCredentials) -} - -fun MatrixMe.engine() = Me( - this.userId, - this.displayName, - this.avatarUrl, - this.homeServerUrl, -) - -fun MatrixRoomInvite.engine() = RoomInvite( - this.from, - this.roomId, - this.inviteMeta.engine(), -) - -fun InviteMeta.engine() = when (this) { - InviteMeta.DirectMessage -> RoomInvite.InviteMeta.DirectMessage - is InviteMeta.Room -> RoomInvite.InviteMeta.Room(this.roomName) -} - -fun MatrixImportResult.engine() = when (this) { - is MatrixImportResult.Error -> ImportResult.Error( - when (val error = this.cause) { - MatrixImportResult.Error.Type.InvalidFile -> ImportResult.Error.Type.InvalidFile - MatrixImportResult.Error.Type.NoKeysFound -> ImportResult.Error.Type.NoKeysFound - MatrixImportResult.Error.Type.UnableToOpenFile -> ImportResult.Error.Type.UnableToOpenFile - MatrixImportResult.Error.Type.UnexpectedDecryptionOutput -> ImportResult.Error.Type.UnexpectedDecryptionOutput - is MatrixImportResult.Error.Type.Unknown -> ImportResult.Error.Type.Unknown(error.cause) - } - ) - - is MatrixImportResult.Success -> ImportResult.Success(this.roomIds, this.totalImportedKeysCount) - is MatrixImportResult.Update -> ImportResult.Update(this.importedKeysCount) -} - -fun MatrixRoomState.engine() = RoomState( - this.roomOverview.engine(), - this.events.map { it.engine() } -) - -fun MatrixRoomEvent.engine(): RoomEvent = when (this) { - is MatrixRoomEvent.Image -> RoomEvent.Image(this.eventId, this.utcTimestamp, this.imageMeta.engine(), this.author, this.meta.engine(), this.edited) - is MatrixRoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, this.content, this.author, this.meta.engine(), this.edited) - is MatrixRoomEvent.Reply -> RoomEvent.Reply(this.message.engine(), this.replyingTo.engine()) - is MatrixRoomEvent.Encrypted -> RoomEvent.Encrypted(this.eventId, this.utcTimestamp, this.author, this.meta.engine()) - is MatrixRoomEvent.Redacted -> RoomEvent.Redacted(this.eventId, this.utcTimestamp, this.author) -} - -fun MatrixRoomEvent.Image.ImageMeta.engine() = RoomEvent.Image.ImageMeta( - this.width, - this.height, - this.url, - this.keys?.let { RoomEvent.Image.ImageMeta.Keys(it.k, it.iv, it.v, it.hashes) } -) - -fun MatrixMessageMeta.engine() = when (this) { - MatrixMessageMeta.FromServer -> MessageMeta.FromServer - is MatrixMessageMeta.LocalEcho -> MessageMeta.LocalEcho( - this.echoId, when (val echo = this.state) { - is MatrixMessageMeta.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error( - echo.message, when (echo.type) { - MatrixMessageMeta.LocalEcho.State.Error.Type.UNKNOWN -> MessageMeta.LocalEcho.State.Error.Type.UNKNOWN - } - ) - - MatrixMessageMeta.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending - MatrixMessageMeta.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent - } - ) -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt deleted file mode 100644 index 93003d1..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ /dev/null @@ -1,224 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.core.Base64 -import app.dapk.st.core.BuildMeta -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.JobBag -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.matrix.MatrixClient -import app.dapk.st.matrix.MatrixTaskRunner -import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator -import app.dapk.st.matrix.auth.authService -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.crypto.MatrixMediaDecrypter -import app.dapk.st.matrix.crypto.cryptoService -import app.dapk.st.matrix.device.KnownDeviceStore -import app.dapk.st.matrix.message.BackgroundScheduler -import app.dapk.st.matrix.message.LocalEchoStore -import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.message.messageService -import app.dapk.st.matrix.push.pushService -import app.dapk.st.matrix.room.MemberStore -import app.dapk.st.matrix.room.ProfileStore -import app.dapk.st.matrix.room.profileService -import app.dapk.st.matrix.room.roomService -import app.dapk.st.matrix.sync.* -import app.dapk.st.olm.OlmStore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import java.io.InputStream -import java.time.Clock - -class MatrixEngine internal constructor( - private val directoryUseCase: Lazy, - private val matrix: Lazy, - private val timelineUseCase: Lazy, - private val sendMessageUseCase: Lazy, - private val matrixMediaDecrypter: Lazy, - private val matrixPushHandler: Lazy, - private val inviteUseCase: Lazy, - private val notificationMessagesUseCase: Lazy, - private val notificationInvitesUseCase: Lazy, -) : ChatEngine { - - override fun directory() = directoryUseCase.value.state() - override fun invites() = inviteUseCase.value.invites() - - override fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow { - return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts) - } - - override fun notificationsMessages(): Flow { - return notificationMessagesUseCase.value.invoke() - } - - override fun notificationsInvites(): Flow { - return notificationInvitesUseCase.value.invoke() - } - - override suspend fun login(request: LoginRequest): LoginResult { - return matrix.value.authService().login(request.engine()).engine() - } - - override suspend fun me(forceRefresh: Boolean): Me { - return matrix.value.profileService().me(forceRefresh).engine() - } - - override suspend fun InputStream.importRoomKeys(password: String): Flow { - return with(matrix.value.cryptoService()) { - importRoomKeys(password).map { it.engine() }.onEach { - when (it) { - is ImportResult.Error, - is ImportResult.Update -> { - // do nothing - } - - is ImportResult.Success -> matrix.value.syncService().forceManualRefresh(it.roomIds) - } - } - } - } - - override suspend fun send(message: SendMessage, room: RoomOverview) { - sendMessageUseCase.value.send(message, room) - } - - override suspend fun registerPushToken(token: String, gatewayUrl: String) { - matrix.value.pushService().registerPush(token, gatewayUrl) - } - - override suspend fun joinRoom(roomId: RoomId) { - matrix.value.roomService().joinRoom(roomId) - } - - override suspend fun rejectJoinRoom(roomId: RoomId) { - matrix.value.roomService().rejectJoinRoom(roomId) - } - - override suspend fun findMembersSummary(roomId: RoomId) = matrix.value.roomService().findMembersSummary(roomId) - - override fun mediaDecrypter(): MediaDecrypter { - val mediaDecrypter = matrixMediaDecrypter.value - return object : MediaDecrypter { - override fun decrypt(input: InputStream, k: String, iv: String): MediaDecrypter.Collector { - return MediaDecrypter.Collector { - mediaDecrypter.decrypt(input, k, iv).collect(it) - } - } - } - } - - override fun pushHandler() = matrixPushHandler.value - - override suspend fun muteRoom(roomId: RoomId) = matrix.value.roomService().muteRoom(roomId) - - override suspend fun unmuteRoom(roomId: RoomId) = matrix.value.roomService().unmuteRoom(roomId) - - override suspend fun runTask(task: ChatEngineTask): TaskRunner.TaskResult { - return when (val result = matrix.value.run(MatrixTaskRunner.MatrixTask(task.type, task.jsonPayload))) { - is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(result.canRetry) - MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success - } - } - - class Factory { - - fun create( - base64: Base64, - buildMeta: BuildMeta, - logger: MatrixLogger, - nameGenerator: DeviceDisplayNameGenerator, - coroutineDispatchers: CoroutineDispatchers, - errorTracker: ErrorTracker, - imageContentReader: ImageContentReader, - backgroundScheduler: BackgroundScheduler, - memberStore: MemberStore, - roomStore: RoomStore, - profileStore: ProfileStore, - syncStore: SyncStore, - overviewStore: OverviewStore, - filterStore: FilterStore, - localEchoStore: LocalEchoStore, - credentialsStore: CredentialsStore, - knownDeviceStore: KnownDeviceStore, - olmStore: OlmStore, - ): ChatEngine { - val lazyMatrix = lazy { - MatrixFactory.createMatrix( - base64, - buildMeta, - logger, - nameGenerator, - coroutineDispatchers, - errorTracker, - imageContentReader, - backgroundScheduler, - memberStore, - roomStore, - profileStore, - syncStore, - overviewStore, - filterStore, - localEchoStore, - credentialsStore, - knownDeviceStore, - olmStore - ) - } - val directoryUseCase = unsafeLazy { - val matrix = lazyMatrix.value - DirectoryUseCase( - matrix.syncService(), - matrix.messageService(), - credentialsStore, - roomStore, - DirectoryMergeWithLocalEchosUseCaseImpl(matrix.roomService()), - ) - } - val timelineUseCase = unsafeLazy { - val matrix = lazyMatrix.value - val mergeWithLocalEchosUseCase = TimelineMergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper())) - val timeline = TimelineUseCaseImpl(matrix.syncService(), matrix.messageService(), matrix.roomService(), mergeWithLocalEchosUseCase) - ReadMarkingTimeline(roomStore, credentialsStore, timeline, matrix.roomService()) - } - - val sendMessageUseCase = unsafeLazy { - val matrix = lazyMatrix.value - SendMessageUseCase(matrix.messageService(), LocalIdFactory(), imageContentReader, Clock.systemUTC()) - } - - val mediaDecrypter = unsafeLazy { MatrixMediaDecrypter(base64) } - val pushHandler = unsafeLazy { - MatrixPushHandler( - backgroundScheduler, - credentialsStore, - lazyMatrix.value.syncService(), - roomStore, - coroutineDispatchers, - JobBag(), - ) - } - - val invitesUseCase = unsafeLazy { InviteUseCase(lazyMatrix.value.syncService()) } - - return MatrixEngine( - directoryUseCase, - lazyMatrix, - timelineUseCase, - sendMessageUseCase, - mediaDecrypter, - pushHandler, - invitesUseCase, - unsafeLazy { ObserveUnreadNotificationsUseCaseImpl(roomStore) }, - unsafeLazy { ObserveInviteNotificationsUseCaseImpl(overviewStore) }, - ) - } - - } - -} - -private fun unsafeLazy(initializer: () -> T): Lazy = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer) diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixFactory.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixFactory.kt deleted file mode 100644 index a2eb715..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixFactory.kt +++ /dev/null @@ -1,258 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.core.Base64 -import app.dapk.st.core.BuildMeta -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.SingletonFlows -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.matrix.MatrixClient -import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator -import app.dapk.st.matrix.auth.installAuthService -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.RoomMembersProvider -import app.dapk.st.matrix.crypto.Verification -import app.dapk.st.matrix.crypto.cryptoService -import app.dapk.st.matrix.crypto.installCryptoService -import app.dapk.st.matrix.device.KnownDeviceStore -import app.dapk.st.matrix.device.deviceService -import app.dapk.st.matrix.device.installEncryptionService -import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory -import app.dapk.st.matrix.message.* -import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.push.installPushService -import app.dapk.st.matrix.room.* -import app.dapk.st.matrix.room.internal.SingleRoomStore -import app.dapk.st.matrix.sync.* -import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent -import app.dapk.st.matrix.sync.internal.room.MessageDecrypter -import app.dapk.st.olm.DeviceKeyFactory -import app.dapk.st.olm.OlmStore -import app.dapk.st.olm.OlmWrapper -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import java.time.Clock - -internal object MatrixFactory { - - fun createMatrix( - base64: Base64, - buildMeta: BuildMeta, - logger: MatrixLogger, - nameGenerator: DeviceDisplayNameGenerator, - coroutineDispatchers: CoroutineDispatchers, - errorTracker: ErrorTracker, - imageContentReader: ImageContentReader, - backgroundScheduler: BackgroundScheduler, - memberStore: MemberStore, - roomStore: RoomStore, - profileStore: ProfileStore, - syncStore: SyncStore, - overviewStore: OverviewStore, - filterStore: FilterStore, - localEchoStore: LocalEchoStore, - credentialsStore: CredentialsStore, - knownDeviceStore: KnownDeviceStore, - olmStore: OlmStore, - ) = MatrixClient( - KtorMatrixHttpClientFactory( - credentialsStore, - includeLogging = buildMeta.isDebug, - ), - logger - ).also { - it.install { - installAuthService(credentialsStore, nameGenerator) - installEncryptionService(knownDeviceStore) - - val singletonFlows = SingletonFlows(coroutineDispatchers) - val olm = OlmWrapper( - olmStore = olmStore, - singletonFlows = singletonFlows, - jsonCanonicalizer = JsonCanonicalizer(), - deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()), - errorTracker = errorTracker, - logger = logger, - clock = Clock.systemUTC(), - coroutineDispatchers = coroutineDispatchers, - ) - installCryptoService( - credentialsStore, - olm, - roomMembersProvider = { services -> - RoomMembersProvider { - services.roomService().joinedMembers(it).map { it.userId } - } - }, - base64 = base64, - coroutineDispatchers = coroutineDispatchers, - ) - installMessageService( - localEchoStore, - backgroundScheduler, - imageContentReader, - messageEncrypter = { - val cryptoService = it.cryptoService() - MessageEncrypter { message -> - val result = cryptoService.encrypt( - roomId = message.roomId, - credentials = credentialsStore.credentials()!!, - messageJson = message.contents, - ) - - MessageEncrypter.EncryptedMessagePayload( - result.algorithmName, - result.senderKey, - result.cipherText, - result.sessionId, - result.deviceId, - ) - } - }, - mediaEncrypter = { - val cryptoService = it.cryptoService() - MediaEncrypter { input -> - val result = cryptoService.encrypt(input) - MediaEncrypter.Result( - uri = result.uri, - contentLength = result.contentLength, - algorithm = result.algorithm, - ext = result.ext, - keyOperations = result.keyOperations, - kty = result.kty, - k = result.k, - iv = result.iv, - hashes = result.hashes, - v = result.v, - ) - } - }, - ) - - installRoomService( - memberStore, - roomMessenger = { - val messageService = it.messageService() - object : RoomMessenger { - override suspend fun enableEncryption(roomId: RoomId) { - messageService.sendEventMessage( - roomId, MessageService.EventMessage.Encryption( - algorithm = AlgorithmName("m.megolm.v1.aes-sha2") - ) - ) - } - } - }, - roomInviteRemover = { - overviewStore.removeInvites(listOf(it)) - }, - singleRoomStore = singleRoomStoreAdapter(roomStore) - ) - - installProfileService(profileStore, singletonFlows, credentialsStore) - - installSyncService( - credentialsStore, - overviewStore, - roomStore, - syncStore, - filterStore, - deviceNotifier = { services -> - val encryption = services.deviceService() - val crypto = services.cryptoService() - DeviceNotifier { userIds, syncToken -> - encryption.updateStaleDevices(userIds) - crypto.updateOlmSession(userIds, syncToken) - } - }, - messageDecrypter = { serviceProvider -> - val cryptoService = serviceProvider.cryptoService() - MessageDecrypter { - cryptoService.decrypt(it) - } - }, - keySharer = { serviceProvider -> - val cryptoService = serviceProvider.cryptoService() - KeySharer { sharedRoomKeys -> - cryptoService.importRoomKeys(sharedRoomKeys) - } - }, - verificationHandler = { services -> - val cryptoService = services.cryptoService() - VerificationHandler { apiEvent -> - logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it") - cryptoService.onVerificationEvent( - when (apiEvent) { - is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.transactionId, - apiEvent.content.methods, - apiEvent.content.timestampPosix, - ) - - is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.transactionId, - apiEvent.content.methods, - ) - - is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.method, - apiEvent.content.protocols, - apiEvent.content.hashes, - apiEvent.content.codes, - apiEvent.content.short, - apiEvent.content.transactionId, - ) - - is ApiToDeviceEvent.VerificationCancel -> TODO() - is ApiToDeviceEvent.VerificationAccept -> TODO() - is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key( - apiEvent.sender, - apiEvent.content.transactionId, - apiEvent.content.key - ) - - is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac( - apiEvent.sender, - apiEvent.content.transactionId, - apiEvent.content.keys, - apiEvent.content.mac, - ) - } - ) - } - }, - oneTimeKeyProducer = { services -> - val cryptoService = services.cryptoService() - MaybeCreateMoreKeys { - cryptoService.maybeCreateMoreKeys(it) - } - }, - roomMembersService = { services -> - val roomService = services.roomService() - object : RoomMembersService { - override suspend fun find(roomId: RoomId, userIds: List) = roomService.findMembers(roomId, userIds) - override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId) - override suspend fun insert(roomId: RoomId, members: List) = roomService.insertMembers(roomId, members) - } - }, - errorTracker = errorTracker, - coroutineDispatchers = coroutineDispatchers, - ) - - installPushService(credentialsStore) - } - } - - private fun singleRoomStoreAdapter(roomStore: RoomStore) = object : SingleRoomStore { - override suspend fun mute(roomId: RoomId) = roomStore.mute(roomId) - override suspend fun unmute(roomId: RoomId) = roomStore.unmute(roomId) - override fun isMuted(roomId: RoomId): Flow = roomStore.observeMuted().map { it.contains(roomId) }.distinctUntilChanged() - } - -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt deleted file mode 100644 index 17e0a47..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt +++ /dev/null @@ -1,85 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.core.AppLogTag -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.JobBag -import app.dapk.st.core.log -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.JsonString -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.message.BackgroundScheduler -import app.dapk.st.matrix.sync.RoomStore -import app.dapk.st.matrix.sync.SyncService -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull - -class MatrixPushHandler( - private val backgroundScheduler: BackgroundScheduler, - private val credentialsStore: CredentialsStore, - private val syncService: SyncService, - private val roomStore: RoomStore, - private val dispatchers: CoroutineDispatchers, - private val jobBag: JobBag, -) : PushHandler { - - override fun onNewToken(payload: JsonString) { - log(AppLogTag.PUSH, "new push token received") - backgroundScheduler.schedule( - key = "2", - task = BackgroundScheduler.Task( - type = "push_token", - jsonPayload = payload - ) - ) - } - - override fun onMessageReceived(eventId: EventId?, roomId: RoomId?) { - log(AppLogTag.PUSH, "push received") - jobBag.replace(MatrixPushHandler::class, dispatchers.global.launch { - when (credentialsStore.credentials()) { - null -> log(AppLogTag.PUSH, "push ignored due to missing api credentials") - else -> doSync(roomId, eventId) - } - }) - } - - private suspend fun doSync(roomId: RoomId?, eventId: EventId?) { - when (roomId) { - null -> { - log(AppLogTag.PUSH, "empty push payload - keeping sync alive until unread changes") - waitForUnreadChange(60_000) ?: log(AppLogTag.PUSH, "timed out waiting for sync") - } - - else -> { - log(AppLogTag.PUSH, "push with eventId payload - keeping sync alive until the event shows up in the sync response") - waitForEvent( - timeout = 60_000, - eventId!!, - ) ?: log(AppLogTag.PUSH, "timed out waiting for sync") - } - } - log(AppLogTag.PUSH, "push sync finished") - } - - private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? { - return withTimeoutOrNull(timeout) { - combine(syncService.startSyncing(), syncService.observeEvent(eventId)) { _, event -> event } - .firstOrNull { - it == eventId - } - } - } - - private suspend fun waitForUnreadChange(timeout: Long): String? { - return withTimeoutOrNull(timeout) { - combine(syncService.startSyncing(), roomStore.observeUnread()) { _, unread -> unread } - .first() - "ignored" - } - } -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt deleted file mode 100644 index 2b9c19c..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt +++ /dev/null @@ -1,44 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.message.MessageService - -internal typealias TimelineMergeWithLocalEchosUseCase = (RoomState, RoomMember, List) -> RoomState - -internal class TimelineMergeWithLocalEchosUseCaseImpl( - private val localEventMapper: LocalEchoMapper, -) : TimelineMergeWithLocalEchosUseCase { - - override fun invoke(roomState: RoomState, member: RoomMember, echos: List): RoomState { - val echosByEventId = echos.associateBy { it.eventId } - val stateByEventId = roomState.events.associateBy { it.eventId } - - val uniqueEchos = uniqueEchos(echos, stateByEventId, member) - val existingWithEcho = updateExistingEventsWithLocalEchoMeta(roomState, echosByEventId) - - val sortedEvents = (existingWithEcho + uniqueEchos) - .sortedByDescending { it.utcTimestamp } - .distinctBy { it.eventId } - return roomState.copy(events = sortedEvents) - } - - private fun uniqueEchos(echos: List, stateByEventId: Map, member: RoomMember): List { - return with(localEventMapper) { - echos - .filter { echo -> echo.eventId == null || stateByEventId[echo.eventId] == null } - .map { localEcho -> localEcho.toMessage(member) } - } - } - - private fun updateExistingEventsWithLocalEchoMeta(roomState: RoomState, echosByEventId: Map): List { - return with(localEventMapper) { - roomState.events.map { roomEvent -> - when (val echo = echosByEventId[roomEvent.eventId]) { - null -> roomEvent - else -> roomEvent.mergeWith(echo) - } - } - } - } -} diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt deleted file mode 100644 index b926f75..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt +++ /dev/null @@ -1,44 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.sync.InviteMeta -import app.dapk.st.matrix.sync.OverviewStore -import app.dapk.st.matrix.sync.RoomInvite -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.* - -internal typealias ObserveInviteNotificationsUseCase = () -> Flow - -class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase { - - override fun invoke(): Flow { - return overviewStore.latestInvites() - .diff() - .drop(1) - .flatten() - .map { - val text = when (val meta = it.inviteMeta) { - InviteMeta.DirectMessage -> "${it.inviterName()} has invited you to chat" - is InviteMeta.Room -> "${it.inviterName()} has invited you to ${meta.roomName ?: "unnamed room"}" - } - InviteNotification(content = text, roomId = it.roomId) - } - } - - private fun Flow>.diff(): Flow> { - val previousInvites = mutableSetOf() - return this.distinctUntilChanged() - .map { - val diff = it.toSet() - previousInvites - previousInvites.clear() - previousInvites.addAll(it) - diff - } - } - - private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value -} - -@OptIn(FlowPreview::class) -private fun Flow>.flatten() = this.flatMapConcat { items -> - flow { items.forEach { this.emit(it) } } -} diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt deleted file mode 100644 index d1008cc..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt +++ /dev/null @@ -1,99 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.core.AppLogTag -import app.dapk.st.core.extensions.clearAndPutAll -import app.dapk.st.core.extensions.containsKey -import app.dapk.st.core.log -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomStore -import kotlinx.coroutines.flow.* -import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent -import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview - -internal typealias ObserveUnreadNotificationsUseCase = () -> Flow - -class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase { - - override fun invoke(): Flow { - return roomStore.observeNotMutedUnread() - .mapWithDiff() - .avoidShowingPreviousNotificationsOnLaunch() - .onlyRenderableChanges() - } - -} - -private fun Flow>>.mapWithDiff(): Flow>, NotificationDiff>> { - val previousUnreadEvents = mutableMapOf>() - return this.map { each -> - val allUnreadIds = each.toTimestampedIds() - val notificationDiff = calculateDiff(allUnreadIds, previousUnreadEvents) - previousUnreadEvents.clearAndPutAll(allUnreadIds) - each to notificationDiff - } -} - -private fun calculateDiff(allUnread: Map>, previousUnread: Map>?): NotificationDiff { - val previousLatestEventTimestamps = previousUnread.toLatestTimestamps() - val newRooms = allUnread.filter { !previousUnread.containsKey(it.key) }.keys - - val unchanged = previousUnread?.filter { - allUnread.containsKey(it.key) && (it.value == allUnread[it.key]) - } ?: emptyMap() - val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) }.mapValues { (key, value) -> - val isChangedRoom = !newRooms.contains(key) - if (isChangedRoom) { - val latest = previousLatestEventTimestamps[key] ?: 0L - value.filter { - val isExistingEvent = (previousUnread?.get(key)?.contains(it) ?: false) - !isExistingEvent && it.second > latest - } - } else { - value - } - }.filter { it.value.isNotEmpty() } - val removed = previousUnread?.filter { !allUnread.containsKey(it.key) } ?: emptyMap() - return NotificationDiff(unchanged.toEventIds(), changedOrNew.toEventIds(), removed.toEventIds(), newRooms) -} - -private fun Map>?.toLatestTimestamps() = this?.mapValues { it.value.maxOf { it.second } } ?: emptyMap() - -private fun Map>.toEventIds() = this.mapValues { it.value.map { it.first } } - -private fun Map>.toTimestampedIds() = this - .mapValues { it.value.toEventIds() } - .mapKeys { it.key.roomId } - -private fun List.toEventIds() = this.map { it.eventId to it.utcTimestamp } - -private fun Flow.avoidShowingPreviousNotificationsOnLaunch() = drop(1) - -private fun Flow>, NotificationDiff>>.onlyRenderableChanges(): Flow { - val inferredCurrentNotifications = mutableMapOf>() - return this - .filter { (_, diff) -> - when { - diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> { - log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes") - false - } - - inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> { - log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read") - false - } - - else -> true - } - } - .onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) } - .map { - val engineModels = it.first - .mapKeys { it.key.engine() } - .mapValues { it.value.map { it.engine() } } - engineModels to it.second - } -} - -typealias TimestampedEventId = Pair \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt deleted file mode 100644 index 43199b5..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt +++ /dev/null @@ -1,55 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.RoomStore -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.* - -class ReadMarkingTimeline( - private val roomStore: RoomStore, - private val credentialsStore: CredentialsStore, - private val observeTimelineUseCase: ObserveTimelineUseCase, - private val roomService: RoomService, -) { - - fun fetch(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow { - return flow { - val credentials = credentialsStore.credentials()!! - roomStore.markRead(roomId) - emit(credentials) - }.flatMapConcat { credentials -> - var lastKnownReadEvent: EventId? = null - observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state -> - state.latestMessageEventFromOthers(self = credentials.userId)?.let { - if (lastKnownReadEvent != it) { - updateRoomReadStateAsync(latestReadEvent = it, state, isReadReceiptsDisabled) - lastKnownReadEvent = it - } - } - } - } - } - - @Suppress("DeferredResultUnused") - private suspend fun updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerPageState, isReadReceiptsDisabled: Boolean) { - coroutineScope { - async { - runCatching { - roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = isReadReceiptsDisabled) - roomStore.markRead(state.roomState.roomOverview.roomId) - } - } - } - } - - private fun MessengerPageState.latestMessageEventFromOthers(self: UserId) = this.roomState.events - .filterIsInstance() - .filterNot { it.author.id == self } - .firstOrNull() - ?.eventId -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/SendMessageUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/SendMessageUseCase.kt deleted file mode 100644 index 04fc601..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/SendMessageUseCase.kt +++ /dev/null @@ -1,61 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.message.internal.ImageContentReader -import java.time.Clock - -internal class SendMessageUseCase( - private val messageService: MessageService, - private val localIdFactory: LocalIdFactory, - private val imageContentReader: ImageContentReader, - private val clock: Clock, -) { - - suspend fun send(message: SendMessage, room: RoomOverview) { - when (message) { - is SendMessage.ImageMessage -> createImageMessage(message, room) - is SendMessage.TextMessage -> messageService.scheduleMessage(createTextMessage(message, room)) - } - } - - private suspend fun createImageMessage(message: SendMessage.ImageMessage, room: RoomOverview) { - val meta = imageContentReader.meta(message.uri) - messageService.scheduleMessage( - MessageService.Message.ImageMessage( - MessageService.Message.Content.ImageContent( - uri = message.uri, - MessageService.Message.Content.ImageContent.Meta( - height = meta.height, - width = meta.width, - size = meta.size, - fileName = meta.fileName, - mimeType = meta.mimeType, - ) - ), - roomId = room.roomId, - sendEncrypted = room.isEncrypted, - localId = localIdFactory.create(), - timestampUtc = clock.millis(), - ) - ) - } - - private fun createTextMessage(message: SendMessage.TextMessage, room: RoomOverview) = MessageService.Message.TextMessage( - content = MessageService.Message.Content.TextContent(RichText.of(message.content)), - roomId = room.roomId, - sendEncrypted = room.isEncrypted, - localId = localIdFactory.create(), - timestampUtc = clock.millis(), - reply = message.reply?.let { - MessageService.Message.TextMessage.Reply( - author = it.author, - originalMessage = RichText.of(it.originalMessage), - replyContent = message.content, - eventId = it.eventId, - timestampUtc = it.timestampUtc, - ) - } - ) - -} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt deleted file mode 100644 index 219a93f..0000000 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.SyncService -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map - -internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow - -internal class TimelineUseCaseImpl( - private val syncService: SyncService, - private val messageService: MessageService, - private val roomService: RoomService, - private val timelineMergeWithLocalEchosUseCase: TimelineMergeWithLocalEchosUseCase, -) : ObserveTimelineUseCase { - - override fun invoke(roomId: RoomId, userId: UserId): Flow { - return combine( - roomDatasource(roomId), - messageService.localEchos(roomId), - syncService.events(roomId), - roomService.observeIsMuted(roomId), - ) { roomState, localEchos, events, isMuted -> - MessengerPageState( - roomState = when { - localEchos.isEmpty() -> roomState - else -> { - timelineMergeWithLocalEchosUseCase.invoke( - roomState, - roomService.findMember(roomId, userId) ?: userId.toFallbackMember(), - localEchos, - ) - } - }, - typing = events.filterIsInstance().firstOrNull { it.roomId == roomId }?.engine(), - self = userId, - isMuted = isMuted, - ) - } - } - - private fun roomDatasource(roomId: RoomId) = combine( - syncService.startSyncing(), - syncService.room(roomId).map { it.engine() } - ) { _, room -> room } -} - -private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null) diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/DirectoryUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/DirectoryUseCaseTest.kt deleted file mode 100644 index 081000e..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/DirectoryUseCaseTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.sync.RoomOverview -import fake.FakeCredentialsStore -import fake.FakeRoomStore -import fake.FakeSyncService -import fixture.aMatrixRoomOverview -import fixture.aRoomMember -import fixture.aTypingEvent -import fixture.aUserCredentials -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import test.delegateReturn - -private val A_ROOM_OVERVIEW = aMatrixRoomOverview() -private const val AN_UNREAD_COUNT = 10 -private const val MUTED_ROOM = true -private val TYPING_MEMBERS = listOf(aRoomMember()) - -class DirectoryUseCaseTest { - - private val fakeSyncService = FakeSyncService() - private val fakeMessageService = FakeMessageService() - private val fakeCredentialsStore = FakeCredentialsStore() - private val fakeRoomStore = FakeRoomStore() - private val fakeMergeLocalEchosUseCase = FakeDirectoryMergeWithLocalEchosUseCase() - - private val useCase = DirectoryUseCase( - fakeSyncService, - fakeMessageService, - fakeCredentialsStore, - fakeRoomStore, - fakeMergeLocalEchosUseCase, - ) - - @Test - fun `given empty values, then reads default directory state and maps to engine`() = runTest { - givenEmitsDirectoryState( - A_ROOM_OVERVIEW, - unreadCount = null, - isMuted = false, - ) - - val result = useCase.state().first() - - result shouldBeEqualTo listOf( - DirectoryItem( - A_ROOM_OVERVIEW.engine(), - unreadCount = UnreadCount(0), - typing = null, - isMuted = false - ) - ) - } - - @Test - fun `given extra state, then reads directory state and maps to engine`() = runTest { - givenEmitsDirectoryState( - A_ROOM_OVERVIEW, - unreadCount = AN_UNREAD_COUNT, - isMuted = MUTED_ROOM, - typing = TYPING_MEMBERS - ) - - val result = useCase.state().first() - - result shouldBeEqualTo listOf( - DirectoryItem( - A_ROOM_OVERVIEW.engine(), - unreadCount = UnreadCount(AN_UNREAD_COUNT), - typing = aTypingEvent(A_ROOM_OVERVIEW.roomId, TYPING_MEMBERS), - isMuted = MUTED_ROOM - ) - ) - } - - private fun givenEmitsDirectoryState( - roomOverview: RoomOverview, - unreadCount: Int? = null, - isMuted: Boolean = false, - typing: List = emptyList(), - ) { - val userCredentials = aUserCredentials() - fakeCredentialsStore.givenCredentials().returns(userCredentials) - - val matrixOverviewState = listOf(roomOverview) - - fakeSyncService.givenStartsSyncing() - fakeSyncService.givenOverview().returns(flowOf(matrixOverviewState)) - fakeSyncService.givenEvents().returns(flowOf(if (typing.isEmpty()) emptyList() else listOf(aTypingSyncEvent(roomOverview.roomId, typing)))) - - fakeMessageService.givenEchos().returns(flowOf(emptyMap())) - fakeRoomStore.givenUnreadByCount().returns(flowOf(unreadCount?.let { mapOf(roomOverview.roomId to it) } ?: emptyMap())) - fakeRoomStore.givenMuted().returns(flowOf(if (isMuted) setOf(roomOverview.roomId) else emptySet())) - - val mappedOverview = roomOverview.engine() - val expectedOverviewState = listOf(mappedOverview) - fakeMergeLocalEchosUseCase.givenMergedEchos(expectedOverviewState, userCredentials.userId, emptyMap()).returns(expectedOverviewState) - } -} - -class FakeDirectoryMergeWithLocalEchosUseCase : DirectoryMergeWithLocalEchosUseCase by mockk() { - fun givenMergedEchos(overviewState: OverviewState, selfId: UserId, echos: Map>) = coEvery { - this@FakeDirectoryMergeWithLocalEchosUseCase.invoke(overviewState, selfId, echos) - }.delegateReturn() -} diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/InviteUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/InviteUseCaseTest.kt deleted file mode 100644 index d534bfd..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/InviteUseCaseTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.sync.InviteMeta -import fake.FakeSyncService -import fixture.aRoomId -import fixture.aRoomMember -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite - -class InviteUseCaseTest { - - private val fakeSyncService = FakeSyncService() - private val useCase = InviteUseCase(fakeSyncService) - - @Test - fun `reads invites from sync service and maps to engine`() = runTest { - val aMatrixRoomInvite = aMatrixRoomInvite() - fakeSyncService.givenStartsSyncing() - fakeSyncService.givenInvites().returns(flowOf(listOf(aMatrixRoomInvite))) - - val result = useCase.invites().first() - - result shouldBeEqualTo listOf(aMatrixRoomInvite.engine()) - } - -} - -fun aMatrixRoomInvite( - from: RoomMember = aRoomMember(), - roomId: RoomId = aRoomId(), - inviteMeta: InviteMeta = InviteMeta.DirectMessage, -) = MatrixRoomInvite(from, roomId, inviteMeta) - diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt deleted file mode 100644 index a2f794d..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.sync.MessageMeta -import fake.FakeMetaMapper -import fixture.* -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private val A_META = MessageMeta.LocalEcho("echo-id", MessageMeta.LocalEcho.State.Sent) -private val AN_ECHO_CONTENT = aTextMessage(localId = "a-local-id") -private val A_ROOM_MEMBER = aRoomMember() - -class LocalEchoMapperTest { - - private val fakeMetaMapper = FakeMetaMapper() - - private val localEchoMapper = LocalEchoMapper(fakeMetaMapper.instance) - - @Test - fun `given echo with event id when mapping to message then uses event id`() = runWith(localEchoMapper) { - val echo = givenEcho(eventId = anEventId("a-known-id")) - - val result = echo.toMessage(A_ROOM_MEMBER) - - result shouldBeEqualTo aRoomMessageEvent( - eventId = echo.eventId!!, - content = AN_ECHO_CONTENT.content.body, - meta = A_META.engine() - ) - } - - @Test - fun `given echo without event id when mapping to message then uses local id`() = runWith(localEchoMapper) { - val echo = givenEcho(eventId = null, localId = "a-local-id") - - val result = echo.toMessage(A_ROOM_MEMBER) - - result shouldBeEqualTo aRoomMessageEvent( - eventId = anEventId(echo.localId), - content = AN_ECHO_CONTENT.content.body, - meta = A_META.engine() - ) - } - - @Test - fun `when merging with echo then updates meta with the echos meta`() = runWith(localEchoMapper) { - val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending).engine() - val event = aRoomMessageEvent(meta = previousMeta) - val echo = aLocalEcho() - fakeMetaMapper.given(echo).returns(A_META.engine() as app.dapk.st.engine.MessageMeta.LocalEcho) - - val result = event.mergeWith(echo) - - result shouldBeEqualTo aRoomMessageEvent(meta = A_META.engine()) - } - - private fun givenEcho(eventId: EventId? = null, localId: String = "", meta: MessageMeta.LocalEcho = A_META): MessageService.LocalEcho { - return aLocalEcho(eventId = eventId, message = aTextMessage(localId = localId)).also { - fakeMetaMapper.given(it).returns(meta.engine() as app.dapk.st.engine.MessageMeta.LocalEcho) - } - } -} - - -fun runWith(context: T, block: T.() -> Unit) { - block(context) -} diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt deleted file mode 100644 index 637b761..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.message.MessageService -import fixture.* -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private val A_ROOM_MESSAGE_EVENT = aRoomMessageEvent(eventId = anEventId("1")) -private val A_ROOM_IMAGE_MESSAGE_EVENT = aRoomMessageEvent(eventId = anEventId("2")) -private val A_LOCAL_ECHO_EVENT_ID = anEventId("2") -private const val A_LOCAL_ECHO_BODY = "body" -private val A_ROOM_MEMBER = aRoomMember() -private val ANOTHER_ROOM_MESSAGE_EVENT = A_ROOM_MESSAGE_EVENT.copy(eventId = anEventId("a second room event")) - -class MergeWithLocalEchosUseCaseTest { - - private val fakeLocalEchoMapper = fake.FakeLocalEventMapper() - private val mergeWithLocalEchosUseCase = TimelineMergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance) - - @Test - fun `given no local echos, when merging text message, then returns original state`() { - val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)) - - val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList()) - - result shouldBeEqualTo roomState - } - - @Test - fun `given no local echos, when merging events, then returns original ordered by timestamp descending`() { - val roomState = aRoomState(events = listOf(A_ROOM_IMAGE_MESSAGE_EVENT.copy(utcTimestamp = 1500), A_ROOM_MESSAGE_EVENT.copy(utcTimestamp = 1000))) - - val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList()) - - result shouldBeEqualTo roomState.copy(events = roomState.events.sortedByDescending { it.utcTimestamp }) - } - - @Test - fun `given local echo with sending state, when merging then maps to room event with local echo state`() { - val second = createLocalEcho(A_LOCAL_ECHO_EVENT_ID, A_LOCAL_ECHO_BODY, state = MessageService.LocalEcho.State.Sending) - fakeLocalEchoMapper.givenMapping(second, A_ROOM_MEMBER).returns(ANOTHER_ROOM_MESSAGE_EVENT) - val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)) - - val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, listOf(second)) - - result shouldBeEqualTo roomState.copy( - events = listOf( - A_ROOM_MESSAGE_EVENT, - ANOTHER_ROOM_MESSAGE_EVENT, - ) - ) - } - - private fun createLocalEcho(eventId: EventId, body: String, state: MessageService.LocalEcho.State) = aLocalEcho( - eventId, - aTextMessage(aTextContent(RichText.of(body))), - state, - ) -} \ No newline at end of file diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MetaMapperTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MetaMapperTest.kt deleted file mode 100644 index eb2cda1..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MetaMapperTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.message.MessageService -import fixture.aLocalEcho -import fixture.aTextMessage -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private const val A_LOCAL_ECHO_ID = "a-local-echo-id" - -class MetaMapperTest { - - private val metaMapper = MetaMapper() - - @Test - fun `given echo with sending meta then maps to sending state`() { - val result = metaMapper.toMeta( - aLocalEcho( - state = MessageService.LocalEcho.State.Sending, - message = aTextMessage(localId = A_LOCAL_ECHO_ID) - ) - ) - - result shouldBeEqualTo MessageMeta.LocalEcho( - echoId = A_LOCAL_ECHO_ID, - state = MessageMeta.LocalEcho.State.Sending - ) - } - - @Test - fun `given echo with sent meta then maps to sent state`() { - val result = metaMapper.toMeta( - aLocalEcho( - state = MessageService.LocalEcho.State.Sent, - message = aTextMessage(localId = A_LOCAL_ECHO_ID) - ) - ) - - result shouldBeEqualTo MessageMeta.LocalEcho( - echoId = A_LOCAL_ECHO_ID, - state = MessageMeta.LocalEcho.State.Sent - ) - } - - @Test - fun `given echo with error meta then maps to error state`() { - val result = metaMapper.toMeta( - aLocalEcho( - state = MessageService.LocalEcho.State.Error("an error", MessageService.LocalEcho.State.Error.Type.UNKNOWN), - message = aTextMessage(localId = A_LOCAL_ECHO_ID) - ) - ) - - result shouldBeEqualTo MessageMeta.LocalEcho( - echoId = A_LOCAL_ECHO_ID, - state = MessageMeta.LocalEcho.State.Error("an error", MessageMeta.LocalEcho.State.Error.Type.UNKNOWN) - ) - } -} \ No newline at end of file diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt deleted file mode 100644 index 7cb1de1..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt +++ /dev/null @@ -1,123 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RichText -import fake.FakeRoomStore -import fixture.NotificationDiffFixtures.aNotificationDiff -import fixture.aMatrixRoomMessageEvent -import fixture.aMatrixRoomOverview -import fixture.aRoomId -import fixture.anEventId -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent -import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview - -private val NO_UNREADS = emptyMap>() -private val A_MESSAGE = aMatrixRoomMessageEvent(eventId = anEventId("1"), content = RichText.of("hello"), utcTimestamp = 1000) -private val A_MESSAGE_2 = aMatrixRoomMessageEvent(eventId = anEventId("2"), content = RichText.of("world"), utcTimestamp = 2000) -private val A_ROOM_OVERVIEW = aMatrixRoomOverview(roomId = aRoomId("1")) -private val A_ROOM_OVERVIEW_2 = aMatrixRoomOverview(roomId = aRoomId("2")) - -private fun MatrixRoomOverview.withUnreads(vararg events: MatrixRoomEvent) = mapOf(this to events.toList()) -private fun MatrixRoomOverview.toDiff(vararg events: MatrixRoomEvent) = mapOf(this.roomId to events.map { it.eventId }) - -class ObserveUnreadRenderNotificationsUseCaseTest { - - private val fakeRoomStore = FakeRoomStore() - - private val useCase = ObserveUnreadNotificationsUseCaseImpl(fakeRoomStore) - - @Test - fun `given no initial unreads, when receiving new message, then emits message`() = runTest { - givenNoInitialUnreads(A_ROOM_OVERVIEW.withUnreads(A_MESSAGE)) - - val result = useCase.invoke().toList() - - result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( - changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), - newRooms = setOf(A_ROOM_OVERVIEW.roomId) - ) - ) - } - - @Test - fun `given no initial unreads, when receiving multiple messages, then emits messages`() = runTest { - givenNoInitialUnreads(A_ROOM_OVERVIEW.withUnreads(A_MESSAGE), A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2)) - - val result = useCase.invoke().toList() - - result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( - changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), - newRooms = setOf(A_ROOM_OVERVIEW.roomId) - ), - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) - ) - } - - @Test - fun `given initial unreads, when receiving new message, then emits all messages`() = runTest { - fakeRoomStore.givenNotMutedUnreadEvents( - flowOf(A_ROOM_OVERVIEW.withUnreads(A_MESSAGE), A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2)) - ) - - val result = useCase.invoke().toList() - - result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) - ) - } - - @Test - fun `given initial unreads, when reading a message, then emits nothing`() = runTest { - fakeRoomStore.givenNotMutedUnreadEvents( - flowOf(A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) + A_ROOM_OVERVIEW_2.withUnreads(A_MESSAGE_2), A_ROOM_OVERVIEW.withUnreads(A_MESSAGE)) - ) - - val result = useCase.invoke().toList() - - result shouldBeEqualTo emptyList() - } - - @Test - fun `given new and then historical message, when reading a message, then only emits the latest`() = runTest { - fakeRoomStore.givenNotMutedUnreadEvents( - flowOf( - NO_UNREADS, - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE), - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE.copy(eventId = anEventId("old"), utcTimestamp = -1)) - ) - ) - - val result = useCase.invoke().toList() - - result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( - changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), - newRooms = setOf(A_ROOM_OVERVIEW.roomId) - ), - ) - } - - @Test - fun `given initial unreads, when reading a duplicate unread, then emits nothing`() = runTest { - fakeRoomStore.givenNotMutedUnreadEvents( - flowOf(A_ROOM_OVERVIEW.withUnreads(A_MESSAGE), A_ROOM_OVERVIEW.withUnreads(A_MESSAGE)) - ) - - val result = useCase.invoke().toList() - - result shouldBeEqualTo emptyList() - } - - private fun givenNoInitialUnreads(vararg unreads: Map>) = - fakeRoomStore.givenNotMutedUnreadEvents(flowOf(NO_UNREADS, *unreads)) -} - -private fun Map>.engine() = this - .mapKeys { it.key.engine() } - .mapValues { it.value.map { it.engine() } } diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ReadMarkingTimelineTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ReadMarkingTimelineTest.kt deleted file mode 100644 index f70b33b..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ReadMarkingTimelineTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserId -import fake.FakeCredentialsStore -import fake.FakeRoomStore -import fixture.* -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import test.delegateReturn -import test.runExpectTest - -private val A_ROOM_ID = aRoomId() -private val A_USER_CREDENTIALS = aUserCredentials() -private val A_ROOM_MESSAGE_FROM_OTHER_USER = aRoomMessageEvent(author = aRoomMember(id = aUserId("another-user"))) -private val A_ROOM_MESSAGE_FROM_SELF = aRoomMessageEvent(author = aRoomMember(id = A_USER_CREDENTIALS.userId)) -private val READ_RECEIPTS_ARE_DISABLED = true - -class ReadMarkingTimelineTest { - - private val fakeRoomStore = FakeRoomStore() - private val fakeCredentialsStore = FakeCredentialsStore().apply { givenCredentials().returns(A_USER_CREDENTIALS) } - private val fakeObserveTimelineUseCase = FakeObserveTimelineUseCase() - private val fakeRoomService = FakeRoomService() - - private val readMarkingTimeline = ReadMarkingTimeline( - fakeRoomStore, - fakeCredentialsStore, - fakeObserveTimelineUseCase, - fakeRoomService, - ) - - @Test - fun `given a message from self, when fetching, then only marks room as read on initial launch`() = runExpectTest { - fakeRoomStore.expectUnit(times = 1) { it.markRead(A_ROOM_ID) } - val messengerState = aMessengerState(roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_FROM_SELF))) - fakeObserveTimelineUseCase.given(A_ROOM_ID, A_USER_CREDENTIALS.userId).returns(flowOf(messengerState)) - - val result = readMarkingTimeline.fetch(A_ROOM_ID, isReadReceiptsDisabled = READ_RECEIPTS_ARE_DISABLED).first() - - result shouldBeEqualTo messengerState - verifyExpects() - } - - @Test - fun `given a message from other user, when fetching, then marks room as read`() = runExpectTest { - fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) } - fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, A_ROOM_MESSAGE_FROM_OTHER_USER.eventId, isPrivate = READ_RECEIPTS_ARE_DISABLED) } - val messengerState = aMessengerState(roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_FROM_OTHER_USER))) - fakeObserveTimelineUseCase.given(A_ROOM_ID, A_USER_CREDENTIALS.userId).returns(flowOf(messengerState)) - - val result = readMarkingTimeline.fetch(A_ROOM_ID, isReadReceiptsDisabled = READ_RECEIPTS_ARE_DISABLED).first() - - result shouldBeEqualTo messengerState - verifyExpects() - } - -} - -class FakeObserveTimelineUseCase : ObserveTimelineUseCase by mockk() { - fun given(roomId: RoomId, userId: UserId) = every { this@FakeObserveTimelineUseCase.invoke(roomId, userId) }.delegateReturn() -} diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/SendMessageUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/SendMessageUseCaseTest.kt deleted file mode 100644 index f675c92..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/SendMessageUseCaseTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.message.internal.ImageContentReader -import fake.FakeLocalIdFactory -import fixture.aRoomMember -import fixture.aRoomOverview -import fixture.anEventId -import io.mockk.every -import io.mockk.mockk -import org.junit.Test -import test.delegateReturn -import test.runExpectTest -import java.time.Clock - -private const val AN_IMAGE_URI = "" -private val AN_IMAGE_META = ImageContentReader.ImageContent( - height = 50, - width = 100, - size = 1000L, - fileName = "a file name", - mimeType = "image/png" -) -private const val A_CURRENT_TIME = 2000L -private const val A_LOCAL_ID = "a local id" -private val A_ROOM_OVERVIEW = aRoomOverview( - isEncrypted = true -) -private val A_REPLY = SendMessage.TextMessage.Reply( - aRoomMember(), - originalMessage = "", - anEventId(), - timestampUtc = 7000 -) -private const val A_TEXT_MESSAGE_CONTENT = "message content" - -class SendMessageUseCaseTest { - - private val fakeMessageService = FakeMessageService() - private val fakeLocalIdFactory = FakeLocalIdFactory().apply { givenCreate().returns(A_LOCAL_ID) } - private val fakeImageContentReader = FakeImageContentReader() - private val fakeClock = FakeClock().apply { givenMillis().returns(A_CURRENT_TIME) } - - private val useCase = SendMessageUseCase( - fakeMessageService, - fakeLocalIdFactory.instance, - fakeImageContentReader, - fakeClock.instance - ) - - @Test - fun `when sending image message, then schedules message`() = runExpectTest { - fakeImageContentReader.givenMeta(AN_IMAGE_URI).returns(AN_IMAGE_META) - val expectedImageMessage = createExpectedImageMessage(A_ROOM_OVERVIEW) - fakeMessageService.expect { it.scheduleMessage(expectedImageMessage) } - - useCase.send(SendMessage.ImageMessage(uri = AN_IMAGE_URI), A_ROOM_OVERVIEW) - - verifyExpects() - } - - @Test - fun `when sending text message, then schedules message`() = runExpectTest { - val expectedTextMessage = createExpectedTextMessage(A_ROOM_OVERVIEW, A_TEXT_MESSAGE_CONTENT, reply = null) - fakeMessageService.expect { it.scheduleMessage(expectedTextMessage) } - - useCase.send( - SendMessage.TextMessage( - content = A_TEXT_MESSAGE_CONTENT, - reply = null, - ), - A_ROOM_OVERVIEW - ) - - verifyExpects() - } - - @Test - fun `given a reply, when sending text message, then schedules message with reply`() = runExpectTest { - val expectedTextMessage = createExpectedTextMessage(A_ROOM_OVERVIEW, A_TEXT_MESSAGE_CONTENT, reply = A_REPLY) - fakeMessageService.expect { it.scheduleMessage(expectedTextMessage) } - - useCase.send( - SendMessage.TextMessage( - content = A_TEXT_MESSAGE_CONTENT, - reply = A_REPLY, - ), - A_ROOM_OVERVIEW - ) - - verifyExpects() - } - - - private fun createExpectedImageMessage(roomOverview: RoomOverview) = MessageService.Message.ImageMessage( - MessageService.Message.Content.ImageContent( - uri = AN_IMAGE_URI, - MessageService.Message.Content.ImageContent.Meta( - height = AN_IMAGE_META.height, - width = AN_IMAGE_META.width, - size = AN_IMAGE_META.size, - fileName = AN_IMAGE_META.fileName, - mimeType = AN_IMAGE_META.mimeType, - ) - ), - roomId = roomOverview.roomId, - sendEncrypted = roomOverview.isEncrypted, - localId = A_LOCAL_ID, - timestampUtc = A_CURRENT_TIME, - ) - - private fun createExpectedTextMessage(roomOverview: RoomOverview, messageContent: String, reply: SendMessage.TextMessage.Reply?) = - MessageService.Message.TextMessage( - content = MessageService.Message.Content.TextContent(RichText.of(messageContent)), - roomId = roomOverview.roomId, - sendEncrypted = roomOverview.isEncrypted, - localId = A_LOCAL_ID, - timestampUtc = A_CURRENT_TIME, - reply = reply?.let { - MessageService.Message.TextMessage.Reply( - author = it.author, - originalMessage = RichText.of(it.originalMessage), - replyContent = messageContent, - eventId = it.eventId, - timestampUtc = it.timestampUtc, - ) - } - ) -} - -class FakeImageContentReader : ImageContentReader by mockk() { - fun givenMeta(uri: String) = every { meta(uri) }.delegateReturn() -} - -class FakeClock { - val instance = mockk() - fun givenMillis() = every { instance.millis() }.delegateReturn() -} diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt deleted file mode 100644 index 38f950b..0000000 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt +++ /dev/null @@ -1,142 +0,0 @@ -package app.dapk.st.engine - -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.RoomState -import app.dapk.st.matrix.sync.SyncService -import fake.FakeSyncService -import fixture.* -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.Test -import test.FlowTestObserver -import test.delegateReturn - -private const val IS_ROOM_MUTED = false -private val A_ROOM_ID = aRoomId() -private val AN_USER_ID = aUserId() -private val A_ROOM_STATE = aMatrixRoomState() -private val A_MERGED_ROOM_STATE = A_ROOM_STATE.copy(events = listOf(aMatrixRoomMessageEvent(content = RichText.of("a merged event")))) -private val A_LOCAL_ECHOS_LIST = listOf(aLocalEcho()) -private val A_ROOM_MEMBER = aRoomMember() - -class TimelineUseCaseTest { - - private val fakeSyncService = FakeSyncService() - private val fakeMessageService = FakeMessageService() - private val fakeRoomService = FakeRoomService() - private val fakeMergeWithLocalEchosUseCase = FakeMergeWithLocalEchosUseCase() - - private val timelineUseCase = TimelineUseCaseImpl( - fakeSyncService, - fakeMessageService, - fakeRoomService, - fakeMergeWithLocalEchosUseCase, - ) - - @Test - fun `when observing timeline, then emits sync emission`() = runTest { - givenSyncEmission(roomState = A_ROOM_STATE) - - timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID) - .test(this) - .assertValues( - listOf( - aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE.engine()) - ) - ) - } - - @Test - fun `given local echos, when observing timeline, then merges room and local echos`() = runTest { - givenSyncEmission(roomState = A_ROOM_STATE, echos = A_LOCAL_ECHOS_LIST) - fakeRoomService.givenFindMember(A_ROOM_ID, AN_USER_ID).returns(A_ROOM_MEMBER) - - fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE.engine()) - - - timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID) - .test(this) - .assertValues( - listOf( - aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE.engine()) - ) - ) - } - - @Test - fun `given sync events from current and other rooms, when observing timeline, then filters by current room`() = runTest { - givenSyncEmission( - events = listOf( - aTypingSyncEvent(aRoomId("another room"), members = listOf(A_ROOM_MEMBER)), - aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER)), - ) - ) - - timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID) - .test(this) - .assertValues( - listOf( - aMessengerState( - self = AN_USER_ID, - roomState = A_ROOM_STATE.engine(), - typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER)).engine() - ) - ) - ) - } - - private fun givenSyncEmission( - roomState: RoomState = A_ROOM_STATE, - echos: List = emptyList(), - events: List = emptyList() - ) { - fakeSyncService.givenStartsSyncing() - fakeSyncService.givenRoom(A_ROOM_ID).returns(flowOf(roomState)) - fakeMessageService.givenEchos(A_ROOM_ID).returns(flowOf(echos)) - fakeSyncService.givenEvents(A_ROOM_ID).returns(flowOf(events)) - fakeRoomService.givenMuted(A_ROOM_ID).returns(flowOf(IS_ROOM_MUTED)) - } -} - -suspend fun Flow.test(scope: CoroutineScope) = FlowTestObserver(scope, this).also { - this.collect() -} - -class FakeMergeWithLocalEchosUseCase : TimelineMergeWithLocalEchosUseCase by mockk() { - fun givenMerging(roomState: RoomState, roomMember: RoomMember, echos: List) = every { - this@FakeMergeWithLocalEchosUseCase.invoke(roomState.engine(), roomMember, echos) - }.delegateReturn() -} - -fun aTypingSyncEvent( - roomId: RoomId = aRoomId(), - members: List = listOf(aRoomMember()) -) = SyncService.SyncEvent.Typing(roomId, members) - -class FakeMessageService : MessageService by mockk() { - fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn() - fun givenEchos() = every { localEchos() }.delegateReturn() -} - -class FakeRoomService : RoomService by mockk() { - fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn() - fun givenMuted(roomId: RoomId) = every { observeIsMuted(roomId) }.delegateReturn() -} - -fun aMessengerState( - self: UserId = aUserId(), - roomState: app.dapk.st.engine.RoomState = aRoomState(), - typing: Typing? = null, - isMuted: Boolean = IS_ROOM_MUTED, -) = MessengerPageState(self, roomState, typing, isMuted) \ No newline at end of file diff --git a/matrix-chat-engine/src/test/kotlin/fake/FakeDirectoryUseCase.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeDirectoryUseCase.kt deleted file mode 100644 index 434e0db..0000000 --- a/matrix-chat-engine/src/test/kotlin/fake/FakeDirectoryUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package fake - -import app.dapk.st.engine.DirectoryUseCase -import io.mockk.every -import io.mockk.mockk -import test.delegateReturn - -internal class FakeDirectoryUseCase { - val instance = mockk() - fun given() = every { instance.state() }.delegateReturn() -} \ No newline at end of file diff --git a/matrix-chat-engine/src/test/kotlin/fake/FakeLocalEventMapper.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeLocalEventMapper.kt deleted file mode 100644 index 2365e9d..0000000 --- a/matrix-chat-engine/src/test/kotlin/fake/FakeLocalEventMapper.kt +++ /dev/null @@ -1,14 +0,0 @@ -package fake - -import app.dapk.st.engine.LocalEchoMapper -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.message.MessageService -import io.mockk.every -import io.mockk.mockk - -internal class FakeLocalEventMapper { - val instance = mockk() - fun givenMapping(echo: MessageService.LocalEcho, roomMember: RoomMember) = every { - with(instance) { echo.toMessage(roomMember) } - } -} \ No newline at end of file diff --git a/matrix-chat-engine/src/test/kotlin/fake/FakeLocalIdFactory.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeLocalIdFactory.kt deleted file mode 100644 index 994f22b..0000000 --- a/matrix-chat-engine/src/test/kotlin/fake/FakeLocalIdFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package fake - -import app.dapk.st.engine.LocalIdFactory -import io.mockk.every -import io.mockk.mockk -import test.delegateReturn - -internal class FakeLocalIdFactory { - val instance = mockk() - fun givenCreate() = every { instance.create() }.delegateReturn() -} \ No newline at end of file diff --git a/matrix-chat-engine/src/test/kotlin/fake/FakeMetaMapper.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeMetaMapper.kt deleted file mode 100644 index 8722d9a..0000000 --- a/matrix-chat-engine/src/test/kotlin/fake/FakeMetaMapper.kt +++ /dev/null @@ -1,12 +0,0 @@ -package fake - -import app.dapk.st.engine.MetaMapper -import app.dapk.st.matrix.message.MessageService -import io.mockk.every -import io.mockk.mockk -import test.delegateReturn - -internal class FakeMetaMapper { - val instance = mockk() - fun given(echo: MessageService.LocalEcho) = every { instance.toMeta(echo) }.delegateReturn() -} \ No newline at end of file diff --git a/matrix-chat-engine/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt deleted file mode 100644 index 886fe27..0000000 --- a/matrix-chat-engine/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package fake - -import app.dapk.st.engine.ObserveInviteNotificationsUseCase -import io.mockk.coEvery -import io.mockk.mockk -import test.delegateEmit - -class FakeObserveInviteNotificationsUseCase : ObserveInviteNotificationsUseCase by mockk() { - fun given() = coEvery { this@FakeObserveInviteNotificationsUseCase.invoke() }.delegateEmit() -} \ No newline at end of file diff --git a/matrix-chat-engine/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt deleted file mode 100644 index d04b782..0000000 --- a/matrix-chat-engine/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package fake - -import app.dapk.st.engine.ObserveUnreadNotificationsUseCase -import io.mockk.coEvery -import io.mockk.mockk -import test.delegateEmit - -class FakeObserveUnreadNotificationsUseCase : ObserveUnreadNotificationsUseCase by mockk() { - fun given() = coEvery { this@FakeObserveUnreadNotificationsUseCase.invoke() }.delegateEmit() -} \ No newline at end of file diff --git a/matrix/common/build.gradle b/matrix/common/build.gradle deleted file mode 100644 index 73df1df..0000000 --- a/matrix/common/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'kotlin' - id 'org.jetbrains.kotlin.plugin.serialization' - id 'java-test-fixtures' -} - -dependencies { - implementation Dependencies.mavenCentral.kotlinSerializationJson - - kotlinTest(it) - kotlinFixtures(it) - testFixturesImplementation(testFixtures(project(":core"))) -} \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/AlgorithmName.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/AlgorithmName.kt deleted file mode 100644 index c2b758e..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/AlgorithmName.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class AlgorithmName(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/AvatarUrl.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/AvatarUrl.kt deleted file mode 100644 index 66a1a8f..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/AvatarUrl.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class AvatarUrl(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/CipherText.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/CipherText.kt deleted file mode 100644 index f40338a..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/CipherText.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class CipherText(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/CredentialsStore.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/CredentialsStore.kt deleted file mode 100644 index 6c289a2..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/CredentialsStore.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.dapk.st.matrix.common - -interface CredentialsStore { - - suspend fun credentials(): UserCredentials? - suspend fun update(credentials: UserCredentials) - suspend fun clear() -} - -suspend fun CredentialsStore.isSignedIn() = this.credentials() != null diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/Curve25519.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/Curve25519.kt deleted file mode 100644 index 55561b8..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/Curve25519.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class Curve25519(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/DecryptionResult.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/DecryptionResult.kt deleted file mode 100644 index d5d1b9f..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/DecryptionResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.dapk.st.matrix.common - -sealed interface DecryptionResult { - data class Failed(val reason: String) : DecryptionResult - data class Success(val payload: JsonString, val isVerified: Boolean) : DecryptionResult -} diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/DeviceId.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/DeviceId.kt deleted file mode 100644 index d353145..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/DeviceId.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class DeviceId(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/Ed25519.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/Ed25519.kt deleted file mode 100644 index 5a802cb..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/Ed25519.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class Ed25519(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EncryptedMessageContent.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EncryptedMessageContent.kt deleted file mode 100644 index 9f958bd..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EncryptedMessageContent.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.dapk.st.matrix.common - -sealed class EncryptedMessageContent { - - data class OlmV1( - val senderId: UserId, - val cipherText: Map, - val senderKey: Curve25519, - ) : EncryptedMessageContent() - - data class MegOlmV1( - val cipherText: CipherText, - val deviceId: DeviceId, - val senderKey: String, - val sessionId: SessionId, - ) : EncryptedMessageContent() - - data class CipherTextInfo( - val body: CipherText, - val type: Int, - ) -} \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventId.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventId.kt deleted file mode 100644 index 1c4c378..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventId.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class EventId(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventType.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventType.kt deleted file mode 100644 index 85a6e4e..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventType.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.dapk.st.matrix.common - -enum class EventType(val value: String) { - ROOM_MESSAGE("m.room.message"), - ENCRYPTED("m.room.encrypted"), - ENCRYPTION("m.room.encryption"), - VERIFICATION_REQUEST("m.key.verification.request"), - VERIFICATION_READY("m.key.verification.ready"), - VERIFICATION_START("m.key.verification.start"), - VERIFICATION_ACCEPT("m.key.verification.accept"), - VERIFICATION_MAC("m.key.verification.mac"), - VERIFICATION_KEY("m.key.verification.key"), - VERIFICATION_DONE("m.key.verification.done"), -} - -enum class MessageType(val value: String) { - TEXT("m.text"), - IMAGE("m.image"), -} \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/HomeServerUrl.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/HomeServerUrl.kt deleted file mode 100644 index d721b18..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/HomeServerUrl.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class HomeServerUrl(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/JsonCanonicalizer.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/JsonCanonicalizer.kt deleted file mode 100644 index 7fa7acf..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/JsonCanonicalizer.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject - -private val CAPTURE_UNICODE = "\\\\u(.{4})".toRegex() - -class JsonCanonicalizer { - - fun canonicalize(input: JsonString): String { - val element = Json.parseToJsonElement(input.value.replace(CAPTURE_UNICODE, " ")).sort() - return Json.encodeToString(element) - } - -} - -private fun JsonElement.sort(): JsonElement { - return when (this) { - is JsonObject -> JsonObject( - this.map { it.key to it.value.sort() }.sortedBy { it.first }.toMap() - ) - is JsonArray -> JsonArray(this.map { it.sort() }) - else -> this - } -} \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/JsonString.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/JsonString.kt deleted file mode 100644 index 86a4636..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/JsonString.kt +++ /dev/null @@ -1,4 +0,0 @@ -package app.dapk.st.matrix.common - -@JvmInline -value class JsonString(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/MatrixLogger.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/MatrixLogger.kt deleted file mode 100644 index 738813d..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/MatrixLogger.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.dapk.st.matrix.common - -enum class MatrixLogTag(val key: String) { - MATRIX("matrix"), - CRYPTO("crypto"), - SYNC("sync"), - VERIFICATION("verification"), - PERF("performance"), - ROOM("room"), -} - -typealias MatrixLogger = (tag: String, message: String) -> Unit - -fun MatrixLogger.crypto(message: Any) = this.matrixLog(MatrixLogTag.CRYPTO, message) - -fun MatrixLogger.matrixLog(tag: MatrixLogTag, message: Any) { - this.invoke(tag.key, message.toString()) -} - -fun MatrixLogger.matrixLog(message: Any) { - matrixLog(tag = MatrixLogTag.MATRIX, message = message) -} - -fun MatrixLogger.logP(area: String): PerfTracker { - val start = System.currentTimeMillis() - var lastCheckpoint = start - return object : PerfTracker { - override fun checkpoint(label: String) { - val now = System.currentTimeMillis() - val timeTaken = (now - lastCheckpoint) - lastCheckpoint = now - matrixLog(MatrixLogTag.PERF, "$area - $label: took $timeTaken ms") - } - - override fun stop() { - val timeTaken = System.currentTimeMillis() - start - matrixLog(MatrixLogTag.PERF, "$area: took $timeTaken ms") - } - } -} - -interface PerfTracker { - fun checkpoint(label: String) - fun stop() -} - -inline fun MatrixLogger.logP(area: String, block: () -> T): T { - val start = System.currentTimeMillis() - return block().also { - val timeTaken = System.currentTimeMillis() - start - matrixLog(MatrixLogTag.PERF, "$area: took $timeTaken ms") - } -} \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/MxUrl.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/MxUrl.kt deleted file mode 100644 index 45bf1cf..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/MxUrl.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable -import java.net.URI - -@Serializable -@JvmInline -value class MxUrl(val value: String) - -fun MxUrl.convertMxUrToUrl(homeServer: HomeServerUrl): String { - val mxcUri = URI.create(this.value) - return "${homeServer.value.ensureHttps().ensureEndsWith("/")}_matrix/media/r0/download/${mxcUri.authority}${mxcUri.path}" -} - -private fun String.ensureEndsWith(suffix: String) = if (endsWith(suffix)) this else "$this$suffix" - -private fun String.ensureHttps() = replace("http://", "https://").ensureStartsWith("https://") - -private fun String.ensureStartsWith(prefix: String) = if (startsWith(prefix)) this else "$prefix$this" diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RichText.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RichText.kt deleted file mode 100644 index 2411c0b..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RichText.kt +++ /dev/null @@ -1,43 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class RichText(@SerialName("parts") val parts: List) { - @Serializable - sealed interface Part { - @Serializable - data class Normal(@SerialName("content") val content: String) : Part - - @Serializable - data class Link(@SerialName("url") val url: String, @SerialName("label") val label: String) : Part - - @Serializable - data class Bold(@SerialName("content") val content: String) : Part - - @Serializable - data class Italic(@SerialName("content") val content: String) : Part - - @Serializable - data class BoldItalic(@SerialName("content") val content: String) : Part - - @Serializable - data class Person(@SerialName("user_id") val userId: UserId, @SerialName("display_name") val displayName: String) : Part - } - - companion object { - fun of(text: String) = RichText(listOf(RichText.Part.Normal(text))) - } -} - -fun RichText.asString() = parts.joinToString(separator = "") { - when(it) { - is RichText.Part.Bold -> it.content - is RichText.Part.BoldItalic -> it.content - is RichText.Part.Italic -> it.content - is RichText.Part.Link -> it.label - is RichText.Part.Normal -> it.content - is RichText.Part.Person -> it.userId.value - } -} \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RoomId.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RoomId.kt deleted file mode 100644 index 5720b81..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RoomId.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class RoomId(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RoomMember.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RoomMember.kt deleted file mode 100644 index fb58fb0..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RoomMember.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class RoomMember( - @SerialName("user_id") val id: UserId, - @SerialName("display_name") val displayName: String?, - @SerialName("avatar_url") val avatarUrl: AvatarUrl?, -) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/ServerKeyCount.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/ServerKeyCount.kt deleted file mode 100644 index aab83a0..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/ServerKeyCount.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class ServerKeyCount(val value: Int) diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SessionId.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SessionId.kt deleted file mode 100644 index e143de5..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SessionId.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class SessionId(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SharedRoomKey.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SharedRoomKey.kt deleted file mode 100644 index 81386bc..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SharedRoomKey.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.dapk.st.matrix.common - -data class SharedRoomKey( - val algorithmName: AlgorithmName, - val roomId: RoomId, - val sessionId: SessionId, - val sessionKey: String, - val isExported: Boolean, -) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SignedJson.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SignedJson.kt deleted file mode 100644 index 5b2224f..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SignedJson.kt +++ /dev/null @@ -1,4 +0,0 @@ -package app.dapk.st.matrix.common - -@JvmInline -value class SignedJson(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SyncToken.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SyncToken.kt deleted file mode 100644 index 278bb13..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/SyncToken.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class SyncToken(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/UserCredentials.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/UserCredentials.kt deleted file mode 100644 index 8c6ac25..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/UserCredentials.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json - -@Serializable -data class UserCredentials( - @SerialName("access_token") val accessToken: String, - @SerialName("home_server") val homeServer: HomeServerUrl, - @SerialName("user_id") override val userId: UserId, - @SerialName("device_id") override val deviceId: DeviceId, -) : DeviceCredentials { - - companion object { - - fun String.fromJson() = Json.decodeFromString(serializer(), this) - fun UserCredentials.toJson() = Json.encodeToString(serializer(), this) - } -} - -interface DeviceCredentials { - val userId: UserId - val deviceId: DeviceId -} diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/UserId.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/UserId.kt deleted file mode 100644 index ced149f..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/UserId.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.common - -import kotlinx.serialization.Serializable - -@Serializable -@JvmInline -value class UserId(val value: String) \ No newline at end of file diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/extensions/JsonStringExtensions.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/extensions/JsonStringExtensions.kt deleted file mode 100644 index bd4ac22..0000000 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/extensions/JsonStringExtensions.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.dapk.st.matrix.common.extensions - -import app.dapk.st.matrix.common.JsonString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.* - -fun Any?.toJsonString(): JsonString = JsonString(Json.encodeToString(this.toJsonElement())) - -private fun Any?.toJsonElement(): JsonElement = when (this) { - null -> JsonNull - is JsonElement -> this - is Number -> JsonPrimitive(this) - is Boolean -> JsonPrimitive(this) - is String -> JsonPrimitive(this) - is Array<*> -> JsonArray(map { it.toJsonElement() }) - is List<*> -> JsonArray(map { it.toJsonElement() }) - is Map<*, *> -> JsonObject(map { it.key.toString() to it.value.toJsonElement() }.toMap()) - else -> throw IllegalArgumentException("Unknown type: $this") -} diff --git a/matrix/common/src/test/kotlin/app/dapk/st/matrix/common/JsonCanonicalizerTest.kt b/matrix/common/src/test/kotlin/app/dapk/st/matrix/common/JsonCanonicalizerTest.kt deleted file mode 100644 index fe483d3..0000000 --- a/matrix/common/src/test/kotlin/app/dapk/st/matrix/common/JsonCanonicalizerTest.kt +++ /dev/null @@ -1,108 +0,0 @@ -package app.dapk.st.matrix.common - -import org.amshove.kluent.ErrorCollectionMode -import org.amshove.kluent.errorCollector -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.throwCollectedErrors -import org.junit.Test - -class JsonCanonicalizerTest { - - private data class Case(val input: String, val expected: String) - - private val jsonCanonicalizer = JsonCanonicalizer() - - @Test - fun `canonicalises json strings`() { - val cases = listOf( - Case( - input = """{}""", - expected = """{}""", - ), - Case( - input = """ - { - "one": 1, - "two": "Two" - } - """.trimIndent(), - expected = """{"one":1,"two":"Two"}""" - ), - Case( - input = """ - { - "b": "2", - "a": "1" - } - """.trimIndent(), - expected = """{"a":"1","b":"2"}""" - ), - Case( - input = """{"b":"2","a":"1"}""", - expected = """{"a":"1","b":"2"}""" - ), - Case( - input = """ - { - "auth": { - "success": true, - "mxid": "@john.doe:example.com", - "profile": { - "display_name": "John Doe", - "three_pids": [ - { - "medium": "email", - "address": "john.doe@example.org" - }, - { - "medium": "msisdn", - "address": "123456789" - } - ] - } - } - } - """.trimIndent(), - expected = """{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}""", - ), - Case( - input = """ - { - "a": " " - } - """.trimIndent(), - expected = """{"a":" "}""", - ), - Case( - input = """ - { - "a": "\u65E5" - } - """.trimIndent(), - expected = """{"a":" "}""" - ), - Case( - input = """ - { - "a": null - } - """.trimIndent(), - expected = """{"a":null}""" - ) - ) - - runCases(cases) { (input, expected) -> - val result = jsonCanonicalizer.canonicalize(JsonString(input)) - - result shouldBeEqualTo expected - } - } -} - -private inline fun runCases(cases: List, action: (T) -> Unit) { - errorCollector.setCollectionMode(ErrorCollectionMode.Soft) - cases.forEach { - action(it) - } - errorCollector.throwCollectedErrors() -} \ No newline at end of file diff --git a/matrix/common/src/testFixtures/kotlin/fake/FakeCredentialsStore.kt b/matrix/common/src/testFixtures/kotlin/fake/FakeCredentialsStore.kt deleted file mode 100644 index a29112c..0000000 --- a/matrix/common/src/testFixtures/kotlin/fake/FakeCredentialsStore.kt +++ /dev/null @@ -1,10 +0,0 @@ -package fake - -import app.dapk.st.matrix.common.CredentialsStore -import io.mockk.coEvery -import io.mockk.mockk -import test.delegateReturn - -class FakeCredentialsStore : CredentialsStore by mockk() { - fun givenCredentials() = coEvery { credentials() }.delegateReturn() -} \ No newline at end of file diff --git a/matrix/common/src/testFixtures/kotlin/fake/FakeMatrixLogger.kt b/matrix/common/src/testFixtures/kotlin/fake/FakeMatrixLogger.kt deleted file mode 100644 index c10c288..0000000 --- a/matrix/common/src/testFixtures/kotlin/fake/FakeMatrixLogger.kt +++ /dev/null @@ -1,9 +0,0 @@ -package fake - -import app.dapk.st.matrix.common.MatrixLogger - -class FakeMatrixLogger : MatrixLogger { - override fun invoke(tag: String, message: String) { - // do nothing - } -} \ No newline at end of file diff --git a/matrix/common/src/testFixtures/kotlin/fixture/DecryptionResultFixture.kt b/matrix/common/src/testFixtures/kotlin/fixture/DecryptionResultFixture.kt deleted file mode 100644 index 8582eaf..0000000 --- a/matrix/common/src/testFixtures/kotlin/fixture/DecryptionResultFixture.kt +++ /dev/null @@ -1,10 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.DecryptionResult -import app.dapk.st.matrix.common.JsonString - -fun aDecryptionSuccessResult( - payload: JsonString = aJsonString(), - isVerified: Boolean = false, -) = DecryptionResult.Success(payload, isVerified) - diff --git a/matrix/common/src/testFixtures/kotlin/fixture/DeviceCredentialsFixture.kt b/matrix/common/src/testFixtures/kotlin/fixture/DeviceCredentialsFixture.kt deleted file mode 100644 index 8aab75a..0000000 --- a/matrix/common/src/testFixtures/kotlin/fixture/DeviceCredentialsFixture.kt +++ /dev/null @@ -1,13 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.DeviceCredentials -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.UserId - -fun aDeviceCredentials( - userId: UserId = aUserId(), - deviceId: DeviceId = aDeviceId(), -) = object : DeviceCredentials { - override val userId = userId - override val deviceId = deviceId -} \ No newline at end of file diff --git a/matrix/common/src/testFixtures/kotlin/fixture/EncryptedMessageFixture.kt b/matrix/common/src/testFixtures/kotlin/fixture/EncryptedMessageFixture.kt deleted file mode 100644 index e74f96f..0000000 --- a/matrix/common/src/testFixtures/kotlin/fixture/EncryptedMessageFixture.kt +++ /dev/null @@ -1,21 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.* - -fun anEncryptedMegOlmV1Message( - cipherText: CipherText = aCipherText(), - deviceId: DeviceId = aDeviceId(), - senderKey: String = "a-sender-key", - sessionId: SessionId = aSessionId(), -) = EncryptedMessageContent.MegOlmV1(cipherText, deviceId, senderKey, sessionId) - -fun anEncryptedOlmV1Message( - senderId: UserId = aUserId(), - cipherText: Map = emptyMap(), - senderKey: Curve25519 = aCurve25519(), -) = EncryptedMessageContent.OlmV1(senderId, cipherText, senderKey) - -fun aCipherTextInfo( - body: CipherText = aCipherText(), - type: Int = 1, -) = EncryptedMessageContent.CipherTextInfo(body, type) diff --git a/matrix/common/src/testFixtures/kotlin/fixture/ModelFixtures.kt b/matrix/common/src/testFixtures/kotlin/fixture/ModelFixtures.kt deleted file mode 100644 index 3a19e4e..0000000 --- a/matrix/common/src/testFixtures/kotlin/fixture/ModelFixtures.kt +++ /dev/null @@ -1,15 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.* - -fun aUserId(value: String = "a-user-id") = UserId(value) -fun aRoomId(value: String = "a-room-id") = RoomId(value) -fun anEventId(value: String = "an-event-id") = EventId(value) -fun aDeviceId(value: String = "a-device-id") = DeviceId(value) -fun aSessionId(value: String = "a-session-id") = SessionId(value) -fun aCipherText(value: String = "cipher-content") = CipherText(value) -fun aCurve25519(value: String = "curve-value") = Curve25519(value) -fun aEd25519(value: String = "ed-value") = Ed25519(value) -fun anAlgorithmName(value: String = "an-algorithm") = AlgorithmName(value) -fun aJsonString(value: String = "{}") = JsonString(value) -fun aSyncToken(value: String = "a-sync-token") = SyncToken(value) \ No newline at end of file diff --git a/matrix/common/src/testFixtures/kotlin/fixture/RoomMemberFixture.kt b/matrix/common/src/testFixtures/kotlin/fixture/RoomMemberFixture.kt deleted file mode 100644 index cf21486..0000000 --- a/matrix/common/src/testFixtures/kotlin/fixture/RoomMemberFixture.kt +++ /dev/null @@ -1,11 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.AvatarUrl -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.common.UserId - -fun aRoomMember( - id: UserId = aUserId(), - displayName: String? = null, - avatarUrl: AvatarUrl? = null -) = RoomMember(id, displayName, avatarUrl) \ No newline at end of file diff --git a/matrix/common/src/testFixtures/kotlin/fixture/SharedRoomKeyFixture.kt b/matrix/common/src/testFixtures/kotlin/fixture/SharedRoomKeyFixture.kt deleted file mode 100644 index 5f09dfd..0000000 --- a/matrix/common/src/testFixtures/kotlin/fixture/SharedRoomKeyFixture.kt +++ /dev/null @@ -1,14 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.SessionId -import app.dapk.st.matrix.common.SharedRoomKey - -fun aSharedRoomKey( - algorithmName: AlgorithmName = anAlgorithmName(), - roomId: RoomId = aRoomId(), - sessionId: SessionId = aSessionId(), - sessionKey: String = "a-session-key", - isExported: Boolean = false, -) = SharedRoomKey(algorithmName, roomId, sessionId, sessionKey, isExported) \ No newline at end of file diff --git a/matrix/common/src/testFixtures/kotlin/fixture/UserCredentialsFixture.kt b/matrix/common/src/testFixtures/kotlin/fixture/UserCredentialsFixture.kt deleted file mode 100644 index 081b3f1..0000000 --- a/matrix/common/src/testFixtures/kotlin/fixture/UserCredentialsFixture.kt +++ /dev/null @@ -1,13 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.HomeServerUrl -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.common.UserId - -fun aUserCredentials( - accessToken: String = "an-access-token", - homeServer: HomeServerUrl = HomeServerUrl("homserver-url"), - userId: UserId = aUserId(), - deviceId: DeviceId = aDeviceId(), -) = UserCredentials(accessToken, homeServer, userId, deviceId) \ No newline at end of file diff --git a/matrix/matrix-http-ktor/build.gradle b/matrix/matrix-http-ktor/build.gradle deleted file mode 100644 index bf47bed..0000000 --- a/matrix/matrix-http-ktor/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id 'kotlin' -} - -dependencies { - implementation project(":matrix:common") - api project(":matrix:matrix-http") - - implementation Dependencies.mavenCentral.ktorCore - implementation Dependencies.mavenCentral.ktorSerialization - implementation Dependencies.mavenCentral.ktorLogging - implementation Dependencies.mavenCentral.ktorContentNegotiation - implementation Dependencies.mavenCentral.ktorJson -} \ No newline at end of file diff --git a/matrix/matrix-http-ktor/src/main/kotlin/app/dapk/st/matrix/http/ktor/KtorMatrixHttpClientFactory.kt b/matrix/matrix-http-ktor/src/main/kotlin/app/dapk/st/matrix/http/ktor/KtorMatrixHttpClientFactory.kt deleted file mode 100644 index 3dcb046..0000000 --- a/matrix/matrix-http-ktor/src/main/kotlin/app/dapk/st/matrix/http/ktor/KtorMatrixHttpClientFactory.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.dapk.st.matrix.http.ktor - -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.ktor.internal.KtorMatrixHttpClient -import io.ktor.client.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.serialization.kotlinx.json.* -import kotlinx.serialization.json.Json - -class KtorMatrixHttpClientFactory( - private val credentialsStore: CredentialsStore, - private val includeLogging: Boolean, -) : MatrixHttpClient.Factory { - - override fun create(jsonInstance: Json): MatrixHttpClient { - val client = HttpClient { - install(ContentNegotiation) { - json(jsonInstance) - } - expectSuccess = true - if (includeLogging) { - install(Logging) { - logger = Logger.SIMPLE - level = LogLevel.ALL - } - } - } - return KtorMatrixHttpClient(client, credentialsStore) - } - -} \ No newline at end of file diff --git a/matrix/matrix-http-ktor/src/main/kotlin/app/dapk/st/matrix/http/ktor/internal/KtorMatrixHttpClient.kt b/matrix/matrix-http-ktor/src/main/kotlin/app/dapk/st/matrix/http/ktor/internal/KtorMatrixHttpClient.kt deleted file mode 100644 index 5cb157e..0000000 --- a/matrix/matrix-http-ktor/src/main/kotlin/app/dapk/st/matrix/http/ktor/internal/KtorMatrixHttpClient.kt +++ /dev/null @@ -1,107 +0,0 @@ -package app.dapk.st.matrix.http.ktor.internal - -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.Method -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.util.* - -internal class KtorMatrixHttpClient( - private val client: HttpClient, - private val tokenProvider: CredentialsStore -) : MatrixHttpClient { - - @Suppress("UNCHECKED_CAST") - override suspend fun execute(request: MatrixHttpClient.HttpRequest): T { - return when { - !request.authenticated -> { - request.execute { buildRequest(credentials = null, request) } - } - else -> authenticatedRequest(request) - } - } - - private suspend fun authenticatedRequest(request: MatrixHttpClient.HttpRequest) = - when (val initialCredentials = tokenProvider.credentials()) { - null -> { - val credentials = authenticate() - request.execute { - buildAuthenticatedRequest( - request, - credentials - ) - } - } - else -> withTokenRetry(initialCredentials) { token -> - request.execute { - buildAuthenticatedRequest( - request, - token - ) - } - } - } - - private suspend fun withTokenRetry(originalCredentials: UserCredentials, request: suspend (UserCredentials) -> T): T { - return try { - request(originalCredentials) - } catch (error: ClientRequestException) { - if (error.response.status.value == 401) { - val token = authenticate() - request(token) - } else { - throw error - } - } - } - - - suspend fun authenticate(): UserCredentials { - throw Error() // TODO -// val tokenResult = client.request { buildRequest(AuthEndpoint.anonAccessToken()) } -// tokenProvider.update(tokenResult.accessToken) -// return tokenResult.accessToken - } - - @OptIn(InternalAPI::class) - private fun HttpRequestBuilder.buildRequest( - credentials: UserCredentials?, - request: MatrixHttpClient.HttpRequest - ) { - val host = - request.baseUrl ?: credentials?.homeServer?.value ?: throw Error() - this.url("$host${request.path}") - this.method = when (request.method) { - Method.GET -> HttpMethod.Get - Method.POST -> HttpMethod.Post - Method.DELETE -> HttpMethod.Delete - Method.PUT -> HttpMethod.Put - } - this.headers.apply { - request.headers.forEach { - append(it.first, it.second) - } - } - this.body = request.body - } - - private fun HttpRequestBuilder.buildAuthenticatedRequest( - request: MatrixHttpClient.HttpRequest, - credentials: UserCredentials - ) { - this.buildRequest(credentials, request) - this.headers.apply { - append(HttpHeaders.Authorization, "Bearer ${credentials.accessToken}") - } - } - - @Suppress("UNCHECKED_CAST") - private suspend fun MatrixHttpClient.HttpRequest.execute(requestBuilder: HttpRequestBuilder.() -> Unit): T { - return client.request { requestBuilder(this) }.call.body(this.typeInfo) as T - } - -} diff --git a/matrix/matrix-http/build.gradle b/matrix/matrix-http/build.gradle deleted file mode 100644 index 7f3ef17..0000000 --- a/matrix/matrix-http/build.gradle +++ /dev/null @@ -1,11 +0,0 @@ -plugins { - id 'kotlin' - id 'java-test-fixtures' -} - -dependencies { - api Dependencies.mavenCentral.ktorCore - implementation Dependencies.mavenCentral.kotlinSerializationJson - - kotlinFixtures(it) -} \ No newline at end of file diff --git a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/HttpExtensions.kt b/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/HttpExtensions.kt deleted file mode 100644 index edf24b3..0000000 --- a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/HttpExtensions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.dapk.st.matrix.http - -fun String.ensureTrailingSlash(): String { - return if (this.endsWith("/")) this else "$this/" -} - -fun String.ensureHttpsIfMissing(): String { - return if (this.startsWith("http")) this else "https://$this" -} diff --git a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/JsonExtensions.kt b/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/JsonExtensions.kt deleted file mode 100644 index 58d9bc2..0000000 --- a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/JsonExtensions.kt +++ /dev/null @@ -1,51 +0,0 @@ -package app.dapk.st.matrix.http - -import io.ktor.http.* -import io.ktor.http.content.* -import kotlinx.serialization.KSerializer -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -fun jsonBody(serializer: KSerializer, payload: T, json: Json = MatrixHttpClient.json): OutgoingContent { - return EqualTextContent( - TextContent( - text = json.encodeToString(serializer, payload), - contentType = ContentType.Application.Json, - ) - ) -} - -inline fun jsonBody(payload: T, json: Json = MatrixHttpClient.json): OutgoingContent { - return EqualTextContent( - TextContent( - text = json.encodeToString(payload), - contentType = ContentType.Application.Json, - ) - ) -} - -fun emptyJsonBody(): OutgoingContent { - return EqualTextContent(TextContent("{}", ContentType.Application.Json)) -} - -class EqualTextContent( - private val textContent: TextContent, -) : OutgoingContent.ByteArrayContent() { - - override fun bytes() = textContent.bytes() - override val contentLength: Long - get() = textContent.contentLength - - override fun toString(): String = textContent.toString() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as EqualTextContent - if (!bytes().contentEquals(other.bytes())) return false - return true - } - - override fun hashCode() = bytes().hashCode() - -} diff --git a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt b/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt deleted file mode 100644 index 6cf64a5..0000000 --- a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/MatrixHttpClient.kt +++ /dev/null @@ -1,61 +0,0 @@ -package app.dapk.st.matrix.http - -import io.ktor.client.utils.* -import io.ktor.http.content.* -import io.ktor.util.reflect.* -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json - -interface MatrixHttpClient { - - suspend fun execute(request: HttpRequest): T - - data class HttpRequest constructor( - val path: String, - val method: Method, - val body: OutgoingContent = EmptyContent, - val headers: List> = emptyList(), - val authenticated: Boolean = true, - val setAcceptLanguage: Boolean = true, - val baseUrl: String? = null, - val typeInfo: TypeInfo, - ) { - - companion object { - inline fun httpRequest( - path: String, - method: Method, - body: OutgoingContent = EmptyContent, - headers: List> = emptyList(), - authenticated: Boolean = true, - setAcceptLanguage: Boolean = true, - baseUrl: String? = null, - ) = HttpRequest( - path, - method, - body, - headers, - authenticated, - setAcceptLanguage, - baseUrl, - typeInfo = typeInfo() - ) - } - - } - - enum class Method { GET, POST, DELETE, PUT } - - companion object { - val json = Json - @OptIn(ExperimentalSerializationApi::class) - val jsonWithDefaults = Json { - encodeDefaults = true - explicitNulls = false - } - } - - fun interface Factory { - fun create(json: Json): MatrixHttpClient - } -} diff --git a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/NullableJsonTransformingSerializer.kt b/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/NullableJsonTransformingSerializer.kt deleted file mode 100644 index 5f17673..0000000 --- a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/NullableJsonTransformingSerializer.kt +++ /dev/null @@ -1,26 +0,0 @@ -package app.dapk.st.matrix.http - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonElement - -abstract class NullableJsonTransformingSerializer( - private val tSerializer: KSerializer, - private val deserializer: (JsonElement) -> JsonElement? -) : KSerializer { - - override val descriptor: SerialDescriptor get() = tSerializer.descriptor - - final override fun deserialize(decoder: Decoder): T? { - require(decoder is JsonDecoder) - val element = decoder.decodeJsonElement() - return deserializer(element)?.let { decoder.json.decodeFromJsonElement(tSerializer, it) } - } - - final override fun serialize(encoder: Encoder, value: T?) { - throw IllegalAccessError("serialize not supported") - } -} \ No newline at end of file diff --git a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/RequestExtensions.kt b/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/RequestExtensions.kt deleted file mode 100644 index e19b0bf..0000000 --- a/matrix/matrix-http/src/main/kotlin/app/dapk/st/matrix/http/RequestExtensions.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.http - -fun queryMap(vararg params: Pair): String { - return params.filterNot { it.second == null }.joinToString(separator = "&") { (key, value) -> - "$key=${value}" - } -} diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt deleted file mode 100644 index fd08667..0000000 --- a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt +++ /dev/null @@ -1,16 +0,0 @@ -package fake - -import io.ktor.client.statement.* -import io.ktor.http.* -import io.mockk.every -import io.mockk.mockk - -class FakeHttpResponse { - - val instance = mockk(relaxed = true) - - fun givenStatus(code: Int) { - every { instance.status } returns HttpStatusCode(code, "") - } - -} \ No newline at end of file diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt deleted file mode 100644 index 1742bfd..0000000 --- a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt +++ /dev/null @@ -1,16 +0,0 @@ -package fake - -import app.dapk.st.matrix.http.MatrixHttpClient -import io.ktor.client.plugins.* -import io.mockk.coEvery -import io.mockk.mockk - -class FakeMatrixHttpClient : MatrixHttpClient by mockk() { - fun given(request: MatrixHttpClient.HttpRequest, response: T) { - coEvery { execute(request) } returns response - } - - fun errors(request: MatrixHttpClient.HttpRequest, cause: Throwable) { - coEvery { execute(request) } throws cause - } -} diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt b/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt deleted file mode 100644 index 7722914..0000000 --- a/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt +++ /dev/null @@ -1,14 +0,0 @@ -package fixture - -import fake.FakeHttpResponse -import io.ktor.client.plugins.* - -fun a404HttpError() = ClientRequestException( - FakeHttpResponse().apply { givenStatus(404) }.instance, - cachedResponseText = "" -) - -fun a403HttpError() = ClientRequestException( - FakeHttpResponse().apply { givenStatus(403) }.instance, - cachedResponseText = "" -) diff --git a/matrix/matrix/build.gradle b/matrix/matrix/build.gradle deleted file mode 100644 index e30048b..0000000 --- a/matrix/matrix/build.gradle +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id 'kotlin' -} - -dependencies { - implementation project(":matrix:matrix-http") - implementation project(":matrix:common") - implementation Dependencies.mavenCentral.kotlinSerializationJson -} \ No newline at end of file diff --git a/matrix/matrix/src/main/kotlin/app/dapk/st/matrix/MatrixClient.kt b/matrix/matrix/src/main/kotlin/app/dapk/st/matrix/MatrixClient.kt deleted file mode 100644 index 695dff4..0000000 --- a/matrix/matrix/src/main/kotlin/app/dapk/st/matrix/MatrixClient.kt +++ /dev/null @@ -1,72 +0,0 @@ -package app.dapk.st.matrix - -import app.dapk.st.matrix.MatrixTaskRunner.MatrixTask -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.http.MatrixHttpClient -import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.SerializersModuleBuilder - -class MatrixClient( - private val httpClientFactory: MatrixHttpClient.Factory, - private val logger: MatrixLogger, -) : MatrixServiceProvider { - - private val serviceInstaller = ServiceInstaller() - - fun install(scope: MatrixServiceInstaller.() -> Unit) { - serviceInstaller.install(httpClientFactory, logger, scope) - } - - override fun getService(key: ServiceKey): T { - return serviceInstaller.getService(key) - } - - suspend fun run(task: MatrixTask): MatrixTaskRunner.TaskResult { - return serviceInstaller.delegate(task) - } -} - -typealias ServiceKey = Any - -interface MatrixService { - fun interface Factory { - fun create(deps: ServiceDependencies): Pair - } -} - -data class ServiceDependencies( - val httpClient: MatrixHttpClient, - val json: Json, - val services: MatrixServiceProvider, - val logger: MatrixLogger, -) - -interface MatrixServiceInstaller { - fun serializers(builder: SerializersModuleBuilder.() -> Unit) - fun install(factory: MatrixService.Factory): InstallExtender -} - -interface InstallExtender { - fun proxy(proxy: (T) -> T) -} - -interface MatrixServiceProvider { - fun getService(key: ServiceKey): T -} - -fun interface ServiceDepFactory { - fun create(services: MatrixServiceProvider): T -} - -interface MatrixTaskRunner { - suspend fun canRun(task: MatrixTask): Boolean = false - suspend fun run(task: MatrixTask): TaskResult = throw IllegalArgumentException("Should only be invoked if canRun == true") - - data class MatrixTask(val type: String, val jsonPayload: String) - - sealed interface TaskResult { - object Success : TaskResult - data class Failure(val canRetry: Boolean) : TaskResult - } - -} diff --git a/matrix/matrix/src/main/kotlin/app/dapk/st/matrix/ServiceInstaller.kt b/matrix/matrix/src/main/kotlin/app/dapk/st/matrix/ServiceInstaller.kt deleted file mode 100644 index a2d6a1c..0000000 --- a/matrix/matrix/src/main/kotlin/app/dapk/st/matrix/ServiceInstaller.kt +++ /dev/null @@ -1,76 +0,0 @@ -package app.dapk.st.matrix - -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.http.MatrixHttpClient -import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.SerializersModuleBuilder - -internal class ServiceInstaller { - - private val services = mutableMapOf() - private val serviceInstaller = object : MatrixServiceInstaller { - - val serviceCollector = mutableListOf MatrixService>>() - val serializers = mutableListOf Unit>() - - override fun serializers(builder: SerializersModuleBuilder.() -> Unit) { - serializers.add(builder) - } - - override fun install(factory: MatrixService.Factory): InstallExtender { - val mutableProxy = MutableProxy() - return object : InstallExtender { - override fun proxy(proxy: (T) -> T) { - mutableProxy.value = proxy - } - }.also { - serviceCollector.add(factory to mutableProxy) - } - } - } - - fun install(httpClientFactory: MatrixHttpClient.Factory, logger: MatrixLogger, scope: MatrixServiceInstaller.() -> Unit) { - scope(serviceInstaller) - val json = Json { - isLenient = true - ignoreUnknownKeys = true - serializersModule = SerializersModule { - serviceInstaller.serializers.forEach { - it.invoke(this) - } - } - } - - val httpClient = httpClientFactory.create(json) - val serviceProvider = object : MatrixServiceProvider { - override fun getService(key: ServiceKey) = this@ServiceInstaller.getService(key) - } - serviceInstaller.serviceCollector.forEach { (factory, extender) -> - val (key, service) = factory.create(ServiceDependencies(httpClient, json, serviceProvider, logger)) - services[key] = extender(service) - } - } - - @Suppress("UNCHECKED_CAST") - fun getService(key: ServiceKey): T { - return services[key] as T - } - - suspend fun delegate(task: MatrixTaskRunner.MatrixTask): MatrixTaskRunner.TaskResult { - return services.values - .filterIsInstance() - .firstOrNull { it.canRun(task) }?.run(task) - ?: throw IllegalArgumentException("No service available to handle ${task.type}") - } - -} - -internal class MutableProxy : (MatrixService) -> MatrixService { - - var value: (T) -> T = { it } - - @Suppress("UNCHECKED_CAST") - override fun invoke(service: MatrixService) = value(service as T) - -} \ No newline at end of file diff --git a/matrix/services/auth/build.gradle b/matrix/services/auth/build.gradle deleted file mode 100644 index 3dcc229..0000000 --- a/matrix/services/auth/build.gradle +++ /dev/null @@ -1 +0,0 @@ -applyMatrixServiceModule(project) diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/AuthService.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/AuthService.kt deleted file mode 100644 index b9639b3..0000000 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/AuthService.kt +++ /dev/null @@ -1,42 +0,0 @@ -package app.dapk.st.matrix.auth - -import app.dapk.st.matrix.InstallExtender -import app.dapk.st.matrix.MatrixClient -import app.dapk.st.matrix.MatrixService -import app.dapk.st.matrix.MatrixServiceInstaller -import app.dapk.st.matrix.auth.internal.DefaultAuthService -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.UserCredentials - -private val SERVICE_KEY = AuthService::class - -interface AuthService : MatrixService { - suspend fun login(request: LoginRequest): LoginResult - suspend fun register(userName: String, password: String, homeServer: String): UserCredentials - - - sealed interface LoginResult { - data class Success(val userCredentials: UserCredentials) : LoginResult - object MissingWellKnown : LoginResult - data class Error(val cause: Throwable) : LoginResult - } - - data class LoginRequest(val userName: String, val password: String, val serverUrl: String?) -} - -fun MatrixServiceInstaller.installAuthService( - credentialsStore: CredentialsStore, - deviceDisplayNameGenerator: DeviceDisplayNameGenerator = DefaultDeviceDisplayNameGenerator, -): InstallExtender { - return this.install { (httpClient, json) -> - SERVICE_KEY to DefaultAuthService(httpClient, credentialsStore, json, deviceDisplayNameGenerator) - } -} - -fun MatrixClient.authService(): AuthService = this.getService(key = SERVICE_KEY) - -fun interface DeviceDisplayNameGenerator { - fun generate(): String? -} - -val DefaultDeviceDisplayNameGenerator = DeviceDisplayNameGenerator { null } \ No newline at end of file diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/AuthRequest.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/AuthRequest.kt deleted file mode 100644 index 94264da..0000000 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/AuthRequest.kt +++ /dev/null @@ -1,106 +0,0 @@ -package app.dapk.st.matrix.auth.internal - -import app.dapk.st.matrix.common.DeviceCredentials -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.HomeServerUrl -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest -import app.dapk.st.matrix.http.emptyJsonBody -import app.dapk.st.matrix.http.jsonBody -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -fun loginRequest(userId: UserId, password: String, baseUrl: String, deviceDisplayName: String?) = httpRequest( - path = "_matrix/client/r0/login", - method = MatrixHttpClient.Method.POST, - body = jsonBody( - PasswordLoginRequest.serializer(), - PasswordLoginRequest(PasswordLoginRequest.UserIdentifier(userId), password, deviceDisplayName), - MatrixHttpClient.jsonWithDefaults - ), - authenticated = false, - baseUrl = baseUrl, -) - -fun registerStartFlowRequest(baseUrl: String) = httpRequest( - path = "_matrix/client/r0/register", - method = MatrixHttpClient.Method.POST, - body = emptyJsonBody(), - authenticated = false, - baseUrl = baseUrl, -) - -internal fun registerRequest(userName: String, password: String, baseUrl: String, auth: Auth?) = httpRequest( - path = "_matrix/client/r0/register", - method = MatrixHttpClient.Method.POST, - body = jsonBody( - PasswordRegisterRequest(userName, password, auth?.let { PasswordRegisterRequest.Auth(it.session, it.type) }), - MatrixHttpClient.jsonWithDefaults - ), - authenticated = false, - baseUrl = baseUrl, -) - -internal fun wellKnownRequest(baseUrl: String) = httpRequest( - path = ".well-known/matrix/client", - method = MatrixHttpClient.Method.GET, - baseUrl = baseUrl, - authenticated = false, -) - -typealias RawResponse = ByteArray - -fun RawResponse.readString() = this.toString(Charsets.UTF_8) - -internal data class Auth( - val session: String, - val type: String, -) - -@Serializable -data class ApiAuthResponse( - @SerialName("access_token") val accessToken: String, - @SerialName("home_server") val homeServer: String, - @SerialName("user_id") override val userId: UserId, - @SerialName("device_id") override val deviceId: DeviceId, - @SerialName("well_known") val wellKnown: ApiWellKnown? = null, -) : DeviceCredentials - -@Serializable -data class ApiWellKnown( - @SerialName("m.homeserver") val homeServer: HomeServer -) { - @Serializable - data class HomeServer( - @SerialName("base_url") val baseUrl: HomeServerUrl, - ) -} - -@Serializable -internal data class PasswordLoginRequest( - @SerialName("identifier") val userName: UserIdentifier, - @SerialName("password") val password: String, - @SerialName("initial_device_display_name") val deviceDisplayName: String?, - @SerialName("type") val type: String = "m.login.password", -) { - - @Serializable - internal data class UserIdentifier( - @SerialName("user") val userName: UserId, - @SerialName("type") val type: String = "m.id.user", - ) -} - -@Serializable -internal data class PasswordRegisterRequest( - @SerialName("username") val userName: String, - @SerialName("password") val password: String, - @SerialName("auth") val auth: Auth?, -) { - @Serializable - data class Auth( - @SerialName("session") val session: String, - @SerialName("type") val type: String, - ) -} diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/DefaultAuthService.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/DefaultAuthService.kt deleted file mode 100644 index b2f30bb..0000000 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/DefaultAuthService.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.dapk.st.matrix.auth.internal - -import app.dapk.st.matrix.auth.AuthService -import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.HomeServerUrl -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.ensureHttpsIfMissing -import app.dapk.st.matrix.http.ensureTrailingSlash -import kotlinx.serialization.json.Json - -internal class DefaultAuthService( - httpClient: MatrixHttpClient, - credentialsStore: CredentialsStore, - json: Json, - deviceDisplayNameGenerator: DeviceDisplayNameGenerator, -) : AuthService { - - private val fetchWellKnownUseCase = FetchWellKnownUseCaseImpl(httpClient, json) - private val loginUseCase = LoginWithUserPasswordUseCase(httpClient, credentialsStore, fetchWellKnownUseCase, deviceDisplayNameGenerator) - private val loginServerUseCase = LoginWithUserPasswordServerUseCase(httpClient, credentialsStore, deviceDisplayNameGenerator) - private val registerCase = RegisterUseCase(httpClient, credentialsStore, json, fetchWellKnownUseCase) - - override suspend fun login(request: AuthService.LoginRequest): AuthService.LoginResult { - return when { - request.serverUrl == null -> loginUseCase.login(request.userName, request.password) - else -> { - val serverUrl = HomeServerUrl(request.serverUrl.ensureHttpsIfMissing().ensureTrailingSlash()) - loginServerUseCase.login(request.userName, request.password, serverUrl) - } - } - } - - override suspend fun register(userName: String, password: String, homeServer: String): UserCredentials { - return registerCase.register(userName, password, homeServer) - } - -} \ No newline at end of file diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/FetchWellKnownUseCaseImpl.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/FetchWellKnownUseCaseImpl.kt deleted file mode 100644 index cc175fa..0000000 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/FetchWellKnownUseCaseImpl.kt +++ /dev/null @@ -1,51 +0,0 @@ -package app.dapk.st.matrix.auth.internal - -import app.dapk.st.matrix.http.MatrixHttpClient -import io.ktor.client.plugins.* -import io.ktor.http.* -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json -import java.net.UnknownHostException -import java.nio.charset.Charset - -internal typealias FetchWellKnownUseCase = suspend (String) -> WellKnownResult - -internal class FetchWellKnownUseCaseImpl( - private val httpClient: MatrixHttpClient, - private val json: Json, -) : FetchWellKnownUseCase { - - override suspend fun invoke(domainUrl: String): WellKnownResult { - return runCatching { - val rawResponse = httpClient.execute(rawWellKnownRequestForServersWithoutContentTypes(domainUrl)) - json.decodeFromString(ApiWellKnown.serializer(), rawResponse.readString()) - } - .fold( - onSuccess = { WellKnownResult.Success(it) }, - onFailure = { - when (it) { - is UnknownHostException -> WellKnownResult.MissingWellKnown - is ClientRequestException -> when { - it.response.status.is404() -> WellKnownResult.MissingWellKnown - else -> WellKnownResult.Error(it) - } - is SerializationException -> WellKnownResult.InvalidWellKnown - else -> WellKnownResult.Error(it) - } - }, - ) - } - - private fun rawWellKnownRequestForServersWithoutContentTypes(domainUrl: String) = wellKnownRequest(domainUrl) - -} - -sealed interface WellKnownResult { - data class Success(val wellKnown: ApiWellKnown) : WellKnownResult - object MissingWellKnown : WellKnownResult - object InvalidWellKnown : WellKnownResult - data class Error(val cause: Throwable) : WellKnownResult - -} - -fun HttpStatusCode.is404() = this.value == 404 \ No newline at end of file diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordServerUseCase.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordServerUseCase.kt deleted file mode 100644 index a62b432..0000000 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordServerUseCase.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.dapk.st.matrix.auth.internal - -import app.dapk.st.matrix.auth.AuthService -import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.HomeServerUrl -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.http.MatrixHttpClient - -class LoginWithUserPasswordServerUseCase( - private val httpClient: MatrixHttpClient, - private val credentialsProvider: CredentialsStore, - private val deviceDisplayNameGenerator: DeviceDisplayNameGenerator, -) { - - suspend fun login(userName: String, password: String, serverUrl: HomeServerUrl): AuthService.LoginResult { - return runCatching { - authenticate(serverUrl, UserId(userName.substringBefore(":")), password) - }.fold( - onSuccess = { AuthService.LoginResult.Success(it) }, - onFailure = { AuthService.LoginResult.Error(it) } - ) - } - - private suspend fun authenticate(baseUrl: HomeServerUrl, fullUserId: UserId, password: String): UserCredentials { - val authResponse = httpClient.execute(loginRequest(fullUserId, password, baseUrl.value, deviceDisplayNameGenerator.generate())) - return UserCredentials( - authResponse.accessToken, - baseUrl, - authResponse.userId, - authResponse.deviceId, - ).also { credentialsProvider.update(it) } - } -} diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordUseCase.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordUseCase.kt deleted file mode 100644 index f13e658..0000000 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/LoginWithUserPasswordUseCase.kt +++ /dev/null @@ -1,73 +0,0 @@ -package app.dapk.st.matrix.auth.internal - -import app.dapk.st.matrix.auth.AuthService -import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.HomeServerUrl -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.ensureTrailingSlash - -private const val MATRIX_DOT_ORG_DOMAIN = "matrix.org" - -class LoginWithUserPasswordUseCase( - private val httpClient: MatrixHttpClient, - private val credentialsProvider: CredentialsStore, - private val fetchWellKnownUseCase: FetchWellKnownUseCase, - private val deviceDisplayNameGenerator: DeviceDisplayNameGenerator, -) { - - suspend fun login(userName: String, password: String): AuthService.LoginResult { - val (domainUrl, fullUserId) = generateUserAccessInfo(userName) - return when (val wellKnownResult = fetchWellKnownUseCase(domainUrl)) { - is WellKnownResult.Success -> { - runCatching { - authenticate(wellKnownResult.wellKnown.homeServer.baseUrl.ensureTrailingSlash(), fullUserId, password) - }.fold( - onSuccess = { AuthService.LoginResult.Success(it) }, - onFailure = { AuthService.LoginResult.Error(it) } - ) - } - - WellKnownResult.InvalidWellKnown -> AuthService.LoginResult.MissingWellKnown - WellKnownResult.MissingWellKnown -> AuthService.LoginResult.MissingWellKnown - is WellKnownResult.Error -> AuthService.LoginResult.Error(wellKnownResult.cause) - } - } - - private fun generateUserAccessInfo(userName: String): Pair { - val cleanedUserName = userName.ensureStartsWithAt().trim() - val domain = cleanedUserName.findDomain(fallback = MATRIX_DOT_ORG_DOMAIN) - val domainUrl = domain.asHttpsUrl() - val fullUserId = cleanedUserName.ensureHasDomain(domain) - return Pair(domainUrl, UserId(fullUserId)) - } - - private suspend fun authenticate(baseUrl: HomeServerUrl, fullUserId: UserId, password: String): UserCredentials { - val authResponse = httpClient.execute(loginRequest(fullUserId, password, baseUrl.value, deviceDisplayNameGenerator.generate())) - return UserCredentials( - authResponse.accessToken, - baseUrl, - authResponse.userId, - authResponse.deviceId, - ).also { credentialsProvider.update(it) } - } - - private fun String.findDomain(fallback: String) = this.substringAfter(":", missingDelimiterValue = fallback) - - private fun String.asHttpsUrl(): String { - return "https://$this".ensureTrailingSlash() - } -} - -private fun HomeServerUrl.ensureTrailingSlash() = HomeServerUrl(this.value.ensureTrailingSlash()) - -private fun String.ensureHasDomain(domain: String) = if (this.endsWith(domain)) this else "$this:$domain" - -private fun String.ensureStartsWithAt(): String { - return when (this.startsWith("@")) { - true -> this - false -> "@$this" - } -} diff --git a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/RegisterUseCase.kt b/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/RegisterUseCase.kt deleted file mode 100644 index d611e88..0000000 --- a/matrix/services/auth/src/main/kotlin/app/dapk/st/matrix/auth/internal/RegisterUseCase.kt +++ /dev/null @@ -1,74 +0,0 @@ -package app.dapk.st.matrix.auth.internal - -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.ensureTrailingSlash -import io.ktor.client.plugins.* -import io.ktor.client.statement.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json - -class RegisterUseCase( - private val httpClient: MatrixHttpClient, - private val credentialsProvider: CredentialsStore, - private val json: Json, - private val fetchWellKnownUseCase: FetchWellKnownUseCase, -) { - - suspend fun register(userName: String, password: String, homeServer: String): UserCredentials { - val baseUrl = homeServer.ifEmpty { "https://${userName.split(":").last()}/" }.ensureTrailingSlash() - - return try { - httpClient.execute(registerStartFlowRequest(baseUrl)) - throw IllegalStateException("the first request is expected to return a 401") - } catch (error: ClientRequestException) { - when (error.response.status.value) { - 401 -> { - val stage0 = json.decodeFromString(ApiUserInteractive.serializer(), error.response.bodyAsText()) - val supportsDummy = stage0.flows.any { it.stages.any { it == "m.login.dummy" } } - if (supportsDummy) { - registerAccount(userName, password, baseUrl, stage0.session) - } else { - throw error - } - } - else -> throw error - } - } - } - - private suspend fun registerAccount(userName: String, password: String, baseUrl: String, session: String): UserCredentials { - val authResponse = httpClient.execute( - registerRequest(userName, password, baseUrl, Auth(session, "m.login.dummy")) - ) - val homeServerUrl = when (authResponse.wellKnown == null) { - true -> when (val wellKnownResult = fetchWellKnownUseCase(baseUrl)) { - is WellKnownResult.Error, -> TODO() - WellKnownResult.InvalidWellKnown -> TODO() - WellKnownResult.MissingWellKnown -> TODO() - is WellKnownResult.Success -> wellKnownResult.wellKnown.homeServer.baseUrl - } - false -> authResponse.wellKnown.homeServer.baseUrl - } - return UserCredentials( - authResponse.accessToken, - homeServerUrl, - authResponse.userId, - authResponse.deviceId, - ).also { credentialsProvider.update(it) } - } -} - -@Serializable -internal data class ApiUserInteractive( - @SerialName("flows") val flows: List, - @SerialName("session") val session: String, -) { - @Serializable - data class Flow( - @SerialName("stages") val stages: List - ) - -} \ No newline at end of file diff --git a/matrix/services/crypto/build.gradle b/matrix/services/crypto/build.gradle deleted file mode 100644 index d9e49d4..0000000 --- a/matrix/services/crypto/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -plugins { id 'java-test-fixtures' } -applyMatrixServiceModule(project) - -dependencies { - implementation project(":core") - implementation project(":matrix:services:device") - - kotlinTest(it) - kotlinFixtures(it) - testImplementation(testFixtures(project(":matrix:common"))) - testImplementation(testFixtures(project(":matrix:matrix-http"))) - testImplementation(testFixtures(project(":core"))) - testImplementation(testFixtures(project(":matrix:services:device"))) - testFixturesImplementation(testFixtures(project(":matrix:common"))) - testFixturesImplementation(testFixtures(project(":matrix:services:device"))) - testFixturesImplementation(testFixtures(project(":core"))) -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt deleted file mode 100644 index d4ac8d8..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt +++ /dev/null @@ -1,193 +0,0 @@ -package app.dapk.st.matrix.crypto - -import app.dapk.st.core.Base64 -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.matrix.* -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.internal.* -import app.dapk.st.matrix.device.deviceService -import kotlinx.coroutines.flow.Flow -import java.io.InputStream -import java.net.URI - -private val SERVICE_KEY = CryptoService::class - -interface CryptoService : MatrixService { - suspend fun encrypt(input: InputStream): Crypto.MediaEncryptionResult - suspend fun encrypt(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult - suspend fun decrypt(encryptedPayload: EncryptedMessageContent): DecryptionResult - suspend fun importRoomKeys(keys: List) - suspend fun InputStream.importRoomKeys(password: String): Flow - - suspend fun maybeCreateMoreKeys(serverKeyCount: ServerKeyCount) - suspend fun updateOlmSession(userIds: List, syncToken: SyncToken?) - - suspend fun onVerificationEvent(payload: Verification.Event) - suspend fun verificationAction(verificationAction: Verification.Action) - fun verificationState(): Flow -} - -interface Crypto { - - data class EncryptionResult( - val algorithmName: AlgorithmName, - val senderKey: String, - val cipherText: CipherText, - val sessionId: SessionId, - val deviceId: DeviceId - ) - - data class MediaEncryptionResult( - val uri: URI, - val contentLength: Long, - val algorithm: String, - val ext: Boolean, - val keyOperations: List, - val kty: String, - val k: String, - val iv: String, - val hashes: Map, - val v: String, - ) - -} - - -object Verification { - - sealed interface State { - object Idle : State - object ReadySent : State - object WaitingForMatchConfirmation : State - object WaitingForDoneConfirmation : State - object Done : State - } - - sealed interface Event { - - data class Requested( - val userId: UserId, - val deviceId: DeviceId, - val transactionId: String, - val methods: List, - val timestamp: Long, - ) : Event - - data class Ready( - val userId: UserId, - val deviceId: DeviceId, - val transactionId: String, - val methods: List, - ) : Event - - data class Started( - val userId: UserId, - val fromDevice: DeviceId, - val method: String, - val protocols: List, - val hashes: List, - val codes: List, - val short: List, - val transactionId: String, - ) : Event - - data class Accepted( - val userId: UserId, - val fromDevice: DeviceId, - val method: String, - val protocol: String, - val hash: String, - val code: String, - val short: List, - val transactionId: String, - ) : Event - - data class Key( - val userId: UserId, - val transactionId: String, - val key: String, - ) : Event - - data class Mac( - val userId: UserId, - val transactionId: String, - val keys: String, - val mac: Map, - ) : Event - - data class Done(val transactionId: String) : Event - - } - - sealed interface Action { - object SecureAccept : Action - object InsecureAccept : Action - object AcknowledgeMatch : Action - data class Request(val userId: UserId, val deviceId: DeviceId) : Action - } -} - -fun MatrixServiceInstaller.installCryptoService( - credentialsStore: CredentialsStore, - olm: Olm, - roomMembersProvider: ServiceDepFactory, - base64: Base64, - coroutineDispatchers: CoroutineDispatchers, -): InstallExtender { - return this.install { (_, _, services, logger) -> - val deviceService = services.deviceService() - val accountCryptoUseCase = FetchAccountCryptoUseCaseImpl(credentialsStore, olm, deviceService) - - val registerOlmSessionUseCase = RegisterOlmSessionUseCaseImpl(olm, deviceService, logger) - val encryptMegolmUseCase = EncryptMessageWithMegolmUseCaseImpl( - olm, - FetchMegolmSessionUseCaseImpl( - olm, - deviceService, - accountCryptoUseCase, - roomMembersProvider.create(services), - registerOlmSessionUseCase, - ShareRoomKeyUseCaseImpl(credentialsStore, deviceService, logger, olm), - logger, - ), - logger, - ) - - val olmCrypto = OlmCrypto( - olm, - encryptMegolmUseCase, - accountCryptoUseCase, - UpdateKnownOlmSessionUseCaseImpl(accountCryptoUseCase, deviceService, registerOlmSessionUseCase, logger), - MaybeCreateAndUploadOneTimeKeysUseCaseImpl(accountCryptoUseCase, olm, credentialsStore, deviceService, logger), - logger - ) - val verificationHandler = VerificationHandler(deviceService, credentialsStore, logger, JsonCanonicalizer(), olm) - val roomKeyImporter = RoomKeyImporter(base64, coroutineDispatchers) - val mediaEncrypter = MediaEncrypter(base64) - - SERVICE_KEY to DefaultCryptoService(olmCrypto, verificationHandler, roomKeyImporter, mediaEncrypter, logger) - } -} - -fun MatrixServiceProvider.cryptoService(): CryptoService = this.getService(key = SERVICE_KEY) - -fun interface RoomMembersProvider { - suspend fun userIdsForRoom(roomId: RoomId): List -} - -sealed interface ImportResult { - data class Success(val roomIds: Set, val totalImportedKeysCount: Long) : ImportResult - data class Error(val cause: Type) : ImportResult { - - sealed interface Type { - data class Unknown(val cause: Throwable) : Type - object NoKeysFound : Type - object UnexpectedDecryptionOutput : Type - object UnableToOpenFile : Type - object InvalidFile : Type - } - - } - - data class Update(val importedKeysCount: Long) : ImportResult -} diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MatrixMediaDecrypter.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MatrixMediaDecrypter.kt deleted file mode 100644 index 65dde9e..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MatrixMediaDecrypter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package app.dapk.st.matrix.crypto - -import app.dapk.st.core.Base64 -import java.io.InputStream -import java.security.MessageDigest -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -private const val CRYPTO_BUFFER_SIZE = 32 * 1024 -private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding" -private const val SECRET_KEY_SPEC_ALGORITHM = "AES" -private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" - -class MatrixMediaDecrypter(private val base64: Base64) { - - fun decrypt(input: InputStream, k: String, iv: String): Collector { - val key = base64.decode(k.replace('-', '+').replace('_', '/')) - val initVectorBytes = base64.decode(iv) - - val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) - val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) - val ivParameterSpec = IvParameterSpec(initVectorBytes) - decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - - val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) - - var read: Int - val d = ByteArray(CRYPTO_BUFFER_SIZE) - var decodedBytes: ByteArray - - return Collector { partial -> - input.use { - read = it.read(d) - while (read != -1) { - messageDigest.update(d, 0, read) - decodedBytes = decryptCipher.update(d, 0, read) - partial(decodedBytes) - read = it.read(d) - } - } - } - } - -} - - -fun interface Collector { - fun collect(partial: (ByteArray) -> Unit) -} diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/Olm.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/Olm.kt deleted file mode 100644 index 4d2c208..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/Olm.kt +++ /dev/null @@ -1,89 +0,0 @@ -package app.dapk.st.matrix.crypto - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.internal.DeviceKeys - -interface Olm { - - companion object { - val ALGORITHM_MEGOLM = AlgorithmName("m.megolm.v1.aes-sha2") - val ALGORITHM_OLM = AlgorithmName("m.olm.v1.curve25519-aes-sha2") - } - - suspend fun ensureAccountCrypto(deviceCredentials: DeviceCredentials, onCreate: suspend (AccountCryptoSession) -> Unit): AccountCryptoSession - suspend fun ensureRoomCrypto(roomId: RoomId, accountSession: AccountCryptoSession): RoomCryptoSession - suspend fun ensureDeviceCrypto(input: OlmSessionInput, olmAccount: AccountCryptoSession): DeviceCryptoSession - suspend fun import(keys: List) - - suspend fun DeviceCryptoSession.encrypt(messageJson: JsonString): EncryptionResult - suspend fun RoomCryptoSession.encrypt(roomId: RoomId, messageJson: JsonString): CipherText - suspend fun AccountCryptoSession.generateOneTimeKeys( - count: Int, - credentials: DeviceCredentials, - publishKeys: suspend (DeviceService.OneTimeKeys) -> Unit - ) - - suspend fun decryptOlm(olmAccount: AccountCryptoSession, senderKey: Curve25519, type: Int, body: CipherText): DecryptionResult - suspend fun decryptMegOlm(sessionId: SessionId, cipherText: CipherText): DecryptionResult - suspend fun verifyExternalUser(keys: Ed25519?, recipeientKeys: Ed25519?): Boolean - suspend fun olmSessions(devices: List, onMissing: suspend (List) -> List): List - suspend fun sasSession(deviceCredentials: DeviceCredentials): SasSession - - interface SasSession { - suspend fun generateCommitment(hash: String, startJsonString: String): String - suspend fun calculateMac( - selfUserId: UserId, - selfDeviceId: DeviceId, - otherUserId: UserId, - otherDeviceId: DeviceId, - transactionId: String - ): MacResult - - fun release() - fun publicKey(): String - fun setTheirPublicKey(key: String) - } - - data class MacResult(val mac: Map, val keys: String) - - data class EncryptionResult( - val cipherText: CipherText, - val type: Long, - ) - - data class OlmSessionInput( - val oneTimeKey: String, - val identity: Curve25519, - val deviceId: DeviceId, - val userId: UserId, - val fingerprint: Ed25519, - ) - - data class DeviceCryptoSession( - val deviceId: DeviceId, - val userId: UserId, - val identity: Curve25519, - val fingerprint: Ed25519, - val olmSession: List, - ) - - data class AccountCryptoSession( - val fingerprint: Ed25519, - val senderKey: Curve25519, - val deviceKeys: DeviceKeys, - val hasKeys: Boolean, - val maxKeys: Int, - val olmAccount: Any, - ) - - data class RoomCryptoSession( - val creationTimestampUtc: Long, - val key: String, - val messageIndex: Int, - val accountCryptoSession: AccountCryptoSession, - val id: SessionId, - val outBound: Any, - ) - -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/AccountCryptoUseCase.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/AccountCryptoUseCase.kt deleted file mode 100644 index e979792..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/AccountCryptoUseCase.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.device.DeviceService - -internal typealias FetchAccountCryptoUseCase = suspend () -> Olm.AccountCryptoSession - -internal class FetchAccountCryptoUseCaseImpl( - private val credentialsStore: CredentialsStore, - private val olm: Olm, - private val deviceService: DeviceService -) : FetchAccountCryptoUseCase { - - override suspend fun invoke(): Olm.AccountCryptoSession { - val credentials = credentialsStore.credentials()!! - return olm.ensureAccountCrypto(credentials) { accountCryptoSession -> - deviceService.uploadDeviceKeys(accountCryptoSession.deviceKeys) - } - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt deleted file mode 100644 index c23cebc..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt +++ /dev/null @@ -1,65 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.core.logP -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.Crypto -import app.dapk.st.matrix.crypto.CryptoService -import app.dapk.st.matrix.crypto.ImportResult -import app.dapk.st.matrix.crypto.Verification -import kotlinx.coroutines.flow.Flow -import java.io.InputStream - -internal class DefaultCryptoService( - private val olmCrypto: OlmCrypto, - private val verificationHandler: VerificationHandler, - private val roomKeyImporter: RoomKeyImporter, - private val mediaEncrypter: MediaEncrypter, - private val logger: MatrixLogger, -) : CryptoService { - - override suspend fun encrypt(input: InputStream): Crypto.MediaEncryptionResult { - return mediaEncrypter.encrypt(input) - } - - override suspend fun encrypt(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult { - return olmCrypto.encryptMessage(roomId, credentials, messageJson) - } - - override suspend fun decrypt(encryptedPayload: EncryptedMessageContent): DecryptionResult { - return olmCrypto.decrypt(encryptedPayload).also { - logger.matrixLog("decrypted: $it") - } - } - - override suspend fun importRoomKeys(keys: List) { - olmCrypto.importRoomKeys(keys) - } - - override suspend fun maybeCreateMoreKeys(serverKeyCount: ServerKeyCount) { - olmCrypto.maybeCreateMoreKeys(serverKeyCount) - } - - override suspend fun updateOlmSession(userIds: List, syncToken: SyncToken?) { - olmCrypto.updateOlmSessions(userIds, syncToken) - } - - override suspend fun onVerificationEvent(event: Verification.Event) { - verificationHandler.onVerificationEvent(event) - } - - override fun verificationState(): Flow { - return verificationHandler.stateFlow - } - - override suspend fun verificationAction(verificationAction: Verification.Action) { - verificationHandler.onUserVerificationAction(verificationAction) - } - - override suspend fun InputStream.importRoomKeys(password: String): Flow { - return with(roomKeyImporter) { - importRoomKeys(password) { - importRoomKeys(it) - }.logP("import room keys") - } - } -} diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/EncryptMessageWithMegolmUseCase.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/EncryptMessageWithMegolmUseCase.kt deleted file mode 100644 index 50ed412..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/EncryptMessageWithMegolmUseCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.DeviceCredentials -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.crypto -import app.dapk.st.matrix.crypto.Crypto -import app.dapk.st.matrix.crypto.Olm - -private val ALGORITHM_MEGOLM = AlgorithmName("m.megolm.v1.aes-sha2") - -internal typealias EncryptMessageWithMegolmUseCase = suspend (DeviceCredentials, MessageToEncrypt) -> Crypto.EncryptionResult - -internal class EncryptMessageWithMegolmUseCaseImpl( - private val olm: Olm, - private val fetchMegolmSessionUseCase: FetchMegolmSessionUseCase, - private val logger: MatrixLogger, -) : EncryptMessageWithMegolmUseCase { - - override suspend fun invoke(credentials: DeviceCredentials, message: MessageToEncrypt): Crypto.EncryptionResult { - logger.crypto("encrypt") - val roomSession = fetchMegolmSessionUseCase.invoke(message.roomId) - val encryptedMessage = with(olm) { roomSession.encrypt(message.roomId, message.json) } - return Crypto.EncryptionResult( - ALGORITHM_MEGOLM, - senderKey = roomSession.accountCryptoSession.senderKey.value, - cipherText = encryptedMessage, - sessionId = roomSession.id, - deviceId = credentials.deviceId - ) - } - -} diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/FetchMegolmSessionUseCase.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/FetchMegolmSessionUseCase.kt deleted file mode 100644 index 7175119..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/FetchMegolmSessionUseCase.kt +++ /dev/null @@ -1,48 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.crypto -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.crypto.RoomMembersProvider -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.internal.DeviceKeys - -internal typealias FetchMegolmSessionUseCase = suspend (RoomId) -> Olm.RoomCryptoSession - -internal class FetchMegolmSessionUseCaseImpl( - private val olm: Olm, - private val deviceService: DeviceService, - private val fetchAccountCryptoUseCase: FetchAccountCryptoUseCase, - private val roomMembersProvider: RoomMembersProvider, - private val registerOlmSessionUseCase: RegisterOlmSessionUseCase, - private val shareRoomKeyUseCase: ShareRoomKeyUseCase, - private val logger: MatrixLogger, -) : FetchMegolmSessionUseCase { - - override suspend fun invoke(roomId: RoomId): Olm.RoomCryptoSession { - logger.crypto("ensureOutboundMegolmSession") - val accountCryptoSession = fetchAccountCryptoUseCase.invoke() - return olm.ensureRoomCrypto(roomId, accountCryptoSession).also { it.maybeUpdateWithNewDevices(roomId, accountCryptoSession) } - } - - private suspend fun Olm.RoomCryptoSession.maybeUpdateWithNewDevices(roomId: RoomId, accountCryptoSession: Olm.AccountCryptoSession) { - val roomMemberIds = roomMembersProvider.userIdsForRoom(roomId) - val newDevices = deviceService.checkForNewDevices(accountCryptoSession.deviceKeys, roomMemberIds, this.id) - if (newDevices.isNotEmpty()) { - logger.crypto("found devices to update with megolm session") - val olmSessions = ensureOlmSessions(newDevices, accountCryptoSession) - shareRoomKeyUseCase.invoke(this, olmSessions, roomId) - } else { - logger.crypto("no devices to update with megolm") - } - } - - private suspend fun ensureOlmSessions(newDevices: List, accountCryptoSession: Olm.AccountCryptoSession): List { - return olm.olmSessions(newDevices, onMissing = { - logger.crypto("found missing olm sessions when creating megolm session ${it.map { "${it.userId}:${it.deviceId}" }}") - registerOlmSessionUseCase.invoke(it, accountCryptoSession) - }) - } - -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt deleted file mode 100644 index f8c2eea..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt +++ /dev/null @@ -1,89 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.core.Base64 -import app.dapk.st.matrix.crypto.Crypto -import java.io.File -import java.io.InputStream -import java.security.MessageDigest -import java.security.SecureRandom -import java.util.* -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -private const val CRYPTO_BUFFER_SIZE = 32 * 1024 -private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding" -private const val SECRET_KEY_SPEC_ALGORITHM = "AES" -private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" - -class MediaEncrypter(private val base64: Base64) { - - fun encrypt(input: InputStream): Crypto.MediaEncryptionResult { - val secureRandom = SecureRandom() - val initVectorBytes = ByteArray(16) { 0.toByte() } - - val ivRandomPart = ByteArray(8) - secureRandom.nextBytes(ivRandomPart) - - System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size) - - val key = ByteArray(32) - secureRandom.nextBytes(key) - - val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) - - val outputFile = File.createTempFile("_encrypt-${UUID.randomUUID()}", ".png") - - outputFile.outputStream().use { s -> - val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) - val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) - val ivParameterSpec = IvParameterSpec(initVectorBytes) - encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) - - val data = ByteArray(CRYPTO_BUFFER_SIZE) - var read: Int - var encodedBytes: ByteArray - - input.use { inputStream -> - read = inputStream.read(data) - var totalRead = read - while (read != -1) { - encodedBytes = encryptCipher.update(data, 0, read) - messageDigest.update(encodedBytes, 0, encodedBytes.size) - s.write(encodedBytes) - read = inputStream.read(data) - totalRead += read - } - } - - encodedBytes = encryptCipher.doFinal() - messageDigest.update(encodedBytes, 0, encodedBytes.size) - s.write(encodedBytes) - } - - return Crypto.MediaEncryptionResult( - uri = outputFile.toURI(), - contentLength = outputFile.length(), - algorithm = "A256CTR", - ext = true, - keyOperations = listOf("encrypt", "decrypt"), - kty = "oct", - k = base64ToBase64Url(base64.encode(key)), - iv = base64.encode(initVectorBytes).replace("\n", "").replace("=", ""), - hashes = mapOf("sha256" to base64ToUnpaddedBase64(base64.encode(messageDigest.digest()))), - v = "v2" - ) - } -} - -private fun base64ToBase64Url(base64: String): String { - return base64.replace("\n".toRegex(), "") - .replace("\\+".toRegex(), "-") - .replace('/', '_') - .replace("=", "") -} - -private fun base64ToUnpaddedBase64(base64: String): String { - return base64.replace("\n".toRegex(), "") - .replace("=", "") -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MessageToEncrypt.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MessageToEncrypt.kt deleted file mode 100644 index 100cfd5..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MessageToEncrypt.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.JsonString -import app.dapk.st.matrix.common.RoomId - -data class MessageToEncrypt(val roomId: RoomId, val json: JsonString) \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/OlmCrypto.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/OlmCrypto.kt deleted file mode 100644 index d244cf6..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/OlmCrypto.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.Crypto -import app.dapk.st.matrix.crypto.Olm - -internal class OlmCrypto( - private val olm: Olm, - private val encryptMessageWithMegolmUseCase: EncryptMessageWithMegolmUseCase, - private val fetchAccountCryptoUseCase: FetchAccountCryptoUseCase, - private val updateKnownOlmSessionUseCase: UpdateKnownOlmSessionUseCase, - private val maybeCreateAndUploadOneTimeKeysUseCase: MaybeCreateAndUploadOneTimeKeysUseCase, - private val logger: MatrixLogger -) { - - suspend fun importRoomKeys(keys: List) { - olm.import(keys) - } - - suspend fun decrypt(payload: EncryptedMessageContent) = when (payload) { - is EncryptedMessageContent.MegOlmV1 -> olm.decryptMegOlm(payload.sessionId, payload.cipherText) - is EncryptedMessageContent.OlmV1 -> decryptOlm(payload) - } - - private suspend fun decryptOlm(payload: EncryptedMessageContent.OlmV1): DecryptionResult { - logger.crypto("decrypt olm: $payload") - val account = fetchAccountCryptoUseCase.invoke() - return payload.cipherFor(account)?.let { olm.decryptOlm(account, payload.senderKey, it.type, it.body) } - ?: DecryptionResult.Failed("Missing cipher for sender : ${account.senderKey}") - } - - suspend fun encryptMessage(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult { - return encryptMessageWithMegolmUseCase.invoke(credentials, MessageToEncrypt(roomId, messageJson)) - } - - suspend fun updateOlmSessions(userId: List, syncToken: SyncToken?) { - updateKnownOlmSessionUseCase.invoke(userId, syncToken) - } - - suspend fun maybeCreateMoreKeys(currentServerKeyCount: ServerKeyCount) { - maybeCreateAndUploadOneTimeKeysUseCase.invoke(currentServerKeyCount) - } -} - -private fun EncryptedMessageContent.OlmV1.cipherFor(account: Olm.AccountCryptoSession) = this.cipherText[account.senderKey] diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/OneTimeKeyUploaderUseCase.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/OneTimeKeyUploaderUseCase.kt deleted file mode 100644 index 4046568..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/OneTimeKeyUploaderUseCase.kt +++ /dev/null @@ -1,54 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.ServerKeyCount -import app.dapk.st.matrix.common.crypto -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.device.DeviceService - -internal typealias MaybeCreateAndUploadOneTimeKeysUseCase = suspend (ServerKeyCount) -> Unit - -internal class MaybeCreateAndUploadOneTimeKeysUseCaseImpl( - private val fetchAccountCryptoUseCase: FetchAccountCryptoUseCase, - private val olm: Olm, - private val credentialsStore: CredentialsStore, - private val deviceService: DeviceService, - private val logger: MatrixLogger, -) : MaybeCreateAndUploadOneTimeKeysUseCase { - - override suspend fun invoke(currentServerKeyCount: ServerKeyCount) { - val cryptoAccount = fetchAccountCryptoUseCase.invoke() - when { - 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") - cryptoAccount.createAndUploadOneTimeKeys(countToCreate = keysDiff + (cryptoAccount.maxKeys / 4)) - } - - else -> { - logger.crypto("current otk: $currentServerKeyCount, not creating new keys") - } - } - } - } - } - - private suspend fun Olm.AccountCryptoSession.createAndUploadOneTimeKeys(countToCreate: Int) { - with(olm) { - generateOneTimeKeys(countToCreate, credentialsStore.credentials()!!) { - kotlin.runCatching { - deviceService.uploadOneTimeKeys(it) - }.onFailure { - logger.crypto("failed to uploading OTK ${it.message}") - } - } - } - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RegisterOlmSessionUseCase.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RegisterOlmSessionUseCase.kt deleted file mode 100644 index 9e05b54..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RegisterOlmSessionUseCase.kt +++ /dev/null @@ -1,55 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.crypto -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.DeviceService.KeyClaim -import app.dapk.st.matrix.device.internal.ClaimKeysResponse -import app.dapk.st.matrix.device.internal.DeviceKeys -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive - -private val KEY_SIGNED_CURVE_25519_TYPE = AlgorithmName("signed_curve25519") - -internal typealias RegisterOlmSessionUseCase = suspend (List, Olm.AccountCryptoSession) -> List - -internal class RegisterOlmSessionUseCaseImpl( - private val olm: Olm, - private val deviceService: DeviceService, - private val logger: MatrixLogger, -) : RegisterOlmSessionUseCase { - - override suspend fun invoke(deviceKeys: List, olmAccount: Olm.AccountCryptoSession): List { - logger.crypto("registering olm session for devices") - val devicesByDeviceId = deviceKeys.associateBy { it.deviceId } - val keyClaims = deviceKeys.map { KeyClaim(it.userId, it.deviceId, algorithmName = KEY_SIGNED_CURVE_25519_TYPE) } - logger.crypto("attempt claim: $keyClaims") - return deviceService.claimKeys(keyClaims) - .toOlmRequests(devicesByDeviceId) - .also { logger.crypto("claim result: $it") } - .map { olm.ensureDeviceCrypto(it, olmAccount) } - } - - private fun ClaimKeysResponse.toOlmRequests(devices: Map) = this.oneTimeKeys.map { (userId, devicesToKeys) -> - devicesToKeys.mapNotNull { (deviceId, payload) -> - when (payload) { - is JsonObject -> { - val key = when (val content = payload.values.first()) { - is JsonObject -> (content["key"] as JsonPrimitive).content - else -> throw RuntimeException("Missing key") - } - val identity = devices.identity(deviceId) - val fingerprint = devices.fingerprint(deviceId) - Olm.OlmSessionInput(oneTimeKey = key, identity = identity, deviceId, userId, fingerprint) - } - else -> null - } - } - }.flatten() -} - -private fun Map.identity(deviceId: DeviceId) = this[deviceId]!!.identity() -private fun Map.fingerprint(deviceId: DeviceId) = this[deviceId]!!.fingerprint() \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt deleted file mode 100644 index 7adcc3c..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt +++ /dev/null @@ -1,221 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.core.Base64 -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.SessionId -import app.dapk.st.matrix.common.SharedRoomKey -import app.dapk.st.matrix.crypto.ImportResult -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import java.io.InputStream -import java.nio.charset.Charset -import javax.crypto.Cipher -import javax.crypto.Mac -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec -import kotlin.experimental.xor - -private const val HEADER_LINE = "-----BEGIN MEGOLM SESSION DATA-----" -private const val TRAILER_LINE = "-----END MEGOLM SESSION DATA-----" -private val importJson = Json { ignoreUnknownKeys = true } - -class RoomKeyImporter( - private val base64: Base64, - private val dispatchers: CoroutineDispatchers, -) { - - suspend fun InputStream.importRoomKeys(password: String, onChunk: suspend (List) -> Unit): Flow { - return flow { - runCatching { this@importRoomKeys.import(password, onChunk, this) } - .onFailure { - when (it) { - is ImportException -> emit(ImportResult.Error(it.type)) - else -> emit(ImportResult.Error(ImportResult.Error.Type.Unknown(it))) - } - } - }.flowOn(dispatchers.io) - } - - private suspend fun InputStream.import(password: String, onChunk: suspend (List) -> Unit, collector: FlowCollector) { - var importedKeysCount = 0L - val roomIds = mutableSetOf() - - this.bufferedReader().use { - with(JsonAccumulator()) { - it.useLines { sequence -> - sequence - .filterNot { it == HEADER_LINE || it == TRAILER_LINE || it.isEmpty() } - .chunked(5) - .decrypt(password) - .accumulateJson() - .map { decoded -> - roomIds.add(decoded.roomId) - SharedRoomKey( - decoded.algorithmName, - decoded.roomId, - decoded.sessionId, - decoded.sessionKey, - isExported = true, - ) - } - .chunked(500) - .forEach { - onChunk(it) - importedKeysCount += it.size - collector.emit(ImportResult.Update(importedKeysCount)) - } - } - } - when { - roomIds.isEmpty() -> collector.emit(ImportResult.Error(ImportResult.Error.Type.NoKeysFound)) - else -> collector.emit(ImportResult.Success(roomIds, importedKeysCount)) - } - } - } - - private fun Sequence>.decrypt(password: String): Sequence { - val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding") - return this.withIndex().map { (index, it) -> - val line = it.joinToString(separator = "").replace("\n", "") - val toByteArray = base64.decode(line) - if (index == 0) { - toByteArray.ensureHasCipherPayloadOrThrow() - val initializer = toByteArray.copyOfRange(0, 37) - decryptCipher.initialize(initializer, password) - val content = toByteArray.copyOfRange(37, toByteArray.size) - content.decrypt(decryptCipher).also { - if (!it.startsWith("[{")) { - throw ImportException(ImportResult.Error.Type.UnexpectedDecryptionOutput) - } - } - } else { - toByteArray.decrypt(decryptCipher) - } - } - } - - private fun ByteArray.ensureHasCipherPayloadOrThrow() { - if (this.size < 37) { - throw ImportException(ImportResult.Error.Type.InvalidFile) - } - } - - private fun Cipher.initialize(payload: ByteArray, passphrase: String) { - val salt = payload.copyOfRange(1, 1 + 16) - val iv = payload.copyOfRange(17, 17 + 16) - val iterations = (payload[33].toUnsignedInt() shl 24) or - (payload[34].toUnsignedInt() shl 16) or - (payload[35].toUnsignedInt() shl 8) or - payload[36].toUnsignedInt() - val deriveKey = deriveKeys(salt, iterations, passphrase) - val secretKeySpec = SecretKeySpec(deriveKey.getAesKey(), "AES") - val ivParameterSpec = IvParameterSpec(iv) - this.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - } - - private fun ByteArray.decrypt(cipher: Cipher): String { - return cipher.update(this).toString(Charset.defaultCharset()) - } - - private fun ByteArray.getAesKey() = this.copyOfRange(0, 32) - - private fun deriveKeys(salt: ByteArray, iterations: Int, password: String): ByteArray { - val prf = Mac.getInstance("HmacSHA512") - prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512")) - - // 512 bits key length - val key = ByteArray(64) - val uc = ByteArray(64) - - // U1 = PRF(Password, Salt || INT_32_BE(i)) - prf.update(salt) - val int32BE = ByteArray(4) { 0.toByte() } - int32BE[3] = 1.toByte() - prf.update(int32BE) - prf.doFinal(uc, 0) - - // copy to the key - System.arraycopy(uc, 0, key, 0, uc.size) - - for (index in 2..iterations) { - // Uc = PRF(Password, Uc-1) - prf.update(uc) - prf.doFinal(uc, 0) - - // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc - for (byteIndex in uc.indices) { - key[byteIndex] = key[byteIndex] xor uc[byteIndex] - } - } - return key - } -} - -private fun Byte.toUnsignedInt() = toInt() and 0xff - -@Serializable -private data class ElementMegolmExportObject( - @SerialName("room_id") val roomId: RoomId, - @SerialName("session_key") val sessionKey: String, - @SerialName("session_id") val sessionId: SessionId, - @SerialName("algorithm") val algorithmName: AlgorithmName, -) - -private class ImportException(val type: ImportResult.Error.Type) : Throwable() - -private class JsonAccumulator { - - private var jsonSegment = "" - - fun Sequence.accumulateJson() = this.mapNotNull { - val withLatest = jsonSegment + it - try { - when (val objectRange = withLatest.findClosingIndex()) { - null -> { - jsonSegment = withLatest - null - } - - else -> { - val string = withLatest.substring(objectRange) - importJson.decodeFromString(ElementMegolmExportObject.serializer(), string).also { - jsonSegment = withLatest.replace(string, "").removePrefix(",") - } - } - } - } catch (error: Throwable) { - jsonSegment = withLatest - null - } - } - - private fun String.findClosingIndex(): IntRange? { - var opens = 0 - var openIndex = -1 - this.forEachIndexed { index, c -> - when { - c == '{' -> { - if (opens == 0) { - openIndex = index - } - opens++ - } - - c == '}' -> { - opens-- - if (opens == 0) { - return IntRange(openIndex, index) - } - } - } - } - return null - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/ShareRoomKeyUseCase.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/ShareRoomKeyUseCase.kt deleted file mode 100644 index 7627a0d..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/ShareRoomKeyUseCase.kt +++ /dev/null @@ -1,65 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.common.extensions.toJsonString -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.ToDevicePayload - -private val ALGORITHM_OLM = AlgorithmName("m.olm.v1.curve25519-aes-sha2") -private val ALGORITHM_MEGOLM = AlgorithmName("m.megolm.v1.aes-sha2") - -internal typealias ShareRoomKeyUseCase = suspend (room: Olm.RoomCryptoSession, List, RoomId) -> Unit - -internal class ShareRoomKeyUseCaseImpl( - private val credentialsStore: CredentialsStore, - private val deviceService: DeviceService, - private val logger: MatrixLogger, - private val olm: Olm, -) : ShareRoomKeyUseCase { - - override suspend fun invoke(roomSessionToShare: Olm.RoomCryptoSession, olmSessionsToEncryptMessage: List, roomId: RoomId) { - val credentials = credentialsStore.credentials()!! - logger.crypto("creating megolm payloads for $roomId: ${olmSessionsToEncryptMessage.map { it.userId to it.deviceId }}") - - val toMessages = olmSessionsToEncryptMessage.map { - val payload = mapOf( - "type" to "m.room_key", - "content" to mapOf( - "algorithm" to ALGORITHM_MEGOLM.value, - "room_id" to roomId.value, - "session_id" to roomSessionToShare.id.value, - "session_key" to roomSessionToShare.key, - "chain_index" to roomSessionToShare.messageIndex, - ), - "sender" to credentials.userId.value, - "sender_device" to credentials.deviceId.value, - "keys" to mapOf( - "ed25519" to roomSessionToShare.accountCryptoSession.fingerprint.value - ), - "recipient" to it.userId.value, - "recipient_keys" to mapOf( - "ed25519" to it.fingerprint.value - ) - ) - - val result = with(olm) { it.encrypt(payload.toJsonString()) } - DeviceService.ToDeviceMessage( - senderId = it.userId, - deviceId = it.deviceId, - ToDevicePayload.EncryptedToDevicePayload( - algorithmName = ALGORITHM_OLM, - senderKey = roomSessionToShare.accountCryptoSession.senderKey, - cipherText = mapOf( - it.identity to ToDevicePayload.EncryptedToDevicePayload.Inner( - cipherText = result.cipherText, - type = result.type, - ) - ) - ), - ) - } - logger.crypto("sharing keys") - deviceService.sendRoomKeyToDevice(roomSessionToShare.id, toMessages) - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/UpdateKnownOlmSessionUseCaseImpl.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/UpdateKnownOlmSessionUseCaseImpl.kt deleted file mode 100644 index 78ad62b..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/UpdateKnownOlmSessionUseCaseImpl.kt +++ /dev/null @@ -1,29 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.SyncToken -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.common.crypto -import app.dapk.st.matrix.device.DeviceService - -internal typealias UpdateKnownOlmSessionUseCase = suspend (List, SyncToken?) -> Unit - -internal class UpdateKnownOlmSessionUseCaseImpl( - private val fetchAccountCryptoUseCase: FetchAccountCryptoUseCase, - private val deviceService: DeviceService, - private val registerOlmSessionUseCase: RegisterOlmSessionUseCase, - private val logger: MatrixLogger, -) : UpdateKnownOlmSessionUseCase { - - override suspend fun invoke(userIds: List, syncToken: SyncToken?) { - logger.crypto("updating olm sessions for ${userIds.map { it.value }}") - val account = fetchAccountCryptoUseCase.invoke() - val keys = deviceService.fetchDevices(userIds, syncToken).filterNot { it.deviceId == account.deviceKeys.deviceId } - if (keys.isNotEmpty()) { - registerOlmSessionUseCase.invoke(keys, account) - } else { - logger.crypto("no valid devices keys found to update") - } - } - -} \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/VerificationHandler.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/VerificationHandler.kt deleted file mode 100644 index 47d6594..0000000 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/VerificationHandler.kt +++ /dev/null @@ -1,215 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.crypto.Verification -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.ToDevicePayload -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.serialization.json.Json -import java.util.* - -internal class VerificationHandler( - private val deviceService: DeviceService, - private val credentialsStore: CredentialsStore, - private val logger: MatrixLogger, - private val jsonCanonicalizer: JsonCanonicalizer, - private val olm: Olm, -) { - - data class VerificationTransaction( - val userId: UserId, - val deviceId: DeviceId, - val transactionId: String, - ) - - val stateFlow = MutableStateFlow(Verification.State.Idle) - - var verificationTransaction = VerificationTransaction(UserId(""), DeviceId(""), "") - var sasSession: Olm.SasSession? = null - var requesterStartPayload: ToDevicePayload.VerificationStart? = null - - suspend fun onUserVerificationAction(action: Verification.Action) { - when (action) { - is Verification.Action.Request -> requestVerification(action.userId, action.deviceId) - Verification.Action.SecureAccept -> { - stateFlow.emit(Verification.State.ReadySent) - } - Verification.Action.InsecureAccept -> { - sendToDevice(ToDevicePayload.VerificationDone(verificationTransaction.transactionId)) - - stateFlow.emit(Verification.State.WaitingForDoneConfirmation) - } - Verification.Action.AcknowledgeMatch -> { - val credentials = credentialsStore.credentials()!! - val mac = sasSession!!.calculateMac( - credentials.userId, - credentials.deviceId, - verificationTransaction.userId, - verificationTransaction.deviceId, - verificationTransaction.transactionId - ) - - sendToDevice( - ToDevicePayload.VerificationMac( - verificationTransaction.transactionId, - mac.keys, - mac.mac - ) - ) - } - } - } - - private suspend fun requestVerification(userId: UserId, deviceId: DeviceId) { - val transactionId = UUID.randomUUID().toString() - - verificationTransaction = VerificationTransaction(userId, deviceId, transactionId) - - sendToDevice( - ToDevicePayload.VerificationRequest( - fromDevice = credentialsStore.credentials()!!.deviceId, - methods = listOf("m.sas.v1"), - transactionId = transactionId, - timestampPosix = System.currentTimeMillis() - ) - ) - } - - suspend fun onVerificationEvent(event: Verification.Event) { - logger.matrixLog(MatrixLogTag.VERIFICATION, "handling event: $event") - when (event) { - is Verification.Event.Requested -> { - stateFlow.emit(Verification.State.ReadySent) - - verificationTransaction = VerificationTransaction( - event.userId, event.deviceId, event.transactionId - ) - - sendToDevice( - ToDevicePayload.VerificationReady( - fromDevice = credentialsStore.credentials()!!.deviceId, - methods = listOf("m.sas.v1"), - event.transactionId, - ) - ) - } - is Verification.Event.Ready -> { - val startPayload = ToDevicePayload.VerificationStart( - fromDevice = verificationTransaction.deviceId, - method = event.methods.first { it == "m.sas.v1" }, - protocols = listOf("curve25519-hkdf-sha256"), - hashes = listOf("sha256"), - codes = listOf("hkdf-hmac-sha256"), - short = listOf("emoji"), - event.transactionId, - ) - requesterStartPayload = startPayload - sendToDevice(startPayload) - } - is Verification.Event.Started -> { - val self = credentialsStore.credentials()!!.userId.value - val shouldSendStart = listOf(verificationTransaction.userId.value, self).minOrNull() == self - - - val startPayload = ToDevicePayload.VerificationStart( - fromDevice = verificationTransaction.deviceId, - method = event.method, - protocols = event.protocols, - hashes = event.hashes, - codes = event.codes, - short = event.short, - event.transactionId, - ) - - val startJson = startPayload.toCanonicalJson() - - logger.matrixLog(MatrixLogTag.VERIFICATION, "startJson: $startJson") - - sasSession = olm.sasSession(credentialsStore.credentials()!!) - - val commitment = sasSession!!.generateCommitment(hash = "sha256", startJson) - - sendToDevice( - ToDevicePayload.VerificationAccept( - transactionId = event.transactionId, - fromDevice = credentialsStore.credentials()!!.deviceId, - method = event.method, - protocol = "curve25519-hkdf-sha256", - hash = "sha256", - code = "hkdf-hmac-sha256", - short = listOf("emoji", "decimal"), - commitment = commitment, - ) - ) - - } - - is Verification.Event.Accepted -> { - sasSession = olm.sasSession(credentialsStore.credentials()!!) - sendToDevice( - ToDevicePayload.VerificationKey( - verificationTransaction.transactionId, - key = sasSession!!.publicKey() - ) - ) - } - is Verification.Event.Key -> { - sasSession!!.setTheirPublicKey(event.key) - sendToDevice( - ToDevicePayload.VerificationKey( - transactionId = event.transactionId, - key = sasSession!!.publicKey() - ) - ) - stateFlow.emit(Verification.State.WaitingForMatchConfirmation) - } - is Verification.Event.Mac -> { -// val credentials = credentialsStore.credentials()!! -// -// val mac = sasSession!!.calculateMac( -// credentials.userId, credentials.deviceId, event.userId, verificationTransaction.deviceId, event.transactionId -// ) -// -// sendToDevice( -// ToDevicePayload.VerificationMac( -// event.transactionId, -// mac.keys, -// mac.mac -// ) -// ) - // TODO verify mac? - sendToDevice(ToDevicePayload.VerificationDone(verificationTransaction.transactionId)) - stateFlow.emit(Verification.State.Done) - } - is Verification.Event.Done -> { - // TODO - } - } - } - - private fun ToDevicePayload.VerificationStart.toCanonicalJson() = jsonCanonicalizer.canonicalize( - JsonString(Json.encodeToString(ToDevicePayload.VerificationStart.serializer(), this)) - ) - - private suspend fun sendToDevice(payload: ToDevicePayload.VerificationPayload) { - logger.matrixLog(MatrixLogTag.VERIFICATION, "sending ${payload::class.java}") - - deviceService.sendToDevice( - when (payload) { - is ToDevicePayload.VerificationRequest -> EventType.VERIFICATION_REQUEST - is ToDevicePayload.VerificationStart -> EventType.VERIFICATION_START - is ToDevicePayload.VerificationDone -> EventType.VERIFICATION_DONE - is ToDevicePayload.VerificationReady -> EventType.VERIFICATION_READY - is ToDevicePayload.VerificationAccept -> EventType.VERIFICATION_ACCEPT - is ToDevicePayload.VerificationMac -> EventType.VERIFICATION_MAC - is ToDevicePayload.VerificationKey -> EventType.VERIFICATION_KEY - }, - verificationTransaction.transactionId, - verificationTransaction.userId, - verificationTransaction.deviceId, - payload as ToDevicePayload - ) - } - -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/EncryptMegolmUseCaseTest.kt b/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/EncryptMegolmUseCaseTest.kt deleted file mode 100644 index e946332..0000000 --- a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/EncryptMegolmUseCaseTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.Crypto -import fake.FakeMatrixLogger -import fake.FakeOlm -import fixture.* -import internalfake.FakeFetchMegolmSessionUseCase -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private val A_ROOM_ID = aRoomId() -private val A_MESSAGE_TO_ENCRYPT = aMessageToEncrypt(roomId = A_ROOM_ID) -private val AN_ENCRYPTION_CIPHER_RESULT = aCipherText() -private val A_DEVICE_CREDENTIALS = aDeviceCredentials() -private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession() -private val A_ROOM_CRYPTO_SESSION = aRoomCryptoSession(accountCryptoSession = AN_ACCOUNT_CRYPTO_SESSION) - -class EncryptMegolmUseCaseTest { - - private val fetchMegolmSessionUseCase = FakeFetchMegolmSessionUseCase() - private val fakeOlm = FakeOlm() - - private val encryptMegolmUseCase = EncryptMessageWithMegolmUseCaseImpl( - fakeOlm, - fetchMegolmSessionUseCase, - FakeMatrixLogger(), - ) - - @Test - fun `given a room crypto session then encrypts messages with megolm`() = runTest { - fetchMegolmSessionUseCase.givenSessionForRoom(A_ROOM_ID, A_ROOM_CRYPTO_SESSION) - fakeOlm.givenEncrypts(A_ROOM_CRYPTO_SESSION, A_MESSAGE_TO_ENCRYPT.roomId, A_MESSAGE_TO_ENCRYPT.json, AN_ENCRYPTION_CIPHER_RESULT) - - val result = encryptMegolmUseCase.invoke(aDeviceCredentials(), A_MESSAGE_TO_ENCRYPT) - - result shouldBeEqualTo anEncryptionResult( - AlgorithmName("m.megolm.v1.aes-sha2"), - senderKey = AN_ACCOUNT_CRYPTO_SESSION.senderKey.value, - cipherText = AN_ENCRYPTION_CIPHER_RESULT, - sessionId = A_ROOM_CRYPTO_SESSION.id, - deviceId = A_DEVICE_CREDENTIALS.deviceId - ) - } -} - -fun aMessageToEncrypt( - roomId: RoomId = aRoomId(), - messageJson: JsonString = aJsonString() -) = MessageToEncrypt(roomId, messageJson) - -fun anEncryptionResult( - algorithmName: AlgorithmName = anAlgorithmName(), - senderKey: String = "a-sender-key", - cipherText: CipherText = aCipherText(), - sessionId: SessionId = aSessionId(), - deviceId: DeviceId = aDeviceId(), -) = Crypto.EncryptionResult(algorithmName, senderKey, cipherText, sessionId, deviceId) \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/FetchAccountCryptoUseCaseTest.kt b/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/FetchAccountCryptoUseCaseTest.kt deleted file mode 100644 index 8bcf4be..0000000 --- a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/FetchAccountCryptoUseCaseTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import fake.FakeCredentialsStore -import fake.FakeDeviceService -import fake.FakeOlm -import fixture.aUserCredentials -import fixture.anAccountCryptoSession -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import test.expect - -private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession() -private val A_USER_CREDENTIALS = aUserCredentials() - -class FetchAccountCryptoUseCaseTest { - - private val credentialsStore = FakeCredentialsStore() - private val olm = FakeOlm() - private val deviceService = FakeDeviceService() - - private val fetchAccountCryptoUseCase = FetchAccountCryptoUseCaseImpl( - credentialsStore, - olm, - deviceService, - ) - - @Test - fun `when creating an account crypto session then also uploads device keys`() = runTest { - credentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) - olm.givenCreatesAccount(A_USER_CREDENTIALS).returns(AN_ACCOUNT_CRYPTO_SESSION) - deviceService.expect { it.uploadDeviceKeys(AN_ACCOUNT_CRYPTO_SESSION.deviceKeys) } - - val result = fetchAccountCryptoUseCase.invoke() - - result shouldBeEqualTo AN_ACCOUNT_CRYPTO_SESSION - } - - @Test - fun `when fetching an existing crypto session then returns`() = runTest { - credentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) - olm.givenAccount(A_USER_CREDENTIALS).returns(AN_ACCOUNT_CRYPTO_SESSION) - - val result = fetchAccountCryptoUseCase.invoke() - - result shouldBeEqualTo AN_ACCOUNT_CRYPTO_SESSION - } -} diff --git a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/FetchMegolmSessionUseCaseTest.kt b/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/FetchMegolmSessionUseCaseTest.kt deleted file mode 100644 index e0bcb63..0000000 --- a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/FetchMegolmSessionUseCaseTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import fake.FakeDeviceService -import fake.FakeMatrixLogger -import fake.FakeOlm -import fixture.* -import internalfake.FakeFetchAccountCryptoUseCase -import internalfake.FakeRegisterOlmSessionUseCase -import internalfake.FakeShareRoomKeyUseCase -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private val A_ROOM_ID = aRoomId() -private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession() -private val A_ROOM_CRYPTO_SESSION = aRoomCryptoSession() -private val USERS_IN_ROOM = listOf(aUserId()) -private val NEW_DEVICES = listOf(aDeviceKeys()) -private val MISSING_OLM_SESSIONS = listOf(aDeviceCryptoSession()) - -class FetchMegolmSessionUseCaseTest { - - private val fakeOlm = FakeOlm() - private val deviceService = FakeDeviceService() - private val roomMembersProvider = FakeRoomMembersProvider() - private val fakeRegisterOlmSessionUseCase = FakeRegisterOlmSessionUseCase() - private val fakeShareRoomKeyUseCase = FakeShareRoomKeyUseCase() - - private val fetchMegolmSessionUseCase = FetchMegolmSessionUseCaseImpl( - fakeOlm, - deviceService, - FakeFetchAccountCryptoUseCase().also { it.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) }, - roomMembersProvider, - fakeRegisterOlmSessionUseCase, - fakeShareRoomKeyUseCase, - FakeMatrixLogger(), - ) - - @Test - fun `given new devices with missing olm sessions when fetching megolm session then creates olm session, megolm session and shares megolm key`() = runTest { - fakeOlm.givenRoomCrypto(A_ROOM_ID, AN_ACCOUNT_CRYPTO_SESSION).returns(A_ROOM_CRYPTO_SESSION) - roomMembersProvider.givenUserIdsForRoom(A_ROOM_ID).returns(USERS_IN_ROOM) - deviceService.givenNewDevices(AN_ACCOUNT_CRYPTO_SESSION.deviceKeys, USERS_IN_ROOM, A_ROOM_CRYPTO_SESSION.id).returns(NEW_DEVICES) - fakeOlm.givenMissingOlmSessions(NEW_DEVICES).returns(MISSING_OLM_SESSIONS) - fakeRegisterOlmSessionUseCase.givenRegistersSessions(NEW_DEVICES, AN_ACCOUNT_CRYPTO_SESSION).returns(MISSING_OLM_SESSIONS) - fakeShareRoomKeyUseCase.expect(A_ROOM_CRYPTO_SESSION, MISSING_OLM_SESSIONS, A_ROOM_ID) - - val result = fetchMegolmSessionUseCase.invoke(aRoomId()) - - result shouldBeEqualTo A_ROOM_CRYPTO_SESSION - } - - @Test - fun `given no new devices when fetching megolm session then returns existing megolm session`() = runTest { - fakeOlm.givenRoomCrypto(A_ROOM_ID, AN_ACCOUNT_CRYPTO_SESSION).returns(A_ROOM_CRYPTO_SESSION) - roomMembersProvider.givenUserIdsForRoom(A_ROOM_ID).returns(USERS_IN_ROOM) - deviceService.givenNewDevices(AN_ACCOUNT_CRYPTO_SESSION.deviceKeys, USERS_IN_ROOM, A_ROOM_CRYPTO_SESSION.id).returns(emptyList()) - - val result = fetchMegolmSessionUseCase.invoke(aRoomId()) - - result shouldBeEqualTo A_ROOM_CRYPTO_SESSION - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/MaybeCreateAndUploadOneTimeKeysUseCaseTest.kt b/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/MaybeCreateAndUploadOneTimeKeysUseCaseTest.kt deleted file mode 100644 index 055f43c..0000000 --- a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/MaybeCreateAndUploadOneTimeKeysUseCaseTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.ServerKeyCount -import app.dapk.st.matrix.device.DeviceService -import fake.FakeCredentialsStore -import fake.FakeDeviceService -import fake.FakeMatrixLogger -import fake.FakeOlm -import fixture.aUserCredentials -import fixture.anAccountCryptoSession -import internalfake.FakeFetchAccountCryptoUseCase -import kotlinx.coroutines.test.runTest -import org.junit.Test -import test.expect - -private const val MAX_KEYS = 100 -private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession(maxKeys = MAX_KEYS) -private val A_USER_CREDENTIALS = aUserCredentials() -private val GENERATED_ONE_TIME_KEYS = DeviceService.OneTimeKeys(listOf()) - -class MaybeCreateAndUploadOneTimeKeysUseCaseTest { - - private val fakeDeviceService = FakeDeviceService() - private val fakeOlm = FakeOlm() - private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(A_USER_CREDENTIALS) } - private val fakeFetchAccountCryptoUseCase = FakeFetchAccountCryptoUseCase() - - private val maybeCreateAndUploadOneTimeKeysUseCase = MaybeCreateAndUploadOneTimeKeysUseCaseImpl( - fakeFetchAccountCryptoUseCase.also { it.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) }, - fakeOlm, - fakeCredentialsStore, - fakeDeviceService, - FakeMatrixLogger(), - ) - - @Test - fun `given more keys than the current max then does nothing`() = runTest { - val moreThanHalfOfMax = ServerKeyCount((MAX_KEYS / 2) + 1) - - maybeCreateAndUploadOneTimeKeysUseCase.invoke(moreThanHalfOfMax) - - 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 - 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) } - val keysToGenerate = (MAX_KEYS * 0.75f).toInt() - fakeOlm.givenGeneratesOneTimeKeys(AN_ACCOUNT_CRYPTO_SESSION, keysToGenerate, A_USER_CREDENTIALS).returns(GENERATED_ONE_TIME_KEYS) - - maybeCreateAndUploadOneTimeKeysUseCase.invoke(ServerKeyCount(0)) - } - - @Test - fun `given less than half of max current keys than generates and uploads 25 percent plus delta from half of the max key capacity`() = runTest { - val deltaFromHalf = 5 - val lessThanHalfOfMax = ServerKeyCount((MAX_KEYS / 2) - deltaFromHalf) - val keysToGenerate = (MAX_KEYS * 0.25).toInt() + deltaFromHalf - fakeDeviceService.expect { it.uploadOneTimeKeys(GENERATED_ONE_TIME_KEYS) } - fakeOlm.givenGeneratesOneTimeKeys(AN_ACCOUNT_CRYPTO_SESSION, keysToGenerate, A_USER_CREDENTIALS).returns(GENERATED_ONE_TIME_KEYS) - - maybeCreateAndUploadOneTimeKeysUseCase.invoke(lessThanHalfOfMax) - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/OlmCryptoTest.kt b/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/OlmCryptoTest.kt deleted file mode 100644 index 73776c3..0000000 --- a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/OlmCryptoTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.* -import fake.FakeMatrixLogger -import fake.FakeOlm -import fixture.* -import internalfake.FakeEncryptMessageWithMegolmUseCase -import internalfake.FakeFetchAccountCryptoUseCase -import internalfake.FakeMaybeCreateAndUploadOneTimeKeysUseCase -import internalfake.FakeUpdateKnownOlmSessionUseCase -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import test.runExpectTest - -private val A_LIST_OF_SHARED_ROOM_KEYS = listOf(aSharedRoomKey()) -private val A_DEVICE_CREDENTIALS = aDeviceCredentials() -private val A_ROOM_ID = aRoomId() -private val A_MESSAGE_JSON_TO_ENCRYPT = aJsonString("message!") -private val AN_EXPECTED_MESSAGE_TO_ENCRYPT = aMessageToEncrypt(A_ROOM_ID, A_MESSAGE_JSON_TO_ENCRYPT) -private val AN_ENCRYPTION_RESULT = anEncryptionResult() -private val A_LIST_OF_USER_IDS_TO_UPDATE = listOf(aUserId()) -private val A_SYNC_TOKEN = aSyncToken() -private val A_SERVER_KEY_COUNT = ServerKeyCount(100) -private val A_MEGOLM_PAYLOAD = anEncryptedMegOlmV1Message() -private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession() -private val AN_OLM_PAYLOAD = anEncryptedOlmV1Message(cipherText = mapOf(AN_ACCOUNT_CRYPTO_SESSION.senderKey to aCipherTextInfo())) -private val A_DECRYPTION_RESULT = aDecryptionSuccessResult() - -internal class OlmCryptoTest { - - private val fakeOlm = FakeOlm() - private val fakeEncryptMessageWithMegolmUseCase = FakeEncryptMessageWithMegolmUseCase() - private val fakeFetchAccountCryptoUseCase = FakeFetchAccountCryptoUseCase() - private val fakeUpdateKnownOlmSessionUseCase = FakeUpdateKnownOlmSessionUseCase() - private val fakeMaybeCreateAndUploadOneTimeKeysUseCase = FakeMaybeCreateAndUploadOneTimeKeysUseCase() - - private val olmCrypto = OlmCrypto( - fakeOlm, - fakeEncryptMessageWithMegolmUseCase, - fakeFetchAccountCryptoUseCase, - fakeUpdateKnownOlmSessionUseCase, - fakeMaybeCreateAndUploadOneTimeKeysUseCase, - FakeMatrixLogger() - ) - - @Test - fun `when importing room keys, then delegates to olm`() = runExpectTest { - fakeOlm.expectUnit { it.import(A_LIST_OF_SHARED_ROOM_KEYS) } - - olmCrypto.importRoomKeys(A_LIST_OF_SHARED_ROOM_KEYS) - - verifyExpects() - } - - @Test - fun `when encrypting message, then delegates to megolm`() = runTest { - fakeEncryptMessageWithMegolmUseCase.givenEncrypt(A_DEVICE_CREDENTIALS, AN_EXPECTED_MESSAGE_TO_ENCRYPT).returns(AN_ENCRYPTION_RESULT) - - val result = olmCrypto.encryptMessage(A_ROOM_ID, A_DEVICE_CREDENTIALS, A_MESSAGE_JSON_TO_ENCRYPT) - - result shouldBeEqualTo AN_ENCRYPTION_RESULT - } - - @Test - fun `when updating olm sessions, then delegates to use case`() = runExpectTest { - fakeUpdateKnownOlmSessionUseCase.expectUnit { it.invoke(A_LIST_OF_USER_IDS_TO_UPDATE, A_SYNC_TOKEN) } - - olmCrypto.updateOlmSessions(A_LIST_OF_USER_IDS_TO_UPDATE, A_SYNC_TOKEN) - - verifyExpects() - } - - @Test - fun `when maybe creating more keys, then delegates to use case`() = runExpectTest { - fakeMaybeCreateAndUploadOneTimeKeysUseCase.expectUnit { it.invoke(A_SERVER_KEY_COUNT) } - - olmCrypto.maybeCreateMoreKeys(A_SERVER_KEY_COUNT) - - verifyExpects() - } - - @Test - fun `given megolm payload, when decrypting, then delegates to olm`() = runTest { - fakeOlm.givenDecrypting(A_MEGOLM_PAYLOAD).returns(A_DECRYPTION_RESULT) - - val result = olmCrypto.decrypt(A_MEGOLM_PAYLOAD) - - result shouldBeEqualTo A_DECRYPTION_RESULT - } - - @Test - fun `given olm payload, when decrypting, then delegates to olm`() = runTest { - fakeFetchAccountCryptoUseCase.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) - fakeOlm.givenDecrypting(AN_OLM_PAYLOAD, AN_ACCOUNT_CRYPTO_SESSION).returns(A_DECRYPTION_RESULT) - - val result = olmCrypto.decrypt(AN_OLM_PAYLOAD) - - result shouldBeEqualTo A_DECRYPTION_RESULT - } -} diff --git a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/RegisterOlmSessionUseCaseTest.kt b/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/RegisterOlmSessionUseCaseTest.kt deleted file mode 100644 index d1475e4..0000000 --- a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/RegisterOlmSessionUseCaseTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.crypto.Olm -import fake.FakeDeviceService -import fake.FakeMatrixLogger -import fake.FakeOlm -import fixture.* -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.* -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private val KEY_SIGNED_CURVE_25519_TYPE = AlgorithmName("signed_curve25519") - -private const val A_CLAIM_KEY_RESPONSE = "a-claimed-key" -private const val A_DEVICE_IDENTITY = "a-claimed-signature" -private const val A_DEVICE_FINGERPRINT = "a-claimed-fingerprint" -private val A_DEVICE_ID_TO_REGISTER = aDeviceId("device-id-to-register") -private val A_USER_ID_TO_REGISTER = aUserId("user-id-to-register") -private val A_DEVICE_KEYS_TO_REGISTER = aDeviceKeys( - userId = A_USER_ID_TO_REGISTER, - deviceId = A_DEVICE_ID_TO_REGISTER, - keys = mapOf( - "ed25519:${A_DEVICE_ID_TO_REGISTER.value}" to A_DEVICE_FINGERPRINT, - "curve25519:${A_DEVICE_ID_TO_REGISTER.value}" to A_DEVICE_IDENTITY, - ) -) -private val A_DEVICE_CRYPTO_SESSION = aDeviceCryptoSession(identity = aCurve25519("an-olm-identity")) -private val A_KEY_CLAIM = aKeyClaim( - userId = A_USER_ID_TO_REGISTER, - deviceId = A_DEVICE_ID_TO_REGISTER, - algorithmName = KEY_SIGNED_CURVE_25519_TYPE -) -private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession() - -class RegisterOlmSessionUseCaseTest { - - private val fakeOlm = FakeOlm() - private val fakeDeviceService = FakeDeviceService() - - private val registerOlmSessionUseCase = RegisterOlmSessionUseCaseImpl( - fakeOlm, - fakeDeviceService, - FakeMatrixLogger() - ) - - @Test - fun `given keys when registering then claims keys and creates olm session`() = runTest { - fakeDeviceService.givenClaimsKeys(listOf(A_KEY_CLAIM)).returns(claimKeysResponse(A_USER_ID_TO_REGISTER, A_DEVICE_ID_TO_REGISTER)) - val expectedInput = expectOlmSessionCreationInput() - fakeOlm.givenDeviceCrypto(expectedInput, AN_ACCOUNT_CRYPTO_SESSION).returns(A_DEVICE_CRYPTO_SESSION) - - val result = registerOlmSessionUseCase.invoke(listOf(A_DEVICE_KEYS_TO_REGISTER), AN_ACCOUNT_CRYPTO_SESSION) - - result shouldBeEqualTo listOf(A_DEVICE_CRYPTO_SESSION) - } - - private fun expectOlmSessionCreationInput() = Olm.OlmSessionInput( - A_CLAIM_KEY_RESPONSE, - A_DEVICE_KEYS_TO_REGISTER.identity(), - A_DEVICE_ID_TO_REGISTER, - A_USER_ID_TO_REGISTER, - A_DEVICE_KEYS_TO_REGISTER.fingerprint() - ) - - private fun claimKeysResponse(userId: UserId, deviceId: DeviceId) = aClaimKeysResponse(oneTimeKeys = mapOf(userId to mapOf(deviceId to jsonElement()))) - - private fun jsonElement() = Json.encodeToJsonElement( - JsonObject( - mapOf( - "signed_curve25519:AAAAHg" to JsonObject( - mapOf( - "key" to JsonPrimitive(A_CLAIM_KEY_RESPONSE), - "signatures" to JsonObject( - mapOf( - A_USER_ID_TO_REGISTER.value to JsonObject( - mapOf("ed25519:${A_DEVICE_ID_TO_REGISTER.value}" to JsonPrimitive(A_DEVICE_FINGERPRINT)) - ) - ) - ) - ) - ) - ) - ) - ) -} diff --git a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/ShareRoomKeyUseCaseTest.kt b/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/ShareRoomKeyUseCaseTest.kt deleted file mode 100644 index 9f48d7d..0000000 --- a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/ShareRoomKeyUseCaseTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.SessionId -import app.dapk.st.matrix.common.extensions.toJsonString -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.ToDevicePayload -import fake.FakeCredentialsStore -import fake.FakeDeviceService -import fake.FakeMatrixLogger -import fake.FakeOlm -import fixture.* -import io.mockk.coVerify -import kotlinx.coroutines.test.runTest -import org.junit.Test -import test.expect - -private val A_USER_CREDENTIALS = aUserCredentials() -private val A_ROOM_CRYPTO_SESSION = aRoomCryptoSession() -private val A_ROOM_ID = aRoomId() -private val ALGORITHM_MEGOLM = AlgorithmName("m.megolm.v1.aes-sha2") -private val ALGORITHM_OLM = AlgorithmName("m.olm.v1.curve25519-aes-sha2") -private val AN_OLM_ENCRPYTION_RESULT = Olm.EncryptionResult(aCipherText(), type = 1) - -class ShareRoomKeyUseCaseTest { - - private val fakeDeviceService = FakeDeviceService() - private val fakeOlm = FakeOlm() - - private val shareRoomKeyUseCase = ShareRoomKeyUseCaseImpl( - FakeCredentialsStore().also { it.givenCredentials().returns(A_USER_CREDENTIALS) }, - fakeDeviceService, - FakeMatrixLogger(), - fakeOlm - ) - - @Test - fun `when sharing room key then encrypts with olm session and sends to device`() = runTest { - fakeDeviceService.expect { it.sendRoomKeyToDevice(SessionId(any()), any()) } - val olmSessionToEncryptWith = aDeviceCryptoSession() - fakeOlm.givenEncrypts(olmSessionToEncryptWith, expectedPayload(olmSessionToEncryptWith)).returns(AN_OLM_ENCRPYTION_RESULT) - - shareRoomKeyUseCase.invoke(A_ROOM_CRYPTO_SESSION, listOf(olmSessionToEncryptWith), A_ROOM_ID) - - coVerify { - fakeDeviceService.sendRoomKeyToDevice(A_ROOM_CRYPTO_SESSION.id, listOf(expectedToDeviceRoomShareMessage(olmSessionToEncryptWith))) - } - } - - private fun expectedToDeviceRoomShareMessage(olmSessionToEncryptWith: Olm.DeviceCryptoSession) = DeviceService.ToDeviceMessage( - olmSessionToEncryptWith.userId, - olmSessionToEncryptWith.deviceId, - ToDevicePayload.EncryptedToDevicePayload( - algorithmName = ALGORITHM_OLM, - senderKey = A_ROOM_CRYPTO_SESSION.accountCryptoSession.senderKey, - cipherText = mapOf( - olmSessionToEncryptWith.identity to ToDevicePayload.EncryptedToDevicePayload.Inner( - cipherText = AN_OLM_ENCRPYTION_RESULT.cipherText, - type = AN_OLM_ENCRPYTION_RESULT.type, - ) - ) - ) - ) - - private fun expectedPayload(deviceCryptoSession: Olm.DeviceCryptoSession) = mapOf( - "type" to "m.room_key", - "content" to mapOf( - "algorithm" to ALGORITHM_MEGOLM.value, - "room_id" to A_ROOM_ID.value, - "session_id" to A_ROOM_CRYPTO_SESSION.id.value, - "session_key" to A_ROOM_CRYPTO_SESSION.key, - "chain_index" to A_ROOM_CRYPTO_SESSION.messageIndex, - ), - "sender" to A_USER_CREDENTIALS.userId.value, - "sender_device" to A_USER_CREDENTIALS.deviceId.value, - "keys" to mapOf( - "ed25519" to A_ROOM_CRYPTO_SESSION.accountCryptoSession.fingerprint.value - ), - "recipient" to deviceCryptoSession.userId.value, - "recipient_keys" to mapOf( - "ed25519" to deviceCryptoSession.fingerprint.value - ) - ).toJsonString() -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/UpdateKnownOlmSessionUseCaseTest.kt b/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/UpdateKnownOlmSessionUseCaseTest.kt deleted file mode 100644 index 987eaff..0000000 --- a/matrix/services/crypto/src/test/kotlin/app/dapk/st/matrix/crypto/internal/UpdateKnownOlmSessionUseCaseTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package app.dapk.st.matrix.crypto.internal - -import fake.FakeDeviceService -import fake.FakeMatrixLogger -import fixture.* -import internalfake.FakeFetchAccountCryptoUseCase -import internalfake.FakeRegisterOlmSessionUseCase -import kotlinx.coroutines.test.runTest -import org.junit.Test - -private val USERS_TO_UPDATE = listOf(aUserId()) -private val A_SYNC_TOKEN = aSyncToken() -private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession(deviceKeys = aDeviceKeys(deviceId = aDeviceId("unique-device-id"))) -private val A_DEVICE_KEYS = listOf(aDeviceKeys()) -private val OWN_DEVICE_KEYS = listOf(AN_ACCOUNT_CRYPTO_SESSION.deviceKeys) -private val IGNORED_REGISTERED_SESSION = listOf(aDeviceCryptoSession()) - -internal class UpdateKnownOlmSessionUseCaseTest { - - private val fakeFetchAccountCryptoUseCase = FakeFetchAccountCryptoUseCase() - private val fakeDeviceService = FakeDeviceService() - private val fakeRegisterOlmSessionUseCase = FakeRegisterOlmSessionUseCase() - - private val updateKnownOlmSessionUseCase = UpdateKnownOlmSessionUseCaseImpl( - fakeFetchAccountCryptoUseCase, - fakeDeviceService, - fakeRegisterOlmSessionUseCase, - FakeMatrixLogger() - ) - - @Test - fun `when updating know olm sessions, then registers device keys`() = runTest { - fakeFetchAccountCryptoUseCase.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) - fakeDeviceService.givenFetchesDevices(USERS_TO_UPDATE, A_SYNC_TOKEN).returns(A_DEVICE_KEYS) - fakeRegisterOlmSessionUseCase.givenRegistersSessions(A_DEVICE_KEYS, AN_ACCOUNT_CRYPTO_SESSION).returns(IGNORED_REGISTERED_SESSION) - - updateKnownOlmSessionUseCase.invoke(USERS_TO_UPDATE, A_SYNC_TOKEN) - - fakeRegisterOlmSessionUseCase.verifyRegistersKeys(A_DEVICE_KEYS, AN_ACCOUNT_CRYPTO_SESSION) - } - - @Test - fun `given device keys contains own device, when updating known olm session, then skips registering`() = runTest { - fakeFetchAccountCryptoUseCase.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) - fakeDeviceService.givenFetchesDevices(USERS_TO_UPDATE, A_SYNC_TOKEN).returns(OWN_DEVICE_KEYS) - - updateKnownOlmSessionUseCase.invoke(USERS_TO_UPDATE, A_SYNC_TOKEN) - - fakeRegisterOlmSessionUseCase.verifyNoInteractions() - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/internalfake/FakeEncryptMessageWithMegolmUseCase.kt b/matrix/services/crypto/src/test/kotlin/internalfake/FakeEncryptMessageWithMegolmUseCase.kt deleted file mode 100644 index e2ba8c5..0000000 --- a/matrix/services/crypto/src/test/kotlin/internalfake/FakeEncryptMessageWithMegolmUseCase.kt +++ /dev/null @@ -1,14 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.common.DeviceCredentials -import app.dapk.st.matrix.crypto.internal.EncryptMessageWithMegolmUseCase -import app.dapk.st.matrix.crypto.internal.MessageToEncrypt -import io.mockk.coEvery -import io.mockk.mockk -import test.delegateReturn - -class FakeEncryptMessageWithMegolmUseCase : EncryptMessageWithMegolmUseCase by mockk() { - fun givenEncrypt(credentials: DeviceCredentials, message: MessageToEncrypt) = coEvery { - this@FakeEncryptMessageWithMegolmUseCase.invoke(credentials, message) - }.delegateReturn() -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/internalfake/FakeFetchAccountCryptoUseCase.kt b/matrix/services/crypto/src/test/kotlin/internalfake/FakeFetchAccountCryptoUseCase.kt deleted file mode 100644 index 8deb1e7..0000000 --- a/matrix/services/crypto/src/test/kotlin/internalfake/FakeFetchAccountCryptoUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.crypto.internal.FetchAccountCryptoUseCase -import io.mockk.coEvery -import io.mockk.mockk -import test.delegateReturn - -class FakeFetchAccountCryptoUseCase : FetchAccountCryptoUseCase by mockk() { - fun givenFetch() = coEvery { this@FakeFetchAccountCryptoUseCase.invoke() }.delegateReturn() -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/internalfake/FakeFetchMegolmSessionUseCase.kt b/matrix/services/crypto/src/test/kotlin/internalfake/FakeFetchMegolmSessionUseCase.kt deleted file mode 100644 index 3a7bb6a..0000000 --- a/matrix/services/crypto/src/test/kotlin/internalfake/FakeFetchMegolmSessionUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.crypto.internal.FetchMegolmSessionUseCase -import io.mockk.coEvery -import io.mockk.mockk - -internal class FakeFetchMegolmSessionUseCase : FetchMegolmSessionUseCase by mockk() { - fun givenSessionForRoom(roomId: RoomId, roomCryptoSession: Olm.RoomCryptoSession) { - coEvery { this@FakeFetchMegolmSessionUseCase.invoke(roomId) } returns roomCryptoSession - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/internalfake/FakeMaybeCreateAndUploadOneTimeKeysUseCase.kt b/matrix/services/crypto/src/test/kotlin/internalfake/FakeMaybeCreateAndUploadOneTimeKeysUseCase.kt deleted file mode 100644 index 38299cf..0000000 --- a/matrix/services/crypto/src/test/kotlin/internalfake/FakeMaybeCreateAndUploadOneTimeKeysUseCase.kt +++ /dev/null @@ -1,6 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.crypto.internal.MaybeCreateAndUploadOneTimeKeysUseCase -import io.mockk.mockk - -class FakeMaybeCreateAndUploadOneTimeKeysUseCase : MaybeCreateAndUploadOneTimeKeysUseCase by mockk() \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/internalfake/FakeRegisterOlmSessionUseCase.kt b/matrix/services/crypto/src/test/kotlin/internalfake/FakeRegisterOlmSessionUseCase.kt deleted file mode 100644 index ea59daf..0000000 --- a/matrix/services/crypto/src/test/kotlin/internalfake/FakeRegisterOlmSessionUseCase.kt +++ /dev/null @@ -1,24 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.crypto.internal.RegisterOlmSessionUseCase -import app.dapk.st.matrix.device.internal.DeviceKeys -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import test.delegateReturn - -internal class FakeRegisterOlmSessionUseCase : RegisterOlmSessionUseCase by mockk() { - - fun givenRegistersSessions(devices: List, account: Olm.AccountCryptoSession) = coEvery { - this@FakeRegisterOlmSessionUseCase.invoke(devices, account) - }.delegateReturn() - - fun verifyRegistersKeys(devices: List, account: Olm.AccountCryptoSession) { - coVerify { this@FakeRegisterOlmSessionUseCase.invoke(devices, account) } - } - - fun verifyNoInteractions() { - coVerify(exactly = 0) { this@FakeRegisterOlmSessionUseCase.invoke(any(), any()) } - } -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/internalfake/FakeShareRoomKeyUseCase.kt b/matrix/services/crypto/src/test/kotlin/internalfake/FakeShareRoomKeyUseCase.kt deleted file mode 100644 index 671bd45..0000000 --- a/matrix/services/crypto/src/test/kotlin/internalfake/FakeShareRoomKeyUseCase.kt +++ /dev/null @@ -1,23 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.crypto.internal.ShareRoomKeyUseCase -import io.mockk.coJustRun -import io.mockk.mockk - -internal class FakeShareRoomKeyUseCase : ShareRoomKeyUseCase { - - private val instance = mockk() - - override suspend fun invoke(room: Olm.RoomCryptoSession, p2: List, p3: RoomId) { - instance.invoke(room, p2, p3) - } - - fun expect(roomCryptoSession: Olm.RoomCryptoSession, olmSessions: List, roomId: RoomId) { - coJustRun { - instance.invoke(roomCryptoSession, olmSessions, roomId) - } - } - -} \ No newline at end of file diff --git a/matrix/services/crypto/src/test/kotlin/internalfake/FakeUpdateKnownOlmSessionUseCase.kt b/matrix/services/crypto/src/test/kotlin/internalfake/FakeUpdateKnownOlmSessionUseCase.kt deleted file mode 100644 index fa449e5..0000000 --- a/matrix/services/crypto/src/test/kotlin/internalfake/FakeUpdateKnownOlmSessionUseCase.kt +++ /dev/null @@ -1,6 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.crypto.internal.UpdateKnownOlmSessionUseCase -import io.mockk.mockk - -class FakeUpdateKnownOlmSessionUseCase : UpdateKnownOlmSessionUseCase by mockk() \ No newline at end of file diff --git a/matrix/services/crypto/src/testFixtures/kotlin/fake/FakeCryptoService.kt b/matrix/services/crypto/src/testFixtures/kotlin/fake/FakeCryptoService.kt deleted file mode 100644 index 40261a3..0000000 --- a/matrix/services/crypto/src/testFixtures/kotlin/fake/FakeCryptoService.kt +++ /dev/null @@ -1,11 +0,0 @@ -package fake - -import app.dapk.st.matrix.crypto.CryptoService -import io.mockk.coEvery -import io.mockk.mockk -import test.delegateReturn -import java.io.InputStream - -class FakeCryptoService : CryptoService by mockk() { - fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn() -} diff --git a/matrix/services/crypto/src/testFixtures/kotlin/fake/FakeOlm.kt b/matrix/services/crypto/src/testFixtures/kotlin/fake/FakeOlm.kt deleted file mode 100644 index a4848bf..0000000 --- a/matrix/services/crypto/src/testFixtures/kotlin/fake/FakeOlm.kt +++ /dev/null @@ -1,74 +0,0 @@ -package fake - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.internal.DeviceKeys -import io.mockk.coEvery -import io.mockk.mockk -import io.mockk.slot -import org.amshove.kluent.shouldBeEqualTo -import test.Returns -import test.delegateReturn -import test.returns - -class FakeOlm : Olm by mockk() { - - fun givenEncrypts(roomCryptoSession: Olm.RoomCryptoSession, roomId: RoomId, messageJson: JsonString, result: CipherText) { - coEvery { roomCryptoSession.encrypt(roomId, messageJson) } returns result - } - - fun givenEncrypts(olmSession: Olm.DeviceCryptoSession, messageJson: JsonString) = coEvery { olmSession.encrypt(messageJson) }.delegateReturn() - - fun givenCreatesAccount(credentials: UserCredentials): Returns { - val slot = slot Unit>() - val mockKStubScope = coEvery { ensureAccountCrypto(credentials, capture(slot)) } - return returns { value -> - mockKStubScope coAnswers { - slot.captured.invoke(value) - value - } - } - } - - fun givenAccount(credentials: UserCredentials): Returns { - return coEvery { ensureAccountCrypto(credentials, any()) }.delegateReturn() - } - - fun givenRoomCrypto(roomId: RoomId, account: Olm.AccountCryptoSession) = coEvery { ensureRoomCrypto(roomId, account) }.delegateReturn() - - fun givenMissingOlmSessions(newDevices: List): Returns> { - val slot = slot) -> List>() - val mockKStubScope = coEvery { olmSessions(newDevices, capture(slot)) } - return returns { value -> - mockKStubScope coAnswers { - slot.captured.invoke(newDevices).also { - value shouldBeEqualTo it - } - } - } - } - - fun givenGeneratesOneTimeKeys( - accountCryptoSession: Olm.AccountCryptoSession, - countToCreate: Int, - credentials: UserCredentials - ): Returns { - val slot = slot Unit>() - val mockKStubScope = coEvery { with(accountCryptoSession) { generateOneTimeKeys(countToCreate, credentials, capture(slot)) } } - return returns { value -> - mockKStubScope coAnswers { - slot.captured.invoke(value) - } - } - } - - fun givenDeviceCrypto(input: Olm.OlmSessionInput, account: Olm.AccountCryptoSession) = coEvery { ensureDeviceCrypto(input, account) }.delegateReturn() - - fun givenDecrypting(payload: EncryptedMessageContent.MegOlmV1) = coEvery { decryptMegOlm(payload.sessionId, payload.cipherText) } - - fun givenDecrypting(payload: EncryptedMessageContent.OlmV1, account: Olm.AccountCryptoSession) = coEvery { - val cipherForAccount = payload.cipherText[account.senderKey]!! - decryptOlm(account, payload.senderKey, cipherForAccount.type, cipherForAccount.body) - }.delegateReturn() -} \ No newline at end of file diff --git a/matrix/services/crypto/src/testFixtures/kotlin/fixture/CryptoSessionFixtures.kt b/matrix/services/crypto/src/testFixtures/kotlin/fixture/CryptoSessionFixtures.kt deleted file mode 100644 index 18d55b7..0000000 --- a/matrix/services/crypto/src/testFixtures/kotlin/fixture/CryptoSessionFixtures.kt +++ /dev/null @@ -1,32 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.Olm -import app.dapk.st.matrix.device.internal.DeviceKeys -import io.mockk.mockk - -fun anAccountCryptoSession( - fingerprint: Ed25519 = aEd25519(), - senderKey: Curve25519 = aCurve25519(), - deviceKeys: DeviceKeys = aDeviceKeys(), - maxKeys: Int = 5, - hasKeys: Boolean = false, - olmAccount: Any = mockk(), -) = Olm.AccountCryptoSession(fingerprint, senderKey, deviceKeys, hasKeys, maxKeys, olmAccount) - -fun aRoomCryptoSession( - creationTimestampUtc: Long = 0L, - key: String = "a-room-key", - messageIndex: Int = 100, - accountCryptoSession: Olm.AccountCryptoSession = anAccountCryptoSession(), - id: SessionId = aSessionId("a-room-crypto-session-id"), - outBound: Any = mockk(), -) = Olm.RoomCryptoSession(creationTimestampUtc, key, messageIndex, accountCryptoSession, id, outBound) - -fun aDeviceCryptoSession( - deviceId: DeviceId = aDeviceId(), - userId: UserId = aUserId(), - identity: Curve25519 = aCurve25519(), - fingerprint: Ed25519 = aEd25519(), - olmSession: List = emptyList(), -) = Olm.DeviceCryptoSession(deviceId, userId, identity, fingerprint, olmSession) diff --git a/matrix/services/crypto/src/testFixtures/kotlin/fixture/FakeRoomMembersProvider.kt b/matrix/services/crypto/src/testFixtures/kotlin/fixture/FakeRoomMembersProvider.kt deleted file mode 100644 index 85303bb..0000000 --- a/matrix/services/crypto/src/testFixtures/kotlin/fixture/FakeRoomMembersProvider.kt +++ /dev/null @@ -1,11 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.crypto.RoomMembersProvider -import io.mockk.coEvery -import io.mockk.mockk -import test.delegateReturn - -class FakeRoomMembersProvider : RoomMembersProvider by mockk() { - fun givenUserIdsForRoom(roomId: RoomId) = coEvery { userIdsForRoom(roomId) }.delegateReturn() -} \ No newline at end of file diff --git a/matrix/services/device/build.gradle b/matrix/services/device/build.gradle deleted file mode 100644 index ef46129..0000000 --- a/matrix/services/device/build.gradle +++ /dev/null @@ -1,9 +0,0 @@ -plugins { id 'java-test-fixtures' } -applyMatrixServiceModule(project) - -dependencies { - kotlinFixtures(it) - testFixturesImplementation(testFixtures(project(":matrix:common"))) - testFixturesImplementation(testFixtures(project(":core"))) - testFixturesImplementation Dependencies.mavenCentral.kotlinSerializationJson -} \ No newline at end of file diff --git a/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/DeviceService.kt b/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/DeviceService.kt deleted file mode 100644 index 1244c65..0000000 --- a/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/DeviceService.kt +++ /dev/null @@ -1,141 +0,0 @@ -package app.dapk.st.matrix.device - -import app.dapk.st.matrix.InstallExtender -import app.dapk.st.matrix.MatrixService -import app.dapk.st.matrix.MatrixServiceInstaller -import app.dapk.st.matrix.MatrixServiceProvider -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.device.internal.ClaimKeysResponse -import app.dapk.st.matrix.device.internal.DefaultDeviceService -import app.dapk.st.matrix.device.internal.DeviceKeys -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -private val SERVICE_KEY = DeviceService::class - -interface DeviceService : MatrixService { - - suspend fun uploadDeviceKeys(deviceKeys: DeviceKeys) - suspend fun uploadOneTimeKeys(oneTimeKeys: OneTimeKeys) - suspend fun fetchDevices(userIds: List, syncToken: SyncToken?): List - suspend fun checkForNewDevices(self: DeviceKeys, userIds: List, id: SessionId): List - suspend fun ensureDevice(userId: UserId, deviceId: DeviceId): DeviceKeys - suspend fun claimKeys(claims: List): ClaimKeysResponse - suspend fun sendRoomKeyToDevice(sessionId: SessionId, messages: List) - suspend fun sendToDevice(eventType: EventType, transactionId: String, userId: UserId, deviceId: DeviceId, payload: ToDevicePayload) - suspend fun updateStaleDevices(userIds: List) - - @JvmInline - value class OneTimeKeys(val keys: List) { - - sealed interface Key { - data class SignedCurve(val keyId: String, val value: String, val signature: Ed25519Signature) : Key { - data class Ed25519Signature(val value: SignedJson, val deviceId: DeviceId, val userId: UserId) - } - } - - } - - data class KeyClaim(val userId: UserId, val deviceId: DeviceId, val algorithmName: AlgorithmName) - - data class ToDeviceMessage( - val senderId: UserId, - val deviceId: DeviceId, - val encryptedMessage: ToDevicePayload.EncryptedToDevicePayload - ) -} - - -@Serializable -sealed class ToDevicePayload { - - @Serializable - data class EncryptedToDevicePayload( - @SerialName("algorithm") val algorithmName: AlgorithmName, - @SerialName("sender_key") val senderKey: Curve25519, - @SerialName("ciphertext") val cipherText: Map, - ) : ToDevicePayload() { - - @Serializable - data class Inner( - @SerialName("body") val cipherText: CipherText, - @SerialName("type") val type: Long, - ) - } - - @Serializable - data class VerificationRequest( - @SerialName("from_device") val fromDevice: DeviceId, - @SerialName("methods") val methods: List, - @SerialName("transaction_id") val transactionId: String, - @SerialName("timestamp") val timestampPosix: Long, - ) : ToDevicePayload(), VerificationPayload - - @Serializable - data class VerificationStart( - @SerialName("from_device") val fromDevice: DeviceId, - @SerialName("method") val method: String, - @SerialName("key_agreement_protocols") val protocols: List, - @SerialName("hashes") val hashes: List, - @SerialName("message_authentication_codes") val codes: List, - @SerialName("short_authentication_string") val short: List, - @SerialName("transaction_id") val transactionId: String, - ) : ToDevicePayload(), VerificationPayload - - @Serializable - data class VerificationAccept( - @SerialName("transaction_id") val transactionId: String, - @SerialName("from_device") val fromDevice: DeviceId, - @SerialName("method") val method: String, - @SerialName("key_agreement_protocol") val protocol: String, - @SerialName("hash") val hash: String, - @SerialName("message_authentication_code") val code: String, - @SerialName("short_authentication_string") val short: List, - @SerialName("commitment") val commitment: String, - ) : ToDevicePayload(), VerificationPayload - - @Serializable - data class VerificationReady( - @SerialName("from_device") val fromDevice: DeviceId, - @SerialName("methods") val methods: List, - @SerialName("transaction_id") val transactionId: String, - ) : ToDevicePayload(), VerificationPayload - - @Serializable - data class VerificationKey( - @SerialName("transaction_id") val transactionId: String, - @SerialName("key") val key: String, - ) : ToDevicePayload(), VerificationPayload - - @Serializable - data class VerificationMac( - @SerialName("transaction_id") val transactionId: String, - @SerialName("keys") val keys: String, - @SerialName("mac") val mac: Map, - ) : ToDevicePayload(), VerificationPayload - - @Serializable - data class VerificationDone( - @SerialName("transaction_id") val transactionId: String, - ) : ToDevicePayload(), VerificationPayload - - - sealed interface VerificationPayload -} - -fun MatrixServiceInstaller.installEncryptionService(knownDeviceStore: KnownDeviceStore): InstallExtender { - return this.install { (httpClient, _, _, logger) -> - SERVICE_KEY to DefaultDeviceService(httpClient, logger, knownDeviceStore) - } -} - -fun MatrixServiceProvider.deviceService(): DeviceService = this.getService(key = SERVICE_KEY) - -interface KnownDeviceStore { - suspend fun updateDevices(devices: Map>): List - suspend fun markOutdated(userIds: List) - suspend fun maybeConsumeOutdated(userIds: List): List - suspend fun devicesMegolmSession(userIds: List, sessionId: SessionId): List - suspend fun associateSession(sessionId: SessionId, deviceIds: List) - suspend fun device(userId: UserId, deviceId: DeviceId): DeviceKeys? -} \ No newline at end of file diff --git a/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/ApiMessage.kt b/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/ApiMessage.kt deleted file mode 100644 index a2b32ad..0000000 --- a/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/ApiMessage.kt +++ /dev/null @@ -1,26 +0,0 @@ -package app.dapk.st.matrix.device.internal - -import app.dapk.st.matrix.common.MessageType -import app.dapk.st.matrix.common.RoomId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -sealed class ApiMessage { - - @Serializable - @SerialName("text_message") - data class TextMessage( - @SerialName("content") val content: TextContent, - @SerialName("room_id") val roomId: RoomId, - @SerialName("type") val type: String, - ) : ApiMessage() { - - @Serializable - data class TextContent( - @SerialName("body") val body: String, - @SerialName("msgtype") val type: String = MessageType.TEXT.value, - ) - } - -} \ No newline at end of file diff --git a/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/DefaultDeviceService.kt b/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/DefaultDeviceService.kt deleted file mode 100644 index ad9fbdf..0000000 --- a/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/DefaultDeviceService.kt +++ /dev/null @@ -1,141 +0,0 @@ -package app.dapk.st.matrix.device.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.DeviceService.OneTimeKeys.Key.SignedCurve.Ed25519Signature -import app.dapk.st.matrix.device.KnownDeviceStore -import app.dapk.st.matrix.device.ToDevicePayload -import app.dapk.st.matrix.http.MatrixHttpClient -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import java.util.* - -internal class DefaultDeviceService( - private val httpClient: MatrixHttpClient, - private val logger: MatrixLogger, - private val knownDeviceStore: KnownDeviceStore, -) : DeviceService { - - override suspend fun uploadOneTimeKeys(oneTimeKeys: DeviceService.OneTimeKeys) { - val jsonCryptoKeys = oneTimeKeys.keys.associate { - when (it) { - is DeviceService.OneTimeKeys.Key.SignedCurve -> { - "signed_curve25519:${it.keyId}" to JsonObject( - content = mapOf( - "key" to JsonPrimitive(it.value), - "signatures" to it.signature.toJson() - ) - ) - } - } - } - - val keyRequest = UploadKeyRequest( - deviceKeys = null, - oneTimeKeys = jsonCryptoKeys - ) - logger.matrixLog("uploading one time keys") - logger.matrixLog(jsonCryptoKeys) - httpClient.execute(uploadKeysRequest(keyRequest)).also { - logger.matrixLog(it) - } - } - - override suspend fun uploadDeviceKeys(deviceKeys: DeviceKeys) { - logger.matrixLog("uploading device keys") - val keyRequest = UploadKeyRequest( - deviceKeys = deviceKeys, - oneTimeKeys = null - ) - logger.matrixLog(keyRequest) - httpClient.execute(uploadKeysRequest(keyRequest)).also { - logger.matrixLog(it) - } - } - - private fun Ed25519Signature.toJson() = JsonObject( - content = mapOf( - this.userId.value to JsonObject( - content = mapOf( - "ed25519:${this.deviceId.value}" to JsonPrimitive(this.value.value) - ) - ) - ) - ) - - override suspend fun fetchDevices(userIds: List, syncToken: SyncToken?): List { - val request = QueryKeysRequest( - deviceKeys = userIds.associateWith { emptyList() }, - token = syncToken?.value, - ) - - logger.crypto("querying keys for: $userIds") - val apiResponse = httpClient.execute(queryKeys(request)) - logger.crypto("got keys for ${apiResponse.deviceKeys.keys}") - - return apiResponse.deviceKeys.values.map { it.values }.flatten().also { - knownDeviceStore.updateDevices(apiResponse.deviceKeys) - } - } - - override suspend fun claimKeys(claims: List): ClaimKeysResponse { - val request = ClaimKeysRequest(oneTimeKeys = claims.groupBy { it.userId }.mapValues { - it.value.associate { it.deviceId to it.algorithmName } - }) - return httpClient.execute(claimKeys(request)) - } - - override suspend fun sendRoomKeyToDevice(sessionId: SessionId, messages: List) { - val associateBy = messages.groupBy { it.senderId }.mapValues { - it.value.associateBy { it.deviceId }.mapValues { it.value.encryptedMessage } - } - - logger.crypto("sending to device: ${associateBy.map { it.key to it.value.keys }}") - - val txnId = UUID.randomUUID().toString() - httpClient.execute(sendToDeviceRequest(EventType.ENCRYPTED, txnId, SendToDeviceRequest(associateBy))) - knownDeviceStore.associateSession(sessionId, messages.map { it.deviceId }) - } - - override suspend fun sendToDevice(eventType: EventType, transactionId: String, userId: UserId, deviceId: DeviceId, payload: ToDevicePayload) { - val messages = mapOf( - userId to mapOf( - deviceId to payload - ) - ) - httpClient.execute(sendToDeviceRequest(eventType, transactionId, SendToDeviceRequest(messages))) - } - - override suspend fun updateStaleDevices(userIds: List) { - logger.matrixLog("devices changed: $userIds") - knownDeviceStore.markOutdated(userIds) - } - - override suspend fun checkForNewDevices(self: DeviceKeys, userIds: List, id: SessionId): List { - val outdatedUsersToNotify = knownDeviceStore.maybeConsumeOutdated(userIds) - logger.crypto("found outdated users: $outdatedUsersToNotify") - val notOutdatedIds = userIds.filterNot { outdatedUsersToNotify.contains(it) } - val knownKeys = knownDeviceStore.devicesMegolmSession(notOutdatedIds, id) - - val knownUsers = knownKeys.map { it.userId } - val usersWithoutKnownSessions = notOutdatedIds - knownUsers.toSet() - logger.crypto("found users without known sessions: $usersWithoutKnownSessions") - - val usersToUpdate = outdatedUsersToNotify + usersWithoutKnownSessions - val newDevices = if (usersToUpdate.isNotEmpty()) { - fetchDevices(usersToUpdate, syncToken = null).filter { - it.deviceId != self.deviceId - } - } else { - logger.crypto("didn't find any new devices") - emptyList() - } - - return newDevices - } - - override suspend fun ensureDevice(userId: UserId, deviceId: DeviceId): DeviceKeys { - return knownDeviceStore.device(userId, deviceId) ?: fetchDevices(listOf(userId), syncToken = null).first() - } -} - diff --git a/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/EncyptionRequests.kt b/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/EncyptionRequests.kt deleted file mode 100644 index 014dbc5..0000000 --- a/matrix/services/device/src/main/kotlin/app/dapk/st/matrix/device/internal/EncyptionRequests.kt +++ /dev/null @@ -1,88 +0,0 @@ -package app.dapk.st.matrix.device.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.device.ToDevicePayload -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest -import app.dapk.st.matrix.http.jsonBody -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement - -internal fun uploadKeysRequest(keyRequest: UploadKeyRequest) = httpRequest( - path = "_matrix/client/r0/keys/upload", - method = MatrixHttpClient.Method.POST, - body = jsonBody(keyRequest, MatrixHttpClient.jsonWithDefaults), -) - -internal fun queryKeys(queryRequest: QueryKeysRequest) = httpRequest( - path = "_matrix/client/r0/keys/query", - method = MatrixHttpClient.Method.POST, - body = jsonBody(queryRequest, MatrixHttpClient.jsonWithDefaults), -) - - -internal fun claimKeys(claimRequest: ClaimKeysRequest) = httpRequest( - path = "_matrix/client/r0/keys/claim", - method = MatrixHttpClient.Method.POST, - body = jsonBody(claimRequest, MatrixHttpClient.jsonWithDefaults), -) - -internal fun sendToDeviceRequest(eventType: EventType, txnId: String, request: SendToDeviceRequest) = httpRequest( - path = "_matrix/client/r0/sendToDevice/${eventType.value}/${txnId}", - method = MatrixHttpClient.Method.PUT, - body = jsonBody(request) -) - -@Serializable -internal data class UploadKeysResponse( - @SerialName("one_time_key_counts") val keyCounts: Map -) - -@Serializable -internal data class SendToDeviceRequest( - @SerialName("messages") val messages: Map> -) - - -@Serializable -internal data class UploadKeyRequest( - @SerialName("device_keys") val deviceKeys: DeviceKeys? = null, - @SerialName("one_time_keys") val oneTimeKeys: Map? = null, -) - -@Serializable -internal data class QueryKeysRequest( - @SerialName("timeout") val timeout: Int = 10000, - @SerialName("device_keys") val deviceKeys: Map>, - @SerialName("token") val token: String? = null, -) - -@Serializable -internal data class QueryKeysResponse( - @SerialName("device_keys") val deviceKeys: Map> -) - -@Serializable -internal data class ClaimKeysRequest( - @SerialName("timeout") val timeout: Int = 10000, - @SerialName("one_time_keys") val oneTimeKeys: Map>, -) - -@Serializable -data class ClaimKeysResponse( - @SerialName("one_time_keys") val oneTimeKeys: Map>, - @SerialName("failures") val failures: Map -) - -@Serializable -data class DeviceKeys( - @SerialName("user_id") val userId: UserId, - @SerialName("device_id") val deviceId: DeviceId, - @SerialName("algorithms") val algorithms: List, - @SerialName("keys") val keys: Map, - @SerialName("signatures") val signatures: Map>, -) { - fun fingerprint() = Ed25519(keys["ed25519:${deviceId.value}"]!!) - fun identity() = Curve25519(keys["curve25519:${deviceId.value}"]!!) -} \ No newline at end of file diff --git a/matrix/services/device/src/testFixtures/kotlin/fake/FakeDeviceService.kt b/matrix/services/device/src/testFixtures/kotlin/fake/FakeDeviceService.kt deleted file mode 100644 index 9deea18..0000000 --- a/matrix/services/device/src/testFixtures/kotlin/fake/FakeDeviceService.kt +++ /dev/null @@ -1,26 +0,0 @@ -package fake - -import app.dapk.st.matrix.common.SessionId -import app.dapk.st.matrix.common.SyncToken -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.device.DeviceService -import app.dapk.st.matrix.device.internal.DeviceKeys -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import test.Returns -import test.delegateReturn - -class FakeDeviceService : DeviceService by mockk() { - fun givenNewDevices(accountKeys: DeviceKeys, usersInRoom: List, roomCryptoSessionId: SessionId): Returns> { - return coEvery { checkForNewDevices(accountKeys, usersInRoom, roomCryptoSessionId) }.delegateReturn() - } - - fun verifyDidntUploadOneTimeKeys() { - coVerify(exactly = 0) { uploadOneTimeKeys(DeviceService.OneTimeKeys(any())) } - } - - fun givenClaimsKeys(claims: List) = coEvery { claimKeys(claims) }.delegateReturn() - - fun givenFetchesDevices(userIds: List, syncToken: SyncToken?) = coEvery { fetchDevices(userIds, syncToken) }.delegateReturn() -} \ No newline at end of file diff --git a/matrix/services/device/src/testFixtures/kotlin/fixture/ClaimKeysResponseFixture.kt b/matrix/services/device/src/testFixtures/kotlin/fixture/ClaimKeysResponseFixture.kt deleted file mode 100644 index 90415c0..0000000 --- a/matrix/services/device/src/testFixtures/kotlin/fixture/ClaimKeysResponseFixture.kt +++ /dev/null @@ -1,11 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.device.internal.ClaimKeysResponse -import kotlinx.serialization.json.JsonElement - -fun aClaimKeysResponse( - oneTimeKeys: Map> = emptyMap(), - failures: Map = emptyMap() -) = ClaimKeysResponse(oneTimeKeys, failures) \ No newline at end of file diff --git a/matrix/services/device/src/testFixtures/kotlin/fixture/DeviceKeysFixutre.kt b/matrix/services/device/src/testFixtures/kotlin/fixture/DeviceKeysFixutre.kt deleted file mode 100644 index 40d5e3c..0000000 --- a/matrix/services/device/src/testFixtures/kotlin/fixture/DeviceKeysFixutre.kt +++ /dev/null @@ -1,15 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.device.internal.DeviceKeys - - -fun aDeviceKeys( - userId: UserId = aUserId(), - deviceId: DeviceId = aDeviceId(), - algorithms: List = listOf(anAlgorithmName()), - keys: Map = emptyMap(), - signatures: Map> = emptyMap(), -) = DeviceKeys(userId, deviceId, algorithms, keys, signatures) \ No newline at end of file diff --git a/matrix/services/device/src/testFixtures/kotlin/fixture/KeyClaimFixture.kt b/matrix/services/device/src/testFixtures/kotlin/fixture/KeyClaimFixture.kt deleted file mode 100644 index 790197c..0000000 --- a/matrix/services/device/src/testFixtures/kotlin/fixture/KeyClaimFixture.kt +++ /dev/null @@ -1,12 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.device.DeviceService - -fun aKeyClaim( - userId: UserId = aUserId(), - deviceId: DeviceId = aDeviceId(), - algorithmName: AlgorithmName = anAlgorithmName(), -) = DeviceService.KeyClaim(userId, deviceId, algorithmName) \ No newline at end of file diff --git a/matrix/services/message/build.gradle b/matrix/services/message/build.gradle deleted file mode 100644 index 9143c32..0000000 --- a/matrix/services/message/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -plugins { id 'java-test-fixtures' } -applyMatrixServiceModule(project) - -dependencies { - implementation project(":core") - - kotlinFixtures(it) - testFixturesImplementation(testFixtures(project(":core"))) - testFixturesImplementation(testFixtures(project(":matrix:common"))) -} diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/ApiSendResponse.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/ApiSendResponse.kt deleted file mode 100644 index 97fdc57..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/ApiSendResponse.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.dapk.st.matrix.message - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.MxUrl -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ApiSendResponse( - @SerialName("event_id") val eventId: EventId, -) - -@Serializable -data class ApiUploadResponse( - @SerialName("content_uri") val contentUri: MxUrl, -) \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/BackgroundScheduler.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/BackgroundScheduler.kt deleted file mode 100644 index 04241e0..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/BackgroundScheduler.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.dapk.st.matrix.message - -import app.dapk.st.matrix.common.JsonString - -interface BackgroundScheduler { - - fun schedule(key: String, task: Task) - - data class Task(val type: String, val jsonPayload: JsonString) -} - diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt deleted file mode 100644 index 10fa046..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.dapk.st.matrix.message - -import java.io.File -import java.io.InputStream -import java.net.URI - -fun interface MediaEncrypter { - - suspend fun encrypt(input: InputStream): Result - - data class Result( - val uri: URI, - val contentLength: Long, - val algorithm: String, - val ext: Boolean, - val keyOperations: List, - val kty: String, - val k: String, - val iv: String, - val hashes: Map, - val v: String, - ) { - - fun openStream() = File(uri).inputStream() - } - -} - -internal object MissingMediaEncrypter : MediaEncrypter { - override suspend fun encrypt(input: InputStream) = throw IllegalStateException("No encrypter instance set") -} diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt deleted file mode 100644 index a9d06a9..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.dapk.st.matrix.message - -import app.dapk.st.matrix.common.* - -fun interface MessageEncrypter { - - suspend fun encrypt(message: ClearMessagePayload): EncryptedMessagePayload - - data class EncryptedMessagePayload( - val algorithmName: AlgorithmName, - val senderKey: String, - val cipherText: CipherText, - val sessionId: SessionId, - val deviceId: DeviceId - ) - - data class ClearMessagePayload( - val roomId: RoomId, - val contents: JsonString, - ) -} - -internal object MissingMessageEncrypter : MessageEncrypter { - override suspend fun encrypt(message: MessageEncrypter.ClearMessagePayload) = throw IllegalStateException("No encrypter instance set") -} diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt deleted file mode 100644 index 063e1b4..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ /dev/null @@ -1,162 +0,0 @@ -package app.dapk.st.matrix.message - -import app.dapk.st.matrix.* -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.message.internal.DefaultMessageService -import app.dapk.st.matrix.message.internal.ImageContentReader -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient - -private val SERVICE_KEY = MessageService::class - -interface MessageService : MatrixService { - - fun localEchos(roomId: RoomId): Flow> - fun localEchos(): Flow>> - - suspend fun sendMessage(message: Message) - suspend fun scheduleMessage(message: Message) - suspend fun sendEventMessage(roomId: RoomId, message: EventMessage) - - sealed interface EventMessage { - - @Serializable - data class Encryption( - @SerialName("algorithm") val algorithm: AlgorithmName - ) : EventMessage - - } - - @Serializable - sealed interface Message { - @Serializable - @SerialName("text_message") - data class TextMessage( - @SerialName("content") val content: Content.TextContent, - @SerialName("send_encrypted") val sendEncrypted: Boolean, - @SerialName("room_id") val roomId: RoomId, - @SerialName("local_id") val localId: String, - @SerialName("timestamp") val timestampUtc: Long, - @SerialName("reply") val reply: Reply? = null, - ) : Message { - @Serializable - data class Reply( - val author: RoomMember, - val originalMessage: RichText, - val replyContent: String, - val eventId: EventId, - val timestampUtc: Long, - ) - } - - @Serializable - @SerialName("image_message") - data class ImageMessage( - @SerialName("content") val content: Content.ImageContent, - @SerialName("send_encrypted") val sendEncrypted: Boolean, - @SerialName("room_id") val roomId: RoomId, - @SerialName("local_id") val localId: String, - @SerialName("timestamp") val timestampUtc: Long, - ) : Message - - @Serializable - sealed class Content { - @Serializable - data class TextContent( - @SerialName("body") val body: RichText, - @SerialName("msgtype") val type: String = MessageType.TEXT.value, - ) : Content() - - @Serializable - data class ImageContent( - @SerialName("uri") val uri: String, - @SerialName("meta") val meta: Meta, - ) : Content() { - - @Serializable - data class Meta( - @SerialName("height") val height: Int, - @SerialName("width") val width: Int, - @SerialName("size") val size: Long, - @SerialName("file_name") val fileName: String, - @SerialName("mime_type") val mimeType: String, - ) - } - - } - } - - @Serializable - data class LocalEcho( - @SerialName("event_id") val eventId: EventId?, - @SerialName("message") val message: Message, - @SerialName("state") val state: State, - ) { - - @Transient - val timestampUtc = when (message) { - is Message.TextMessage -> message.timestampUtc - is Message.ImageMessage -> message.timestampUtc - } - - @Transient - val roomId = when (message) { - is Message.TextMessage -> message.roomId - is Message.ImageMessage -> message.roomId - } - - @Transient - val localId = when (message) { - is Message.TextMessage -> message.localId - is Message.ImageMessage -> message.localId - } - - @Serializable - sealed class State { - @Serializable - @SerialName("sending") - object Sending : State() - - @Serializable - @SerialName("sent") - object Sent : State() - - @Serializable - @SerialName("error") - data class Error( - @SerialName("message") val message: String, - @SerialName("error_type") val errorType: Type, - ) : State() { - - @Serializable - enum class Type { - UNKNOWN - } - } - } - } - -} - -fun MatrixServiceInstaller.installMessageService( - localEchoStore: LocalEchoStore, - backgroundScheduler: BackgroundScheduler, - imageContentReader: ImageContentReader, - messageEncrypter: ServiceDepFactory = ServiceDepFactory { MissingMessageEncrypter }, - mediaEncrypter: ServiceDepFactory = ServiceDepFactory { MissingMediaEncrypter }, -): InstallExtender { - return this.install { (httpClient, _, installedServices) -> - SERVICE_KEY to DefaultMessageService( - httpClient, - localEchoStore, - backgroundScheduler, - messageEncrypter.create(installedServices), - mediaEncrypter.create(installedServices), - imageContentReader - ) - } -} - -fun MatrixServiceProvider.messageService(): MessageService = this.getService(key = SERVICE_KEY) diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/Store.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/Store.kt deleted file mode 100644 index 92c7a30..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/Store.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.dapk.st.matrix.message - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import kotlinx.coroutines.flow.Flow - -interface LocalEchoStore { - - suspend fun preload() - suspend fun messageTransaction(message: MessageService.Message, action: suspend () -> EventId) - fun observeLocalEchos(roomId: RoomId): Flow> - fun observeLocalEchos(): Flow>> - fun markSending(message: MessageService.Message) -} diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt deleted file mode 100644 index d71b6da..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt +++ /dev/null @@ -1,92 +0,0 @@ -package app.dapk.st.matrix.message.internal - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.MessageType -import app.dapk.st.matrix.common.MxUrl -import app.dapk.st.matrix.common.RoomId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -sealed class ApiMessage { - - @Serializable - @SerialName("text_message") - data class TextMessage( - @SerialName("content") val content: TextContent, - @SerialName("room_id") val roomId: RoomId, - @SerialName("type") val type: String, - ) : ApiMessage() { - - @Serializable - data class TextContent( - @SerialName("body") val body: String, - @SerialName("m.relates_to") val relatesTo: RelatesTo? = null, - @SerialName("formatted_body") val formattedBody: String? = null, - @SerialName("format") val format: String? = null, - ) : ApiMessageContent { - - @SerialName("msgtype") - val type: String = MessageType.TEXT.value - } - } - - @Serializable - data class RelatesTo( - @SerialName("m.in_reply_to") val inReplyTo: InReplyTo - ) { - - @Serializable - data class InReplyTo( - @SerialName("event_id") val eventId: EventId - ) - - } - - @Serializable - @SerialName("image_message") - data class ImageMessage( - @SerialName("content") val content: ImageContent, - @SerialName("room_id") val roomId: RoomId, - @SerialName("type") val type: String, - ) : ApiMessage() { - - @Serializable - data class ImageContent( - @SerialName("url") val url: MxUrl?, - @SerialName("body") val filename: String, - @SerialName("info") val info: Info, - @SerialName("msgtype") val type: String = MessageType.IMAGE.value, - @SerialName("file") val file: File? = null, - ) : ApiMessageContent { - - @Serializable - data class Info( - @SerialName("h") val height: Int, - @SerialName("w") val width: Int, - @SerialName("mimetype") val mimeType: String, - @SerialName("size") val size: Long, - ) - - @Serializable - data class File( - @SerialName("url") val url: MxUrl, - @SerialName("key") val key: EncryptionMeta, - @SerialName("iv") val iv: String, - @SerialName("hashes") val hashes: Map, - @SerialName("v") val v: String - ) { - @Serializable - data class EncryptionMeta( - @SerialName("alg") val algorithm: String, - @SerialName("ext") val ext: Boolean, - @SerialName("key_ops") val keyOperations: List, - @SerialName("kty") val kty: String, - @SerialName("k") val k: String - ) - } - } - } -} - -sealed interface ApiMessageContent diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt deleted file mode 100644 index 7dd056f..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt +++ /dev/null @@ -1,87 +0,0 @@ -package app.dapk.st.matrix.message.internal - -import app.dapk.st.matrix.MatrixTaskRunner -import app.dapk.st.matrix.common.JsonString -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.message.* -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.json.Json -import java.net.SocketException -import java.net.UnknownHostException - -private const val MATRIX_MESSAGE_TASK_TYPE = "matrix-text-message" -private const val MATRIX_IMAGE_MESSAGE_TASK_TYPE = "matrix-image-message" - -internal class DefaultMessageService( - httpClient: MatrixHttpClient, - private val localEchoStore: LocalEchoStore, - private val backgroundScheduler: BackgroundScheduler, - messageEncrypter: MessageEncrypter, - mediaEncrypter: MediaEncrypter, - imageContentReader: ImageContentReader, -) : MessageService, MatrixTaskRunner { - - private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter, mediaEncrypter, imageContentReader) - private val sendEventMessageUseCase = SendEventMessageUseCase(httpClient) - - override suspend fun canRun(task: MatrixTaskRunner.MatrixTask) = task.type == MATRIX_MESSAGE_TASK_TYPE || task.type == MATRIX_IMAGE_MESSAGE_TASK_TYPE - - override suspend fun run(task: MatrixTaskRunner.MatrixTask): MatrixTaskRunner.TaskResult { - val message = when (task.type) { - MATRIX_MESSAGE_TASK_TYPE -> Json.decodeFromString(MessageService.Message.TextMessage.serializer(), task.jsonPayload) - MATRIX_IMAGE_MESSAGE_TASK_TYPE -> Json.decodeFromString(MessageService.Message.ImageMessage.serializer(), task.jsonPayload) - else -> throw IllegalStateException("Unhandled task type: ${task.type}") - } - return try { - sendMessage(message) - MatrixTaskRunner.TaskResult.Success - } catch (error: Throwable) { - val canRetry = error is UnknownHostException || error is SocketException - MatrixTaskRunner.TaskResult.Failure(canRetry) - } - } - - override fun localEchos(roomId: RoomId): Flow> { - return localEchoStore.observeLocalEchos(roomId) - } - - override fun localEchos(): Flow>> { - return localEchoStore.observeLocalEchos() - } - - override suspend fun scheduleMessage(message: MessageService.Message) { - localEchoStore.markSending(message) - val localId = when (message) { - is MessageService.Message.TextMessage -> message.localId - is MessageService.Message.ImageMessage -> message.localId - } - backgroundScheduler.schedule(key = localId, message.toTask()) - } - - override suspend fun sendMessage(message: MessageService.Message) { - localEchoStore.messageTransaction(message) { - sendMessageUseCase.sendMessage(message) - } - } - - private fun MessageService.Message.toTask(): BackgroundScheduler.Task { - return when (this) { - is MessageService.Message.TextMessage -> { - BackgroundScheduler.Task( - type = MATRIX_MESSAGE_TASK_TYPE, - JsonString(Json.encodeToString(MessageService.Message.TextMessage.serializer(), this)) - ) - } - - is MessageService.Message.ImageMessage -> BackgroundScheduler.Task( - type = MATRIX_IMAGE_MESSAGE_TASK_TYPE, - JsonString(Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this)) - ) - } - } - - override suspend fun sendEventMessage(roomId: RoomId, message: MessageService.EventMessage) { - sendEventMessageUseCase.sendMessage(roomId, message) - } -} \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt deleted file mode 100644 index ffce257..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.dapk.st.matrix.message.internal - -import java.io.InputStream - -interface ImageContentReader { - fun meta(uri: String): ImageContent - fun inputStream(uri: String): InputStream - - data class ImageContent( - val height: Int, - val width: Int, - val size: Long, - val fileName: String, - val mimeType: String, - ) -} \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendEventMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendEventMessageUseCase.kt deleted file mode 100644 index 30f135c..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendEventMessageUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.dapk.st.matrix.message.internal - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.EventType -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.message.MessageService - -internal class SendEventMessageUseCase( - private val httpClient: MatrixHttpClient, -) { - - suspend fun sendMessage(roomId: RoomId, message: MessageService.EventMessage): EventId { - return when (message) { - is MessageService.EventMessage.Encryption -> { - httpClient.execute( - sendRequest( - roomId = roomId, - eventType = EventType.ENCRYPTION, - content = message, - ) - ).eventId - } - } - } - -} diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt deleted file mode 100644 index 8241ac9..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt +++ /dev/null @@ -1,206 +0,0 @@ -package app.dapk.st.matrix.message.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest -import app.dapk.st.matrix.message.ApiSendResponse -import app.dapk.st.matrix.message.MediaEncrypter -import app.dapk.st.matrix.message.MessageEncrypter -import app.dapk.st.matrix.message.MessageService.Message - -internal class SendMessageUseCase( - private val httpClient: MatrixHttpClient, - private val messageEncrypter: MessageEncrypter, - private val mediaEncrypter: MediaEncrypter, - private val imageContentReader: ImageContentReader, -) { - - private val mapper = ApiMessageMapper() - - suspend fun sendMessage(message: Message): EventId { - return with(mapper) { - when (message) { - is Message.TextMessage -> { - val request = textMessageRequest(message) - httpClient.execute(request).eventId - } - - is Message.ImageMessage -> { - val request = imageMessageRequest(message) - httpClient.execute(request).eventId - } - } - } - } - - private suspend fun ApiMessageMapper.textMessageRequest(message: Message.TextMessage): HttpRequest { - val contents = message.toContents(message.reply) - return when (message.sendEncrypted) { - true -> sendRequest( - roomId = message.roomId, - eventType = EventType.ENCRYPTED, - txId = message.localId, - content = messageEncrypter.encrypt( - MessageEncrypter.ClearMessagePayload( - message.roomId, - contents.toMessageJson(message.roomId) - ) - ), - relatesTo = contents.relatesTo - ) - - false -> sendRequest( - roomId = message.roomId, - eventType = EventType.ROOM_MESSAGE, - txId = message.localId, - content = contents, - ) - } - } - - private suspend fun imageMessageRequest(message: Message.ImageMessage): HttpRequest { - val imageMeta = message.content.meta - return when (message.sendEncrypted) { - true -> { - val result = mediaEncrypter.encrypt(imageContentReader.inputStream(message.content.uri)) - val uri = httpClient.execute( - uploadRequest( - result.openStream(), - result.contentLength, - imageMeta.fileName, - "application/octet-stream" - ) - ).contentUri - - val content = ApiMessage.ImageMessage.ImageContent( - url = null, - filename = imageMeta.fileName, - file = ApiMessage.ImageMessage.ImageContent.File( - url = uri, - key = ApiMessage.ImageMessage.ImageContent.File.EncryptionMeta( - algorithm = result.algorithm, - ext = result.ext, - keyOperations = result.keyOperations, - kty = result.kty, - k = result.k, - ), - iv = result.iv, - hashes = result.hashes, - v = result.v, - ), - info = ApiMessage.ImageMessage.ImageContent.Info( - height = imageMeta.height, - width = imageMeta.width, - size = imageMeta.size, - mimeType = imageMeta.mimeType, - ) - ) - - val json = JsonString( - MatrixHttpClient.jsonWithDefaults.encodeToString( - ApiMessage.ImageMessage.serializer(), - ApiMessage.ImageMessage( - content = content, - roomId = message.roomId, - type = EventType.ROOM_MESSAGE.value, - ) - ) - ) - - sendRequest( - roomId = message.roomId, - eventType = EventType.ENCRYPTED, - txId = message.localId, - content = messageEncrypter.encrypt(MessageEncrypter.ClearMessagePayload(message.roomId, json)), - relatesTo = null - ) - } - - false -> { - val uri = httpClient.execute( - uploadRequest( - imageContentReader.inputStream(message.content.uri), - imageMeta.size, - imageMeta.fileName, - imageMeta.mimeType - ) - ).contentUri - sendRequest( - roomId = message.roomId, - eventType = EventType.ROOM_MESSAGE, - txId = message.localId, - content = ApiMessage.ImageMessage.ImageContent( - url = uri, - filename = imageMeta.fileName, - ApiMessage.ImageMessage.ImageContent.Info( - height = imageMeta.height, - width = imageMeta.width, - size = imageMeta.size, - mimeType = imageMeta.mimeType, - ) - ), - ) - } - } - } - -} - -private val MX_REPLY_REGEX = ".*".toRegex() - -class ApiMessageMapper { - - fun Message.TextMessage.toContents(reply: Message.TextMessage.Reply?) = when (reply) { - null -> ApiMessage.TextMessage.TextContent( - body = this.content.body.asString(), - ) - - else -> ApiMessage.TextMessage.TextContent( - body = buildReplyFallback(reply.originalMessage.asString(), reply.author.id, reply.replyContent), - relatesTo = ApiMessage.RelatesTo(ApiMessage.RelatesTo.InReplyTo(reply.eventId)), - formattedBody = buildFormattedReply(reply.author.id, reply.originalMessage.asString(), reply.replyContent, this.roomId, reply.eventId), - format = "org.matrix.custom.html" - ) - } - - fun ApiMessage.TextMessage.TextContent.toMessageJson(roomId: RoomId) = JsonString( - MatrixHttpClient.jsonWithDefaults.encodeToString( - ApiMessage.TextMessage.serializer(), - ApiMessage.TextMessage( - content = this, - roomId = roomId, - type = EventType.ROOM_MESSAGE.value - ) - ) - ) - - private fun buildReplyFallback(originalMessage: String, originalSenderId: UserId, reply: String): String { - return buildString { - append("> <") - append(originalSenderId.value) - append(">") - - val lines = originalMessage.split("\n") - lines.forEachIndexed { index, s -> - if (index == 0) { - append(" $s") - } else { - append("\n> $s") - } - } - append("\n\n") - append(reply) - } - } - - private fun buildFormattedReply(userId: UserId, originalMessage: String, reply: String, roomId: RoomId, eventId: EventId): String { - val permalink = "https://matrix.to/#/${roomId.value}/${eventId.value}" - val userLink = "https://matrix.to/#/${userId.value}" - val cleanOriginalMessage = originalMessage.replace(MX_REPLY_REGEX, "") - return """ -

In reply to ${userId.value}
${cleanOriginalMessage}
$reply - """.trimIndent() - - } - -} diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt deleted file mode 100644 index 1d64872..0000000 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package app.dapk.st.matrix.message.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest -import app.dapk.st.matrix.http.jsonBody -import app.dapk.st.matrix.message.ApiSendResponse -import app.dapk.st.matrix.message.ApiUploadResponse -import app.dapk.st.matrix.message.MessageEncrypter -import app.dapk.st.matrix.message.MessageService.EventMessage -import app.dapk.st.matrix.message.internal.ApiMessage.ImageMessage -import app.dapk.st.matrix.message.internal.ApiMessage.TextMessage -import io.ktor.http.* -import io.ktor.http.content.* -import io.ktor.utils.io.jvm.javaio.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import java.io.InputStream -import java.util.* - -internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: ApiMessageContent) = httpRequest( - path = "_matrix/client/r0/rooms/${roomId.value}/send/${eventType.value}/${txId}", - method = MatrixHttpClient.Method.PUT, - body = when (content) { - is TextMessage.TextContent -> jsonBody(TextMessage.TextContent.serializer(), content, MatrixHttpClient.jsonWithDefaults) - is ImageMessage.ImageContent -> jsonBody(ImageMessage.ImageContent.serializer(), content, MatrixHttpClient.jsonWithDefaults) - } -) - -internal fun sendRequest( - roomId: RoomId, - eventType: EventType, - txId: String, - content: MessageEncrypter.EncryptedMessagePayload, - relatesTo: ApiMessage.RelatesTo? -) = httpRequest( - path = "_matrix/client/r0/rooms/${roomId.value}/send/${eventType.value}/${txId}", - method = MatrixHttpClient.Method.PUT, - body = jsonBody(ApiEncryptedMessage.serializer(), content.let { - val apiEncryptedMessage = ApiEncryptedMessage( - algorithmName = content.algorithmName, - senderKey = content.senderKey, - cipherText = content.cipherText, - sessionId = content.sessionId, - deviceId = content.deviceId, - ) - when (relatesTo) { - null -> apiEncryptedMessage - else -> apiEncryptedMessage.copy(relatesTo = relatesTo) - } - - }) -) - -internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMessage) = httpRequest( - path = "_matrix/client/r0/rooms/${roomId.value}/send/${eventType.value}/${txId()}", - method = MatrixHttpClient.Method.PUT, - body = when (content) { - is EventMessage.Encryption -> jsonBody(EventMessage.Encryption.serializer(), content, MatrixHttpClient.jsonWithDefaults) - } -) - -internal fun uploadRequest(stream: InputStream, contentLength: Long, filename: String, contentType: String) = httpRequest( - path = "_matrix/media/r0/upload/?filename=$filename", - headers = listOf("Content-Type" to contentType), - method = MatrixHttpClient.Method.POST, - body = ChannelWriterContent( - body = { stream.copyTo(this) }, - contentType = ContentType.parse(contentType), - contentLength = contentLength, - ), -) - -fun txId() = "local.${UUID.randomUUID()}" - -@Serializable -data class ApiEncryptedMessage( - @SerialName("algorithm") val algorithmName: AlgorithmName, - @SerialName("sender_key") val senderKey: String, - @SerialName("ciphertext") val cipherText: CipherText, - @SerialName("session_id") val sessionId: SessionId, - @SerialName("device_id") val deviceId: DeviceId, - @SerialName("m.relates_to") val relatesTo: ApiMessage.RelatesTo? = null, -) \ No newline at end of file diff --git a/matrix/services/message/src/testFixtures/kotlin/fixture/LocalEchoFixture.kt b/matrix/services/message/src/testFixtures/kotlin/fixture/LocalEchoFixture.kt deleted file mode 100644 index 28506b2..0000000 --- a/matrix/services/message/src/testFixtures/kotlin/fixture/LocalEchoFixture.kt +++ /dev/null @@ -1,10 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.message.MessageService - -fun aLocalEcho( - eventId: EventId? = anEventId(), - message: MessageService.Message = aTextMessage(), - state: MessageService.LocalEcho.State = MessageService.LocalEcho.State.Sending, -) = MessageService.LocalEcho(eventId, message, state) diff --git a/matrix/services/message/src/testFixtures/kotlin/fixture/MessageFixture.kt b/matrix/services/message/src/testFixtures/kotlin/fixture/MessageFixture.kt deleted file mode 100644 index 479b696..0000000 --- a/matrix/services/message/src/testFixtures/kotlin/fixture/MessageFixture.kt +++ /dev/null @@ -1,19 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.MessageType -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.message.MessageService - -fun aTextMessage( - content: MessageService.Message.Content.TextContent = aTextContent(), - sendEncrypted: Boolean = false, - roomId: RoomId = aRoomId(), - localId: String = "a-local-id", - timestampUtc: Long = 0, -) = MessageService.Message.TextMessage(content, sendEncrypted, roomId, localId, timestampUtc) - -fun aTextContent( - body: RichText = RichText.of("text content body"), - type: String = MessageType.TEXT.value, -) = MessageService.Message.Content.TextContent(body, type) diff --git a/matrix/services/profile/build.gradle b/matrix/services/profile/build.gradle deleted file mode 100644 index 6440905..0000000 --- a/matrix/services/profile/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { id 'java-test-fixtures' } -applyMatrixServiceModule(project) - -dependencies { - implementation project(":core") - - kotlinTest(it) - kotlinFixtures(it) - testImplementation(testFixtures(project(":matrix:common"))) - testImplementation(testFixtures(project(":matrix:matrix-http"))) - testImplementation(testFixtures(project(":core"))) - testFixturesImplementation(testFixtures(project(":matrix:common"))) - testFixturesImplementation(testFixtures(project(":core"))) -} \ No newline at end of file diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt deleted file mode 100644 index f5d6039..0000000 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt +++ /dev/null @@ -1,41 +0,0 @@ -package app.dapk.st.matrix.room - -import app.dapk.st.core.SingletonFlows -import app.dapk.st.matrix.InstallExtender -import app.dapk.st.matrix.MatrixService -import app.dapk.st.matrix.MatrixServiceInstaller -import app.dapk.st.matrix.MatrixServiceProvider -import app.dapk.st.matrix.common.AvatarUrl -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.HomeServerUrl -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.room.internal.DefaultProfileService -import app.dapk.st.matrix.room.internal.FetchMeUseCase - -private val SERVICE_KEY = ProfileService::class - -interface ProfileService : MatrixService { - - suspend fun me(forceRefresh: Boolean): Me - - data class Me( - val userId: UserId, - val displayName: String?, - val avatarUrl: AvatarUrl?, - val homeServerUrl: HomeServerUrl, - ) - -} - -fun MatrixServiceInstaller.installProfileService( - profileStore: ProfileStore, - singletonFlows: SingletonFlows, - credentialsStore: CredentialsStore, -): InstallExtender { - return this.install { (httpClient, _, _, _) -> - val fetchMeUseCase = FetchMeUseCase(httpClient, credentialsStore) - SERVICE_KEY to DefaultProfileService(profileStore, singletonFlows, fetchMeUseCase) - } -} - -fun MatrixServiceProvider.profileService(): ProfileService = this.getService(key = SERVICE_KEY) diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileStore.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileStore.kt deleted file mode 100644 index 991bd18..0000000 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileStore.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.dapk.st.matrix.room - -interface ProfileStore { - - suspend fun storeMe(me: ProfileService.Me) - suspend fun readMe(): ProfileService.Me? - -} \ No newline at end of file diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt deleted file mode 100644 index 168808d..0000000 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.dapk.st.matrix.room.internal - -import app.dapk.st.core.SingletonFlows -import app.dapk.st.matrix.room.ProfileService -import app.dapk.st.matrix.room.ProfileStore -import kotlinx.coroutines.flow.first - -internal class DefaultProfileService( - private val profileStore: ProfileStore, - private val singletonFlows: SingletonFlows, - private val fetchMeUseCase: FetchMeUseCase, -) : ProfileService { - - override suspend fun me(forceRefresh: Boolean): ProfileService.Me { - return when (forceRefresh) { - true -> fetchMe().also { profileStore.storeMe(it) } - false -> singletonFlows.getOrPut("me") { - profileStore.readMe() ?: fetchMe().also { profileStore.storeMe(it) } - }.first() - } - } - - private suspend fun fetchMe(): ProfileService.Me { - return fetchMeUseCase.fetchMe() - } -} - diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt deleted file mode 100644 index 10401f3..0000000 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.dapk.st.matrix.room.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.room.ProfileService -import io.ktor.client.plugins.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -internal class FetchMeUseCase( - private val httpClient: MatrixHttpClient, - private val credentialsStore: CredentialsStore, -) { - suspend fun fetchMe(): ProfileService.Me { - val credentials = credentialsStore.credentials()!! - val userId = credentials.userId - return runCatching { httpClient.execute(profileRequest(userId)) }.fold( - onSuccess = { - ProfileService.Me( - userId, - it.displayName, - it.avatarUrl?.convertMxUrToUrl(credentials.homeServer)?.let { AvatarUrl(it) }, - homeServerUrl = credentials.homeServer, - ) - }, - onFailure = { - when { - it is ClientRequestException && it.response.status.value == 404 -> { - ProfileService.Me( - userId, - displayName = null, - avatarUrl = null, - homeServerUrl = credentials.homeServer, - ) - } - - else -> throw it - } - } - ) - } -} - -internal fun profileRequest(userId: UserId) = MatrixHttpClient.HttpRequest.httpRequest( - path = "_matrix/client/r0/profile/${userId.value}/", - method = MatrixHttpClient.Method.GET, -) - -@Serializable -internal data class ApiMe( - @SerialName("displayname") val displayName: String? = null, - @SerialName("avatar_url") val avatarUrl: MxUrl? = null, -) diff --git a/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt b/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt deleted file mode 100644 index 5ecf4a5..0000000 --- a/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package app.dapk.st.matrix.room.internal - -import app.dapk.st.matrix.room.ProfileService -import fake.FakeCredentialsStore -import fake.FakeMatrixHttpClient -import fixture.a403HttpError -import fixture.a404HttpError -import fixture.aUserCredentials -import fixture.aUserId -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.coInvoking -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldThrow -import org.junit.Test - -private val A_USER_CREDENTIALS = aUserCredentials() -private val A_USER_ID = aUserId() -private val AN_API_ME_RESPONSE = ApiMe( - displayName = "a display name", - avatarUrl = null, -) -private val AN_UNHANDLED_ERROR = RuntimeException() - -class FetchMeUseCaseTest { - - private val fakeHttpClient = FakeMatrixHttpClient() - private val fakeCredentialsStore = FakeCredentialsStore() - - private val useCase = FetchMeUseCase(fakeHttpClient, fakeCredentialsStore) - - @Test - fun `when fetching me, then returns Me instance`() = runTest { - fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) - fakeHttpClient.given(profileRequest(aUserId()), AN_API_ME_RESPONSE) - - val result = useCase.fetchMe() - - result shouldBeEqualTo ProfileService.Me( - userId = A_USER_ID, - displayName = AN_API_ME_RESPONSE.displayName, - avatarUrl = null, - homeServerUrl = fakeCredentialsStore.credentials()!!.homeServer - ) - } - - @Test - fun `given unhandled error, when fetching me, then throws`() = runTest { - fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) - fakeHttpClient.errors(profileRequest(aUserId()), AN_UNHANDLED_ERROR) - - coInvoking { useCase.fetchMe() } shouldThrow AN_UNHANDLED_ERROR - } - - @Test - fun `given 403, when fetching me, then throws`() = runTest { - val error = a403HttpError() - fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) - fakeHttpClient.errors(profileRequest(aUserId()), error) - - coInvoking { useCase.fetchMe() } shouldThrow error - } - - @Test - fun `given 404, when fetching me, then returns Me instance with empty profile fields`() = runTest { - fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS) - fakeHttpClient.errors(profileRequest(aUserId()), a404HttpError()) - - val result = useCase.fetchMe() - - result shouldBeEqualTo ProfileService.Me( - userId = A_USER_ID, - displayName = null, - avatarUrl = null, - homeServerUrl = fakeCredentialsStore.credentials()!!.homeServer - ) - } - -} \ No newline at end of file diff --git a/matrix/services/push/build.gradle b/matrix/services/push/build.gradle deleted file mode 100644 index 3dcc229..0000000 --- a/matrix/services/push/build.gradle +++ /dev/null @@ -1 +0,0 @@ -applyMatrixServiceModule(project) diff --git a/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/PushService.kt b/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/PushService.kt deleted file mode 100644 index 34026a6..0000000 --- a/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/PushService.kt +++ /dev/null @@ -1,49 +0,0 @@ -package app.dapk.st.matrix.push - -import app.dapk.st.matrix.InstallExtender -import app.dapk.st.matrix.MatrixClient -import app.dapk.st.matrix.MatrixService -import app.dapk.st.matrix.MatrixServiceInstaller -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.push.internal.DefaultPushService -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -private val SERVICE_KEY = PushService::class - -interface PushService : MatrixService { - - suspend fun registerPush(token: String, gatewayUrl: String) - - @Serializable - data class PushRequest( - @SerialName("pushkey") val pushKey: String, - @SerialName("kind") val kind: String?, - @SerialName("app_id") val appId: String, - @SerialName("app_display_name") val appDisplayName: String? = null, - @SerialName("device_display_name") val deviceDisplayName: String? = null, - @SerialName("profile_tag") val profileTag: String? = null, - @SerialName("lang") val lang: String? = null, - @SerialName("data") val data: Payload? = null, - @SerialName("append") val append: Boolean? = false, - ) { - - @Serializable - data class Payload( - @SerialName("url") val url: String, - @SerialName("format") val format: String? = null, - @SerialName("brand") val brand: String? = null, - ) - } -} - -fun MatrixServiceInstaller.installPushService( - credentialsStore: CredentialsStore, -): InstallExtender { - return this.install { (httpClient, _, _, logger) -> - SERVICE_KEY to DefaultPushService(httpClient, credentialsStore, logger) - } -} - -fun MatrixClient.pushService(): PushService = this.getService(key = SERVICE_KEY) - diff --git a/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/DefaultPushService.kt b/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/DefaultPushService.kt deleted file mode 100644 index 92a462a..0000000 --- a/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/DefaultPushService.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.dapk.st.matrix.push.internal - -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.push.PushService - -class DefaultPushService( - httpClient: MatrixHttpClient, - credentialsStore: CredentialsStore, - logger: MatrixLogger, -) : PushService { - - private val useCase = RegisterPushUseCase(httpClient, credentialsStore, logger) - - override suspend fun registerPush(token: String, gatewayUrl: String) { - useCase.registerPushToken(token, gatewayUrl) - } - -} \ No newline at end of file diff --git a/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/PushRequest.kt b/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/PushRequest.kt deleted file mode 100644 index 2989870..0000000 --- a/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/PushRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.dapk.st.matrix.push.internal - -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest -import app.dapk.st.matrix.http.jsonBody -import app.dapk.st.matrix.push.PushService - -fun registerPushRequest(pushRequest: PushService.PushRequest) = httpRequest( - path = "_matrix/client/r0/pushers/set", - method = MatrixHttpClient.Method.POST, - body = jsonBody(PushService.PushRequest.serializer(), pushRequest, MatrixHttpClient.jsonWithDefaults), -) \ No newline at end of file diff --git a/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/RegisterPushUseCase.kt b/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/RegisterPushUseCase.kt deleted file mode 100644 index 45711c3..0000000 --- a/matrix/services/push/src/main/kotlin/app/dapk/st/matrix/push/internal/RegisterPushUseCase.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.dapk.st.matrix.push.internal - -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.isSignedIn -import app.dapk.st.matrix.common.matrixLog -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.push.PushService.PushRequest - -internal class RegisterPushUseCase( - private val matrixClient: MatrixHttpClient, - private val credentialsStore: CredentialsStore, - private val logger: MatrixLogger, -) { - - suspend fun registerPushToken(token: String, gatewayUrl: String) { - if (credentialsStore.isSignedIn()) { - logger.matrixLog("register push token: $token") - matrixClient.execute( - registerPushRequest( - PushRequest( - pushKey = token, - kind = "http", - appId = "app.dapk.st", - appDisplayName = "st-android", - deviceDisplayName = "device-a", - lang = "en", - profileTag = "mobile_${credentialsStore.credentials()!!.userId.hashCode()}", - append = false, - data = PushRequest.Payload( - format = "event_id_only", - url = gatewayUrl, - ), - ) - ) - ) - } - } -} \ No newline at end of file diff --git a/matrix/services/room/build.gradle b/matrix/services/room/build.gradle deleted file mode 100644 index eaa3259..0000000 --- a/matrix/services/room/build.gradle +++ /dev/null @@ -1,5 +0,0 @@ -applyMatrixServiceModule(project) - -dependencies { - implementation project(":core") -} \ No newline at end of file diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt deleted file mode 100644 index 1da2df3..0000000 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt +++ /dev/null @@ -1,68 +0,0 @@ -package app.dapk.st.matrix.room - -import app.dapk.st.matrix.* -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.room.internal.* -import kotlinx.coroutines.flow.Flow - -private val SERVICE_KEY = RoomService::class - -interface RoomService : MatrixService { - - suspend fun joinedMembers(roomId: RoomId): List - suspend fun markFullyRead(roomId: RoomId, eventId: EventId, isPrivate: Boolean) - - suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember? - suspend fun findMembers(roomId: RoomId, userIds: List): List - suspend fun findMembersSummary(roomId: RoomId): List - suspend fun insertMembers(roomId: RoomId, members: List) - - suspend fun createDm(userId: UserId, encrypted: Boolean): RoomId - - suspend fun joinRoom(roomId: RoomId) - suspend fun rejectJoinRoom(roomId: RoomId) - - suspend fun muteRoom(roomId: RoomId) - suspend fun unmuteRoom(roomId: RoomId) - fun observeIsMuted(roomId: RoomId): Flow - - data class JoinedMember( - val userId: UserId, - val displayName: String?, - val avatarUrl: String?, - ) - -} - -fun MatrixServiceInstaller.installRoomService( - memberStore: MemberStore, - roomMessenger: ServiceDepFactory, - roomInviteRemover: RoomInviteRemover, - singleRoomStore: SingleRoomStore, -): InstallExtender { - return this.install { (httpClient, _, services, logger) -> - SERVICE_KEY to DefaultRoomService( - httpClient, - logger, - RoomMembers(memberStore, RoomMembersCache()), - roomMessenger.create(services), - roomInviteRemover, - singleRoomStore, - ) - } -} - -fun MatrixServiceProvider.roomService(): RoomService = this.getService(key = SERVICE_KEY) - -interface MemberStore { - suspend fun insert(roomId: RoomId, members: List) - suspend fun query(roomId: RoomId, userIds: List): List - suspend fun query(roomId: RoomId, limit: Int): List -} - -interface RoomMessenger { - suspend fun enableEncryption(roomId: RoomId) -} \ No newline at end of file diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt deleted file mode 100644 index b5b4472..0000000 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt +++ /dev/null @@ -1,179 +0,0 @@ -package app.dapk.st.matrix.room.internal - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.common.MatrixLogTag.ROOM -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest -import app.dapk.st.matrix.http.emptyJsonBody -import app.dapk.st.matrix.http.jsonBody -import app.dapk.st.matrix.room.RoomMessenger -import app.dapk.st.matrix.room.RoomService -import io.ktor.client.plugins.* -import io.ktor.http.* -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -class DefaultRoomService( - private val httpClient: MatrixHttpClient, - private val logger: MatrixLogger, - private val roomMembers: RoomMembers, - private val roomMessenger: RoomMessenger, - private val roomInviteRemover: RoomInviteRemover, - private val singleRoomStore: SingleRoomStore, -) : RoomService { - - override suspend fun joinedMembers(roomId: RoomId): List { - val response = httpClient.execute(joinedMembersRequest(roomId)) - return response.joined.map { (userId, member) -> - RoomService.JoinedMember(userId, member.displayName, member.avatarUrl) - }.also { - logger.matrixLog(ROOM, "found members for $roomId : size: ${it.size}") - } - } - - override suspend fun markFullyRead(roomId: RoomId, eventId: EventId, isPrivate: Boolean) { - logger.matrixLog(ROOM, "marking room fully read ${roomId.value}") - httpClient.execute(markFullyReadRequest(roomId, eventId, isPrivate)) - } - - override suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember? { - return roomMembers.findMember(roomId, userId) - } - - override suspend fun findMembers(roomId: RoomId, userIds: List): List { - return roomMembers.findMembers(roomId, userIds) - } - - override suspend fun findMembersSummary(roomId: RoomId): List { - return roomMembers.findMembersSummary(roomId) - } - - override suspend fun insertMembers(roomId: RoomId, members: List) { - roomMembers.insert(roomId, members) - } - - override suspend fun createDm(userId: UserId, encrypted: Boolean): RoomId { - logger.matrixLog("creating DM $userId") - val roomResponse = httpClient.execute( - createRoomRequest( - invites = listOf(userId), - isDM = true, - visibility = RoomVisibility.private - ) - ) - - if (encrypted) { - roomMessenger.enableEncryption(roomResponse.roomId) - } - return roomResponse.roomId - } - - override suspend fun joinRoom(roomId: RoomId) { - httpClient.execute(joinRoomRequest(roomId)) - } - - override suspend fun rejectJoinRoom(roomId: RoomId) { - runCatching { httpClient.execute(rejectJoinRoomRequest(roomId)) }.fold( - onSuccess = {}, - onFailure = { - when (it) { - is ClientRequestException -> { - if (it.response.status == HttpStatusCode.Forbidden) { - // allow error - } else { - throw it - } - - } - - else -> throw it - } - } - ) - roomInviteRemover.remove(roomId) - } - - override suspend fun muteRoom(roomId: RoomId) { - singleRoomStore.mute(roomId) - } - - override suspend fun unmuteRoom(roomId: RoomId) { - singleRoomStore.unmute(roomId) - } - - override fun observeIsMuted(roomId: RoomId): Flow = singleRoomStore.isMuted(roomId) -} - -interface SingleRoomStore { - suspend fun mute(roomId: RoomId) - suspend fun unmute(roomId: RoomId) - fun isMuted(roomId: RoomId): Flow -} - -internal fun joinedMembersRequest(roomId: RoomId) = httpRequest( - path = "_matrix/client/r0/rooms/${roomId.value}/joined_members", - method = MatrixHttpClient.Method.GET, -) - -internal fun markFullyReadRequest(roomId: RoomId, eventId: EventId, isPrivate: Boolean) = httpRequest( - path = "_matrix/client/r0/rooms/${roomId.value}/read_markers", - method = MatrixHttpClient.Method.POST, - body = jsonBody(MarkFullyReadRequest(eventId, eventId, hidden = isPrivate)) -) - -internal fun createRoomRequest(invites: List, isDM: Boolean, visibility: RoomVisibility, name: String? = null) = httpRequest( - path = "_matrix/client/r0/createRoom", - method = MatrixHttpClient.Method.POST, - body = jsonBody(CreateRoomRequest(invites, isDM, visibility, name)) -) - -internal fun joinRoomRequest(roomId: RoomId) = httpRequest( - path = "_matrix/client/r0/rooms/${roomId.value}/join", - method = MatrixHttpClient.Method.POST, - body = emptyJsonBody() -) - -internal fun rejectJoinRoomRequest(roomId: RoomId) = httpRequest( - path = "_matrix/client/r0/rooms/${roomId.value}/leave", - method = MatrixHttpClient.Method.POST, - body = emptyJsonBody() -) - - -@Suppress("EnumEntryName") -@Serializable -enum class RoomVisibility { - public, private -} - -@Serializable -internal data class CreateRoomRequest( - @SerialName("invite") val invites: List, - @SerialName("is_direct") val isDM: Boolean, - @SerialName("visibility") val visibility: RoomVisibility, - @SerialName("name") val name: String? = null, -) - -@Serializable -internal data class ApiCreateRoomResponse( - @SerialName("room_id") val roomId: RoomId, -) - -@Serializable -internal data class MarkFullyReadRequest( - @SerialName("m.fully_read") val eventId: EventId, - @SerialName("m.read") val read: EventId, - @SerialName("m.hidden") val hidden: Boolean -) - -@Serializable -internal data class JoinedMembersResponse( - @SerialName("joined") val joined: Map -) - -@Serializable -internal data class ApiJoinedMember( - @SerialName("display_name") val displayName: String? = null, - @SerialName("avatar_url") val avatarUrl: String? = null, -) \ No newline at end of file diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomInviteRemover.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomInviteRemover.kt deleted file mode 100644 index e5da0a9..0000000 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomInviteRemover.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.matrix.room.internal - -import app.dapk.st.matrix.common.RoomId - -fun interface RoomInviteRemover { - suspend fun remove(roomId: RoomId) -} \ No newline at end of file diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt deleted file mode 100644 index d9004d0..0000000 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt +++ /dev/null @@ -1,61 +0,0 @@ -package app.dapk.st.matrix.room.internal - -import app.dapk.st.core.LRUCache -import app.dapk.st.core.isNullOrEmpty -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.room.MemberStore - -class RoomMembers(private val memberStore: MemberStore, private val membersCache: RoomMembersCache) { - - suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember? { - return findMembers(roomId, listOf(userId)).firstOrNull() - } - - suspend fun findMembers(roomId: RoomId, userIds: List): List { - val roomCache = membersCache.room(roomId) - - return if (roomCache.isNullOrEmpty()) { - memberStore.query(roomId, userIds).also { membersCache.insert(roomId, it) } - } else { - val (cachedMembers, missingIds) = userIds.fold(mutableListOf() to mutableListOf()) { acc, current -> - when (val member = roomCache?.get(current)) { - null -> acc.second.add(current) - else -> acc.first.add(member) - } - acc - } - - when { - missingIds.isNotEmpty() -> { - (memberStore.query(roomId, missingIds).also { membersCache.insert(roomId, it) } + cachedMembers) - } - - else -> cachedMembers - } - } - } - - suspend fun findMembersSummary(roomId: RoomId) = memberStore.query(roomId, limit = 8) - - suspend fun insert(roomId: RoomId, members: List) { - membersCache.insert(roomId, members) - memberStore.insert(roomId, members) - } -} - -private const val ROOMS_TO_CACHE_MEMBERS_FOR_SIZE = 12 -private const val MEMBERS_TO_CACHE_PER_ROOM = 25 - -class RoomMembersCache { - - private val cache = LRUCache>(maxSize = ROOMS_TO_CACHE_MEMBERS_FOR_SIZE) - - fun room(roomId: RoomId) = cache.get(roomId) - - fun insert(roomId: RoomId, members: List) { - val map = cache.getOrPut(roomId) { LRUCache(maxSize = MEMBERS_TO_CACHE_PER_ROOM) } - members.forEach { map.put(it.id, it) } - } -} diff --git a/matrix/services/sync/build.gradle b/matrix/services/sync/build.gradle deleted file mode 100644 index bf20c08..0000000 --- a/matrix/services/sync/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { id 'java-test-fixtures' } -applyMatrixServiceModule(project) - -dependencies { - implementation project(":core") - - kotlinTest(it) - kotlinFixtures(it) - testImplementation(testFixtures(project(":matrix:common"))) - testImplementation(testFixtures(project(":matrix:matrix-http"))) - testImplementation(testFixtures(project(":core"))) - testFixturesImplementation(testFixtures(project(":core"))) - testFixturesImplementation(testFixtures(project(":matrix:common"))) -} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt deleted file mode 100644 index 75bf8ad..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt +++ /dev/null @@ -1,48 +0,0 @@ -package app.dapk.st.matrix.sync - -import app.dapk.st.matrix.common.AvatarUrl -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -typealias OverviewState = List -typealias InviteState = List - -@Serializable -data class RoomOverview( - @SerialName("room_id") val roomId: RoomId, - @SerialName("room_creation_utc") val roomCreationUtc: Long, - @SerialName("room_name") val roomName: String?, - @SerialName("room_avatar") val roomAvatarUrl: AvatarUrl?, - @SerialName("last_message") val lastMessage: LastMessage?, - @SerialName("is_group") val isGroup: Boolean, - @SerialName("fully_read_marker") val readMarker: EventId?, - @SerialName("is_encrypted") val isEncrypted: Boolean, -) - -@Serializable -data class LastMessage( - @SerialName("content") val content: String, - @SerialName("timestamp") val utcTimestamp: Long, - @SerialName("author") val author: RoomMember, -) - -@Serializable -data class RoomInvite( - @SerialName("from") val from: RoomMember, - @SerialName("room_id") val roomId: RoomId, - @SerialName("meta") val inviteMeta: InviteMeta, -) - -@Serializable -sealed class InviteMeta { - @Serializable - @SerialName("direct_message") - object DirectMessage : InviteMeta() - - @Serializable - @SerialName("room") - data class Room(val roomName: String? = null) : InviteMeta() -} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt deleted file mode 100644 index 79ed163..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt +++ /dev/null @@ -1,146 +0,0 @@ -package app.dapk.st.matrix.sync - -import app.dapk.st.matrix.common.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -data class RoomState( - val roomOverview: RoomOverview, - val events: List, -) - -@Serializable -sealed class RoomEvent { - - abstract val eventId: EventId - abstract val utcTimestamp: Long - abstract val author: RoomMember - abstract val meta: MessageMeta - - @Serializable - @SerialName("encrypted") - data class Encrypted( - @SerialName("event_id") override val eventId: EventId, - @SerialName("timestamp") override val utcTimestamp: Long, - @SerialName("author") override val author: RoomMember, - @SerialName("meta") override val meta: MessageMeta, - @SerialName("edited") val edited: Boolean = false, - @SerialName("encrypted_content") val encryptedContent: MegOlmV1, - ) : RoomEvent() { - - @Serializable - data class MegOlmV1( - @SerialName("ciphertext") val cipherText: CipherText, - @SerialName("device_id") val deviceId: DeviceId, - @SerialName("sender_key") val senderKey: String, - @SerialName("session_id") val sessionId: SessionId, - ) - - } - - @Serializable - @SerialName("redacted") - data class Redacted( - @SerialName("event_id") override val eventId: EventId, - @SerialName("timestamp") override val utcTimestamp: Long, - @SerialName("author") override val author: RoomMember, - ) : RoomEvent() { - override val meta: MessageMeta = MessageMeta.FromServer - } - - @Serializable - @SerialName("message") - data class Message( - @SerialName("event_id") override val eventId: EventId, - @SerialName("timestamp") override val utcTimestamp: Long, - @SerialName("content") val content: RichText, - @SerialName("author") override val author: RoomMember, - @SerialName("meta") override val meta: MessageMeta, - @SerialName("edited") val edited: Boolean = false, - ) : RoomEvent() - - @Serializable - @SerialName("reply") - data class Reply( - @SerialName("message") val message: RoomEvent, - @SerialName("in_reply_to") val replyingTo: RoomEvent, - ) : RoomEvent() { - - override val eventId: EventId = message.eventId - override val utcTimestamp: Long = message.utcTimestamp - override val author: RoomMember = message.author - override val meta: MessageMeta = message.meta - - } - - @Serializable - @SerialName("image") - data class Image( - @SerialName("event_id") override val eventId: EventId, - @SerialName("timestamp") override val utcTimestamp: Long, - @SerialName("image_meta") val imageMeta: ImageMeta, - @SerialName("author") override val author: RoomMember, - @SerialName("meta") override val meta: MessageMeta, - @SerialName("edited") val edited: Boolean = false, - ) : RoomEvent() { - - @Serializable - data class ImageMeta( - @SerialName("width") val width: Int?, - @SerialName("height") val height: Int?, - @SerialName("url") val url: String, - @SerialName("keys") val keys: Keys?, - ) { - - @Serializable - data class Keys( - @SerialName("k") val k: String, - @SerialName("iv") val iv: String, - @SerialName("v") val v: String, - @SerialName("hashes") val hashes: Map, - ) - - } - } - -} - -@Serializable -sealed class MessageMeta { - - @Serializable - @SerialName("from_server") - object FromServer : MessageMeta() - - @Serializable - @SerialName("local_echo") - data class LocalEcho( - @SerialName("echo_id") val echoId: String, - @SerialName("state") val state: State - ) : MessageMeta() { - - @Serializable - sealed class State { - @Serializable - @SerialName("loading") - object Sending : State() - - @Serializable - @SerialName("success") - object Sent : State() - - @SerialName("error") - @Serializable - data class Error( - @SerialName("message") val message: String, - @SerialName("type") val type: Type, - ) : State() { - - @Serializable - enum class Type { - UNKNOWN - } - } - } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt deleted file mode 100644 index d5b644c..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt +++ /dev/null @@ -1,71 +0,0 @@ -package app.dapk.st.matrix.sync - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.SyncToken -import kotlinx.coroutines.flow.Flow - -interface RoomStore : MuteableStore { - - suspend fun persist(roomId: RoomId, events: List) - suspend fun remove(rooms: List) - suspend fun remove(eventId: EventId) - suspend fun retrieve(roomId: RoomId): RoomState? - fun latest(roomId: RoomId): Flow - suspend fun insertUnread(roomId: RoomId, eventIds: List) - suspend fun markRead(roomId: RoomId) - fun observeUnread(): Flow>> - fun observeUnreadCountById(): Flow> - fun observeNotMutedUnread(): Flow>> - fun observeEvent(eventId: EventId): Flow - suspend fun findEvent(eventId: EventId): RoomEvent? - -} - -interface MuteableStore { - suspend fun mute(roomId: RoomId) - suspend fun unmute(roomId: RoomId) - suspend fun isMuted(roomId: RoomId): Boolean - fun observeMuted(): Flow> -} - -interface FilterStore { - - suspend fun store(key: String, filterId: String) - suspend fun read(key: String): String? -} - -interface OverviewStore { - - suspend fun removeRooms(roomsToRemove: List) - suspend fun persistInvites(invite: List) - suspend fun persist(overviewState: OverviewState) - - suspend fun retrieve(): OverviewState? - - fun latest(): Flow - fun latestInvites(): Flow> - suspend fun removeInvites(map: List) -} - -interface SyncStore { - - suspend fun store(key: SyncKey, syncToken: SyncToken) - suspend fun read(key: SyncKey): SyncToken? - suspend fun remove(key: SyncKey) - - sealed interface SyncKey { - - val value: String - - object Overview : SyncKey { - - override val value = "overview-sync-token" - } - - data class Room(val roomId: RoomId) : SyncKey { - - override val value = "room-sync-token-${roomId.value}" - } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt deleted file mode 100644 index a5726df..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt +++ /dev/null @@ -1,150 +0,0 @@ -package app.dapk.st.matrix.sync - -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.matrix.* -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.internal.DefaultSyncService -import app.dapk.st.matrix.sync.internal.request.* -import app.dapk.st.matrix.sync.internal.room.MessageDecrypter -import app.dapk.st.matrix.sync.internal.room.MissingMessageDecrypter -import app.dapk.st.matrix.sync.internal.sync.message.RichMessageParser -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow - -private val SERVICE_KEY = SyncService::class - -interface SyncService : MatrixService { - - fun invites(): Flow - fun overview(): Flow - fun room(roomId: RoomId): Flow - - /** - * Subscribe to keep the background syncing alive - * Emits once, either when the initial sync completes or immediately if has already sync'd once - */ - fun startSyncing(): Flow - fun events(roomId: RoomId? = null): Flow> - suspend fun observeEvent(eventId: EventId): Flow - suspend fun forceManualRefresh(roomIds: Set) - - @JvmInline - value class FilterId(val value: String) - - sealed interface SyncEvent { - val roomId: RoomId - - data class Typing(override val roomId: RoomId, val members: List) : SyncEvent - } - -} - -fun MatrixServiceInstaller.installSyncService( - credentialsStore: CredentialsStore, - overviewStore: OverviewStore, - roomStore: RoomStore, - syncStore: SyncStore, - filterStore: FilterStore, - deviceNotifier: ServiceDepFactory, - messageDecrypter: ServiceDepFactory = ServiceDepFactory { MissingMessageDecrypter }, - keySharer: ServiceDepFactory = ServiceDepFactory { NoOpKeySharer }, - verificationHandler: ServiceDepFactory = ServiceDepFactory { NoOpVerificationHandler }, - oneTimeKeyProducer: ServiceDepFactory, - roomMembersService: ServiceDepFactory, - errorTracker: ErrorTracker, - coroutineDispatchers: CoroutineDispatchers, - - syncConfig: SyncConfig = SyncConfig(), -): InstallExtender { - this.serializers { - polymorphicDefault(ApiTimelineEvent::class) { - ApiTimelineEvent.Ignored.serializer() - } - polymorphicDefault(ApiToDeviceEvent::class) { - ApiToDeviceEvent.Ignored.serializer() - } - polymorphicDefault(ApiAccountEvent::class) { - ApiAccountEvent.Ignored.serializer() - } - polymorphicDefault(ApiEphemeralEvent::class) { - ApiEphemeralEvent.Ignored.serializer() - } - polymorphicDefault(ApiStrippedEvent::class) { - ApiStrippedEvent.Ignored.serializer() - } - polymorphicDefault(DecryptedContent::class) { - DecryptedContent.Ignored.serializer() - } - } - - return this.install { (httpClient, json, services, logger) -> - SERVICE_KEY to DefaultSyncService( - httpClient = httpClient, - syncStore = syncStore, - overviewStore = overviewStore, - roomStore = roomStore, - filterStore = filterStore, - messageDecrypter = messageDecrypter.create(services), - keySharer = keySharer.create(services), - verificationHandler = verificationHandler.create(services), - deviceNotifier = deviceNotifier.create(services), - json = json, - oneTimeKeyProducer = oneTimeKeyProducer.create(services), - scope = CoroutineScope(coroutineDispatchers.io), - credentialsStore = credentialsStore, - roomMembersService = roomMembersService.create(services), - logger = logger, - errorTracker = errorTracker, - coroutineDispatchers = coroutineDispatchers, - syncConfig = syncConfig, - richMessageParser = RichMessageParser() - ) - } -} - -fun MatrixClient.syncService(): SyncService = this.getService(key = SERVICE_KEY) - -fun interface KeySharer { - suspend fun share(keys: List) -} - -fun interface VerificationHandler { - suspend fun handle(apiVerificationEvent: ApiToDeviceEvent.ApiVerificationEvent) -} - -internal object NoOpVerificationHandler : VerificationHandler { - override suspend fun handle(apiVerificationEvent: ApiToDeviceEvent.ApiVerificationEvent) { - // do nothing - } -} - -fun interface MaybeCreateMoreKeys { - suspend fun onServerKeyCount(count: ServerKeyCount) -} - - -fun interface DeviceNotifier { - suspend fun notifyChanges(userId: List, syncToken: SyncToken?) -} - -internal object NoOpKeySharer : KeySharer { - override suspend fun share(keys: List) { - // do nothing - } -} - -interface RoomMembersService { - suspend fun find(roomId: RoomId, userIds: List): List - suspend fun findSummary(roomId: RoomId): List - suspend fun insert(roomId: RoomId, members: List) -} - -suspend fun RoomMembersService.find(roomId: RoomId, userId: UserId): RoomMember? { - return this.find(roomId, listOf(userId)).firstOrNull() -} - -data class SyncConfig( - val loopTimeout: Long = 30_000L, - val allowSharedFlows: Boolean = true -) \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt deleted file mode 100644 index 1d92479..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt +++ /dev/null @@ -1,137 +0,0 @@ -package app.dapk.st.matrix.sync.internal - -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.core.withIoContext -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.sync.* -import app.dapk.st.matrix.sync.internal.filter.FilterUseCase -import app.dapk.st.matrix.sync.internal.overview.ReducedSyncFilterUseCase -import app.dapk.st.matrix.sync.internal.room.MessageDecrypter -import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter -import app.dapk.st.matrix.sync.internal.room.SyncEventDecrypter -import app.dapk.st.matrix.sync.internal.room.SyncSideEffects -import app.dapk.st.matrix.sync.internal.sync.* -import app.dapk.st.matrix.sync.internal.sync.message.RichMessageParser -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.* -import kotlinx.serialization.json.Json -import java.util.concurrent.atomic.AtomicInteger - -private val syncSubscriptionCount = AtomicInteger() - -internal class DefaultSyncService( - httpClient: MatrixHttpClient, - private val syncStore: SyncStore, - private val overviewStore: OverviewStore, - private val roomStore: RoomStore, - filterStore: FilterStore, - messageDecrypter: MessageDecrypter, - keySharer: KeySharer, - verificationHandler: VerificationHandler, - deviceNotifier: DeviceNotifier, - json: Json, - oneTimeKeyProducer: MaybeCreateMoreKeys, - scope: CoroutineScope, - private val credentialsStore: CredentialsStore, - roomMembersService: RoomMembersService, - logger: MatrixLogger, - errorTracker: ErrorTracker, - private val coroutineDispatchers: CoroutineDispatchers, - syncConfig: SyncConfig, - richMessageParser: RichMessageParser, -) : SyncService { - - private val syncEventsFlow = MutableStateFlow>(emptyList()) - - private val roomDataSource by lazy { RoomDataSource(roomStore, logger) } - private val eventDecrypter by lazy { SyncEventDecrypter(messageDecrypter, json, logger) } - private val roomEventsDecrypter by lazy { RoomEventsDecrypter(messageDecrypter, richMessageParser, json, logger) } - private val roomRefresher by lazy { RoomRefresher(roomDataSource, roomEventsDecrypter, logger) } - - private val sync2 by lazy { - val roomDataSource = RoomDataSource(roomStore, logger) - val syncReducer = SyncReducer( - RoomProcessor( - roomMembersService, - roomDataSource, - TimelineEventsProcessor( - RoomEventCreator(roomMembersService, errorTracker, RoomEventFactory(roomMembersService, richMessageParser), richMessageParser), - roomEventsDecrypter, - eventDecrypter, - EventLookupUseCase(roomStore) - ), - RoomOverviewProcessor(roomMembersService), - UnreadEventsProcessor(roomStore, logger), - EphemeralEventsUseCase(roomMembersService, syncEventsFlow), - ), - roomRefresher, - roomDataSource, - logger, - errorTracker, - coroutineDispatchers, - ) - SyncUseCase( - overviewStore, - SideEffectFlowIterator(logger, errorTracker), - SyncSideEffects(keySharer, verificationHandler, deviceNotifier, messageDecrypter, json, oneTimeKeyProducer, logger), - httpClient, - syncStore, - syncReducer, - credentialsStore, - logger, - ReducedSyncFilterUseCase(FilterUseCase(httpClient, filterStore)), - syncConfig, - ) - } - - private val syncFlow by lazy { - sync2.sync().let { - if (syncConfig.allowSharedFlows) { - it.shareIn(scope, SharingStarted.WhileSubscribed(5000)) - } else { - it - } - } - .onStart { - val subscriptions = syncSubscriptionCount.incrementAndGet() - logger.matrixLog(MatrixLogTag.SYNC, "flow onStart - count: $subscriptions") - } - .onCompletion { - val subscriptions = syncSubscriptionCount.decrementAndGet() - logger.matrixLog(MatrixLogTag.SYNC, "flow onCompletion - count: $subscriptions") - } - } - - override fun startSyncing(): Flow { - return flow { emit(syncStore.read(SyncStore.SyncKey.Overview) != null) }.flatMapConcat { hasSynced -> - when (hasSynced) { - true -> syncFlow.filter { false }.onStart { emit(Unit) } - false -> { - var counter = 0 - syncFlow.filter { counter < 1 }.onEach { counter++ } - } - } - } - } - - override fun invites() = overviewStore.latestInvites() - override fun overview() = overviewStore.latest() - override fun room(roomId: RoomId) = roomStore.latest(roomId) - override fun events(roomId: RoomId?) = roomId?.let { syncEventsFlow.map { it.filter { it.roomId == roomId } }.distinctUntilChanged() } ?: syncEventsFlow - override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId) - override suspend fun forceManualRefresh(roomIds: Set) { - coroutineDispatchers.withIoContext { - roomIds.map { - async { - roomRefresher.refreshRoomContent(it, credentialsStore.credentials()!!)?.also { - overviewStore.persist(listOf(it.roomOverview)) - } - } - }.awaitAll() - } - } -} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt deleted file mode 100644 index e10368d..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.dapk.st.matrix.sync.internal - -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.matrix.common.MatrixLogTag.SYNC -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.matrixLog -import kotlinx.coroutines.* - -internal class SideEffectFlowIterator(private val logger: MatrixLogger, private val errorTracker: ErrorTracker) { - suspend fun loop(initial: T?, onPost: suspend () -> Unit, onIteration: suspend (T?) -> T?) { - var previousState = initial - - while (currentCoroutineContext().isActive) { - logger.matrixLog(SYNC, "loop iteration") - try { - previousState = withContext(NonCancellable) { - onIteration(previousState) - } - onPost() - } catch (error: Throwable) { - logger.matrixLog(SYNC, "on loop error: ${error.message}") - errorTracker.track(error, "sync loop error") - delay(10000L) - } - } - logger.matrixLog(SYNC, "isActive: ${currentCoroutineContext().isActive}") - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterRequest.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterRequest.kt deleted file mode 100644 index 3273284..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.dapk.st.matrix.sync.internal.filter - -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest -import app.dapk.st.matrix.http.jsonBody -import app.dapk.st.matrix.sync.internal.request.ApiFilterResponse -import app.dapk.st.matrix.sync.internal.request.FilterRequest - -internal fun filterRequest(userId: UserId, filterRequest: FilterRequest) = httpRequest( - path = "_matrix/client/r0/user/${userId.value}/filter", - method = MatrixHttpClient.Method.POST, - body = jsonBody(FilterRequest.serializer(), filterRequest), -) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterUseCase.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterUseCase.kt deleted file mode 100644 index 33988f2..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterUseCase.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.dapk.st.matrix.sync.internal.filter - -import app.dapk.st.core.extensions.ifNull -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.sync.FilterStore -import app.dapk.st.matrix.sync.SyncService -import app.dapk.st.matrix.sync.internal.request.FilterRequest - -internal class FilterUseCase( - private val client: MatrixHttpClient, - private val filterStore: FilterStore, -) { - - suspend fun filter(key: String, userId: UserId, filterRequest: FilterRequest): SyncService.FilterId { - val filterId = filterStore.read(key).ifNull { - client.execute(filterRequest(userId, filterRequest)).id.also { - filterStore.store(key, it) - } - } - return SyncService.FilterId(filterId) - } - -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/overview/ReducedSyncFilterUseCase.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/overview/ReducedSyncFilterUseCase.kt deleted file mode 100644 index 55c4bed..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/overview/ReducedSyncFilterUseCase.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.dapk.st.matrix.sync.internal.overview - -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.sync.SyncService.FilterId -import app.dapk.st.matrix.sync.internal.filter.FilterUseCase -import app.dapk.st.matrix.sync.internal.request.EventFilter -import app.dapk.st.matrix.sync.internal.request.FilterRequest -import app.dapk.st.matrix.sync.internal.request.RoomEventFilter -import app.dapk.st.matrix.sync.internal.request.RoomFilter - -private const val FIlTER_KEY = "reduced-filter-key" - -internal class ReducedSyncFilterUseCase( - private val filterUseCase: FilterUseCase, -) { - - suspend fun reducedFilter(userId: UserId): FilterId { - return filterUseCase.filter( - key = FIlTER_KEY, - userId = userId, - filterRequest = reduced() - ) - } - -} - -private fun reduced() = FilterRequest( - roomFilter = RoomFilter( - timelineFilter = RoomEventFilter( - lazyLoadMembers = true, - ), - stateFilter = RoomEventFilter( - lazyLoadMembers = true, - ), - ephemeralFilter = RoomEventFilter(types = listOf("m.typing")), - accountFilter = RoomEventFilter(types = listOf("m.fully_read")), - ), - account = EventFilter(types = listOf("m.direct")), -) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiAccountEvent.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiAccountEvent.kt deleted file mode 100644 index 6d0cb1b..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiAccountEvent.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -sealed class ApiAccountEvent { - - @Serializable - @SerialName("m.direct") - data class Direct( - @SerialName("content") val content: Map> - ) : ApiAccountEvent() - - @Serializable - @SerialName("m.fully_read") - data class FullyRead( - @SerialName("content") val content: Content, - ) : ApiAccountEvent() { - - @Serializable - data class Content( - @SerialName("event_id") val eventId: EventId, - ) - - } - - @Serializable - object Ignored : ApiAccountEvent() -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiEncryptedContent.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiEncryptedContent.kt deleted file mode 100644 index 5551ecd..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiEncryptedContent.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import app.dapk.st.matrix.common.CipherText -import app.dapk.st.matrix.common.Curve25519 -import app.dapk.st.matrix.common.DeviceId -import app.dapk.st.matrix.common.SessionId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable(with = EncryptedContentDeserializer::class) -internal sealed class ApiEncryptedContent { - @Serializable - data class OlmV1( - @SerialName("ciphertext") val cipherText: Map, - @SerialName("sender_key") val senderKey: Curve25519, - ) : ApiEncryptedContent() - - @Serializable - data class MegOlmV1( - @SerialName("ciphertext") val cipherText: CipherText, - @SerialName("device_id") val deviceId: DeviceId, - @SerialName("sender_key") val senderKey: String, - @SerialName("session_id") val sessionId: SessionId, - @SerialName("m.relates_to") val relation: ApiTimelineEvent.TimelineMessage.Relation? = null, - ) : ApiEncryptedContent() - - @Serializable - data class CipherTextInfo( - @SerialName("body") val body: CipherText, - @SerialName("type") val type: Int, - ) - - @Serializable - object Unknown : ApiEncryptedContent() -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiFilterResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiFilterResponse.kt deleted file mode 100644 index 0e61c41..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiFilterResponse.kt +++ /dev/null @@ -1,41 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import app.dapk.st.matrix.common.RoomId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class ApiFilterResponse( - @SerialName("filter_id") val id: String -) - -@Serializable -internal data class FilterRequest( - @SerialName("event_fields") val eventFields: List? = null, - @SerialName("room") val roomFilter: RoomFilter? = null, - @SerialName("account_data") val account: EventFilter? = null, -) - -@Serializable -internal data class RoomFilter( - @SerialName("rooms") val rooms: List? = null, - @SerialName("timeline") val timelineFilter: RoomEventFilter? = null, - @SerialName("state") val stateFilter: RoomEventFilter? = null, - @SerialName("ephemeral") val ephemeralFilter: RoomEventFilter? = null, - @SerialName("account_data") val accountFilter: RoomEventFilter? = null, -) - -@Serializable -internal data class RoomEventFilter( - @SerialName("limit") val limit: Int? = null, - @SerialName("types") val types: List? = null, - @SerialName("rooms") val rooms: List? = null, - @SerialName("lazy_load_members") val lazyLoadMembers: Boolean = false, -) - -@Serializable -internal data class EventFilter( - @SerialName("limit") val limit: Int? = null, - @SerialName("not_types") val notTypes: List? = null, - @SerialName("types") val types: List? = null, -) \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiStrippedEvent.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiStrippedEvent.kt deleted file mode 100644 index efaee47..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiStrippedEvent.kt +++ /dev/null @@ -1,41 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import app.dapk.st.matrix.common.MxUrl -import app.dapk.st.matrix.common.UserId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -sealed class ApiStrippedEvent { - - @Serializable - @SerialName("m.room.member") - internal data class RoomMember( - @SerialName("content") val content: Content, - @SerialName("sender") val sender: UserId, - ) : ApiStrippedEvent() { - - @Serializable - internal data class Content( - @SerialName("displayname") val displayName: String? = null, - @SerialName("membership") val membership: ApiTimelineEvent.RoomMember.Content.Membership? = null, - @SerialName("is_direct") val isDirect: Boolean? = null, - @SerialName("avatar_url") val avatarUrl: MxUrl? = null, - ) - } - - @Serializable - @SerialName("m.room.name") - internal data class RoomName( - @SerialName("content") val content: Content, - ) : ApiStrippedEvent() { - - @Serializable - internal data class Content( - @SerialName("name") val name: String? = null - ) - } - - @Serializable - object Ignored : ApiStrippedEvent() -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt deleted file mode 100644 index b4f417c..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt +++ /dev/null @@ -1,113 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.ServerKeyCount -import app.dapk.st.matrix.common.SyncToken -import app.dapk.st.matrix.common.UserId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class ApiSyncResponse( - @SerialName("device_lists") val deviceLists: DeviceLists? = null, - @SerialName("account_data") val accountData: ApiAccountData? = null, - @SerialName("rooms") val rooms: ApiSyncRooms? = null, - @SerialName("to_device") val toDevice: ToDevice? = null, - @SerialName("device_one_time_keys_count") val oneTimeKeysCount: Map? = null, - @SerialName("next_batch") val nextBatch: SyncToken, - @SerialName("prev_batch") val prevBatch: SyncToken? = null, -) - -@Serializable -data class ApiAccountData( - @SerialName("events") val events: List -) - -@Serializable -internal data class DeviceLists( - @SerialName("changed") val changed: List? = null -) - -@Serializable -internal data class ToDevice( - @SerialName("events") val events: List -) - -@Serializable -internal data class ApiSyncRooms( - @SerialName("join") val join: Map? = null, - @SerialName("invite") val invite: Map? = null, - @SerialName("leave") val leave: Map? = null, -) - -@Serializable -internal data class ApiSyncRoomInvite( - @SerialName("invite_state") val state: ApiInviteEvents, -) - -@Serializable -internal data class ApiInviteEvents( - @SerialName("events") val events: List -) - -@Serializable -internal data class ApiSyncRoom( - @SerialName("timeline") val timeline: ApiSyncRoomTimeline, - @SerialName("state") val state: ApiSyncRoomState? = null, - @SerialName("account_data") val accountData: ApiAccountData? = null, - @SerialName("ephemeral") val ephemeral: ApiEphemeral? = null, - @SerialName("summary") val summary: ApiRoomSummary? = null, -) - -@Serializable -internal data class ApiRoomSummary( - @SerialName("m.heroes") val heroes: List? = null -) - -@Serializable -internal data class ApiEphemeral( - @SerialName("events") val events: List -) - -@Serializable -internal sealed class ApiEphemeralEvent { - - @Serializable - @SerialName("m.typing") - internal data class Typing( - @SerialName("content") val content: Content, - ) : ApiEphemeralEvent() { - @Serializable - internal data class Content( - @SerialName("user_ids") val userIds: List - ) - } - - @Serializable - object Ignored : ApiEphemeralEvent() -} - - -@Serializable -internal data class ApiSyncRoomState( - @SerialName("events") val stateEvents: List, -) - -@Serializable -internal data class ApiSyncRoomTimeline( - @SerialName("events") val apiTimelineEvents: List, -) - - -@Serializable -internal sealed class DecryptedContent { - - @Serializable - @SerialName("m.room.message") - internal data class TimelineText( - @SerialName("content") val content: ApiTimelineEvent.TimelineMessage.Content, - ) : DecryptedContent() - - @Serializable - object Ignored : DecryptedContent() -} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt deleted file mode 100644 index 0081e26..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineEvent.kt +++ /dev/null @@ -1,223 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import app.dapk.st.matrix.common.AlgorithmName -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.MxUrl -import app.dapk.st.matrix.common.UserId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal sealed class ApiTimelineEvent { - - @Serializable - @SerialName("m.room.create") - internal data class RoomCreate( - @SerialName("event_id") val id: EventId, - @SerialName("origin_server_ts") val utcTimestamp: Long, - @SerialName("content") val content: Content, - ) : ApiTimelineEvent() { - - @Serializable - internal data class Content( - @SerialName("type") val type: String? = null - ) { - - object Type { - const val SPACE = "m.space" - } - - } - } - - @Serializable - @SerialName("m.room.topic") - internal data class RoomTopic( - @SerialName("event_id") val id: EventId, - @SerialName("content") val content: Content, - ) : ApiTimelineEvent() { - - @Serializable - internal data class Content( - @SerialName("topic") val topic: String - ) - } - - @Serializable - @SerialName("m.room.name") - internal data class RoomName( - @SerialName("event_id") val id: EventId, - @SerialName("content") val content: Content, - ) : ApiTimelineEvent() { - - @Serializable - internal data class Content( - @SerialName("name") val name: String - ) - } - - @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? = null - ) - } - - @Serializable - @SerialName("m.room.avatar") - internal data class RoomAvatar( - @SerialName("event_id") val id: EventId, - @SerialName("content") val content: Content, - ) : ApiTimelineEvent() { - - @Serializable - internal data class Content( - @SerialName("url") val url: MxUrl? = null - ) - } - - @Serializable - @SerialName("m.room.member") - internal data class RoomMember( - @SerialName("event_id") val id: EventId, - @SerialName("content") val content: Content, - @SerialName("sender") val senderId: UserId, - ) : ApiTimelineEvent() { - - @Serializable - internal data class Content( - @SerialName("displayname") val displayName: String? = null, - @SerialName("membership") val membership: Membership, - @SerialName("avatar_url") val avatarUrl: MxUrl? = null, - ) { - - @JvmInline - @Serializable - value class Membership(val value: String) { - fun isJoin() = value == "join" - fun isInvite() = value == "invite" - fun isLeave() = value == "leave" - } - - } - } - - @Serializable - @SerialName("m.room.redaction") - internal data class RoomRedcation( - @SerialName("event_id") val id: EventId, - @SerialName("redacts") val redactedId: EventId, - @SerialName("origin_server_ts") val utcTimestamp: Long, - @SerialName("sender") val senderId: UserId, - ) : ApiTimelineEvent() - - @Serializable - internal data class DecryptionStatus( - @SerialName("is_verified") val isVerified: Boolean - ) - - @Serializable - @SerialName("m.room.message") - internal data class TimelineMessage( - @SerialName("event_id") val id: EventId, - @SerialName("sender") val senderId: UserId, - @SerialName("content") val content: Content, - @SerialName("origin_server_ts") val utcTimestamp: Long, - @SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null - ) : ApiTimelineEvent() { - - @Serializable(with = ApiTimelineMessageContentDeserializer::class) - internal sealed interface Content { - val relation: Relation? - - @Serializable - data class Text( - @SerialName("body") val body: String? = null, - @SerialName("formatted_body") val formattedBody: String? = null, - @SerialName("m.relates_to") override val relation: Relation? = null, - @SerialName("msgtype") val messageType: String = "m.text", - ) : Content - - @Serializable - data class Image( - @SerialName("url") val url: MxUrl? = null, - @SerialName("file") val file: File? = null, - @SerialName("info") val info: Info? = null, - @SerialName("m.relates_to") override val relation: Relation? = null, - @SerialName("msgtype") val messageType: String = "m.image", - ) : Content { - - @Serializable - data class File( - @SerialName("url") val url: MxUrl, - @SerialName("iv") val iv: String, - @SerialName("v") val v: String, - @SerialName("hashes") val hashes: Map, - @SerialName("key") val key: Key, - ) { - - @Serializable - data class Key( - @SerialName("k") val k: String, - ) - - } - - @Serializable - internal data class Info( - @SerialName("h") val height: Int? = null, - @SerialName("w") val width: Int? = null, - ) - } - - @Serializable - object Ignored : Content { - override val relation: Relation? = null - } - } - - @Serializable - data class Relation( - @SerialName("m.in_reply_to") val inReplyTo: InReplyTo? = null, - @SerialName("rel_type") val relationType: String? = null, - @SerialName("event_id") val eventId: EventId? = null - ) - - @Serializable - data class InReplyTo( - @SerialName("event_id") val eventId: EventId - ) - } - - - @Serializable - @SerialName("m.room.encryption") - data class Encryption( - @SerialName("content") val content: Content, - ) : ApiTimelineEvent() { - @Serializable - data class Content( - @SerialName("algorithm") val algorithm: AlgorithmName, - @SerialName("rotation_period_ms") val rotationMs: Long? = null, - @SerialName("rotation_period_msgs") val rotationMessages: Long? = null, - ) - } - - @Serializable - @SerialName("m.room.encrypted") - internal data class Encrypted( - @SerialName("sender") val senderId: UserId, - @SerialName("content") val encryptedContent: ApiEncryptedContent, - @SerialName("event_id") val eventId: EventId, - @SerialName("origin_server_ts") val utcTimestamp: Long, - ) : ApiTimelineEvent() - - @Serializable - object Ignored : ApiTimelineEvent() -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineMessageContentDeserializer.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineMessageContentDeserializer.kt deleted file mode 100644 index 324e73e..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiTimelineMessageContentDeserializer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive - -internal object ApiTimelineMessageContentDeserializer : KSerializer { - - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("messageContent") - - override fun deserialize(decoder: Decoder): ApiTimelineEvent.TimelineMessage.Content { - require(decoder is JsonDecoder) - val element = decoder.decodeJsonElement() - return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) { - "m.text" -> decoder.json.decodeFromJsonElement(ApiTimelineEvent.TimelineMessage.Content.Text.serializer(), element) - "m.image" -> decoder.json.decodeFromJsonElement(ApiTimelineEvent.TimelineMessage.Content.Image.serializer(), element) - else -> ApiTimelineEvent.TimelineMessage.Content.Ignored - } - } - - override fun serialize(encoder: Encoder, value: ApiTimelineEvent.TimelineMessage.Content) = when (value) { - ApiTimelineEvent.TimelineMessage.Content.Ignored -> {} - is ApiTimelineEvent.TimelineMessage.Content.Image -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().serialize(encoder, value) - is ApiTimelineEvent.TimelineMessage.Content.Text -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().serialize(encoder, value) - } - -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiToDeviceEvent.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiToDeviceEvent.kt deleted file mode 100644 index eacd1e4..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiToDeviceEvent.kt +++ /dev/null @@ -1,151 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import app.dapk.st.matrix.common.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -sealed class ApiToDeviceEvent { - - @Serializable - @SerialName("m.room.encrypted") - internal data class Encrypted( - @SerialName("sender") val senderId: UserId, - @SerialName("content") val content: ApiEncryptedContent, - ) : ApiToDeviceEvent() - - @Serializable - @SerialName("m.room_key") - data class RoomKey( - @SerialName("sender") val sender: UserId, - @SerialName("content") val content: Content, - ) : ApiToDeviceEvent() { - @Serializable - data class Content( - @SerialName("room_id") val roomId: RoomId, - @SerialName("algorithm") val algorithmName: AlgorithmName, - @SerialName("session_id") val sessionId: SessionId, - @SerialName("session_key") val sessionKey: String, - @SerialName("chain_index") val chainIndex: Long, - ) - } - - @Serializable - @SerialName("m.key.verification.request") - data class VerificationRequest( - @SerialName("content") val content: Content, - @SerialName("sender") val sender: UserId, - ) : ApiToDeviceEvent(), ApiVerificationEvent { - - @Serializable - data class Content( - @SerialName("from_device") val fromDevice: DeviceId, - @SerialName("methods") val methods: List, - @SerialName("timestamp") val timestampPosix: Long, - @SerialName("transaction_id") val transactionId: String, - ) - } - - @Serializable - @SerialName("m.key.verification.ready") - data class VerificationReady( - @SerialName("content") val content: Content, - @SerialName("sender") val sender: UserId, - ) : ApiToDeviceEvent(), ApiVerificationEvent { - - @Serializable - data class Content( - @SerialName("from_device") val fromDevice: DeviceId, - @SerialName("methods") val methods: List, - @SerialName("transaction_id") val transactionId: String, - ) - } - - @Serializable - @SerialName("m.key.verification.start") - data class VerificationStart( - @SerialName("content") val content: Content, - @SerialName("sender") val sender: UserId, - ) : ApiToDeviceEvent(), ApiVerificationEvent { - - @Serializable - data class Content( - @SerialName("from_device") val fromDevice: DeviceId, - @SerialName("method") val method: String, - @SerialName("key_agreement_protocols") val protocols: List, - @SerialName("hashes") val hashes: List, - @SerialName("message_authentication_codes") val codes: List, - @SerialName("short_authentication_string") val short: List, - @SerialName("transaction_id") val transactionId: String, - ) - } - - @Serializable - @SerialName("m.key.verification.accept") - data class VerificationAccept( - @SerialName("content") val content: Content, - @SerialName("sender") val sender: UserId, - ) : ApiToDeviceEvent(), ApiVerificationEvent { - - @Serializable - data class Content( - @SerialName("from_device") val fromDevice: DeviceId, - @SerialName("method") val method: String, - @SerialName("key_agreement_protocol") val protocol: String, - @SerialName("hash") val hash: String, - @SerialName("message_authentication_code") val code: String, - @SerialName("short_authentication_string") val short: List, - @SerialName("commitment") val commitment: String, - @SerialName("transaction_id") val transactionId: String, - ) - } - - @Serializable - @SerialName("m.key.verification.key") - data class VerificationKey( - @SerialName("content") val content: Content, - @SerialName("sender") val sender: UserId, - ) : ApiToDeviceEvent(), ApiVerificationEvent { - - @Serializable - data class Content( - @SerialName("transaction_id") val transactionId: String, - @SerialName("key") val key: String, - ) - } - - @Serializable - @SerialName("m.key.verification.mac") - data class VerificationMac( - @SerialName("content") val content: Content, - @SerialName("sender") val sender: UserId, - ) : ApiToDeviceEvent(), ApiVerificationEvent { - - @Serializable - data class Content( - @SerialName("transaction_id") val transactionId: String, - @SerialName("keys") val keys: String, - @SerialName("mac") val mac: Map, - ) - } - - @Serializable - @SerialName("m.key.verification.cancel") - data class VerificationCancel( - @SerialName("content") val content: Content, - ) : ApiToDeviceEvent(), ApiVerificationEvent { - - @Serializable - data class Content( - @SerialName("code") val code: String, - @SerialName("reason") val reason: String, - @SerialName("transaction_id") val transactionId: String, - ) - } - - @Serializable - object Ignored : ApiToDeviceEvent() - - - sealed interface ApiVerificationEvent -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/EncryptedContentDeserializer.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/EncryptedContentDeserializer.kt deleted file mode 100644 index cd3c529..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/EncryptedContentDeserializer.kt +++ /dev/null @@ -1,29 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive - -internal object EncryptedContentDeserializer : KSerializer { - - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("encryptedContent") - - override fun deserialize(decoder: Decoder): ApiEncryptedContent { - require(decoder is JsonDecoder) - val element = decoder.decodeJsonElement() - return when (val algorithm = element.jsonObject["algorithm"]?.jsonPrimitive?.content) { - "m.olm.v1.curve25519-aes-sha2" -> decoder.json.decodeFromJsonElement(ApiEncryptedContent.OlmV1.serializer(), element) - "m.megolm.v1.aes-sha2" -> decoder.json.decodeFromJsonElement(ApiEncryptedContent.MegOlmV1.serializer(), element) - null -> ApiEncryptedContent.Unknown - else -> throw IllegalArgumentException("Unknown algorithm : $algorithm") - } - } - - override fun serialize(encoder: Encoder, value: ApiEncryptedContent) = TODO("Not yet implemented") - -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/SyncRequest.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/SyncRequest.kt deleted file mode 100644 index 3f2f1fb..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/SyncRequest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.dapk.st.matrix.sync.internal.request - -import app.dapk.st.matrix.common.SyncToken -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest -import app.dapk.st.matrix.http.queryMap -import app.dapk.st.matrix.sync.SyncService.FilterId - -internal fun syncRequest(lastSyncToken: SyncToken?, filterId: FilterId?, timeoutMs: Long) = - httpRequest( - path = "_matrix/client/r0/sync?${ - queryMap( - "since" to lastSyncToken?.value, - "filter" to filterId?.value, - "timeout" to timeoutMs.toString(), - ) - }", - method = MatrixHttpClient.Method.GET, - ) \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt deleted file mode 100644 index 79dbf2e..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt +++ /dev/null @@ -1,81 +0,0 @@ -package app.dapk.st.matrix.sync.internal.room - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent.TimelineMessage.Content.Image -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent.TimelineMessage.Content.Text -import app.dapk.st.matrix.sync.internal.request.DecryptedContent -import app.dapk.st.matrix.sync.internal.sync.message.RichMessageParser -import kotlinx.serialization.json.Json - -internal class RoomEventsDecrypter( - private val messageDecrypter: MessageDecrypter, - private val richMessageParser: RichMessageParser, - private val json: Json, - private val logger: MatrixLogger, -) { - - suspend fun decryptRoomEvents(userCredentials: UserCredentials, events: List) = events.map { event -> - decryptEvent(event, userCredentials) - } - - private suspend fun decryptEvent(event: RoomEvent, userCredentials: UserCredentials): RoomEvent = when (event) { - is RoomEvent.Encrypted -> event.decrypt(userCredentials) - is RoomEvent.Message -> event - is RoomEvent.Reply -> RoomEvent.Reply( - message = decryptEvent(event.message, userCredentials), - replyingTo = decryptEvent(event.replyingTo, userCredentials), - ) - - is RoomEvent.Image -> event - is RoomEvent.Redacted -> event - } - - private suspend fun RoomEvent.Encrypted.decrypt(userCredentials: UserCredentials) = when (val result = this.decryptContent()) { - is DecryptionResult.Failed -> this.also { logger.crypto("Failed to decrypt ${it.eventId}") } - is DecryptionResult.Success -> when (val model = result.payload.toModel()) { - DecryptedContent.Ignored -> this - is DecryptedContent.TimelineText -> when (val content = model.content) { - ApiTimelineEvent.TimelineMessage.Content.Ignored -> this - is Image -> createImageEvent(content, userCredentials) - is Text -> createMessageEvent(content) - } - } - } - - private suspend fun RoomEvent.Encrypted.decryptContent() = messageDecrypter.decrypt(this.encryptedContent.toModel()) - - private fun RoomEvent.Encrypted.createMessageEvent(content: Text) = RoomEvent.Message( - eventId = this.eventId, - utcTimestamp = this.utcTimestamp, - author = this.author, - meta = this.meta, - edited = this.edited, - content = richMessageParser.parse(content.body ?: "") - ) - - private fun RoomEvent.Encrypted.createImageEvent(content: Image, userCredentials: UserCredentials) = RoomEvent.Image( - eventId = this.eventId, - utcTimestamp = this.utcTimestamp, - author = this.author, - meta = this.meta, - edited = this.edited, - imageMeta = RoomEvent.Image.ImageMeta( - width = content.info?.width, - height = content.info?.height, - url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer), - keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) } - ), - ) - - private fun JsonString.toModel() = json.decodeFromString(DecryptedContent.serializer(), this.value) - -} - -private fun RoomEvent.Encrypted.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1( - this.cipherText, - this.deviceId, - this.senderKey, - this.sessionId, -) \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomStateReducer.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomStateReducer.kt deleted file mode 100644 index a92ebb8..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomStateReducer.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.dapk.st.matrix.sync.internal.room - -import app.dapk.st.matrix.common.DecryptionResult -import app.dapk.st.matrix.common.EncryptedMessageContent -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent - -internal fun ApiEncryptedContent.export(senderId: UserId): EncryptedMessageContent? { - return when (this) { - is ApiEncryptedContent.MegOlmV1 -> EncryptedMessageContent.MegOlmV1( - this.cipherText, this.deviceId, this.senderKey, this.sessionId - ) - is ApiEncryptedContent.OlmV1 -> EncryptedMessageContent.OlmV1( - senderId = senderId, - this.cipherText.mapValues { EncryptedMessageContent.CipherTextInfo(it.value.body, it.value.type) }, - this.senderKey - ) - ApiEncryptedContent.Unknown -> null - } -} - -fun interface MessageDecrypter { - suspend fun decrypt(event: EncryptedMessageContent): DecryptionResult -} - -internal object MissingMessageDecrypter : MessageDecrypter { - override suspend fun decrypt(event: EncryptedMessageContent) = throw IllegalStateException("No encrypter instance set") -} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncEventDecrypter.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncEventDecrypter.kt deleted file mode 100644 index 55d370d..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncEventDecrypter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package app.dapk.st.matrix.sync.internal.room - -import app.dapk.st.matrix.common.DecryptionResult -import app.dapk.st.matrix.common.EncryptedMessageContent -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.matrixLog -import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent -import app.dapk.st.matrix.sync.internal.request.DecryptedContent -import kotlinx.serialization.json.Json - -internal class SyncEventDecrypter( - private val messageDecrypter: MessageDecrypter, - private val json: Json, - private val logger: MatrixLogger, -) { - - suspend fun decryptTimelineEvents(events: List) = events.map { event -> - when (event) { - is ApiTimelineEvent.Encrypted -> { - event.encryptedContent.export(event.senderId)?.let { encryptedContent -> - decrypt(encryptedContent, event) - } ?: event - } - else -> event - } - } - - private suspend fun decrypt(it: EncryptedMessageContent, event: ApiTimelineEvent.Encrypted) = messageDecrypter.decrypt(it).let { - when (it) { - is DecryptionResult.Failed -> event - is DecryptionResult.Success -> json.decodeFromString(DecryptedContent.serializer(), it.payload.value).let { - val relation = when (event.encryptedContent) { - is ApiEncryptedContent.MegOlmV1 -> event.encryptedContent.relation - is ApiEncryptedContent.OlmV1 -> null - ApiEncryptedContent.Unknown -> null - } - when (it) { - is DecryptedContent.TimelineText -> ApiTimelineEvent.TimelineMessage( - event.eventId, - event.senderId, - when (it.content) { - is ApiTimelineEvent.TimelineMessage.Content.Image -> it.content.copy(relation = relation) - is ApiTimelineEvent.TimelineMessage.Content.Text -> it.content.copy(relation = relation) - ApiTimelineEvent.TimelineMessage.Content.Ignored -> it.content - }, - event.utcTimestamp, - ).also { logger.matrixLog("decrypted to timeline text: $it") } - DecryptedContent.Ignored -> ApiTimelineEvent.Ignored - } - } - } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt deleted file mode 100644 index ad02e65..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt +++ /dev/null @@ -1,85 +0,0 @@ -package app.dapk.st.matrix.sync.internal.room - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.DeviceNotifier -import app.dapk.st.matrix.sync.KeySharer -import app.dapk.st.matrix.sync.MaybeCreateMoreKeys -import app.dapk.st.matrix.sync.VerificationHandler -import app.dapk.st.matrix.sync.internal.request.ApiSyncResponse -import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json - -internal class SyncSideEffects( - private val keySharer: KeySharer, - private val verificationHandler: VerificationHandler, - private val notifyDevicesUpdated: DeviceNotifier, - private val messageDecrypter: MessageDecrypter, - private val json: Json, - private val oneTimeKeyProducer: MaybeCreateMoreKeys, - private val logger: MatrixLogger, -) { - - suspend fun blockingSideEffects(userId: UserId, response: ApiSyncResponse, requestToken: SyncToken?): SideEffectResult { - return withContext(Dispatchers.IO) { - logger.matrixLog("process side effects") - response.deviceLists?.changed?.ifEmpty { null }?.let { - notifyDevicesUpdated.notifyChanges(it, requestToken) - } - - oneTimeKeyProducer.onServerKeyCount(response.oneTimeKeysCount?.get("signed_curve25519") ?: ServerKeyCount(0)) - - val decryptedToDeviceEvents = decryptedToDeviceEvents(response) - val roomKeys = handleRoomKeyShares(decryptedToDeviceEvents) - - checkForVerificationRequests(userId, decryptedToDeviceEvents) - SideEffectResult(roomKeys?.map { it.roomId } ?: emptyList()) - } - } - - private suspend fun checkForVerificationRequests(selfId: UserId, toDeviceEvents: List?) { - toDeviceEvents?.filterIsInstance() - ?.ifEmpty { null } - ?.also { - if (it.size > 1) { - logger.matrixLog(MatrixLogTag.VERIFICATION, "found more verification events than expected, using first") - } - verificationHandler.handle(it.first()) - } - } - - private suspend fun handleRoomKeyShares(toDeviceEvents: List?): List? { - return toDeviceEvents?.filterIsInstance()?.map { - SharedRoomKey( - it.content.algorithmName, - it.content.roomId, - it.content.sessionId, - it.content.sessionKey, - isExported = false - ) - }?.also { keySharer.share(it) } - } - - private suspend fun decryptedToDeviceEvents(response: ApiSyncResponse) = response.toDevice?.events - ?.mapNotNull { - when (it) { - is ApiToDeviceEvent.Encrypted -> decryptEncryptedToDevice(it) - else -> it - } - } - - private suspend fun decryptEncryptedToDevice(it: ApiToDeviceEvent.Encrypted): ApiToDeviceEvent? { - logger.matrixLog("got encrypted toDevice event: from ${it.senderId}: $") - return it.content.export(it.senderId)?.let { - messageDecrypter.decrypt(it).let { - when (it) { - is DecryptionResult.Failed -> null - is DecryptionResult.Success -> json.decodeFromString(ApiToDeviceEvent.serializer(), it.payload.value) - } - } - } - } -} - -data class SideEffectResult(val roomsWithNewKeys: List) \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/EphemeralEventsUseCase.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/EphemeralEventsUseCase.kt deleted file mode 100644 index 52f4372..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/EphemeralEventsUseCase.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.core.extensions.ifNotEmpty -import app.dapk.st.matrix.sync.RoomMembersService -import app.dapk.st.matrix.sync.SyncService -import app.dapk.st.matrix.sync.internal.request.ApiEphemeralEvent -import kotlinx.coroutines.flow.MutableSharedFlow - -internal class EphemeralEventsUseCase( - private val roomMembersService: RoomMembersService, - private val syncEventsFlow: MutableSharedFlow>, -) { - - suspend fun processEvents(roomToProcess: RoomToProcess) { - val syncEvents = roomToProcess.apiSyncRoom.ephemeral?.events?.filterIsInstance()?.map { - val members = it.content.userIds.ifNotEmpty { roomMembersService.find(roomToProcess.roomId, it) } - SyncService.SyncEvent.Typing(roomToProcess.roomId, members) - } - syncEvents?.let { syncEventsFlow.tryEmit(it) } - } - -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCase.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCase.kt deleted file mode 100644 index 9eda9dd..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCase.kt +++ /dev/null @@ -1,30 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.sync.RoomStore -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent - -internal class EventLookupUseCase( - private val roomStore: RoomStore, -) { - - suspend fun lookup(eventId: EventId, decryptedTimeline: DecryptedTimeline, decryptedPreviousEvents: DecryptedRoomEvents): LookupResult { - return decryptedTimeline.lookup(eventId) - ?: decryptedPreviousEvents.lookup(eventId) - ?: lookupFromPersistence(eventId) - ?: LookupResult(apiTimelineEvent = null, roomEvent = null) - } - - private fun DecryptedTimeline.lookup(id: EventId) = this.value - .filterIsInstance() - .firstOrNull { it.id == id } - ?.let { LookupResult(apiTimelineEvent = it, roomEvent = null) } - - private fun DecryptedRoomEvents.lookup(id: EventId) = this.value - .firstOrNull { it.eventId == id } - ?.let { LookupResult(apiTimelineEvent = null, roomEvent = it) } - - private suspend fun lookupFromPersistence(eventId: EventId) = roomStore.findEvent(eventId)?.let { - LookupResult(apiTimelineEvent = null, roomEvent = it) - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/LookupResult.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/LookupResult.kt deleted file mode 100644 index 24aea5a..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/LookupResult.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent - -internal data class LookupResult( - private val apiTimelineEvent: ApiTimelineEvent.TimelineMessage?, - private val roomEvent: RoomEvent?, -) { - - inline fun fold( - onApiTimelineEvent: (ApiTimelineEvent.TimelineMessage) -> T?, - onRoomEvent: (RoomEvent) -> T?, - onEmpty: () -> T?, - ): T? { - return when { - apiTimelineEvent != null -> onApiTimelineEvent(apiTimelineEvent) - roomEvent != null -> onRoomEvent(roomEvent) - else -> onEmpty() - } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt deleted file mode 100644 index 794ff24..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt +++ /dev/null @@ -1,62 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomState -import app.dapk.st.matrix.sync.RoomStore - -class RoomDataSource( - private val roomStore: RoomStore, - private val logger: MatrixLogger, -) { - - private val roomCache = mutableMapOf() - - fun contains(roomId: RoomId) = roomCache.containsKey(roomId) - - suspend fun read(roomId: RoomId) = when (val cached = roomCache[roomId]) { - null -> roomStore.retrieve(roomId)?.also { roomCache[roomId] = it } - else -> cached - } - - suspend fun persist(roomId: RoomId, previousState: RoomState?, newState: RoomState) { - if (newState == previousState) { - logger.matrixLog(MatrixLogTag.SYNC, "no changes, not persisting") - } else { - roomCache[roomId] = newState - roomStore.persist(roomId, newState.events) - } - } - - suspend fun remove(roomsLeft: List) { - roomsLeft.forEach { roomCache.remove(it) } - roomStore.remove(roomsLeft) - } - - suspend fun redact(roomId: RoomId, eventId: EventId) { - val eventToRedactFromCache = roomCache[roomId]?.events?.find { it.eventId == eventId } - val redactedEvent = when { - eventToRedactFromCache != null -> { - eventToRedactFromCache.redact().also { redacted -> - val cachedRoomState = roomCache[roomId] - requireNotNull(cachedRoomState) - roomCache[roomId] = cachedRoomState.replaceEvent(eventToRedactFromCache, redacted) - } - } - - else -> roomStore.findEvent(eventId)?.redact() - } - - redactedEvent?.let { roomStore.persist(roomId, listOf(it)) } - } -} - -private fun RoomEvent.redact() = RoomEvent.Redacted(this.eventId, this.utcTimestamp, this.author) - -private fun RoomState.replaceEvent(old: RoomEvent, new: RoomEvent): RoomState { - val updatedEvents = this.events.toMutableList().apply { - remove(old) - add(new) - } - return this.copy(events = updatedEvents) -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt deleted file mode 100644 index 24fcef9..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt +++ /dev/null @@ -1,192 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.core.extensions.ifOrNull -import app.dapk.st.core.extensions.nullAndTrack -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.sync.MessageMeta -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomMembersService -import app.dapk.st.matrix.sync.find -import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent -import app.dapk.st.matrix.sync.internal.sync.message.RichMessageParser - -private typealias Lookup = suspend (EventId) -> LookupResult - -internal class RoomEventCreator( - private val roomMembersService: RoomMembersService, - private val errorTracker: ErrorTracker, - private val roomEventFactory: RoomEventFactory, - private val richMessageParser: RichMessageParser, -) { - - suspend fun ApiTimelineEvent.Encrypted.toRoomEvent(roomId: RoomId): RoomEvent? { - return when (this.encryptedContent) { - is ApiEncryptedContent.MegOlmV1 -> { - RoomEvent.Encrypted( - eventId = this.eventId, - author = roomMembersService.find(roomId, this.senderId)!!, - utcTimestamp = this.utcTimestamp, - meta = MessageMeta.FromServer, - encryptedContent = RoomEvent.Encrypted.MegOlmV1( - this.encryptedContent.cipherText, - this.encryptedContent.deviceId, - this.encryptedContent.senderKey, - this.encryptedContent.sessionId - ) - ) - } - - is ApiEncryptedContent.OlmV1 -> errorTracker.nullAndTrack(IllegalStateException("unexpected encryption, got OlmV1 for a room event")) - ApiEncryptedContent.Unknown -> errorTracker.nullAndTrack(IllegalStateException("unknown room event encryption")) - } - } - - suspend fun ApiTimelineEvent.TimelineMessage.toRoomEvent(userCredentials: UserCredentials, roomId: RoomId, lookup: Lookup): RoomEvent? { - return TimelineEventMapper(userCredentials, roomId, roomEventFactory, richMessageParser).mapToRoomEvent(this, lookup) - } -} - -internal class TimelineEventMapper( - private val userCredentials: UserCredentials, - private val roomId: RoomId, - private val roomEventFactory: RoomEventFactory, - private val richMessageParser: RichMessageParser, -) { - - suspend fun mapToRoomEvent(event: ApiTimelineEvent.TimelineMessage, lookup: Lookup): RoomEvent? { - return when { - event.content == ApiTimelineEvent.TimelineMessage.Content.Ignored -> null - event.isEdit() -> event.handleEdit(editedEventId = event.content.relation!!.eventId!!, lookup) - event.isReply() -> event.handleReply(replyToId = event.content.relation!!.inReplyTo!!.eventId, lookup) - else -> roomEventFactory.mapToRoomEvent(event) - } - } - - private suspend fun ApiTimelineEvent.TimelineMessage.handleReply(replyToId: EventId, lookup: Lookup): RoomEvent { - val relationEvent = lookup(replyToId).fold( - onApiTimelineEvent = { it.toMessage() }, - onRoomEvent = { it }, - onEmpty = { null } - ) - - return when (relationEvent) { - null -> this.toMessage() - else -> { - RoomEvent.Reply( - message = roomEventFactory.mapToRoomEvent(this), - replyingTo = when (relationEvent) { - is RoomEvent.Message -> relationEvent - is RoomEvent.Reply -> relationEvent.message - is RoomEvent.Image -> relationEvent - is RoomEvent.Encrypted -> relationEvent - is RoomEvent.Redacted -> relationEvent - } - ) - } - } - } - - private suspend fun ApiTimelineEvent.TimelineMessage.toMessage() = when (this.content) { - is ApiTimelineEvent.TimelineMessage.Content.Image -> this.toImageMessage() - is ApiTimelineEvent.TimelineMessage.Content.Text -> this.toFallbackTextMessage() - ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException() - } - - private suspend fun ApiTimelineEvent.TimelineMessage.toFallbackTextMessage() = this.toTextMessage(content = this.asTextContent().body ?: "") - - private suspend fun ApiTimelineEvent.TimelineMessage.handleEdit(editedEventId: EventId, lookup: Lookup): RoomEvent? { - return lookup(editedEventId).fold( - onApiTimelineEvent = { editApiEvent(original = it, incomingEdit = this) }, - onRoomEvent = { editRoomEvent(original = it, incomingEdit = this) }, - onEmpty = { this.toTextMessage(edited = true) } - ) - } - - private fun editRoomEvent(original: RoomEvent, incomingEdit: ApiTimelineEvent.TimelineMessage): RoomEvent? { - return ifOrNull(incomingEdit.utcTimestamp > original.utcTimestamp) { - when (original) { - is RoomEvent.Message -> original.edited(incomingEdit) - is RoomEvent.Reply -> original.copy( - message = when (original.message) { - is RoomEvent.Image -> original.message - is RoomEvent.Message -> original.message.edited(incomingEdit) - is RoomEvent.Reply -> original.message - is RoomEvent.Encrypted -> original.message - is RoomEvent.Redacted -> original.message - } - ) - - is RoomEvent.Image -> { - // can't edit images - null - } - - is RoomEvent.Encrypted -> { - // can't edit encrypted messages - null - } - - is RoomEvent.Redacted -> { - // can't edit redacted - null - } - } - } - } - - private suspend fun editApiEvent(original: ApiTimelineEvent.TimelineMessage, incomingEdit: ApiTimelineEvent.TimelineMessage): RoomEvent? { - return ifOrNull(incomingEdit.utcTimestamp > original.utcTimestamp) { - when (original.content) { - is ApiTimelineEvent.TimelineMessage.Content.Image -> original.toImageMessage( - utcTimestamp = incomingEdit.utcTimestamp, - edited = true, - ) - - is ApiTimelineEvent.TimelineMessage.Content.Text -> original.toTextMessage( - utcTimestamp = incomingEdit.utcTimestamp, - content = incomingEdit.asTextContent().let { it.formattedBody ?: it.body }?.removePrefix(" * ") ?: "", - edited = true, - ) - - ApiTimelineEvent.TimelineMessage.Content.Ignored -> null - } - } - } - - private fun RoomEvent.Message.edited(edit: ApiTimelineEvent.TimelineMessage) = this.copy( - content = richMessageParser.parse(edit.asTextContent().let { it.formattedBody ?: it.body }?.removePrefix(" * ") ?: ""), - edited = true, - ) - - private suspend fun RoomEventFactory.mapToRoomEvent(source: ApiTimelineEvent.TimelineMessage): RoomEvent { - return when (source.content) { - is ApiTimelineEvent.TimelineMessage.Content.Image -> source.toImageMessage(userCredentials, roomId) - is ApiTimelineEvent.TimelineMessage.Content.Text -> source.toTextMessage( - roomId, - content = source.asTextContent().formattedBody ?: source.content.body ?: "" - ) - - ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException() - } - } - - private suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage( - content: String = this.asTextContent().formattedBody ?: this.asTextContent().body ?: "", - edited: Boolean = false, - utcTimestamp: Long = this.utcTimestamp, - ) = with(roomEventFactory) { toTextMessage(roomId, content, edited, utcTimestamp) } - - private suspend fun ApiTimelineEvent.TimelineMessage.toImageMessage( - edited: Boolean = false, - utcTimestamp: Long = this.utcTimestamp, - ) = with(roomEventFactory) { toImageMessage(userCredentials, roomId, edited, utcTimestamp) } - -} - -private fun ApiTimelineEvent.TimelineMessage.isEdit() = this.content.relation?.relationType == "m.replace" && this.content.relation?.eventId != null -private fun ApiTimelineEvent.TimelineMessage.isReply() = this.content.relation?.inReplyTo != null -private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt deleted file mode 100644 index dbfc70c..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt +++ /dev/null @@ -1,56 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.MessageMeta -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomMembersService -import app.dapk.st.matrix.sync.find -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent -import app.dapk.st.matrix.sync.internal.sync.message.RichMessageParser - -private val UNKNOWN_AUTHOR = RoomMember(id = UserId("unknown"), displayName = null, avatarUrl = null) - -internal class RoomEventFactory( - private val roomMembersService: RoomMembersService, - private val richMessageParser: RichMessageParser, -) { - - suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage( - roomId: RoomId, - content: String, - edited: Boolean = false, - utcTimestamp: Long = this.utcTimestamp, - ) = RoomEvent.Message( - eventId = this.id, - content = richMessageParser.parse(content), - author = roomMembersService.find(roomId, this.senderId) ?: UNKNOWN_AUTHOR, - utcTimestamp = utcTimestamp, - meta = MessageMeta.FromServer, - edited = edited, - ) - - suspend fun ApiTimelineEvent.TimelineMessage.toImageMessage( - userCredentials: UserCredentials, - roomId: RoomId, - edited: Boolean = false, - utcTimestamp: Long = this.utcTimestamp, - imageMeta: RoomEvent.Image.ImageMeta = this.readImageMeta(userCredentials) - ) = RoomEvent.Image( - eventId = this.id, - imageMeta = imageMeta, - author = roomMembersService.find(roomId, this.senderId) ?: UNKNOWN_AUTHOR, - utcTimestamp = utcTimestamp, - meta = MessageMeta.FromServer, - edited = edited, - ) - - private fun ApiTimelineEvent.TimelineMessage.readImageMeta(userCredentials: UserCredentials): RoomEvent.Image.ImageMeta { - val content = this.content as ApiTimelineEvent.TimelineMessage.Content.Image - return RoomEvent.Image.ImageMeta( - content.info?.width, - content.info?.height, - content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer), - keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) } - ) - } -} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt deleted file mode 100644 index 0218d81..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt +++ /dev/null @@ -1,92 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.LastMessage -import app.dapk.st.matrix.sync.RoomMembersService -import app.dapk.st.matrix.sync.RoomOverview -import app.dapk.st.matrix.sync.find -import app.dapk.st.matrix.sync.internal.request.ApiAccountEvent -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent - -internal class RoomOverviewProcessor( - private val roomMembersService: RoomMembersService, -) { - - suspend fun process(roomToProcess: RoomToProcess, previousState: RoomOverview?, lastMessage: LastMessage?): RoomOverview? { - val combinedEvents = (roomToProcess.apiSyncRoom.state?.stateEvents.orEmpty()) + roomToProcess.apiSyncRoom.timeline.apiTimelineEvents - val isEncrypted = combinedEvents.any { it is ApiTimelineEvent.Encryption } - val readMarker = roomToProcess.apiSyncRoom.accountData?.events?.filterIsInstance()?.firstOrNull()?.content?.eventId - return when (previousState) { - null -> combinedEvents.filterIsInstance().first().let { roomCreate -> - when (roomCreate.content.type) { - ApiTimelineEvent.RoomCreate.Content.Type.SPACE -> null - else -> { - val roomName = roomDisplayName(roomToProcess, combinedEvents) - val isGroup = roomToProcess.directMessage == null - val processedName = roomName ?: roomToProcess.directMessage?.let { - roomMembersService.find(roomToProcess.roomId, it)?.let { it.displayName ?: it.id.value } - } - RoomOverview( - roomName = processedName, - roomCreationUtc = roomCreate.utcTimestamp, - lastMessage = lastMessage, - roomId = roomToProcess.roomId, - isGroup = isGroup, - roomAvatarUrl = roomAvatar( - roomToProcess.roomId, - roomMembersService, - roomToProcess.directMessage, - combinedEvents, - roomToProcess.userCredentials.homeServer - ), - readMarker = readMarker, - isEncrypted = isEncrypted, - ) - } - } - } - - else -> { - previousState.copy( - roomName = previousState.roomName ?: roomDisplayName(roomToProcess, combinedEvents), - lastMessage = lastMessage ?: previousState.lastMessage, - roomAvatarUrl = previousState.roomAvatarUrl ?: roomAvatar( - roomToProcess.roomId, - roomMembersService, - roomToProcess.directMessage, - combinedEvents, - roomToProcess.userCredentials.homeServer, - ), - readMarker = readMarker ?: previousState.readMarker, - isEncrypted = isEncrypted || previousState.isEncrypted - ) - } - } - } - - private suspend fun roomDisplayName(roomToProcess: RoomToProcess, combinedEvents: List): String? { - val roomName = combinedEvents.filterIsInstance().lastOrNull()?.content?.name - ?: combinedEvents.filterIsInstance().lastOrNull()?.content?.alias?.takeIf { it.isNotEmpty() } - ?: roomToProcess.heroes?.let { - roomMembersService.find(roomToProcess.roomId, it).joinToString { it.displayName ?: it.id.value } - } - return roomName?.takeIf { it.isNotEmpty() } - } - - private suspend fun roomAvatar( - roomId: RoomId, - membersService: RoomMembersService, - dmUser: UserId?, - combinedEvents: List, - homeServerUrl: HomeServerUrl - ): AvatarUrl? { - return when (dmUser) { - null -> { - val filterIsInstance = combinedEvents.filterIsInstance() - filterIsInstance.lastOrNull()?.content?.url?.convertMxUrToUrl(homeServerUrl)?.let { AvatarUrl(it) } - } - - else -> membersService.find(roomId, dmUser)?.avatarUrl - } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt deleted file mode 100644 index 5f1d3f7..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt +++ /dev/null @@ -1,83 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.* -import app.dapk.st.matrix.sync.internal.request.ApiSyncRoom -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent - -internal class RoomProcessor( - private val roomMembersService: RoomMembersService, - private val roomDataSource: RoomDataSource, - private val timelineEventsProcessor: TimelineEventsProcessor, - private val roomOverviewProcessor: RoomOverviewProcessor, - private val unreadEventsProcessor: UnreadEventsProcessor, - private val ephemeralEventsUseCase: EphemeralEventsUseCase, -) { - - suspend fun processRoom(roomToProcess: RoomToProcess, isInitialSync: Boolean): RoomState? { - val members = roomToProcess.apiSyncRoom.collectMembers(roomToProcess.userCredentials) - roomMembersService.insert(roomToProcess.roomId, members) - - roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.filterIsInstance().forEach { - roomDataSource.redact(roomToProcess.roomId, it.redactedId) - } - - val previousState = roomDataSource.read(roomToProcess.roomId) - - val (newEvents, distinctEvents) = timelineEventsProcessor.process( - roomToProcess, - previousState?.events ?: emptyList(), - ) - - return createRoomOverview(distinctEvents, roomToProcess, previousState)?.let { - unreadEventsProcessor.processUnreadState(it, previousState?.roomOverview, newEvents, roomToProcess.userCredentials.userId, isInitialSync) - - RoomState(it, distinctEvents).also { - roomDataSource.persist(roomToProcess.roomId, previousState, it) - ephemeralEventsUseCase.processEvents(roomToProcess) - } - } - } - - private suspend fun createRoomOverview(distinctEvents: List, roomToProcess: RoomToProcess, previousState: RoomState?): RoomOverview? { - val lastMessage = distinctEvents.sortedByDescending { it.utcTimestamp }.findLastMessage() - return roomOverviewProcessor.process(roomToProcess, previousState?.roomOverview, lastMessage) - } - -} - -private fun ApiSyncRoom.collectMembers(userCredentials: UserCredentials): List { - return (this.state?.stateEvents.orEmpty() + this.timeline.apiTimelineEvents) - .filterIsInstance() - .mapNotNull { - when { - it.content.membership.isJoin() -> { - RoomMember( - displayName = it.content.displayName, - id = it.senderId, - avatarUrl = it.content.avatarUrl?.convertMxUrToUrl(userCredentials.homeServer)?.let { AvatarUrl(it) }, - ) - } - - else -> null - } - } -} - -internal fun List.findLastMessage(): LastMessage? { - return this.firstOrNull()?.let { - LastMessage( - content = it.toTextContent(), - utcTimestamp = it.utcTimestamp, - author = it.author, - ) - } -} - -private fun RoomEvent.toTextContent(): String = when (this) { - is RoomEvent.Image -> "\uD83D\uDCF7" - is RoomEvent.Message -> this.content.asString() - is RoomEvent.Reply -> this.message.toTextContent() - is RoomEvent.Encrypted -> "Encrypted message" - is RoomEvent.Redacted -> "Message deleted" -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresher.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresher.kt deleted file mode 100644 index 6be702a..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresher.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomState -import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter - -internal class RoomRefresher( - private val roomDataSource: RoomDataSource, - private val roomEventsDecrypter: RoomEventsDecrypter, - private val logger: MatrixLogger -) { - - suspend fun refreshRoomContent(roomId: RoomId, userCredentials: UserCredentials): RoomState? { - logger.matrixLog(MatrixLogTag.SYNC, "reducing side effect: $roomId") - return when (val previousState = roomDataSource.read(roomId)) { - null -> null.also { logger.matrixLog(MatrixLogTag.SYNC, "no previous state to update") } - else -> { - logger.matrixLog(MatrixLogTag.SYNC, "previous state updated") - val decryptedEvents = previousState.events.decryptEvents(userCredentials) - val lastMessage = decryptedEvents.sortedByDescending { it.utcTimestamp }.findLastMessage() - - previousState.copy(events = decryptedEvents, roomOverview = previousState.roomOverview.copy(lastMessage = lastMessage)).also { - roomDataSource.persist(roomId, previousState, it) - } - } - } - } - - private suspend fun List.decryptEvents(userCredentials: UserCredentials) = roomEventsDecrypter.decryptRoomEvents(userCredentials, this) - -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomToProcess.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomToProcess.kt deleted file mode 100644 index 9c4d7c9..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomToProcess.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.sync.internal.request.ApiSyncRoom - -internal data class RoomToProcess( - val roomId: RoomId, - val apiSyncRoom: ApiSyncRoom, - val directMessage: UserId?, - val userCredentials: UserCredentials, - val heroes: List?, -) \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt deleted file mode 100644 index a606895..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt +++ /dev/null @@ -1,107 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.core.withIoContextAsync -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.common.MatrixLogTag.SYNC -import app.dapk.st.matrix.sync.InviteMeta -import app.dapk.st.matrix.sync.RoomInvite -import app.dapk.st.matrix.sync.RoomState -import app.dapk.st.matrix.sync.internal.request.* -import app.dapk.st.matrix.sync.internal.room.SideEffectResult -import kotlinx.coroutines.awaitAll - -internal class SyncReducer( - private val roomProcessor: RoomProcessor, - private val roomRefresher: RoomRefresher, - private val roomDataSource: RoomDataSource, - private val logger: MatrixLogger, - private val errorTracker: ErrorTracker, - private val coroutineDispatchers: CoroutineDispatchers, -) { - - data class ReducerResult( - val newRoomsJoined: List, - val roomState: List, - val invites: List, - val roomsLeft: List - ) - - suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult { - val directMessages = response.directMessages() - val invites = response.rooms?.invite?.map { roomInvite(it, userCredentials) } ?: emptyList() - val roomsLeft = findRoomsLeft(response, userCredentials) - val newRooms = response.rooms?.join?.keys?.filterNot { roomDataSource.contains(it) } ?: emptyList() - - val apiUpdatedRooms = response.rooms?.join?.keepRoomsWithChanges() - val apiRoomsToProcess = apiUpdatedRooms?.mapNotNull { (roomId, apiRoom) -> - logger.matrixLog(SYNC, "reducing: $roomId") - coroutineDispatchers.withIoContextAsync { - runCatching { - roomProcessor.processRoom( - roomToProcess = RoomToProcess( - roomId = roomId, - apiSyncRoom = apiRoom, - directMessage = directMessages[roomId], - userCredentials = userCredentials, - heroes = apiRoom.summary?.heroes, - ), - isInitialSync = isInitialSync - ) - } - .onFailure { errorTracker.track(it, "failed to reduce: $roomId, skipping") } - .getOrNull() - } - } ?: emptyList() - - val roomsWithSideEffects = sideEffects.roomsToRefresh(alreadyHandledRooms = apiUpdatedRooms?.keys ?: emptySet()).map { roomId -> - coroutineDispatchers.withIoContextAsync { - roomRefresher.refreshRoomContent(roomId, userCredentials) - } - } - - return ReducerResult( - newRooms, - (apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(), - invites, - roomsLeft - ) - } - - private fun findRoomsLeft(response: ApiSyncResponse, userCredentials: UserCredentials) = response.rooms?.leave?.filter { - it.value.state?.stateEvents.orEmpty().filterIsInstance().any { - it.content.membership.isLeave() && it.senderId == userCredentials.userId - } - }?.map { it.key } ?: emptyList() - - private fun roomInvite(entry: Map.Entry, userCredentials: UserCredentials): RoomInvite { - val memberEvents = entry.value.state.events.filterIsInstance() - val invitee = memberEvents.first { it.content.membership?.isInvite() ?: false } - val from = memberEvents.first { it.sender == invitee.sender } - return RoomInvite( - RoomMember(from.sender, from.content.displayName, from.content.avatarUrl?.convertMxUrToUrl(userCredentials.homeServer)?.let { AvatarUrl(it) }), - roomId = entry.key, - inviteMeta = when (invitee.content.isDirect) { - true -> InviteMeta.DirectMessage - null, false -> InviteMeta.Room(entry.value.state.events.filterIsInstance().firstOrNull()?.content?.name) - }, - ) - } -} - -private fun Map.keepRoomsWithChanges() = this.filter { - it.value.state?.stateEvents.orEmpty().isNotEmpty() || - it.value.timeline.apiTimelineEvents.isNotEmpty() || - it.value.accountData?.events?.isNotEmpty() == true || - it.value.ephemeral?.events?.isNotEmpty() == true -} - -private fun SideEffectResult.roomsToRefresh(alreadyHandledRooms: Set) = this.roomsWithNewKeys.filterNot { alreadyHandledRooms.contains(it) } - -private fun ApiSyncResponse.directMessages() = this.accountData?.events?.filterIsInstance()?.firstOrNull()?.let { - it.content.entries.fold(mutableMapOf()) { acc, current -> - current.value.forEach { roomId -> acc[roomId] = current.key } - acc - } -} ?: emptyMap() diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt deleted file mode 100644 index 42a3b69..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt +++ /dev/null @@ -1,87 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.common.MatrixLogTag.SYNC -import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.sync.* -import app.dapk.st.matrix.sync.internal.SideEffectFlowIterator -import app.dapk.st.matrix.sync.internal.overview.ReducedSyncFilterUseCase -import app.dapk.st.matrix.sync.internal.request.syncRequest -import app.dapk.st.matrix.sync.internal.room.SyncSideEffects -import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.flow - -internal class SyncUseCase( - private val persistence: OverviewStore, - private val flowIterator: SideEffectFlowIterator, - private val syncSideEffects: SyncSideEffects, - private val client: MatrixHttpClient, - private val syncStore: SyncStore, - private val syncReducer: SyncReducer, - private val credentialsStore: CredentialsStore, - private val logger: MatrixLogger, - private val filterUseCase: ReducedSyncFilterUseCase, - private val syncConfig: SyncConfig, -) { - - private val _flow = flow { - val credentials = credentialsStore.credentials()!! - val filterId = filterUseCase.reducedFilter(credentials.userId) - with(flowIterator) { - loop( - initial = null, - onPost = { emit(Unit) }, - onIteration = { onEachSyncIteration(filterId, credentials, previousState = it) } - ) - } - }.cancellable() - - private suspend fun onEachSyncIteration(filterId: SyncService.FilterId, credentials: UserCredentials, previousState: OverviewState?): OverviewState? { - val syncToken = syncStore.read(key = SyncStore.SyncKey.Overview) - val response = doSyncRequest(filterId, syncToken) - return if (credentialsStore.isSignedIn()) { - logger.logP("sync processing") { - syncStore.store(key = SyncStore.SyncKey.Overview, syncToken = response.nextBatch) - val sideEffects = logger.logP("side effects processing") { - syncSideEffects.blockingSideEffects(credentials.userId, response, syncToken) - } - - val isInitialSync = syncToken == null - val nextState = logger.logP("reducing") { syncReducer.reduce(isInitialSync, sideEffects, response, credentials) } - val overview = nextState.roomState.map { it.roomOverview } - - if (nextState.roomsLeft.isNotEmpty()) { - persistence.removeRooms(nextState.roomsLeft) - } - if (nextState.invites.isNotEmpty()) { - persistence.persistInvites(nextState.invites) - } - if (nextState.newRoomsJoined.isNotEmpty()) { - persistence.removeInvites(nextState.newRoomsJoined) - } - - when { - previousState == overview -> previousState.also { logger.matrixLog(SYNC, "no changes, not persisting new state") } - overview.isNotEmpty() -> overview.also { persistence.persist(overview) } - else -> previousState.also { logger.matrixLog(SYNC, "nothing to do") } - } - } - } else { - logger.matrixLog(SYNC, "sync processing skipped due to being signed out") - null - } - } - - private suspend fun doSyncRequest(filterId: SyncService.FilterId, syncToken: SyncToken?) = logger.logP("sync api") { - client.execute( - syncRequest( - lastSyncToken = syncToken, - filterId = filterId, - timeoutMs = syncConfig.loopTimeout, - ) - ) - } - - fun sync() = _flow - -} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessor.kt deleted file mode 100644 index 419bd9c..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessor.kt +++ /dev/null @@ -1,61 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent -import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter -import app.dapk.st.matrix.sync.internal.room.SyncEventDecrypter - -private typealias NewEvents = List -private typealias AllDistinctEvents = List - -internal class TimelineEventsProcessor( - private val roomEventCreator: RoomEventCreator, - private val roomEventsDecrypter: RoomEventsDecrypter, - private val eventDecrypter: SyncEventDecrypter, - private val eventLookupUseCase: EventLookupUseCase -) { - - suspend fun process(roomToProcess: RoomToProcess, previousEvents: List): Pair { - val newEvents = processNewEvents(roomToProcess, previousEvents) - return newEvents to (newEvents + previousEvents).distinctBy { it.eventId } - } - - private suspend fun processNewEvents(roomToProcess: RoomToProcess, previousEvents: List): List { - val decryptedTimeline = roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.decryptEvents() - val decryptedPreviousEvents = previousEvents.decryptEvents(roomToProcess.userCredentials) - - val newEvents = with(roomEventCreator) { - decryptedTimeline.value.mapNotNull { event -> - val roomEvent = when (event) { - is ApiTimelineEvent.Encrypted -> event.toRoomEvent(roomToProcess.roomId) - is ApiTimelineEvent.TimelineMessage -> event.toRoomEvent(roomToProcess.userCredentials, roomToProcess.roomId) { eventId -> - eventLookupUseCase.lookup(eventId, decryptedTimeline, decryptedPreviousEvents) - } - is ApiTimelineEvent.RoomRedcation -> null - is ApiTimelineEvent.Encryption -> null - is ApiTimelineEvent.RoomAvatar -> null - is ApiTimelineEvent.RoomCreate -> null - is ApiTimelineEvent.RoomMember -> null - is ApiTimelineEvent.RoomName -> null - is ApiTimelineEvent.RoomTopic -> null - is ApiTimelineEvent.CanonicalAlias -> null - ApiTimelineEvent.Ignored -> null - } - roomEvent - } - } - return newEvents - } - - private suspend fun List.decryptEvents() = DecryptedTimeline(eventDecrypter.decryptTimelineEvents(this)) - private suspend fun List.decryptEvents(userCredentials: UserCredentials) = - DecryptedRoomEvents(roomEventsDecrypter.decryptRoomEvents(userCredentials, this)) - -} - -@JvmInline -internal value class DecryptedTimeline(val value: List) - -@JvmInline -internal value class DecryptedRoomEvents(val value: List) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessor.kt deleted file mode 100644 index 6c5a0c6..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessor.kt +++ /dev/null @@ -1,58 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.MatrixLogTag -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.common.matrixLog -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview -import app.dapk.st.matrix.sync.RoomStore - -internal class UnreadEventsProcessor( - private val roomStore: RoomStore, - private val logger: MatrixLogger, -) { - - suspend fun processUnreadState( - overview: RoomOverview, - previousState: RoomOverview?, - newEvents: List, - selfId: UserId, - isInitialSync: Boolean, - ) { - val areWeViewingRoom = false // TODO - - when { - isInitialSync -> { - // let's assume everything is read - } - - previousState?.readMarker != overview.readMarker -> { - // assume the user has viewed the room - logger.matrixLog(MatrixLogTag.SYNC, "marking room read due to new read marker") - roomStore.markRead(overview.roomId) - } - - areWeViewingRoom -> { - logger.matrixLog(MatrixLogTag.SYNC, "marking room read") - roomStore.markRead(overview.roomId) - } - - newEvents.isNotEmpty() -> { - logger.matrixLog(MatrixLogTag.SYNC, "insert new unread events") - - val eventsFromOthers = newEvents.filterNot { - when (it) { - is RoomEvent.Message -> it.author.id == selfId - is RoomEvent.Reply -> it.message.author.id == selfId - is RoomEvent.Image -> it.author.id == selfId - is RoomEvent.Encrypted -> it.author.id == selfId - is RoomEvent.Redacted -> it.author.id == selfId - } - }.map { it.eventId } - roomStore.insertUnread(overview.roomId, eventsFromOthers) - } - } - } - -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/AccumulatingRichTextContentParser.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/AccumulatingRichTextContentParser.kt deleted file mode 100644 index 17afd8f..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/AccumulatingRichTextContentParser.kt +++ /dev/null @@ -1,85 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync.message - -import app.dapk.st.matrix.sync.internal.sync.message.html.HtmlProcessor -import app.dapk.st.matrix.sync.internal.sync.message.url.UrlParser - -private const val MAX_NESTING_LIMIT = 20 - -class AccumulatingRichTextContentParser : AccumulatingContentParser { - - private val urlParser = UrlParser() - private val tagProcessor = HtmlProcessor() - - override fun parse(input: String, accumulator: ContentAccumulator, nestingLevel: Int): ContentAccumulator { - if (nestingLevel >= MAX_NESTING_LIMIT) { - accumulator.appendText(input) - } else { - iterate { index -> - process( - input, - index, - processTag = { - prependTextBeforeCapture(input, index, it, accumulator) - tagProcessor.process(input, it, accumulator, nestingLevel, nestedParser = this) - }, - processUrl = { - prependTextBeforeCapture(input, index, it, accumulator) - urlParser.parseUrl(input, it, accumulator) - } - ).also { - if (it == -1) { - appendRemainingText(index, input, accumulator) - } - } - } - } - return accumulator - } - - private inline fun iterate(action: (Int) -> Int) { - var result = 0 - while (result != -1) { - result = action(result) - } - } - - private fun process(input: String, searchIndex: Int, processTag: (Int) -> Int, processUrl: (Int) -> Int): Int { - val tagOpen = input.indexOf('<', startIndex = searchIndex) - val httpOpen = input.indexOf("http", startIndex = searchIndex) - return selectProcessor( - tagOpen, - httpOpen, - processTag = { processTag(tagOpen) }, - processUrl = { processUrl(httpOpen) } - ) - } - - private inline fun selectProcessor(tagOpen: Int, httpOpen: Int, processTag: () -> Int, processUrl: () -> Int) = when { - tagOpen == -1 && httpOpen == -1 -> -1 - tagOpen != -1 && httpOpen == -1 -> processTag() - tagOpen == -1 && httpOpen != -1 -> processUrl() - tagOpen == httpOpen -> { - // favour tags as urls can existing within tags - processTag() - } - - else -> { - when (tagOpen < httpOpen) { - true -> processTag() - false -> processUrl() - } - } - } - - private fun prependTextBeforeCapture(input: String, index: Int, captureIndex: Int, accumulator: ContentAccumulator) { - if (index < captureIndex) { - accumulator.appendText(input.substring(index, captureIndex)) - } - } - - private fun appendRemainingText(index: Int, input: String, accumulator: ContentAccumulator) { - if (index < input.length) { - accumulator.appendText(input.substring(index, input.length)) - } - } -} diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/RichTextMessageParser.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/RichTextMessageParser.kt deleted file mode 100644 index 45719e3..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/RichTextMessageParser.kt +++ /dev/null @@ -1,34 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync.message - -import app.dapk.st.matrix.common.RichText - -fun interface NestedParser { - fun parse(content: String, accumulator: ContentAccumulator) -} - -fun interface TagParser { - fun parse(tagName: String, attributes: Map, content: String, accumulator: ContentAccumulator, parser: NestedParser) -} - -fun interface AccumulatingContentParser { - fun parse(input: String, accumulator: ContentAccumulator, nestingLevel: Int): ContentAccumulator -} - -class RichMessageParser( - private val accumulatingParser: AccumulatingContentParser = AccumulatingRichTextContentParser() -) { - - fun parse(source: String): RichText { - val input = source - .removeHtmlEntities() - .dropTextFallback() - return RichText(accumulatingParser.parse(input, RichTextPartBuilder(), nestingLevel = 0).build()) - } - -} - -private fun String.removeHtmlEntities() = this.replace(""", "\"").replace("'", "'").replace("'", "'").replace("&", "&") - -private fun String.dropTextFallback() = this.lines() - .dropWhile { it.startsWith("> ") || it.isEmpty() } - .joinToString(separator = "\n") diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/RichTextPartBuilder.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/RichTextPartBuilder.kt deleted file mode 100644 index c7499e5..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/RichTextPartBuilder.kt +++ /dev/null @@ -1,76 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync.message - -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.common.UserId - -interface ContentAccumulator { - fun appendText(value: String) - fun appendItalic(value: String) - fun appendBold(value: String) - fun appendPerson(userId: UserId, displayName: String) - fun appendLink(url: String, label: String?) - fun build(): List -} - -class RichTextPartBuilder : ContentAccumulator { - - private var normalBuffer = StringBuilder() - - private val parts = mutableListOf() - - override fun appendText(value: String) { - normalBuffer.append(value.cleanFirstTextLine()) - } - - override fun appendItalic(value: String) { - flushNormalBuffer() - parts.add(RichText.Part.Italic(value.cleanFirstTextLine())) - } - - override fun appendBold(value: String) { - flushNormalBuffer() - parts.add(RichText.Part.Bold(value.cleanFirstTextLine())) - } - - private fun String.cleanFirstTextLine() = if (parts.isEmpty() && normalBuffer.isEmpty()) this.trimStart() else this - - override fun appendPerson(userId: UserId, displayName: String) { - flushNormalBuffer() - parts.add(RichText.Part.Person(userId, displayName)) - } - - override fun appendLink(url: String, label: String?) { - flushNormalBuffer() - parts.add(RichText.Part.Link(url, label ?: url)) - } - - override fun build(): List { - flushNormalBuffer() - return when (parts.isEmpty()) { - true -> parts - else -> { - val last = parts.last() - if (last is RichText.Part.Normal) { - parts.removeLast() - val newContent = last.content.trimEnd() - if (newContent.isNotEmpty()) { - parts.add(last.copy(content = newContent)) - } - } - parts - } - } - } - - private fun flushNormalBuffer() { - if (normalBuffer.isNotEmpty()) { - parts.add(RichText.Part.Normal(normalBuffer.toString())) - normalBuffer.clear() - } - } -} - -internal fun ContentAccumulator.appendNewline() { - this.appendText("\n") -} - diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/HtmlProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/HtmlProcessor.kt deleted file mode 100644 index 98856d1..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/HtmlProcessor.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync.message.html - -import app.dapk.st.matrix.sync.internal.sync.message.AccumulatingContentParser -import app.dapk.st.matrix.sync.internal.sync.message.ContentAccumulator - -class HtmlProcessor { - - private val tagCaptor = TagCaptor() - private val htmlTagParser = RichTextHtmlTagParser() - - fun process(input: String, tagOpen: Int, partBuilder: ContentAccumulator, nestingLevel: Int, nestedParser: AccumulatingContentParser): Int { - val afterTagCaptureIndex = tagCaptor.tagCapture(input, tagOpen) { tagName, attributes, tagContent -> - htmlTagParser.parse(tagName, attributes, tagContent, partBuilder) { nestedContent, accumulator -> - nestedParser.parse(nestedContent, accumulator, nestingLevel + 1) - } - } - return when (afterTagCaptureIndex) { - -1 -> { - partBuilder.appendText(input[tagOpen].toString()) - tagOpen + 1 - } - - else -> afterTagCaptureIndex - } - } - -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/ListAccumulator.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/ListAccumulator.kt deleted file mode 100644 index 4d6dbc1..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/ListAccumulator.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync.message.html - -import app.dapk.st.matrix.sync.internal.sync.message.ContentAccumulator - -internal interface ListAccumulator { - fun appendLinePrefix(index: Int?) -} - -internal class OrderedListAccumulator(delegate: ContentAccumulator) : ContentAccumulator by delegate, ListAccumulator { - - private var currentIndex = 1 - - override fun appendLinePrefix(index: Int?) { - currentIndex = index ?: currentIndex - appendText("$currentIndex. ") - currentIndex++ - } -} - -internal class UnorderedListAccumulator(delegate: ContentAccumulator) : ContentAccumulator by delegate, ListAccumulator { - override fun appendLinePrefix(index: Int?) = appendText("- ") -} - diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/RichTextHtmlTagParser.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/RichTextHtmlTagParser.kt deleted file mode 100644 index a3e3366..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/RichTextHtmlTagParser.kt +++ /dev/null @@ -1,95 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync.message.html - -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.sync.internal.sync.message.* - -class RichTextHtmlTagParser : TagParser { - - override fun parse( - tagName: String, - attributes: Map, - content: String, - accumulator: ContentAccumulator, - parser: NestedParser - ) { - when { - tagName.startsWith('@') -> { - accumulator.appendPerson(UserId(tagName), tagName) - } - - else -> when (tagName) { - "br" -> { - accumulator.appendNewline() - } - - "a" -> { - attributes["href"]?.let { url -> - when { - url.startsWith("https://matrix.to/#/@") -> { - val userId = UserId(url.substringAfter("https://matrix.to/#/").substringBeforeLast("\"")) - accumulator.appendPerson(userId, "@${content.removePrefix("@")}") - } - - else -> accumulator.appendLink(url, content) - - } - } ?: accumulator.appendText(content) - } - - "p" -> { - parser.parse(content.trim(), accumulator) - accumulator.appendNewline() - } - - "blockquote" -> { - accumulator.appendText("> ") - parser.parse(content.trim(), accumulator) - } - - "strong", "b" -> { - accumulator.appendBold(content) - } - - "em", "i" -> { - accumulator.appendItalic(content) - } - - "h1", "h2", "h3", "h4", "h5" -> { - accumulator.appendBold(content) - accumulator.appendNewline() - } - - "ul", "ol" -> { - when (tagName) { - "ol" -> parser.parse(content, OrderedListAccumulator(accumulator)) - "ul" -> parser.parse(content, UnorderedListAccumulator(accumulator)) - } - } - - "li" -> { - (accumulator as ListAccumulator).appendLinePrefix(attributes["value"]?.toInt()) - - val nestedList = when { - content.contains("