diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 6541566..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: 2 -updates: -- package-ecosystem: gradle - directory: / - schedule: - interval: daily - open-pull-requests-limit: 3 diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml index 26935f8..cfa74bc 100644 --- a/.github/workflows/assemble.yml +++ b/.github/workflows/assemble.yml @@ -16,7 +16,10 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + 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..59b8483 100644 --- a/.github/workflows/check_size.yml +++ b/.github/workflows/check_size.yml @@ -13,8 +13,11 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - uses: actions/setup-java@v3 with: distribution: 'adopt' java-version: '11' diff --git a/.github/workflows/comment_size.yml b/.github/workflows/comment_size.yml index 288b98c..db1dbc1 100644 --- a/.github/workflows/comment_size.yml +++ b/.github/workflows/comment_size.yml @@ -28,14 +28,14 @@ jobs: id: size - name: Find Comment - uses: peter-evans/find-comment@v1 + uses: peter-evans/find-comment@v2 id: fc with: issue-number: ${{ steps.size.outputs.PR_NUMBER }} comment-author: 'github-actions[bot]' body-includes: APK Size - name: Publish size to PR - uses: peter-evans/create-or-update-comment@v1 + uses: peter-evans/create-or-update-comment@v2 with: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ steps.size.outputs.PR_NUMBER }} diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index c07920d..e71be9a 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -15,8 +15,11 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - uses: actions/setup-java@v3 with: distribution: 'adopt' java-version: '11' diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml index f9f2a85..b959ba8 100644 --- a/.github/workflows/release-train.yml +++ b/.github/workflows/release-train.yml @@ -15,7 +15,9 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + submodules: 'recursive' - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd29ec1..dc50de0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,32 +16,19 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + - uses: actions/setup-java@v3 with: distribution: 'adopt' java-version: '11' - uses: gradle/gradle-build-action@v2 - - name: Create pip requirements - run: | - echo "matrix-synapse" > requirements.txt - - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - cache: 'pip' - - - name: Start synapse server - run: | - pip install -r requirements.txt - curl -sL https://gist.githubusercontent.com/ouchadam/e3ad09ec382bd91a66d88ab575ea7c31/raw/run.sh \ - | bash -s -- --no-rate-limit - - name: Run all unit tests - run: ./gradlew clean allCodeCoverageReport --no-daemon + run: ./gradlew allCodeCoverageReport - - uses: codecov/codecov-action@v2 + - uses: codecov/codecov-action@v3 with: - files: ./build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml + verbose: true + files: ./build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml \ No newline at end of file diff --git a/.github/workflows/update-gradle-wrapper.yml b/.github/workflows/update-gradle-wrapper.yml deleted file mode 100644 index 50143be..0000000 --- a/.github/workflows/update-gradle-wrapper.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Update Gradle Wrapper - -on: - schedule: - - cron: "0 0 * * *" - -jobs: - update-gradle-wrapper: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Update Gradle Wrapper - uses: gradle-update/update-gradle-wrapper-action@v1 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5e8cc8f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "screen-state"] + path = screen-state + url = git@github.com:ouchadam/screen-state.git +[submodule "chat-engine"] + path = chat-engine + url = git@github.com:ouchadam/chat-engine.git +[submodule "tools/conventions"] + path = tools/conventions + url = git@github.com:ouchadam/conventions.git diff --git a/README.md b/README.md index d45604f..659e735 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 @@ -118,4 +118,25 @@ --- + +#### Repositories + +`SmallTalk` is split into multiple repositories through git submodules. + +##### [small-talk](https://github.com/ouchadam/small-talk) +- The main repository, responsibile for rendering data into _screens_ and generating the android application artifact. + +##### [chat-engine](https://github.com/ouchadam/chat-engine) +- All things chat and matrix, where the vast majority of business logic sits. +- Pure kotlin with no UI. + +##### [conventions](https://github.com/ouchadam/conventions) +- Reusable gradle plugins for modular android projects + +##### [screen-state](https://github.com/ouchadam/screen-state) +- Reusable state management and generic screen flow components. +- Wrapper around android's `ViewModel` focused on testability. + +--- + #### Join the conversation @ https://matrix.to/#/#small-talk:iswell.cool diff --git a/app/build.gradle b/app/build.gradle index 0b091ca..b68fb93 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,76 +1,27 @@ plugins { - id 'com.android.application' - id 'kotlin-android' + id "st-application-conventions" + alias libs.plugins.firebase.crashlytics apply false } -applyCommonAndroidParameters(project) -applyCrashlyticsIfRelease(project) +applyCrashlyticsIfRelease() android { - ndkVersion "25.0.8141415" + namespace "app.dapk.st" defaultConfig { applicationId "app.dapk.st" - def versionJson = new groovy.json.JsonSlurper().parseText(rootProject.file('version.json').text) - versionCode versionJson.code - versionName versionJson.name - - if (isDebugBuild) { - resConfigs "en", "xxhdpi" - } else { - resConfigs "en" - } - - if (isFoss()) { - archivesBaseName = "$archivesBaseName-foss" - } } - - bundle { - abi.enableSplit true - density.enableSplit true - language.enableSplit true - } - buildTypes { - debug { - versionNameSuffix = " [debug]" - matchingFallbacks = ['release'] - signingConfig.storeFile rootProject.file("tools/debug.keystore") - } release { - minifyEnabled true - shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard/app.pro', "proguard/serializationx.pro", "proguard/olm.pro" - - if (project.hasProperty("unsigned")) { - // releases are signed externally - } else { - signingConfig = buildTypes.debug.signingConfig - } } } - - compileOptions { - coreLibraryDesugaringEnabled true - } - - packagingOptions { - resources.excludes += "DebugProbesKt.bin" - } -} - -if (isDebugBuild) { - androidComponents { - def release = selector().withBuildType("release") - beforeVariants(release) { it.enabled = false } - } } dependencies { - coreLibraryDesugaring Dependencies.google.jdkLibs + coreLibraryDesugaring libs.android.desugar implementation project(":features:home") implementation project(":features:directory") @@ -82,38 +33,37 @@ 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 - implementation Dependencies.mavenCentral.sqldelightAndroid - implementation Dependencies.mavenCentral.matrixOlm + implementation libs.ktor.android + implementation libs.sqldelight.android + implementation libs.matrix.olm - implementation Dependencies.mavenCentral.kotlinSerializationJson - debugImplementation Dependencies.mavenCentral.leakCanary + implementation libs.kotlin.serialization + debugImplementation libs.leakcanary +} + +def applyCrashlyticsIfRelease() { + if (isReleaseBuild && !isFoss()) { + project.apply plugin: libs.plugins.firebase.crashlytics.get().pluginId + project.afterEvaluate { + project.tasks.withType(com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask).configureEach { + it.googleServicesResourceRoot.set(project.file("src/release/res/")) + } + } + } } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index b2bbec0..e467a56 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,6 +1,5 @@ - + - + diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index 90a3a50..3310b37 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,23 +54,20 @@ 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) } } - @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY") + @Suppress("UNCHECKED_CAST") override fun provide(klass: KClass): T { return when (klass) { DirectoryModule::class -> featureModules.directoryModule @@ -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..9a059c1 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, @@ -173,6 +190,9 @@ internal class FeatureModules internal constructor( storeModule.value.applicationStore(), buildMeta, ), + profileModule, + loginModule, + directoryModule ) } val settingsModule by unsafeLazy { @@ -220,7 +240,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 +251,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 +305,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/build.gradle b/build.gradle index 997f289..5b6c4ca 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,5 @@ -buildscript { - apply from: "dependencies.gradle" - - repositories { - Dependencies._repositories.call(it) - } - dependencies { - classpath Dependencies.google.androidGradlePlugin - classpath Dependencies.mavenCentral.kotlinGradlePlugin - classpath Dependencies.mavenCentral.sqldelightGradlePlugin - classpath Dependencies.mavenCentral.kotlinSerializationGradlePlugin - classpath Dependencies.google.firebaseCrashlyticsPlugin - } +plugins { + id "st-base-conventions" apply false } def launchTask = getGradle() @@ -18,7 +7,7 @@ def launchTask = getGradle() .getTaskRequests() .toString() .toLowerCase() -def isReleaseBuild = launchTask.contains("release") +ext.isReleaseBuild = launchTask.contains("bundlerelease") || launchTask.contains("assemblerelease") ext.isDebugBuild = !isReleaseBuild subprojects { @@ -37,112 +26,17 @@ task clean(type: Delete) { delete rootProject.buildDir } -ext.applyMatrixServiceModule = { project -> - project.apply plugin: 'kotlin' - project.apply plugin: 'org.jetbrains.kotlin.plugin.serialization' - - def dependencies = project.dependencies - - dependencies.api project.project(":matrix:matrix") - dependencies.api project.project(":matrix:common") - dependencies.implementation project.project(":matrix:matrix-http") - dependencies.implementation Dependencies.mavenCentral.kotlinSerializationJson -} - -ext.applyLibraryPlugins = { project -> - project.apply plugin: 'com.android.library' - project.apply plugin: 'kotlin-android' -} - -ext.androidSdkVersion = 33 - -ext.applyCommonAndroidParameters = { project -> - def android = project.android - android.compileSdk androidSdkVersion - android.compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - incremental = true - } - android.defaultConfig { - minSdkVersion 24 - targetSdkVersion androidSdkVersion - } -} - -ext.applyLibraryModuleOptimisations = { project -> - project.android { - variantFilter { variant -> - if (variant.name == "debug") { - variant.ignore = true - } - } - - buildFeatures { - buildConfig = false - dataBinding = false - aidl = false - renderScript = false - resValues = false - shaders = false - viewBinding = false - } - } -} - -ext.applyCompose = { project -> - def dependencies = project.dependencies - - dependencies.implementation Dependencies.google.androidxComposeUi - dependencies.implementation Dependencies.google.androidxComposeFoundation - dependencies.implementation Dependencies.google.androidxComposeMaterial - dependencies.implementation Dependencies.google.androidxComposeIconsExtended - dependencies.implementation Dependencies.google.androidxActivityCompose - - def android = project.android - android.buildFeatures.compose = true - android.composeOptions { - kotlinCompilerExtensionVersion = Dependencies.google.kotlinCompilerExtensionVersion - } -} - -ext.applyAndroidComposeLibraryModule = { project -> - applyAndroidLibraryModule(project) - applyCompose(project) -} - -ext.applyAndroidLibraryModule = { project -> - applyLibraryPlugins(project) - applyCommonAndroidParameters(project) - applyLibraryModuleOptimisations(project) -} - -ext.applyCrashlyticsIfRelease = { project -> - if (isReleaseBuild && !isFoss()) { - project.apply plugin: 'com.google.firebase.crashlytics' - project.afterEvaluate { - project.tasks.withType(com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask).configureEach { - it.googleServicesResourceRoot.set(project.file("src/release/res/")) - } - } - } -} - ext.kotlinTest = { dependencies -> - dependencies.testImplementation Dependencies.mavenCentral.kluent - dependencies.testImplementation Dependencies.mavenCentral.kotlinTest - dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.7.20" - dependencies.testImplementation Dependencies.mavenCentral.mockk - dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' - - dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' - dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1' + dependencies.testImplementation libs.kluent + dependencies.testImplementation libs.kotlin.test + dependencies.testImplementation libs.mockk + dependencies.testImplementation libs.kotlin.coroutines.test } ext.kotlinFixtures = { dependencies -> - dependencies.testFixturesImplementation Dependencies.mavenCentral.mockk - dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent - dependencies.testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore + dependencies.testFixturesImplementation libs.mockk + dependencies.testFixturesImplementation libs.kluent + dependencies.testFixturesImplementation libs.kotlin.coroutines } ext.androidImportFixturesWorkaround = { project, fixtures -> @@ -163,7 +57,6 @@ ext.firebase = { dependencies, name -> } } - if (launchTask.contains("codeCoverageReport".toLowerCase())) { apply from: 'tools/coverage.gradle' } diff --git a/chat-engine b/chat-engine new file mode 160000 index 0000000..0417816 --- /dev/null +++ b/chat-engine @@ -0,0 +1 @@ +Subproject commit 04178168e2107c8a11d8259d7cb3be499f55f30c 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 bf5ff6f..0000000 --- a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt +++ /dev/null @@ -1,24 +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() - -} \ 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/build.gradle b/core/build.gradle index b573f32..b83ef43 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -4,9 +4,9 @@ plugins { } dependencies { - api Dependencies.mavenCentral.kotlinCoroutinesCore - testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore - testFixturesImplementation Dependencies.mavenCentral.kluent - testFixturesImplementation Dependencies.mavenCentral.mockk - testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest + api libs.kotlin.coroutines + testFixturesImplementation libs.kotlin.coroutines + testFixturesImplementation libs.kluent + testFixturesImplementation libs.mockk + testFixturesImplementation libs.kotlin.coroutines.test } \ 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/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/design-library/build.gradle b/design-library/build.gradle index 578435a..2854c00 100644 --- a/design-library/build.gradle +++ b/design-library/build.gradle @@ -1,7 +1,13 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-android-compose-library-conventions" +} + +android { + namespace "app.dapk.st.design" +} dependencies { implementation project(":core") - implementation Dependencies.mavenCentral.coil - implementation Dependencies.mavenCentral.accompanistSystemuicontroller -} \ No newline at end of file + implementation libs.compose.coil + implementation libs.accompanist.systemuicontroller +} diff --git a/design-library/src/main/AndroidManifest.xml b/design-library/src/main/AndroidManifest.xml deleted file mode 100644 index 03faabd..0000000 --- a/design-library/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file 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..a16dc93 100644 --- a/domains/android/compose-core/build.gradle +++ b/domains/android/compose-core/build.gradle @@ -1,9 +1,10 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-android-compose-library-conventions" +} dependencies { implementation project(":core") 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/ComposeExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt index 7458cd9..117190a 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -27,6 +28,10 @@ class StartScope(private val scope: CoroutineScope) { fun SharedFlow.launch(onEach: suspend (T) -> Unit) { this.onEach(onEach).launchIn(scope) } + + fun Flow.launch(onEach: suspend (T) -> Unit) { + this.onEach(onEach).launchIn(scope) + } } interface EffectScope { 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/android/core/build.gradle b/domains/android/core/build.gradle index 6a11a06..d3781c1 100644 --- a/domains/android/core/build.gradle +++ b/domains/android/core/build.gradle @@ -1,4 +1,6 @@ -plugins { id 'kotlin' } +plugins { + id "kotlin" +} dependencies { compileOnly project(":domains:android:stub") diff --git a/domains/android/imageloader/build.gradle b/domains/android/imageloader/build.gradle index 45821be..da7037a 100644 --- a/domains/android/imageloader/build.gradle +++ b/domains/android/imageloader/build.gradle @@ -1,6 +1,8 @@ -applyAndroidLibraryModule(project) +plugins { + id "st-android-library-conventions" +} dependencies { implementation project(":core") - implementation Dependencies.mavenCentral.coil + implementation libs.compose.coil } diff --git a/domains/android/push/build.gradle b/domains/android/push/build.gradle index e057b0f..3fa616d 100644 --- a/domains/android/push/build.gradle +++ b/domains/android/push/build.gradle @@ -1,18 +1,21 @@ -applyAndroidLibraryModule(project) -apply plugin: "org.jetbrains.kotlin.plugin.serialization" +plugins { + id "st-android-library-conventions" + alias libs.plugins.kotlin.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") - implementation Dependencies.mavenCentral.kotlinSerializationJson - implementation Dependencies.jitPack.unifiedPush + implementation libs.kotlin.serialization + implementation libs.unifiedpush kotlinTest(it) + testImplementation 'chat-engine:chat-engine-test' 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/android/stub/build.gradle b/domains/android/stub/build.gradle index 4a7ad4a..28b2175 100644 --- a/domains/android/stub/build.gradle +++ b/domains/android/stub/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'kotlin' + id "kotlin" id 'java-test-fixtures' } @@ -12,7 +12,7 @@ if (localProperties.exists()) { } dependencies { - def androidVer = androidSdkVersion + def androidVer = 33 api files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar") kotlinFixtures(it) diff --git a/domains/android/tracking/build.gradle b/domains/android/tracking/build.gradle index 25e3ca2..793ba27 100644 --- a/domains/android/tracking/build.gradle +++ b/domains/android/tracking/build.gradle @@ -1,4 +1,6 @@ -applyAndroidLibraryModule(project) +plugins { + id "st-android-library-conventions" +} dependencies { implementation project(':core') 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/domains/android/viewmodel-stub/build.gradle b/domains/android/viewmodel-stub/build.gradle index 8ae4bae..5e66da6 100644 --- a/domains/android/viewmodel-stub/build.gradle +++ b/domains/android/viewmodel-stub/build.gradle @@ -1 +1,3 @@ -plugins { id 'kotlin' } +plugins { + id "kotlin" +} diff --git a/domains/android/viewmodel/build.gradle b/domains/android/viewmodel/build.gradle index c3cce48..0bfcb26 100644 --- a/domains/android/viewmodel/build.gradle +++ b/domains/android/viewmodel/build.gradle @@ -1,15 +1,15 @@ plugins { - id 'kotlin' + id "kotlin" id 'java-test-fixtures' } dependencies { compileOnly project(":domains:android:viewmodel-stub") - implementation Dependencies.mavenCentral.kotlinCoroutinesCore + implementation libs.kotlin.coroutines kotlinFixtures(it) - testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore - testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest + testFixturesImplementation libs.kotlin.coroutines + testFixturesImplementation libs.kotlin.coroutines.test testFixturesImplementation testFixtures(project(":core")) testFixturesCompileOnly project(":domains:android:viewmodel-stub") } \ No newline at end of file diff --git a/domains/android/work/build.gradle b/domains/android/work/build.gradle index ee78269..9b21339 100644 --- a/domains/android/work/build.gradle +++ b/domains/android/work/build.gradle @@ -1,4 +1,6 @@ -applyAndroidLibraryModule(project) +plugins { + id "st-android-library-conventions" +} dependencies { implementation project(':core') diff --git a/domains/firebase/crashlytics-noop/build.gradle b/domains/firebase/crashlytics-noop/build.gradle index 132fe20..0321dd4 100644 --- a/domains/firebase/crashlytics-noop/build.gradle +++ b/domains/firebase/crashlytics-noop/build.gradle @@ -1,4 +1,6 @@ -plugins { id 'kotlin' } +plugins { + id "kotlin" +} dependencies { implementation project(':core') diff --git a/domains/firebase/crashlytics/build.gradle b/domains/firebase/crashlytics/build.gradle index cbb5990..7b968a0 100644 --- a/domains/firebase/crashlytics/build.gradle +++ b/domains/firebase/crashlytics/build.gradle @@ -1,7 +1,9 @@ -applyAndroidLibraryModule(project) +plugins { + id "st-android-library-conventions" +} dependencies { implementation project(':core') - implementation platform(Dependencies.google.firebaseBom) + implementation platform(libs.firebase.bom) implementation 'com.google.firebase:firebase-crashlytics' } diff --git a/domains/firebase/messaging-noop/build.gradle b/domains/firebase/messaging-noop/build.gradle index 10bf518..3a564a9 100644 --- a/domains/firebase/messaging-noop/build.gradle +++ b/domains/firebase/messaging-noop/build.gradle @@ -1,6 +1,8 @@ -applyAndroidLibraryModule(project) +plugins { + id "st-android-library-conventions" +} 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..bcf36f6 100644 --- a/domains/firebase/messaging/build.gradle +++ b/domains/firebase/messaging/build.gradle @@ -1,9 +1,11 @@ -applyAndroidLibraryModule(project) +plugins { + id "st-android-library-conventions" +} dependencies { implementation project(':core') implementation project(':domains:android:core') - implementation project(':matrix:common') - implementation platform('com.google.firebase:firebase-bom:29.0.3') + implementation "chat-engine:chat-engine" + implementation platform(libs.firebase.bom) 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/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/domains/store/build.gradle b/domains/store/build.gradle index 90b79fa..56790db 100644 --- a/domains/store/build.gradle +++ b/domains/store/build.gradle @@ -1,28 +1,22 @@ plugins { id 'kotlin' - id 'com.squareup.sqldelight' - id 'org.jetbrains.kotlin.plugin.serialization' + alias libs.plugins.kotlin.serialization + alias libs.plugins.sqldelight id 'java-test-fixtures' } 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 Dependencies.mavenCentral.kotlinSerializationJson - implementation Dependencies.mavenCentral.kotlinCoroutinesCore - implementation "com.squareup.sqldelight:coroutines-extensions:1.5.4" + implementation "chat-engine:chat-engine" + implementation libs.kotlin.serialization + implementation libs.sqldelight.extensions kotlinFixtures(it) testImplementation(testFixtures(project(":core"))) diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt index a0a4d59..dfddea9 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt @@ -16,6 +16,5 @@ class ApplicationPreferences( } -@JvmInline -value class ApplicationVersion(val value: Int) +data class ApplicationVersion(val value: Int) 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/external/jolm.jar b/external/jolm.jar deleted file mode 100644 index cd1c9cc..0000000 Binary files a/external/jolm.jar and /dev/null differ diff --git a/features/directory/build.gradle b/features/directory/build.gradle index d1d22ef..b3b02ef 100644 --- a/features/directory/build.gradle +++ b/features/directory/build.gradle @@ -1,22 +1,24 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-feature-conventions" +} + +android { + namespace "app.dapk.st.directory" +} dependencies { - implementation project(":chat-engine") implementation project(":domains:android:compose-core") - implementation project(":domains:android:viewmodel") - implementation project(":domains:state") + implementation "chat-engine:chat-engine" + implementation 'screen-state:screen-android' implementation project(":features:messenger") implementation project(":core") implementation project(":design-library") - implementation Dependencies.mavenCentral.coil + implementation libs.compose.coil kotlinTest(it) - androidImportFixturesWorkaround(project, project(":matrix:common")) + testImplementation 'screen-state:state-test' + testImplementation 'chat-engine:chat-engine-test' 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/directory/src/main/AndroidManifest.xml b/features/directory/src/main/AndroidManifest.xml deleted file mode 100644 index 49d4f8a..0000000 --- a/features/directory/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file 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..23318cb 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 @@ -1,12 +1,13 @@ 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.core.JobBag +import app.dapk.st.core.ProvidableModule +import app.dapk.st.directory.state.DirectoryEvent import app.dapk.st.directory.state.DirectoryState import app.dapk.st.directory.state.directoryReducer import app.dapk.st.engine.ChatEngine +import app.dapk.st.state.createStateViewModel class DirectoryModule( private val context: Context, @@ -14,6 +15,8 @@ class DirectoryModule( ) : ProvidableModule { fun directoryState(): DirectoryState { - return createStateViewModel { directoryReducer(chatEngine, ShortcutHandler(context), JobBag(), it) } + return createStateViewModel { directoryReducer(it) } } + + fun directoryReducer(eventEmitter: suspend (DirectoryEvent) -> Unit) = directoryReducer(chatEngine, ShortcutHandler(context), JobBag(), eventEmitter) } 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/directory/src/test/kotlin/app/dapk/st/directory/DirectoryReducerTest.kt b/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryReducerTest.kt index 9ca650d..ded2965 100644 --- a/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryReducerTest.kt +++ b/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryReducerTest.kt @@ -8,7 +8,6 @@ import fixture.aRoomOverview import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import org.junit.Test -import test.expect import test.testReducer private val AN_OVERVIEW = aRoomOverview() diff --git a/features/home/build.gradle b/features/home/build.gradle index 0237cb0..4be7225 100644 --- a/features/home/build.gradle +++ b/features/home/build.gradle @@ -1,16 +1,28 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-feature-conventions" +} + +android { + namespace "app.dapk.st.home" +} dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" + implementation 'screen-state:screen-android' implementation project(":features:directory") implementation project(":features:login") implementation project(":features:settings") implementation project(":features:profile") implementation project(":domains:android:compose-core") - implementation project(":domains:android:viewmodel") implementation project(':domains:store') - implementation project(':domains:state') implementation project(":core") implementation project(":design-library") - implementation Dependencies.mavenCentral.coil + implementation libs.compose.coil + + kotlinTest(it) + + testImplementation 'screen-state:state-test' + testImplementation 'chat-engine:chat-engine-test' + androidImportFixturesWorkaround(project, project(":core")) + androidImportFixturesWorkaround(project, project(":domains:android:stub")) } \ No newline at end of file diff --git a/features/home/src/main/AndroidManifest.xml b/features/home/src/main/AndroidManifest.xml index e9b8247..f0be1d3 100644 --- a/features/home/src/main/AndroidManifest.xml +++ b/features/home/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ - + - + \ No newline at end of file diff --git a/features/home/src/main/kotlin/app/dapk/st/home/BetaVersionUpgradeUseCase.kt b/features/home/src/main/kotlin/app/dapk/st/home/BetaVersionUpgradeUseCase.kt index 565e810..2ba36a4 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/BetaVersionUpgradeUseCase.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/BetaVersionUpgradeUseCase.kt @@ -20,7 +20,8 @@ class BetaVersionUpgradeUseCase( } private suspend fun hasChangedVersion(): Boolean { - val previousVersion = applicationPreferences.readVersion()?.value + val readVersion = applicationPreferences.readVersion() + val previousVersion = readVersion?.value val currentVersion = buildMeta.versionCode return when (previousVersion) { null -> false 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..8077f32 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 @@ -1,28 +1,51 @@ package app.dapk.st.home +import app.dapk.st.core.JobBag import app.dapk.st.core.ProvidableModule -import app.dapk.st.directory.state.DirectoryState +import app.dapk.st.directory.DirectoryModule 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.home.state.homeReducer +import app.dapk.st.login.LoginModule +import app.dapk.st.profile.ProfileModule +import app.dapk.st.state.State +import app.dapk.st.state.createStateViewModel +import app.dapk.state.Action +import app.dapk.state.DynamicReducers +import app.dapk.state.combineReducers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance class HomeModule( private val chatEngine: ChatEngine, private val storeModule: StoreModule, val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase, + private val profileModule: ProfileModule, + private val loginModule: LoginModule, + private val directoryModule: DirectoryModule, ) : ProvidableModule { - internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel { - return HomeViewModel( - chatEngine, - storeModule.credentialsStore(), - directory, - login, - profileViewModel, - storeModule.cacheCleaner(), - betaVersionUpgradeUseCase, - ) + internal fun compositeHomeState(): DynamicState { + return createStateViewModel { + combineReducers( + listOf( + homeReducerFactory(it), + loginModule.loginReducer(it), + profileModule.profileReducer(), + directoryModule.directoryReducer(it) + ) + ) + } } -} \ No newline at end of file + private fun homeReducerFactory(eventEmitter: suspend (Any) -> Unit) = + homeReducer(chatEngine, storeModule.cacheCleaner(), betaVersionUpgradeUseCase, JobBag(), eventEmitter) +} + +typealias DynamicState = State + +inline fun DynamicState.childState() = object : State { + override fun dispatch(action: Action) = this@childState.dispatch(action) + override val events: Flow = this@childState.events.filterIsInstance() + override val current: S = this@childState.current.getState() +} diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt index 5f8ce7d..22802a1 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt @@ -10,34 +10,39 @@ import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.components.CenteredLoading import app.dapk.st.design.components.CircleishAvatar import app.dapk.st.directory.DirectoryScreen -import app.dapk.st.home.HomeScreenState.* -import app.dapk.st.home.HomeScreenState.Page.Directory -import app.dapk.st.home.HomeScreenState.Page.Profile +import app.dapk.st.directory.state.DirectoryState +import app.dapk.st.home.state.HomeAction +import app.dapk.st.home.state.HomeScreenState.* +import app.dapk.st.home.state.HomeScreenState.Page.Directory +import app.dapk.st.home.state.HomeScreenState.Page.Profile +import app.dapk.st.home.state.HomeState import app.dapk.st.login.LoginScreen +import app.dapk.st.login.state.LoginState import app.dapk.st.profile.ProfileScreen +import app.dapk.st.profile.state.ProfileState @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun HomeScreen(homeViewModel: HomeViewModel) { +internal fun HomeScreen(homeState: HomeState, directoryState: DirectoryState, loginState: LoginState, profileState: ProfileState) { LifecycleEffect( - onStart = { homeViewModel.start() }, - onStop = { homeViewModel.stop() } + onStart = { homeState.dispatch(HomeAction.LifecycleVisible) }, + onStop = { homeState.dispatch(HomeAction.LifecycleGone) } ) - when (val state = homeViewModel.state) { + when (val state = homeState.current) { Loading -> CenteredLoading() is SignedIn -> { Scaffold( bottomBar = { - BottomBar(state, homeViewModel) + BottomBar(state, homeState) }, content = { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { when (state.page) { - Directory -> DirectoryScreen(homeViewModel.directory()) + Directory -> DirectoryScreen(directoryState) Profile -> { - ProfileScreen(homeViewModel.profile()) { - homeViewModel.changePage(Directory) + ProfileScreen(profileState) { + homeState.dispatch(HomeAction.ChangePage(Directory)) } } } @@ -47,8 +52,8 @@ internal fun HomeScreen(homeViewModel: HomeViewModel) { } SignedOut -> { - LoginScreen(homeViewModel.login()) { - homeViewModel.loggedIn() + LoginScreen(loginState) { + homeState.dispatch(HomeAction.LoggedIn) } } } @@ -56,7 +61,7 @@ internal fun HomeScreen(homeViewModel: HomeViewModel) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun BottomBar(state: SignedIn, homeViewModel: HomeViewModel) { +private fun BottomBar(state: SignedIn, homeState: HomeState) { Column { Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black.copy(alpha = 0.2f), thickness = 0.5.dp) NavigationBar(containerColor = Color.Transparent, modifier = Modifier.height(IntrinsicSize.Min)) { @@ -67,8 +72,8 @@ private fun BottomBar(state: SignedIn, homeViewModel: HomeViewModel) { selected = state.page == page, onClick = { when { - state.page == page -> homeViewModel.scrollToTopOfMessages() - else -> homeViewModel.changePage(page) + state.page == page -> homeState.dispatch(HomeAction.ScrollToTop) + else -> homeState.dispatch(HomeAction.ChangePage(page)) } }, ) @@ -86,7 +91,7 @@ private fun BottomBar(state: SignedIn, homeViewModel: HomeViewModel) { } }, selected = state.page == page, - onClick = { homeViewModel.changePage(page) }, + onClick = { homeState.dispatch(HomeAction.ChangePage(page)) }, ) } } 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 deleted file mode 100644 index 7aa055f..0000000 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ /dev/null @@ -1,136 +0,0 @@ -package app.dapk.st.home - -import androidx.lifecycle.viewModelScope -import app.dapk.st.directory.state.ComponentLifecycle -import app.dapk.st.directory.state.DirectorySideEffect -import app.dapk.st.directory.state.DirectoryState -import app.dapk.st.domain.StoreCleaner -import app.dapk.st.engine.ChatEngine -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.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 - -internal class HomeViewModel( - private val chatEngine: ChatEngine, - private val credentialsProvider: CredentialsStore, - private val directoryState: DirectoryState, - private val loginViewModel: LoginViewModel, - private val profileViewModel: ProfileViewModel, - private val cacheCleaner: StoreCleaner, - private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase, -) : DapkViewModel( - initialState = Loading -) { - - private var listenForInvitesJob: Job? = null - - fun directory() = directoryState - fun login() = loginViewModel - fun profile() = profileViewModel - - fun start() { - viewModelScope.launch { - state = if (credentialsProvider.isSignedIn()) { - _events.emit(HomeEvent.OnShowContent) - initialHomeContent() - } else { - SignedOut - } - } - - viewModelScope.launch { - if (credentialsProvider.isSignedIn()) { - listenForInviteChanges() - } - } - - } - - 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) - } - } - - fun loggedIn() { - viewModelScope.launch { - state = initialHomeContent() - _events.emit(HomeEvent.OnShowContent) - listenForInviteChanges() - } - } - - private fun CoroutineScope.listenForInviteChanges() { - listenForInvitesJob?.cancel() - listenForInvitesJob = chatEngine.invites() - .onEach { invites -> - when (val currentState = state) { - is SignedIn -> updateState { currentState.copy(invites = invites.size) } - Loading, - SignedOut -> { - // do nothing - } - } - }.launchIn(this) - } - - fun hasVersionChanged() = betaVersionUpgradeUseCase.hasVersionChanged() - - fun clearCache() { - viewModelScope.launch { - cacheCleaner.cleanCache(removeCredentials = false) - betaVersionUpgradeUseCase.notifyUpgraded() - _events.emit(HomeEvent.Relaunch) - } - } - - fun scrollToTopOfMessages() { - directoryState.dispatch(DirectorySideEffect.ScrollToTop) - } - - fun changePage(page: Page) { - state = when (val current = state) { - Loading -> current - is SignedIn -> { - when (page) { - current.page -> current - else -> current.copy(page = page).also { - pageChangeSideEffects(page) - } - } - } - - SignedOut -> current - } - } - - private fun pageChangeSideEffects(page: Page) { - when (page) { - Page.Directory -> { - // do nothing - } - - Page.Profile -> { - directoryState.dispatch(ComponentLifecycle.OnGone) - profileViewModel.reset() - } - } - } - - fun stop() { - // do nothing - } -} 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..268840c 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 @@ -12,26 +12,24 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import app.dapk.st.core.DapkActivity +import app.dapk.st.core.extensions.unsafeLazy 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.home.state.HomeAction +import app.dapk.st.home.state.HomeEvent +import app.dapk.st.home.state.HomeState +import app.dapk.st.state.state import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach 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 homeViewModel by viewModel { module().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) } + private val homeModule by unsafeLazy { module() } + private val compositeState by state { homeModule.compositeHomeState() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val pushPermissionLauncher = registerPushPermission() - homeViewModel.events.onEach { + compositeState.events.onEach { when (it) { HomeEvent.Relaunch -> recreate() HomeEvent.OnShowContent -> pushPermissionLauncher?.invoke() @@ -39,11 +37,12 @@ class MainActivity : DapkActivity() { }.launchIn(lifecycleScope) setContent { - if (homeViewModel.hasVersionChanged()) { - BetaUpgradeDialog() + val homeState: HomeState = compositeState.childState() + if (homeModule.betaVersionUpgradeUseCase.hasVersionChanged()) { + BetaUpgradeDialog(homeState) } else { Surface(Modifier.fillMaxSize()) { - HomeScreen(homeViewModel) + HomeScreen(homeState, compositeState.childState(), compositeState.childState(), compositeState.childState()) } } } @@ -56,20 +55,20 @@ class MainActivity : DapkActivity() { null } } - - @Composable - private fun BetaUpgradeDialog() { - AlertDialog( - title = { Text(text = "BETA") }, - text = { Text(text = "During the BETA, version upgrades require a cache clear") }, - onDismissRequest = { - - }, - confirmButton = { - TextButton(onClick = { homeViewModel.clearCache() }) { - Text(text = "Clear cache".uppercase()) - } - }, - ) - } +} + +@Composable +private fun BetaUpgradeDialog(homeState: HomeState) { + AlertDialog( + title = { Text(text = "BETA") }, + text = { Text(text = "During the BETA, version upgrades require a cache clear") }, + onDismissRequest = { + + }, + confirmButton = { + TextButton(onClick = { homeState.dispatch(HomeAction.ClearCache) }) { + Text(text = "Clear cache".uppercase()) + } + }, + ) } diff --git a/features/home/src/main/kotlin/app/dapk/st/home/state/HomeAction.kt b/features/home/src/main/kotlin/app/dapk/st/home/state/HomeAction.kt new file mode 100644 index 0000000..fc58d2e --- /dev/null +++ b/features/home/src/main/kotlin/app/dapk/st/home/state/HomeAction.kt @@ -0,0 +1,21 @@ +package app.dapk.st.home.state + +import app.dapk.st.engine.Me +import app.dapk.st.home.state.HomeScreenState.Page +import app.dapk.state.Action + +sealed interface HomeAction : Action { + object LifecycleVisible : HomeAction + object LifecycleGone : HomeAction + + object ScrollToTop : HomeAction + object ClearCache : HomeAction + object LoggedIn : HomeAction + + data class ChangePage(val page: Page) : HomeAction + data class ChangePageSideEffect(val page: Page) : HomeAction + data class UpdateInvitesCount(val invitesCount: Int) : HomeAction + data class UpdateToSignedIn(val me: Me) : HomeAction + data class UpdateState(val state: HomeScreenState) : HomeAction + object InitialHome : HomeAction +} diff --git a/features/home/src/main/kotlin/app/dapk/st/home/state/HomeReducer.kt b/features/home/src/main/kotlin/app/dapk/st/home/state/HomeReducer.kt new file mode 100644 index 0000000..f788da1 --- /dev/null +++ b/features/home/src/main/kotlin/app/dapk/st/home/state/HomeReducer.kt @@ -0,0 +1,124 @@ +package app.dapk.st.home.state + +import app.dapk.st.core.JobBag +import app.dapk.st.directory.state.ComponentLifecycle +import app.dapk.st.directory.state.DirectorySideEffect +import app.dapk.st.domain.StoreCleaner +import app.dapk.st.engine.ChatEngine +import app.dapk.st.home.BetaVersionUpgradeUseCase +import app.dapk.st.profile.state.ProfileAction +import app.dapk.state.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +fun homeReducer( + chatEngine: ChatEngine, + cacheCleaner: StoreCleaner, + betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase, + jobBag: JobBag, + eventEmitter: suspend (HomeEvent) -> Unit, +): ReducerFactory { + return createReducer( + initialState = HomeScreenState.Loading, + + change(HomeAction.UpdateState::class) { action, _ -> + action.state + }, + + change(HomeAction.UpdateToSignedIn::class) { action, state -> + val me = action.me + when (state) { + HomeScreenState.Loading -> HomeScreenState.SignedIn(HomeScreenState.Page.Directory, me, invites = 0) + is HomeScreenState.SignedIn -> state.copy(me = me, invites = state.invites) + HomeScreenState.SignedOut -> HomeScreenState.SignedIn(HomeScreenState.Page.Directory, me, invites = 0) + } + }, + + change(HomeAction.UpdateInvitesCount::class) { action, state -> + when (state) { + HomeScreenState.Loading -> state + is HomeScreenState.SignedIn -> state.copy(invites = action.invitesCount) + HomeScreenState.SignedOut -> state + } + }, + + async(HomeAction.LifecycleVisible::class) { _ -> + if (chatEngine.isSignedIn()) { + eventEmitter.invoke(HomeEvent.OnShowContent) + dispatch(HomeAction.InitialHome) + } else { + dispatch(HomeAction.UpdateState(HomeScreenState.SignedOut)) + } + }, + + async(HomeAction.InitialHome::class) { + val me = chatEngine.me(forceRefresh = false) + dispatch(HomeAction.UpdateToSignedIn(me)) + listenForInviteChanges(chatEngine, jobBag) + }, + + async(HomeAction.LoggedIn::class) { + dispatch(HomeAction.InitialHome) + eventEmitter.invoke(HomeEvent.OnShowContent) + }, + + async(HomeAction.ChangePageSideEffect::class) { action -> + when (action.page) { + HomeScreenState.Page.Directory -> { + // do nothing + } + + HomeScreenState.Page.Profile -> { + dispatch(ComponentLifecycle.OnGone) + dispatch(ProfileAction.Reset) + } + } + }, + + multi(HomeAction.ChangePage::class) { action -> + change { _, state -> + when (state) { + is HomeScreenState.SignedIn -> when (action.page) { + state.page -> state + else -> state.copy(page = action.page) + } + + HomeScreenState.Loading -> state + HomeScreenState.SignedOut -> state + } + } + async { + val state = getState() + if (state is HomeScreenState.SignedIn && state.page != action.page) { + dispatch(HomeAction.ChangePageSideEffect(action.page)) + } + } + }, + + async(HomeAction.ScrollToTop::class) { + dispatch(DirectorySideEffect.ScrollToTop) + }, + + sideEffect(HomeAction.ClearCache::class) { _, _ -> + cacheCleaner.cleanCache(removeCredentials = false) + betaVersionUpgradeUseCase.notifyUpgraded() + eventEmitter.invoke(HomeEvent.Relaunch) + }, + ) +} + +private fun ReducerScope.listenForInviteChanges(chatEngine: ChatEngine, jobBag: JobBag) { + jobBag.replace( + "invites-count", + chatEngine.invites() + .onEach { invites -> + when (getState()) { + is HomeScreenState.SignedIn -> dispatch(HomeAction.UpdateInvitesCount(invites.size)) + HomeScreenState.Loading, + HomeScreenState.SignedOut -> { + // do nothing + } + } + }.launchIn(coroutineScope) + ) +} \ No newline at end of file diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt b/features/home/src/main/kotlin/app/dapk/st/home/state/HomeState.kt similarity index 84% rename from features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt rename to features/home/src/main/kotlin/app/dapk/st/home/state/HomeState.kt index 22ec6aa..151ae60 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/state/HomeState.kt @@ -1,10 +1,13 @@ -package app.dapk.st.home +package app.dapk.st.home.state import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Settings import androidx.compose.ui.graphics.vector.ImageVector import app.dapk.st.engine.Me +import app.dapk.st.state.State + +typealias HomeState = State sealed interface HomeScreenState { diff --git a/features/home/src/test/kotlin/app/dapk/st/home/BetaVersionUpgradeUseCaseTest.kt b/features/home/src/test/kotlin/app/dapk/st/home/BetaVersionUpgradeUseCaseTest.kt new file mode 100644 index 0000000..5a7d592 --- /dev/null +++ b/features/home/src/test/kotlin/app/dapk/st/home/BetaVersionUpgradeUseCaseTest.kt @@ -0,0 +1,68 @@ +package app.dapk.st.home + +import app.dapk.st.core.BuildMeta +import app.dapk.st.domain.ApplicationPreferences +import app.dapk.st.domain.ApplicationVersion +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.async +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import test.delegateReturn +import test.expect + +class BetaVersionUpgradeUseCaseTest { + + private val buildMeta = BuildMeta(versionName = "a-version-name", versionCode = 100, isDebug = false) + private val fakeApplicationPreferences = FakeApplicationPreferences() + + private val useCase = BetaVersionUpgradeUseCase( + fakeApplicationPreferences.instance, + buildMeta + ) + + @Test + fun `given same stored version, when hasVersionChanged then is false`() = runTest { + fakeApplicationPreferences.givenVersion().returns(ApplicationVersion(buildMeta.versionCode)) + + val result = useCase.hasVersionChanged() + + result shouldBeEqualTo false + } + + // Should be impossible + @Test + fun `given higher stored version, when hasVersionChanged then is false`() = runTest { + fakeApplicationPreferences.givenVersion().returns(ApplicationVersion(buildMeta.versionCode + 1)) + + val result = useCase.hasVersionChanged() + + result shouldBeEqualTo false + } + + @Test + fun `given lower stored version, when hasVersionChanged then is true`() = runTest { + fakeApplicationPreferences.givenVersion().returns(ApplicationVersion(buildMeta.versionCode - 1)) + + val result = useCase.hasVersionChanged() + + result shouldBeEqualTo true + } + + @Test + fun `given version has changed, when waiting, then blocks until notified of upgrade`() = runTest { + fakeApplicationPreferences.givenVersion().returns(ApplicationVersion(buildMeta.versionCode - 1)) + fakeApplicationPreferences.instance.expect { it.setVersion(ApplicationVersion(buildMeta.versionCode)) } + + val waitUntilReady = async { useCase.waitUnitReady() } + async { useCase.notifyUpgraded() } + waitUntilReady.await() + } +} + +private class FakeApplicationPreferences { + val instance = mockk() + + fun givenVersion() = coEvery { instance.readVersion() }.delegateReturn() +} \ No newline at end of file diff --git a/features/home/src/test/kotlin/app/dapk/st/home/state/HomeReducerTest.kt b/features/home/src/test/kotlin/app/dapk/st/home/state/HomeReducerTest.kt new file mode 100644 index 0000000..4c25f0c --- /dev/null +++ b/features/home/src/test/kotlin/app/dapk/st/home/state/HomeReducerTest.kt @@ -0,0 +1,224 @@ +package app.dapk.st.home.state + +import app.dapk.st.directory.state.ComponentLifecycle +import app.dapk.st.directory.state.DirectorySideEffect +import app.dapk.st.domain.StoreCleaner +import app.dapk.st.engine.Me +import app.dapk.st.home.BetaVersionUpgradeUseCase +import app.dapk.st.matrix.common.HomeServerUrl +import app.dapk.st.profile.state.ProfileAction +import fake.FakeChatEngine +import fake.FakeJobBag +import fixture.aRoomId +import fixture.aRoomInvite +import fixture.aUserId +import io.mockk.mockk +import org.junit.Test +import test.* + +private val A_ME = Me(aUserId(), displayName = null, avatarUrl = null, homeServerUrl = HomeServerUrl("ignored")) +private val A_SIGNED_IN_STATE = HomeScreenState.SignedIn( + HomeScreenState.Page.Directory, + me = A_ME, + invites = 0, +) + +class HomeReducerTest { + + private val fakeStoreCleaner = FakeStoreCleaner() + private val fakeChatEngine = FakeChatEngine() + private val fakeBetaVersionUpgradeUseCase = FakeBetaVersionUpgradeUseCase() + private val fakeJobBag = FakeJobBag() + + private val runReducerTest = testReducer { fakeEventSource -> + homeReducer( + fakeChatEngine, + fakeStoreCleaner, + fakeBetaVersionUpgradeUseCase.instance, + fakeJobBag.instance, + fakeEventSource, + ) + } + + @Test + fun `initial state is loading`() = runReducerTest { + assertInitialState(HomeScreenState.Loading) + } + + @Test + fun `when UpdateState, then replaces state`() = runReducerTest { + reduce(HomeAction.UpdateState(HomeScreenState.SignedOut)) + + assertOnlyStateChange(HomeScreenState.SignedOut) + } + + @Test + fun `given SignedIn, when UpdateInviteCount, then updates invite count`() = runReducerTest { + setState(A_SIGNED_IN_STATE) + + reduce(HomeAction.UpdateInvitesCount(invitesCount = 90)) + + assertOnlyStateChange(A_SIGNED_IN_STATE.copy(invites = 90)) + } + + @Test + fun `when ScrollToTop, then forwards to directory scroll event`() = runReducerTest { + reduce(HomeAction.ScrollToTop) + + assertOnlyDispatches(DirectorySideEffect.ScrollToTop) + } + + @Test + fun `when ClearCache, then clears store cache, upgrades and relaunches`() = runReducerTest { + fakeStoreCleaner.expect { it.cleanCache(removeCredentials = false) } + fakeBetaVersionUpgradeUseCase.instance.expect { it.notifyUpgraded() } + + reduce(HomeAction.ClearCache) + + assertOnlyEvents(HomeEvent.Relaunch) + } + + @Test + fun `given SignedIn and invites update, when Visible, then show content and update on invite changes`() = runReducerTest { + fakeChatEngine.givenIsSignedIn().returns(true) + + reduce(HomeAction.LifecycleVisible) + + assertEvents(HomeEvent.OnShowContent) + assertDispatches(HomeAction.InitialHome) + assertNoStateChange() + } + + @Test + fun `given SignedOut and invites update, when Visible, then show content and update on invite changes`() = runReducerTest { + fakeChatEngine.givenIsSignedIn().returns(false) + + reduce(HomeAction.LifecycleVisible) + + assertOnlyDispatches(HomeAction.UpdateState(HomeScreenState.SignedOut)) + } + + @Test + fun `given SignedIn, when InitialHome, then updates me state and listens to invite changes`() = runReducerTest { + setState(A_SIGNED_IN_STATE) + fakeChatEngine.givenMe(forceRefresh = false).returns(A_ME) + givenInvites(count = 5) + + reduce(HomeAction.InitialHome) + + assertOnlyDispatches( + HomeAction.UpdateToSignedIn(A_ME), + HomeAction.UpdateInvitesCount(5) + ) + } + + @Test + fun `given SignedIn, when UpdateToSignedIn, then updates me state`() = runReducerTest { + setState(A_SIGNED_IN_STATE) + val expectedMe = A_ME.copy(aUserId("another-user")) + + reduce(HomeAction.UpdateToSignedIn(expectedMe)) + + assertOnlyStateChange(A_SIGNED_IN_STATE.copy(me = expectedMe)) + } + + @Test + fun `given Loading, when UpdateToSignedIn, then set SignedIn and updates me state`() = runReducerTest { + setState(HomeScreenState.Loading) + val expectedMe = A_ME.copy(aUserId("another-user")) + + reduce(HomeAction.UpdateToSignedIn(expectedMe)) + + assertOnlyStateChange(A_SIGNED_IN_STATE.copy(me = expectedMe)) + } + + @Test + fun `given SignedOut, when UpdateToSignedIn, then set SignedIn and updates me state`() = runReducerTest { + setState(HomeScreenState.SignedOut) + val expectedMe = A_ME.copy(aUserId("another-user")) + + reduce(HomeAction.UpdateToSignedIn(expectedMe)) + + assertOnlyStateChange(A_SIGNED_IN_STATE.copy(me = expectedMe)) + } + + @Test + fun `when LoggedIn, then emit show content and fetch initial home`() = runReducerTest { + setState(HomeScreenState.SignedOut) + givenInvites(count = 0) + + reduce(HomeAction.LoggedIn) + + assertDispatches(HomeAction.InitialHome) + assertEvents(HomeEvent.OnShowContent) + assertNoStateChange() + } + + @Test + fun `given SignedOut, when ChangePage, then does nothing`() = runReducerTest { + setState(HomeScreenState.SignedOut) + + reduce(HomeAction.ChangePage(HomeScreenState.Page.Directory)) + + assertNoChanges() + } + + @Test + fun `given Loading, when ChangePage, then does nothing`() = runReducerTest { + setState(HomeScreenState.Loading) + + reduce(HomeAction.ChangePage(HomeScreenState.Page.Directory)) + + assertNoChanges() + } + + @Test + fun `given SignedIn, when ChangePage to same page, then does nothing`() = runReducerTest { + val page = HomeScreenState.Page.Directory + setState(A_SIGNED_IN_STATE.copy(page = page)) + + reduce(HomeAction.ChangePage(page)) + + assertNoChanges() + } + + @Test + fun `given SignedIn, when ChangePage to different page, then updates page and emits side effect`() = runReducerTest { + val expectedPage = HomeScreenState.Page.Profile + setState(A_SIGNED_IN_STATE.copy(page = HomeScreenState.Page.Directory)) + + reduce(HomeAction.ChangePage(expectedPage)) + + assertStateChange(A_SIGNED_IN_STATE.copy(page = expectedPage)) + assertDispatches(HomeAction.ChangePageSideEffect(expectedPage)) + } + + @Test + fun `when ChangePageSide is Directory, then does nothing`() = runReducerTest { + reduce(HomeAction.ChangePageSideEffect(HomeScreenState.Page.Directory)) + + assertNoChanges() + } + + @Test + fun `when ChangePageSide is Profile, then mark directory gone and resets profile`() = runReducerTest { + reduce(HomeAction.ChangePageSideEffect(HomeScreenState.Page.Profile)) + + assertOnlyDispatches( + ComponentLifecycle.OnGone, + ProfileAction.Reset + ) + } + + private fun givenInvites(count: Int) { + fakeJobBag.instance.expect { it.replace("invites-count", any()) } + val invites = List(count) { aRoomInvite(roomId = aRoomId(it.toString())) } + fakeChatEngine.givenInvites().emits(invites) + } +} + +class FakeStoreCleaner : StoreCleaner by mockk() + +class FakeBetaVersionUpgradeUseCase { + val instance = mockk() +} \ No newline at end of file diff --git a/features/login/build.gradle b/features/login/build.gradle index ac3340b..e1d5d90 100644 --- a/features/login/build.gradle +++ b/features/login/build.gradle @@ -1,10 +1,24 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-feature-conventions" +} + +android { + namespace "app.dapk.st.login" +} dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" + implementation 'screen-state:screen-android' + implementation project(":domains:android:compose-core") implementation project(":domains:android:push") implementation project(":domains:android:viewmodel") implementation project(":design-library") implementation project(":core") + + kotlinTest(it) + + testImplementation 'screen-state:state-test' + testImplementation 'chat-engine:chat-engine-test' + androidImportFixturesWorkaround(project, project(":core")) } \ No newline at end of file diff --git a/features/login/src/main/AndroidManifest.xml b/features/login/src/main/AndroidManifest.xml deleted file mode 100644 index 403513e..0000000 --- a/features/login/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/login/src/main/kotlin/app/dapk/st/login/LoginModule.kt b/features/login/src/main/kotlin/app/dapk/st/login/LoginModule.kt index c745f9f..b3bf5b6 100644 --- a/features/login/src/main/kotlin/app/dapk/st/login/LoginModule.kt +++ b/features/login/src/main/kotlin/app/dapk/st/login/LoginModule.kt @@ -3,7 +3,10 @@ package app.dapk.st.login import app.dapk.st.core.ProvidableModule import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.engine.ChatEngine +import app.dapk.st.login.state.* import app.dapk.st.push.PushModule +import app.dapk.st.state.createStateViewModel +import app.dapk.state.ReducerFactory class LoginModule( private val chatEngine: ChatEngine, @@ -11,7 +14,14 @@ class LoginModule( private val errorTracker: ErrorTracker, ) : ProvidableModule { - fun loginViewModel(): LoginViewModel { - return LoginViewModel(chatEngine, pushModule.pushTokenRegistrar(), errorTracker) + fun loginState(): LoginState { + return createStateViewModel { + loginReducer(it) + } + } + + fun loginReducer(eventEmitter: suspend (LoginEvent) -> Unit): ReducerFactory { + val loginUseCase = LoginUseCase(chatEngine, pushModule.pushTokenRegistrars(), errorTracker) + return loginReducer(loginUseCase, eventEmitter) } } \ No newline at end of file diff --git a/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt b/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt index fd56d73..643df37 100644 --- a/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt +++ b/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt @@ -33,15 +33,18 @@ import androidx.compose.ui.unit.sp import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading import app.dapk.st.design.components.GenericError -import app.dapk.st.login.LoginEvent.LoginComplete -import app.dapk.st.login.LoginScreenState.* +import app.dapk.st.login.state.LoginAction +import app.dapk.st.login.state.LoginEvent.LoginComplete +import app.dapk.st.login.state.LoginEvent.WellKnownMissing +import app.dapk.st.login.state.LoginScreenState +import app.dapk.st.login.state.LoginState @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @Composable -fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { - loginViewModel.ObserveEvents(onLoggedIn) +fun LoginScreen(loginState: LoginState, onLoggedIn: () -> Unit) { + loginState.ObserveEvents(onLoggedIn) LaunchedEffect(true) { - loginViewModel.start() + loginState.dispatch(LoginAction.ComponentLifecycle.Visible) } var userName by rememberSaveable { mutableStateOf("") } @@ -49,11 +52,12 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { var serverUrl by rememberSaveable { mutableStateOf("") } val keyboardController = LocalSoftwareKeyboardController.current - when (val state = loginViewModel.state) { - is Error -> GenericError(cause = state.cause, action = { loginViewModel.start() }) - Loading -> CenteredLoading() + when (val content = loginState.current.content) { + is LoginScreenState.Content.Error -> GenericError(cause = content.cause, action = { loginState.dispatch(LoginAction.ComponentLifecycle.Visible) }) + LoginScreenState.Content.Loading -> CenteredLoading() - is Content -> + is LoginScreenState.Content.Idle -> { + val showServerUrl = loginState.current.showServerUrl Row { Spacer(modifier = Modifier.weight(0.1f)) Column( @@ -88,7 +92,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { keyboardOptions = KeyboardOptions(autoCorrect = false, keyboardType = KeyboardType.Email, imeAction = ImeAction.Next) ) - val canDoLoginAttempt = if (state.showServerUrl) { + val canDoLoginAttempt = if (showServerUrl) { userName.isNotEmpty() && password.isNotEmpty() && serverUrl.isNotEmpty() } else { userName.isNotEmpty() && password.isNotEmpty() @@ -106,12 +110,12 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { Icon(imageVector = Icons.Outlined.Lock, contentDescription = null) }, keyboardActions = KeyboardActions( - onDone = { loginViewModel.login(userName, password, serverUrl) }, + onDone = { loginState.dispatch(LoginAction.Login(userName, password, serverUrl)) }, onNext = { focusManager.moveFocus(FocusDirection.Down) }, ), keyboardOptions = KeyboardOptions( autoCorrect = false, - imeAction = ImeAction.Done.takeIf { canDoLoginAttempt } ?: ImeAction.Next.takeIf { state.showServerUrl } ?: ImeAction.None, + imeAction = ImeAction.Done.takeIf { canDoLoginAttempt } ?: ImeAction.Next.takeIf { showServerUrl } ?: ImeAction.None, keyboardType = KeyboardType.Password ), visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(), @@ -123,7 +127,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { } ) - if (state.showServerUrl) { + if (showServerUrl) { TextField( modifier = Modifier.fillMaxWidth(), value = serverUrl, @@ -133,7 +137,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { leadingIcon = { Icon(imageVector = Icons.Default.Web, contentDescription = null) }, - keyboardActions = KeyboardActions(onDone = { loginViewModel.login(userName, password, serverUrl) }), + keyboardActions = KeyboardActions(onDone = { loginState.dispatch(LoginAction.Login(userName, password, serverUrl)) }), keyboardOptions = KeyboardOptions( autoCorrect = false, imeAction = ImeAction.Done.takeIf { canDoLoginAttempt } ?: ImeAction.None, @@ -148,7 +152,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { modifier = Modifier.fillMaxWidth(), onClick = { keyboardController?.hide() - loginViewModel.login(userName, password, serverUrl) + loginState.dispatch(LoginAction.Login(userName, password, serverUrl)) }, enabled = canDoLoginAttempt ) { @@ -157,6 +161,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { } Spacer(modifier = Modifier.weight(0.1f)) } + } } } @@ -183,13 +188,13 @@ private fun Modifier.autofill( } @Composable -private fun LoginViewModel.ObserveEvents(onLoggedIn: () -> Unit) { +private fun LoginState.ObserveEvents(onLoggedIn: () -> Unit) { val context = LocalContext.current StartObserving { this@ObserveEvents.events.launch { when (it) { LoginComplete -> onLoggedIn() - LoginEvent.WellKnownMissing -> { + WellKnownMissing -> { Toast.makeText(context, "Couldn't find the homeserver, please enter the server URL", Toast.LENGTH_LONG).show() } } diff --git a/features/login/src/main/kotlin/app/dapk/st/login/LoginState.kt b/features/login/src/main/kotlin/app/dapk/st/login/LoginState.kt deleted file mode 100644 index f4d0d24..0000000 --- a/features/login/src/main/kotlin/app/dapk/st/login/LoginState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.dapk.st.login - -sealed interface LoginScreenState { - - data class Content(val showServerUrl: Boolean) : LoginScreenState - object Loading : LoginScreenState - data class Error(val cause: Throwable) : LoginScreenState -} - -sealed interface LoginEvent { - object LoginComplete : LoginEvent - object WellKnownMissing : LoginEvent -} - diff --git a/features/login/src/main/kotlin/app/dapk/st/login/LoginViewModel.kt b/features/login/src/main/kotlin/app/dapk/st/login/LoginViewModel.kt deleted file mode 100644 index c8efb13..0000000 --- a/features/login/src/main/kotlin/app/dapk/st/login/LoginViewModel.kt +++ /dev/null @@ -1,65 +0,0 @@ -package app.dapk.st.login - -import androidx.lifecycle.viewModelScope -import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.core.logP -import app.dapk.st.engine.ChatEngine -import app.dapk.st.engine.LoginRequest -import app.dapk.st.engine.LoginResult -import app.dapk.st.login.LoginEvent.LoginComplete -import app.dapk.st.login.LoginScreenState.* -import app.dapk.st.push.PushTokenRegistrar -import app.dapk.st.viewmodel.DapkViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.launch - -class LoginViewModel( - private val chatEngine: ChatEngine, - private val pushTokenRegistrar: PushTokenRegistrar, - private val errorTracker: ErrorTracker, -) : DapkViewModel( - initialState = Content(showServerUrl = false) -) { - - private var previousState: LoginScreenState? = null - - fun login(userName: String, password: String, serverUrl: String?) { - state = Loading - viewModelScope.launch { - logP("login") { - when (val result = chatEngine.login(LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) { - is LoginResult.Success -> { - runCatching { - listOf( - async { pushTokenRegistrar.registerCurrentToken() }, - async { preloadMe() }, - ).awaitAll() - } - _events.tryEmit(LoginComplete) - } - - is LoginResult.Error -> { - errorTracker.track(result.cause) - state = Error(result.cause) - } - - LoginResult.MissingWellKnown -> { - _events.tryEmit(LoginEvent.WellKnownMissing) - state = Content(showServerUrl = true) - } - } - } - } - } - - private suspend fun preloadMe() = chatEngine.me(forceRefresh = false) - - fun start() { - val showServerUrl = previousState?.let { it is Content && it.showServerUrl } ?: false - state = Content(showServerUrl = showServerUrl) - } -} - - -private fun String?.takeIfNotEmpty() = this?.takeIf { it.isNotEmpty() } \ No newline at end of file diff --git a/features/login/src/main/kotlin/app/dapk/st/login/state/LoginAction.kt b/features/login/src/main/kotlin/app/dapk/st/login/state/LoginAction.kt new file mode 100644 index 0000000..3689c66 --- /dev/null +++ b/features/login/src/main/kotlin/app/dapk/st/login/state/LoginAction.kt @@ -0,0 +1,15 @@ +package app.dapk.st.login.state + +import app.dapk.state.Action + +sealed interface LoginAction : Action { + + sealed interface ComponentLifecycle : LoginAction { + object Visible : ComponentLifecycle + } + + data class Login(val userName: String, val password: String, val serverUrl: String?) : LoginAction + + data class UpdateContent(val content: LoginScreenState.Content) : LoginAction + data class UpdateState(val state: LoginScreenState) : LoginAction +} \ No newline at end of file diff --git a/features/login/src/main/kotlin/app/dapk/st/login/state/LoginReducer.kt b/features/login/src/main/kotlin/app/dapk/st/login/state/LoginReducer.kt new file mode 100644 index 0000000..375fa54 --- /dev/null +++ b/features/login/src/main/kotlin/app/dapk/st/login/state/LoginReducer.kt @@ -0,0 +1,46 @@ +package app.dapk.st.login.state + +import app.dapk.st.core.logP +import app.dapk.st.engine.LoginRequest +import app.dapk.st.engine.LoginResult +import app.dapk.state.async +import app.dapk.state.change +import app.dapk.state.createReducer +import kotlinx.coroutines.launch + +fun loginReducer( + loginUseCase: LoginUseCase, + eventEmitter: suspend (LoginEvent) -> Unit, +) = createReducer( + initialState = LoginScreenState(showServerUrl = false, content = LoginScreenState.Content.Idle), + + change(LoginAction.ComponentLifecycle.Visible::class) { _, state -> + LoginScreenState(state.showServerUrl, content = LoginScreenState.Content.Idle) + }, + + change(LoginAction.UpdateContent::class) { action, state -> state.copy(content = action.content) }, + + change(LoginAction.UpdateState::class) { action, _ -> action.state }, + + async(LoginAction.Login::class) { action -> + coroutineScope.launch { + logP("login") { + dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Loading)) + val request = LoginRequest(action.userName, action.password, action.serverUrl.takeIfNotEmpty()) + + when (val result = loginUseCase.login(request)) { + is LoginResult.Error -> dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Error(result.cause))) + + LoginResult.MissingWellKnown -> { + eventEmitter.invoke(LoginEvent.WellKnownMissing) + dispatch(LoginAction.UpdateState(LoginScreenState(showServerUrl = true, content = LoginScreenState.Content.Idle))) + } + + is LoginResult.Success -> eventEmitter.invoke(LoginEvent.LoginComplete) + } + } + } + }, +) + +private fun String?.takeIfNotEmpty() = this?.takeIf { it.isNotEmpty() } diff --git a/features/login/src/main/kotlin/app/dapk/st/login/state/LoginState.kt b/features/login/src/main/kotlin/app/dapk/st/login/state/LoginState.kt new file mode 100644 index 0000000..47a6163 --- /dev/null +++ b/features/login/src/main/kotlin/app/dapk/st/login/state/LoginState.kt @@ -0,0 +1,22 @@ +package app.dapk.st.login.state + +import app.dapk.st.state.State + +typealias LoginState = State + +data class LoginScreenState( + val showServerUrl: Boolean, + val content: Content, +) { + + sealed interface Content { + object Idle : Content + object Loading : Content + data class Error(val cause: Throwable) : Content + } +} + +sealed interface LoginEvent { + object LoginComplete : LoginEvent + object WellKnownMissing : LoginEvent +} diff --git a/features/login/src/main/kotlin/app/dapk/st/login/state/LoginUseCase.kt b/features/login/src/main/kotlin/app/dapk/st/login/state/LoginUseCase.kt new file mode 100644 index 0000000..8445683 --- /dev/null +++ b/features/login/src/main/kotlin/app/dapk/st/login/state/LoginUseCase.kt @@ -0,0 +1,44 @@ +package app.dapk.st.login.state + +import app.dapk.st.core.extensions.ErrorTracker +import app.dapk.st.core.logP +import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.LoginRequest +import app.dapk.st.engine.LoginResult +import app.dapk.st.push.PushTokenRegistrar +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +class LoginUseCase( + private val chatEngine: ChatEngine, + private val pushTokenRegistrar: PushTokenRegistrar, + private val errorTracker: ErrorTracker, +) { + suspend fun login(request: LoginRequest): LoginResult { + return logP("login") { + when (val result = chatEngine.login(request)) { + is LoginResult.Success -> { + coroutineScope { + runCatching { + listOf( + async { pushTokenRegistrar.registerCurrentToken() }, + async { chatEngine.preloadMe() }, + ).awaitAll() + } + result + } + } + + is LoginResult.Error -> { + errorTracker.track(result.cause) + result + } + + LoginResult.MissingWellKnown -> result + } + } + } + + private suspend fun ChatEngine.preloadMe() = this.me(forceRefresh = false) +} \ No newline at end of file diff --git a/features/login/src/test/kotlin/app/dapk/st/login/state/LoginReducerTest.kt b/features/login/src/test/kotlin/app/dapk/st/login/state/LoginReducerTest.kt new file mode 100644 index 0000000..8940a67 --- /dev/null +++ b/features/login/src/test/kotlin/app/dapk/st/login/state/LoginReducerTest.kt @@ -0,0 +1,103 @@ +package app.dapk.st.login.state + +import app.dapk.st.engine.LoginRequest +import app.dapk.st.engine.LoginResult +import app.dapk.st.login.state.fakes.FakeLoginUseCase +import app.dapk.st.matrix.common.DeviceId +import app.dapk.st.matrix.common.HomeServerUrl +import app.dapk.st.matrix.common.UserCredentials +import fixture.aUserId +import org.junit.Test +import test.assertDispatches +import test.assertEvents +import test.assertOnlyDispatches +import test.testReducer + +private val A_LOGIN_ACTION = LoginAction.Login( + userName = "a-username", + password = "a-password", + serverUrl = "a-server-url", +) + +private val AN_ERROR_CAUSE = RuntimeException() + +class LoginReducerTest { + + private val fakeLoginUseCase = FakeLoginUseCase() + + private val runReducerTest = testReducer { events: (LoginEvent) -> Unit -> + loginReducer(fakeLoginUseCase.instance, events) + } + + @Test + fun `initial state is idle without server url`() = runReducerTest { + assertInitialState(LoginScreenState(showServerUrl = false, content = LoginScreenState.Content.Idle)) + } + + @Test + fun `given non initial state, when Visible, then updates state to Idle with previous showServerUrl`() = runReducerTest { + setState(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Loading)) + + reduce(LoginAction.ComponentLifecycle.Visible) + + assertOnlyStateChange(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Idle)) + } + + @Test + fun `when UpdateContent, then only updates content state`() = runReducerTest { + reduce(LoginAction.UpdateContent(LoginScreenState.Content.Loading)) + + assertOnlyStateChange { + it.copy(content = LoginScreenState.Content.Loading) + } + } + + @Test + fun `when UpdateState, then only updates state`() = runReducerTest { + reduce(LoginAction.UpdateState(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Loading))) + + assertOnlyStateChange(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Loading)) + } + + @Test + fun `given login errors, when Login, then updates content with loading and error`() = runReducerTest { + fakeLoginUseCase.given(A_LOGIN_ACTION.toRequest()).returns(LoginResult.Error(AN_ERROR_CAUSE)) + + reduce(A_LOGIN_ACTION) + + assertOnlyDispatches( + LoginAction.UpdateContent(LoginScreenState.Content.Loading), + LoginAction.UpdateContent(LoginScreenState.Content.Error(AN_ERROR_CAUSE)), + ) + } + + @Test + fun `given login fails with WellKnownMissing, when Login, then emits WellKnownMissing event and updates content with loading and Idle showing server url`() = + runReducerTest { + fakeLoginUseCase.given(A_LOGIN_ACTION.toRequest()).returns(LoginResult.MissingWellKnown) + + reduce(A_LOGIN_ACTION) + + assertDispatches( + LoginAction.UpdateContent(LoginScreenState.Content.Loading), + LoginAction.UpdateState(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Idle)), + ) + assertEvents(LoginEvent.WellKnownMissing) + assertNoStateChange() + } + + @Test + fun `given login success, when Login, then emits LoginComplete event`() = runReducerTest { + fakeLoginUseCase.given(A_LOGIN_ACTION.toRequest()).returns(LoginResult.Success(aUserCredentials())) + + reduce(A_LOGIN_ACTION) + + assertDispatches(LoginAction.UpdateContent(LoginScreenState.Content.Loading)) + assertEvents(LoginEvent.LoginComplete) + assertNoStateChange() + } +} + +private fun LoginAction.Login.toRequest() = LoginRequest(this.userName, this.password, this.serverUrl) + +private fun aUserCredentials() = UserCredentials("ignored", HomeServerUrl("ignored"), aUserId(), DeviceId("ignored")) \ No newline at end of file diff --git a/features/login/src/test/kotlin/app/dapk/st/login/state/LoginUseCaseTest.kt b/features/login/src/test/kotlin/app/dapk/st/login/state/LoginUseCaseTest.kt new file mode 100644 index 0000000..a4ddf7f --- /dev/null +++ b/features/login/src/test/kotlin/app/dapk/st/login/state/LoginUseCaseTest.kt @@ -0,0 +1,72 @@ +package app.dapk.st.login.state + +import app.dapk.st.engine.LoginRequest +import app.dapk.st.engine.LoginResult +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.push.PushTokenRegistrar +import fake.FakeChatEngine +import fake.FakeErrorTracker +import fixture.aUserId +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import test.expect + +private val A_LOGIN_ERROR = LoginResult.Error(RuntimeException()) +private val A_LOGIN_SUCCESS = LoginResult.Success(aUserCredentials()) + +private val A_LOGIN_REQUEST = LoginRequest( + userName = "a-username", + password = "a-password", + serverUrl = "a-server-url", +) + +class LoginUseCaseTest { + + private val fakeChatEngine = FakeChatEngine() + private val fakePushTokenRegistrar = FakePushTokenRegistrar() + private val fakeErrorTracker = FakeErrorTracker() + + private val useCase = LoginUseCase( + fakeChatEngine, + fakePushTokenRegistrar, + fakeErrorTracker, + ) + + @Test + fun `when logging in succeeds, then registers push token and preload me`() = runTest { + fakeChatEngine.givenLogin(A_LOGIN_REQUEST).returns(A_LOGIN_SUCCESS) + fakePushTokenRegistrar.expect { it.registerCurrentToken() } + fakeChatEngine.expect { it.me(forceRefresh = false) } + + val result = useCase.login(A_LOGIN_REQUEST) + + result shouldBeEqualTo A_LOGIN_SUCCESS + } + + @Test + fun `when logging in fails with MissingWellKnown, then does nothing`() = runTest { + fakeChatEngine.givenLogin(A_LOGIN_REQUEST).returns(LoginResult.MissingWellKnown) + + val result = useCase.login(A_LOGIN_REQUEST) + + result shouldBeEqualTo LoginResult.MissingWellKnown + } + + @Test + fun `when logging in errors, then tracks cause`() = runTest { + fakeChatEngine.givenLogin(A_LOGIN_REQUEST).returns(A_LOGIN_ERROR) + fakeErrorTracker.expect { it.track(A_LOGIN_ERROR.cause) } + + val result = useCase.login(A_LOGIN_REQUEST) + + result shouldBeEqualTo A_LOGIN_ERROR + } +} + +class FakePushTokenRegistrar : PushTokenRegistrar by mockk() + +private fun aUserCredentials() = UserCredentials("ignored", HomeServerUrl("ignored"), aUserId(), DeviceId("ignored")) diff --git a/features/login/src/test/kotlin/app/dapk/st/login/state/fakes/FakeLoginUseCase.kt b/features/login/src/test/kotlin/app/dapk/st/login/state/fakes/FakeLoginUseCase.kt new file mode 100644 index 0000000..eab9708 --- /dev/null +++ b/features/login/src/test/kotlin/app/dapk/st/login/state/fakes/FakeLoginUseCase.kt @@ -0,0 +1,15 @@ +package app.dapk.st.login.state.fakes + +import app.dapk.st.engine.LoginRequest +import app.dapk.st.login.state.LoginUseCase +import io.mockk.coEvery +import io.mockk.mockk +import test.delegateReturn + +class FakeLoginUseCase { + + val instance = mockk() + + fun given(loginRequest: LoginRequest) = coEvery { instance.login(loginRequest) }.delegateReturn() + +} \ No newline at end of file diff --git a/features/messenger/build.gradle b/features/messenger/build.gradle index 59d3ed8..66277a9 100644 --- a/features/messenger/build.gradle +++ b/features/messenger/build.gradle @@ -1,24 +1,30 @@ -applyAndroidComposeLibraryModule(project) -apply plugin: 'kotlin-parcelize' +plugins { + id "st-feature-conventions" + id "org.jetbrains.kotlin.plugin.parcelize" +} + +android { + namespace "app.dapk.st.messenger" +} 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") - implementation project(":domains:state") + implementation 'screen-state:screen-android' implementation project(":core") implementation project(":features:navigator") implementation project(":design-library") - implementation Dependencies.mavenCentral.coil + implementation libs.compose.coil kotlinTest(it) - androidImportFixturesWorkaround(project, project(":matrix:common")) + testImplementation 'screen-state:state-test' + testImplementation 'chat-engine:chat-engine-test' + 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")) + androidImportFixturesWorkaround(project, project(":domains:store")) } \ No newline at end of file diff --git a/features/messenger/src/main/AndroidManifest.xml b/features/messenger/src/main/AndroidManifest.xml index d81f786..04e5197 100644 --- a/features/messenger/src/main/AndroidManifest.xml +++ b/features/messenger/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + 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/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 0f15864..c46a71d 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -67,7 +67,7 @@ internal fun MessengerScreen( ) { val state = viewModel.current - viewModel.ObserveEvents(galleryLauncher) + viewModel.ObserveEvents(galleryLauncher, navigator) LifecycleEffect( onStart = { viewModel.dispatch(ComponentLifecycle.Visible) }, onStop = { viewModel.dispatch(ComponentLifecycle.Gone) } @@ -85,6 +85,30 @@ internal fun MessengerScreen( onImageClick = { viewModel.dispatch(ComposerStateChange.ImagePreview.Show(it)) } ) + when (val dialog = state.dialogState) { + null -> { + // do nothing + } + + is DialogState.PositiveNegative -> { + AlertDialog( + onDismissRequest = { viewModel.dispatch(ScreenAction.LeaveRoomConfirmation.Deny) }, + confirmButton = { + Button(onClick = { viewModel.dispatch(ScreenAction.LeaveRoomConfirmation.Confirm) }) { + Text("Leave room") + } + }, + dismissButton = { + Button(onClick = { viewModel.dispatch(ScreenAction.LeaveRoomConfirmation.Deny) }) { + Text("Cancel") + } + }, + title = { Text(dialog.title) }, + text = { Text(dialog.subtitle) } + ) + } + } + Column { Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = { state.roomState.takeIfContent()?.let { @@ -98,8 +122,10 @@ internal fun MessengerScreen( viewModel.dispatch(ScreenAction.Notifications.Mute) }) } + DropdownMenuItem(text = { Text("Leave room", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = { + viewModel.dispatch(ScreenAction.LeaveRoom) + }) } - } }) @@ -207,7 +233,7 @@ private fun ZoomableImage(viewerState: ViewerState) { } @Composable -private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher) { +private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher, navigator: Navigator) { val context = LocalContext.current StartObserving { this@ObserveEvents.events.launch { @@ -221,6 +247,8 @@ private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher is MessengerEvent.Toast -> { Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show() } + + MessengerEvent.OnLeftRoom -> navigator.navigate.upToHome() } } } 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/MessengerAction.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerAction.kt index 07541ca..2cfa295 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerAction.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerAction.kt @@ -10,11 +10,19 @@ sealed interface ScreenAction : Action { data class CopyToClipboard(val model: BubbleModel) : ScreenAction object SendMessage : ScreenAction object OpenGalleryPicker : ScreenAction + object LeaveRoom : ScreenAction sealed interface Notifications : ScreenAction { object Mute : Notifications object Unmute : Notifications } + + sealed interface LeaveRoomConfirmation : ScreenAction { + object Confirm : LeaveRoomConfirmation + object Deny : LeaveRoomConfirmation + } + + data class UpdateDialogState(val dialogState: DialogState?): ScreenAction } sealed interface ComponentLifecycle : Action { diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt index b7e7c8f..fc90d92 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt @@ -19,6 +19,7 @@ import app.dapk.st.navigator.MessageAttachment import app.dapk.state.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlin.reflect.KClass internal fun messengerReducer( jobBag: JobBag, @@ -36,6 +37,7 @@ internal fun messengerReducer( roomState = Lce.Loading(), composerState = initialComposerState(initialAttachments), viewerState = null, + dialogState = null, ), async(ComponentLifecycle::class) { action -> @@ -83,12 +85,22 @@ internal fun messengerReducer( change(ComposerStateChange.ReplyMode::class) { action, state -> when (action) { - is ComposerStateChange.ReplyMode.Enter -> state.copy( - composerState = when (state.composerState) { - is ComposerState.Attachments -> state.composerState.copy(reply = action.replyingTo) - is ComposerState.Text -> state.composerState.copy(reply = action.replyingTo) + is ComposerStateChange.ReplyMode.Enter -> { + when (action.replyingTo) { + is RoomEvent.Message -> state.copy( + composerState = when (state.composerState) { + is ComposerState.Attachments -> state.composerState.copy(reply = action.replyingTo) + is ComposerState.Text -> state.composerState.copy(reply = action.replyingTo) + } + ) + + // TODO support replying to more message types + is RoomEvent.Encrypted, + is RoomEvent.Image, + is RoomEvent.Redacted, + is RoomEvent.Reply -> state } - ) + } ComposerStateChange.ReplyMode.Exit -> state.copy( composerState = when (state.composerState) { @@ -159,9 +171,43 @@ internal fun messengerReducer( ) ) }, + + change(ScreenAction.UpdateDialogState::class) { action, state -> + state.copy(dialogState = action.dialogState) + }, + + rewrite(ScreenAction.LeaveRoom::class) { + ScreenAction.UpdateDialogState( + DialogState.PositiveNegative( + title = "Leave room", + subtitle = "Are you sure you want you leave the room? If the room is private you will need to be invited again to rejoin.", + negativeAction = ScreenAction.LeaveRoomConfirmation.Deny, + positiveAction = ScreenAction.LeaveRoomConfirmation.Confirm, + ) + ) + }, + + async(ScreenAction.LeaveRoomConfirmation::class) { action -> + dispatch(ScreenAction.UpdateDialogState(dialogState = null)) + + when (action) { + ScreenAction.LeaveRoomConfirmation.Confirm -> { + runCatching { chatEngine.rejectRoom(getState().roomId) }.fold( + onSuccess = { eventEmitter.invoke(MessengerEvent.OnLeftRoom) }, + onFailure = { eventEmitter.invoke(MessengerEvent.Toast("Failed to leave room")) }, + ) + } + + ScreenAction.LeaveRoomConfirmation.Deny -> { + // do nothing + } + } + }, ) } +private fun rewrite(klass: KClass, mapper: (A) -> Action) = async(klass) { action -> dispatch(mapper(action)) } + private suspend fun ChatEngine.sendTextMessage(content: MessengerPageState, composerState: ComposerState.Text) { val roomState = content.roomState val message = SendMessage.TextMessage( 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..095c509 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,12 +1,13 @@ package app.dapk.st.messenger.state import app.dapk.st.core.Lce -import app.dapk.st.core.State import app.dapk.st.design.components.BubbleModel import app.dapk.st.engine.MessengerPageState import app.dapk.st.engine.RoomEvent import app.dapk.st.matrix.common.RoomId import app.dapk.st.navigator.MessageAttachment +import app.dapk.st.state.State +import app.dapk.state.Action typealias MessengerState = State @@ -15,15 +16,26 @@ data class MessengerScreenState( val roomState: Lce, val composerState: ComposerState, val viewerState: ViewerState?, + val dialogState: DialogState?, ) data class ViewerState( val event: BubbleModel.Image, ) +sealed interface DialogState { + data class PositiveNegative( + val title: String, + val subtitle: String, + val positiveAction: Action, + val negativeAction: Action, + ) : DialogState +} + sealed interface MessengerEvent { object SelectImageAttachment : MessengerEvent data class Toast(val message: String) : MessengerEvent + object OnLeftRoom : MessengerEvent } sealed interface ComposerState { diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerReducerTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerReducerTest.kt index 5ac645d..478ec19 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerReducerTest.kt +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerReducerTest.kt @@ -15,14 +15,12 @@ import fake.FakeChatEngine import fake.FakeJobBag import fake.FakeMessageOptionsStore import fixture.* +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import org.junit.Test -import test.ReducerTestScope -import test.delegateReturn -import test.expect -import test.testReducer +import test.* private const val READ_RECEIPTS_ARE_DISABLED = true private val A_ROOM_ID = aRoomId("messenger state room id") @@ -31,17 +29,23 @@ private val AN_EVENT_ID = anEventId("state event") private val A_SELF_ID = aUserId("self") private val A_MESSENGER_PAGE_STATE = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID) private val A_MESSAGE_ATTACHMENT = MessageAttachment(AndroidUri("a-uri"), MimeType.Image) -private val A_REPLY = aRoomReplyMessageEvent() +private val A_REPLY = aRoomMessageEvent() +private val AN_SUPPORTED_REPLY = aRoomReplyMessageEvent() private val AN_IMAGE_BUBBLE = BubbleModel.Image( BubbleModel.Image.ImageContent(100, 200, "a-url"), mockk(), BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27") ) - private val A_TEXT_BUBBLE = BubbleModel.Text( content = RichText(listOf(RichText.Part.Normal(A_MESSAGE_CONTENT))), BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27") ) +private val A_DIALOG_STATE = DialogState.PositiveNegative( + "a title", + "a subtitle", + positiveAction = ScreenAction.LeaveRoomConfirmation.Confirm, + negativeAction = ScreenAction.LeaveRoomConfirmation.Deny, +) class MessengerReducerTest { @@ -72,6 +76,7 @@ class MessengerReducerTest { roomState = Lce.Loading(), composerState = ComposerState.Text(value = "", reply = null), viewerState = null, + dialogState = null, ) ) } @@ -84,6 +89,7 @@ class MessengerReducerTest { roomState = Lce.Loading(), composerState = ComposerState.Text(value = "", reply = null), viewerState = null, + dialogState = null, ) ) } @@ -96,6 +102,7 @@ class MessengerReducerTest { roomState = Lce.Loading(), composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null), viewerState = null, + dialogState = null, ) ) } @@ -178,7 +185,7 @@ class MessengerReducerTest { } @Test - fun `given text composer, when Enter ReplyMode, then updates composer state with reply`() = runReducerTest { + fun `given message text composer, when Enter ReplyMode, then updates composer state with reply`() = runReducerTest { setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = null)) } reduce(ComposerStateChange.ReplyMode.Enter(A_REPLY)) @@ -188,6 +195,15 @@ class MessengerReducerTest { } } + @Test + fun `given text composer, when Enter ReplyMode with unsupported content, then does nothing`() = runReducerTest { + setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = null)) } + + reduce(ComposerStateChange.ReplyMode.Enter(AN_SUPPORTED_REPLY)) + + assertNoChanges() + } + @Test fun `given text composer, when Exit ReplyMode, then updates composer state`() = runReducerTest { setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = A_REPLY)) } @@ -221,6 +237,60 @@ class MessengerReducerTest { } } + @Test + fun `when LeaveRoom, then updates dialog state with leave room confirmation`() = runReducerTest { + reduce(ScreenAction.LeaveRoom) + + assertOnlyDispatches( + ScreenAction.UpdateDialogState( + DialogState.PositiveNegative( + title = "Leave room", + subtitle = "Are you sure you want you leave the room? If the room is private you will need to be invited again to rejoin.", + negativeAction = ScreenAction.LeaveRoomConfirmation.Deny, + positiveAction = ScreenAction.LeaveRoomConfirmation.Confirm, + ) + ) + ) + } + + @Test + fun `when UpdateDialogState, then updates dialog state`() = runReducerTest { + reduce(ScreenAction.UpdateDialogState(dialogState = A_DIALOG_STATE)) + + assertOnlyStateChange { it.copy(dialogState = A_DIALOG_STATE) } + } + + @Test + fun `given can leave room, when LeaveConfirmation Confirm, then removes dialog and rejects room and emits OnLeftRoom`() = runReducerTest { + fakeChatEngine.expect { it.rejectRoom(A_ROOM_ID) } + + reduce(ScreenAction.LeaveRoomConfirmation.Confirm) + + assertDispatches(ScreenAction.UpdateDialogState(dialogState = null)) + assertEvents(MessengerEvent.OnLeftRoom) + assertNoStateChange() + } + + @Test + fun `given leave room fails, when LeaveConfirmation Confirm, then removes dialog and emits toast`() = runReducerTest { + fakeChatEngine.expectError(error = RuntimeException("an error")) { fakeChatEngine.rejectRoom(A_ROOM_ID) } + + reduce(ScreenAction.LeaveRoomConfirmation.Confirm) + + assertDispatches(ScreenAction.UpdateDialogState(dialogState = null)) + assertEvents(MessengerEvent.Toast("Failed to leave room")) + assertNoStateChange() + } + + @Test + fun `when LeaveConfirmation Deny, then removes dialog and does nothing`() = runReducerTest { + reduce(ScreenAction.LeaveRoomConfirmation.Deny) + + assertDispatches(ScreenAction.UpdateDialogState(dialogState = null)) + assertNoEvents() + assertNoStateChange() + } + @Test fun `when OpenGalleryPicker, then emits event`() = runReducerTest { reduce(ScreenAction.OpenGalleryPicker) @@ -273,8 +343,8 @@ class MessengerReducerTest { @Test fun `given text composer with reply, when SendMessage, then clear composer and sends text message`() = runReducerTest { - setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = A_REPLY.message), roomState = Lce.Content(A_MESSENGER_PAGE_STATE)) } - fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT, reply = A_REPLY.message), A_MESSENGER_PAGE_STATE.roomState.roomOverview) } + setState { it.copy(composerState = ComposerState.Text(A_MESSAGE_CONTENT, reply = A_REPLY), roomState = Lce.Content(A_MESSENGER_PAGE_STATE)) } + fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT, reply = A_REPLY), A_MESSENGER_PAGE_STATE.roomState.roomOverview) } reduce(ScreenAction.SendMessage) 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/navigator/build.gradle b/features/navigator/build.gradle index f3bacd7..92f9797 100644 --- a/features/navigator/build.gradle +++ b/features/navigator/build.gradle @@ -1,8 +1,14 @@ -applyAndroidLibraryModule(project) -apply plugin: 'kotlin-parcelize' +plugins { + id "st-feature-conventions" + id "org.jetbrains.kotlin.plugin.parcelize" +} + +android { + namespace "app.dapk.st.navigator" +} 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/navigator/src/main/AndroidManifest.xml b/features/navigator/src/main/AndroidManifest.xml deleted file mode 100644 index 9926105..0000000 --- a/features/navigator/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt index e52d34c..ad70891 100644 --- a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt +++ b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.os.Parcel import android.os.Parcelable +import androidx.core.app.NavUtils import app.dapk.st.core.AndroidUri import app.dapk.st.core.MimeType import app.dapk.st.matrix.common.RoomId @@ -33,7 +34,8 @@ interface Navigator { } fun upToHome() { - activity.navigateUpTo(intentFactory.home(activity)) + activity.finish() + activity.startActivity(intentFactory.home(activity)) } fun toMessenger(roomId: RoomId, attachments: List) { diff --git a/features/notifications/build.gradle b/features/notifications/build.gradle index b7d1171..e186dd5 100644 --- a/features/notifications/build.gradle +++ b/features/notifications/build.gradle @@ -1,8 +1,13 @@ -applyAndroidLibraryModule(project) +plugins { + id "st-android-library-conventions" +} + +android { + namespace "app.dapk.st.notifications" +} dependencies { - implementation project(":chat-engine") - implementation project(':domains:store') + implementation "chat-engine:chat-engine" implementation project(":domains:android:work") implementation project(':domains:android:push') implementation project(":domains:android:core") @@ -11,13 +16,10 @@ dependencies { implementation project(":features:messenger") implementation project(":features:navigator") - - implementation Dependencies.mavenCentral.kotlinSerializationJson + implementation libs.kotlin.serialization kotlinTest(it) - + testImplementation 'chat-engine:chat-engine-test' androidImportFixturesWorkaround(project, project(":core")) - androidImportFixturesWorkaround(project, project(":matrix:common")) - androidImportFixturesWorkaround(project, project(":chat-engine")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) } \ No newline at end of file diff --git a/features/notifications/src/main/AndroidManifest.xml b/features/notifications/src/main/AndroidManifest.xml index ebdd9bb..ffb7f70 100644 --- a/features/notifications/src/main/AndroidManifest.xml +++ b/features/notifications/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/features/profile/build.gradle b/features/profile/build.gradle index 89e50f1..921a30d 100644 --- a/features/profile/build.gradle +++ b/features/profile/build.gradle @@ -1,11 +1,23 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-feature-conventions" +} + +android { + namespace "app.dapk.st.profile" +} dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" implementation project(":features:settings") - implementation project(':domains:store') + implementation 'screen-state:screen-android' implementation project(":domains:android:compose-core") - implementation project(":domains:android:viewmodel") implementation project(":design-library") implementation project(":core") + + kotlinTest(it) + + testImplementation 'screen-state:state-test' + testImplementation 'chat-engine:chat-engine-test' + androidImportFixturesWorkaround(project, project(":core")) + androidImportFixturesWorkaround(project, project(":domains:android:stub")) } \ No newline at end of file diff --git a/features/profile/src/main/AndroidManifest.xml b/features/profile/src/main/AndroidManifest.xml deleted file mode 100644 index 8f0f1fd..0000000 --- a/features/profile/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ 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..f2be4a2 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,23 @@ package app.dapk.st.profile +import app.dapk.st.core.JobBag import app.dapk.st.core.ProvidableModule +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 +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() } } + fun profileReducer() = 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..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 @@ -23,15 +23,20 @@ import app.dapk.st.core.components.CenteredLoading 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 +import app.dapk.state.SpiderPage +import app.dapk.state.page.PageAction @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 +44,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, toolbar = { navigate, title -> Toolbar(navigate, title) }) { item(Page.Routes.profile) { ProfilePage(context, viewModel, it) } @@ -54,7 +59,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 +72,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 +116,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 +124,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 +138,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 +159,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..9955e86 --- /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.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 + +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.rejectRoom(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 67% 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..3869bb8 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.design.components.Route -import app.dapk.st.design.components.SpiderPage +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 -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..ba533ed --- /dev/null +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/state/ProfileUseCase.kt @@ -0,0 +1,35 @@ +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 +import kotlinx.coroutines.flow.map + +class ProfileUseCase( + private val chatEngine: ChatEngine, + private val errorTracker: ErrorTracker, +) { + + private var meCache: Me? = null + + fun content(): Flow> { + 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/ProfileReducerTest.kt b/features/profile/src/test/kotlin/app/dapk/st/profile/state/ProfileReducerTest.kt new file mode 100644 index 0000000..9ab036a --- /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.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 +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.rejectRoom(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() +} 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 diff --git a/features/settings/build.gradle b/features/settings/build.gradle index d88337d..29fc211 100644 --- a/features/settings/build.gradle +++ b/features/settings/build.gradle @@ -1,22 +1,24 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-feature-conventions" +} dependencies { - implementation project(":chat-engine") + implementation "chat-engine:chat-engine" implementation project(":features:navigator") implementation project(':domains:store') 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) - androidImportFixturesWorkaround(project, project(":matrix:common")) + testImplementation 'screen-state:state-test' + testImplementation 'chat-engine:chat-engine-test' 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")) + androidImportFixturesWorkaround(project, project(":domains:store")) } \ No newline at end of file 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/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, ) } 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..5cfdaa8 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 @@ -148,7 +144,6 @@ internal fun settingsReducer( } Ignored -> { - nothing() } ToggleDynamicTheme -> async { 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/features/share-entry/build.gradle b/features/share-entry/build.gradle index 176afb2..3be412b 100644 --- a/features/share-entry/build.gradle +++ b/features/share-entry/build.gradle @@ -1,10 +1,12 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-feature-conventions" +} 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..d90df6f 100644 --- a/features/verification/build.gradle +++ b/features/verification/build.gradle @@ -1,7 +1,9 @@ -applyAndroidComposeLibraryModule(project) +plugins { + id "st-feature-conventions" +} 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/gradle.properties b/gradle.properties index 895b20c..2aeafe7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,7 @@ org.gradle.jvmargs=-Xmx6144M -Xms2048M -Dfile.encoding=UTF-8 -XX:+UseParallelGC org.gradle.daemon=true org.gradle.parallel=true org.gradle.unsafe.configuration-cache=true +org.gradle.unsafe.configuration-cache-problems=warn org.gradle.caching=true org.gradle.configureondemand=true org.gradle.vfs.watch=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..da007be --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,40 @@ +[versions] +kotlin = "1.8.0" +coroutines = "1.6.4" +sqldelight = "1.5.4" +ktor = "2.2.2" + +[plugins] +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "2.9.2" } +sqldelight = { id = "com.squareup.sqldelight", version.ref = "sqldelight" } + +[libraries] +android-desugar = { group = "com.android.tools", name = "desugar_jdk_libs", version = "1.1.5" } + +compose-coil = { group = "io.coil-kt", name = "coil-compose", version = "2.2.2" } +accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version = "0.28.0" } + +matrix-olm = { group = "org.matrix.android", name = "olm-sdk", version = "3.2.12" } +unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version = "2.1.1" } + +firebase-bom = { module = "com.google.firebase:firebase-bom", version = "31.1.1" } + +kotlin-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.4.1" } +kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +kotlin-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } +kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" } + +sqldelight-android = { group = "com.squareup.sqldelight", name = "android-driver", version.ref = "sqldelight" } +sqldelight-test = { group = "com.squareup.sqldelight", name = "sqlite-driver", version.ref = "sqldelight" } +sqldelight-extensions = { group = "com.squareup.sqldelight", name = "coroutines-extensions", version.ref = "sqldelight" } + +ktor-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" } + +junit = { group = "junit", name = "junit", version = "4.13.2" } +kluent = { group = "org.amshove.kluent", name = "kluent", version = "1.72" } +mockk = { group = "io.mockk", name = "mockk", version = "1.13.3" } + +leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version = "2.10" } diff --git a/dependencies.gradle b/gradle/repositories.gradle similarity index 53% rename from dependencies.gradle rename to gradle/repositories.gradle index 2623c1c..bc6db05 100644 --- a/dependencies.gradle +++ b/gradle/repositories.gradle @@ -1,8 +1,6 @@ -ext.Dependencies = new DependenciesContainer() - -ext.Dependencies.with { - _repositories = { repositories -> - repositories.google { +ext.applyRepositories = { dependencyResolutionManagement -> + dependencyResolutionManagement.repositories { + google { content { includeGroupByRegex "com\\.android.*" includeGroupByRegex "com\\.google.*" @@ -10,14 +8,14 @@ ext.Dependencies.with { } } - repositories.maven { + maven { url 'https://jitpack.io' content { includeGroup "com.github.UnifiedPush" } } - repositories.mavenCentral { + mavenCentral { content { includeGroupByRegex "org\\.jetbrains.*" includeGroupByRegex "com\\.google.*" @@ -96,82 +94,4 @@ ext.Dependencies.with { } } } - - def kotlinVer = "1.7.20" - def sqldelightVer = "1.5.4" - def composeVer = "1.2.1" - def ktorVer = "2.1.3" - - google = new DependenciesContainer() - google.with { - androidGradlePlugin = "com.android.tools.build:gradle:7.3.1" - - androidxComposeUi = "androidx.compose.ui:ui:${composeVer}" - androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}" - androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-rc01" - androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}" - androidxActivityCompose = "androidx.activity:activity-compose:1.6.0" - kotlinCompilerExtensionVersion = "1.3.2" - - firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.2" - firebaseBom = "com.google.firebase:firebase-bom:31.0.1" - - jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5" - } - - mavenCentral = new DependenciesContainer() - mavenCentral.with { - kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}" - kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}" - kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" - kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" - kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' - kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}" - - sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${sqldelightVer}" - sqldelightAndroid = "com.squareup.sqldelight:android-driver:${sqldelightVer}" - sqldelightInMemory = "com.squareup.sqldelight:sqlite-driver:${sqldelightVer}" - - leakCanary = 'com.squareup.leakcanary:leakcanary-android:2.9.1' - - ktorAndroid = "io.ktor:ktor-client-android:${ktorVer}" - ktorCore = "io.ktor:ktor-client-core:${ktorVer}" - ktorSerialization = "io.ktor:ktor-client-serialization:${ktorVer}" - ktorJson = "io.ktor:ktor-serialization-kotlinx-json:${ktorVer}" - ktorLogging = "io.ktor:ktor-client-logging-jvm:${ktorVer}" - ktorJava = "io.ktor:ktor-client-java:${ktorVer}" - 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" - - junit = "junit:junit:4.13.2" - kluent = "org.amshove.kluent:kluent:1.72" - mockk = 'io.mockk:mockk:1.13.2' - - matrixOlm = "org.matrix.android:olm-sdk:3.2.12" - } - - jitPack = new DependenciesContainer() - jitPack.with { - unifiedPush = "com.github.UnifiedPush:android-connector:2.0.1" - } - } - -class DependenciesContainer extends GroovyObjectSupport { - - private final Map storage = new HashMap(); - - @Override - Object getProperty(String name) { - return storage.get(name); - } - - @Override - void setProperty(String name, Object newValue) { - storage.put(name, newValue); - } -} - - diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b916c04..070cb70 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists 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 13a9076..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), - 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 16a6a3b..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/FlowIterator.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.dapk.st.matrix.sync.internal - -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?) { - 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}") - error.printStackTrace() - 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("
    ") -> "
      " - content.contains("
        ") -> "
          " - else -> null - } - - if (nestedList == null) { - parser.parse(content.trim(), accumulator) - accumulator.appendNewline() - } else { - val firstItemInNested = content.substringBefore(nestedList) - parser.parse(firstItemInNested.trim(), accumulator) - accumulator.appendNewline() - parser.parse(content.substring(content.indexOf(nestedList)).trim(), accumulator) - } - } - - else -> { - // skip tag - } - } - } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/TagCaptor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/TagCaptor.kt deleted file mode 100644 index 37b756a..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/html/TagCaptor.kt +++ /dev/null @@ -1,78 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync.message.html - -class TagCaptor { - - fun tagCapture(input: String, startIndex: Int, tagFactory: (String, Map, String) -> Unit): Int { - return when (val closeIndex = input.indexOf('>', startIndex = startIndex)) { - -1 -> -1 - else -> { - val fullTag = input.substring(startIndex, closeIndex + 1) - val tagName = input.substring(startIndex + 1, closeIndex) - when { - fullTag.isExitlessTag() -> { - val trim = fullTag.removeSurrounding("<", ">").trim() - tagFactory(trim, emptyMap(), "") - closeIndex + 1 - } - - fullTag.isSelfClosing() -> { - val trim = fullTag.removeSuffix("/>").removePrefix("<").trim() - tagFactory(trim, emptyMap(), "") - closeIndex + 1 - } - - else -> { - val exitTag = if (tagName.contains(' ')) { - "" - } else { - "" - } - - val exitIndex = input.findTagClose(tagName, exitTag, searchIndex = closeIndex + 1) - if (exitIndex == -1) { - -1 - } else { - val exitTagCloseIndex = exitIndex + exitTag.length - if (tagName.contains(' ')) { - val parts = tagName.split(' ') - val attributes = parts.drop(1).associate { - val (key, value) = it.split("=") - key to value.removeSurrounding("\"") - } - tagFactory(parts.first(), attributes, input.substring(closeIndex + 1, exitIndex)) - } else { - tagFactory(tagName, emptyMap(), input.substring(closeIndex + 1, exitIndex)) - } - exitTagCloseIndex - } - } - } - } - } - } - - private fun String.findTagClose(tagName: String, exitTag: String, searchIndex: Int, open: Int = 1): Int { - val exitIndex = this.indexOf(exitTag, startIndex = searchIndex) - val nextOpen = this.indexOf("<$tagName", startIndex = searchIndex) - return when { - open == 1 && (nextOpen == -1 || exitIndex < nextOpen) -> exitIndex - open > 8 || open < 1 -> { - // something has gone wrong, lets exit - -1 - } - - exitIndex == -1 -> -1 - nextOpen == -1 || nextOpen > exitIndex -> this.findTagClose(tagName, exitTag, exitIndex + 1, open - 1) - - nextOpen < exitIndex -> { - this.findTagClose(tagName, exitTag, nextOpen + 1, open + 1) - } - - else -> -1 - } - } -} - -private fun String.isExitlessTag() = this == "
          " || (this.startsWith("<@") && this.endsWith('>')) - -private fun String.isSelfClosing() = this.endsWith("/>") diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/url/UrlParser.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/url/UrlParser.kt deleted file mode 100644 index 290e05e..0000000 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/message/url/UrlParser.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync.message.url - -import app.dapk.st.matrix.sync.internal.sync.message.ContentAccumulator - -private const val END_SEARCH = -1 -private const val INVALID_TRAILING_CHARS = ",.:;?<>" - -internal class UrlParser { - - fun parseUrl(input: String, urlIndex: Int, accumulator: ContentAccumulator): Int { - return if (urlIndex == END_SEARCH) END_SEARCH else { - val originalUrl = input.substring(urlIndex) - var index = 0 - val maybeUrl = originalUrl.takeWhile { - it != '\n' && it != ' ' && !originalUrl.hasLookAhead(index++, " { - val cleanedUrl = originalUrl.bestGuessStripTrailingUrlChar() - accumulator.appendLink(url = cleanedUrl, label = null) - if (cleanedUrl != originalUrl) { - accumulator.appendText(originalUrl.last().toString()) - } - input.length + 1 - } - - else -> { - val originalUrl = input.substring(urlIndex, urlEndIndex) - val cleanedUrl = originalUrl.bestGuessStripTrailingUrlChar() - accumulator.appendLink(url = cleanedUrl, label = null) - if (originalUrl == cleanedUrl) urlEndIndex else urlEndIndex - 1 - } - } - } - } -} - -private fun String.hasLookAhead(current: Int, value: String): Boolean { - return length > current + value.length && this.substring(current, current + value.length) == value -} - -private fun String.bestGuessStripTrailingUrlChar(): String { - val last = this.last() - return if (INVALID_TRAILING_CHARS.contains(last)) { - this.dropLast(1) - } else { - this - } -} diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterUseCaseTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterUseCaseTest.kt deleted file mode 100644 index 569fc36..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/filter/FilterUseCaseTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package app.dapk.st.matrix.sync.internal.filter - -import app.dapk.st.matrix.sync.SyncService -import app.dapk.st.matrix.sync.internal.request.ApiFilterResponse -import app.dapk.st.matrix.sync.internal.request.FilterRequest -import fake.FakeFilterStore -import fake.FakeMatrixHttpClient -import fixture.aUserId -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import test.expect - -private const val A_FILTER_KEY = "a-filter-key" -private const val A_FILTER_ID_VALUE = "a-filter-id" -private val A_FILTER_REQUEST = FilterRequest() - -internal class FilterUseCaseTest { - - private val fakeClient = FakeMatrixHttpClient() - private val fakeFilterStore = FakeFilterStore() - - private val filterUseCase = FilterUseCase(fakeClient, fakeFilterStore) - - @Test - fun `given cached filter then returns cached value`() = runTest { - fakeFilterStore.givenCachedFilter(A_FILTER_KEY, A_FILTER_ID_VALUE) - - val result = filterUseCase.filter(A_FILTER_KEY, aUserId(), A_FILTER_REQUEST) - - result shouldBeEqualTo SyncService.FilterId(A_FILTER_ID_VALUE) - } - - @Test - fun `given no cached filter then fetches upstream filter and caches id result`() = runTest { - fakeFilterStore.givenCachedFilter(A_FILTER_KEY, filterIdValue = null) - fakeFilterStore.expect { it.store(A_FILTER_KEY, A_FILTER_ID_VALUE) } - fakeClient.given(request = filterRequest(aUserId(), A_FILTER_REQUEST), response = ApiFilterResponse(A_FILTER_ID_VALUE)) - - val result = filterUseCase.filter(A_FILTER_KEY, aUserId(), A_FILTER_REQUEST) - - result shouldBeEqualTo SyncService.FilterId(A_FILTER_ID_VALUE) - } -} diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt deleted file mode 100644 index ee3bbd0..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -package app.dapk.st.matrix.sync.internal.room - -import app.dapk.st.matrix.common.EncryptedMessageContent -import app.dapk.st.matrix.common.JsonString -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.internal.request.DecryptedContent -import app.dapk.st.matrix.sync.internal.sync.message.RichMessageParser -import fake.FakeMatrixLogger -import fake.FakeMessageDecrypter -import fixture.* -import internalfixture.aTimelineTextEventContent -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.Json -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private const val A_DECRYPTED_MESSAGE_CONTENT = "decrypted - content" -private val AN_ENCRYPTED_ROOM_CONTENT = aMegolmV1() -private val AN_ENCRYPTED_ROOM_MESSAGE = anEncryptedRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT) -private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent( - message = AN_ENCRYPTED_ROOM_MESSAGE, - replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event")) -) -private val A_DECRYPTED_TEXT_CONTENT = DecryptedContent.TimelineText(aTimelineTextEventContent(body = A_DECRYPTED_MESSAGE_CONTENT)) -private val A_USER_CREDENTIALS = aUserCredentials() - -private val json = Json { encodeDefaults = true } - -class RoomEventsDecrypterTest { - - private val fakeMessageDecrypter = FakeMessageDecrypter() - - private val roomEventsDecrypter = RoomEventsDecrypter( - fakeMessageDecrypter, - RichMessageParser(), - Json, - FakeMatrixLogger(), - ) - - @Test - fun `given clear message event, when decrypting, then does nothing`() = runTest { - val aClearMessageEvent = aMatrixRoomMessageEvent() - val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent)) - - result shouldBeEqualTo listOf(aClearMessageEvent) - } - - @Test - fun `given encrypted message event, when decrypting, then applies decrypted body and removes encrypted content`() = runTest { - givenEncryptedMessage(AN_ENCRYPTED_ROOM_MESSAGE, decryptsTo = A_DECRYPTED_TEXT_CONTENT) - - val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(AN_ENCRYPTED_ROOM_MESSAGE)) - - result shouldBeEqualTo listOf(AN_ENCRYPTED_ROOM_MESSAGE.toText(A_DECRYPTED_MESSAGE_CONTENT)) - } - - @Test - fun `given encrypted reply event, when decrypting, then decrypts message and replyTo`() = runTest { - givenEncryptedReply(AN_ENCRYPTED_ROOM_REPLY, decryptsTo = A_DECRYPTED_TEXT_CONTENT) - - val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(AN_ENCRYPTED_ROOM_REPLY)) - - result shouldBeEqualTo listOf( - AN_ENCRYPTED_ROOM_REPLY.copy( - message = (AN_ENCRYPTED_ROOM_REPLY.message as RoomEvent.Encrypted).toText(A_DECRYPTED_MESSAGE_CONTENT), - replyingTo = (AN_ENCRYPTED_ROOM_REPLY.replyingTo as RoomEvent.Encrypted).toText(A_DECRYPTED_MESSAGE_CONTENT), - ) - ) - } - - private fun givenEncryptedMessage(roomMessage: RoomEvent.Encrypted, decryptsTo: DecryptedContent) { - val model = roomMessage.encryptedContent.toModel() - fakeMessageDecrypter.givenDecrypt(model) - .returns(aDecryptionSuccessResult(payload = JsonString(json.encodeToString(DecryptedContent.serializer(), decryptsTo)))) - } - - private fun givenEncryptedReply(roomReply: RoomEvent.Reply, decryptsTo: DecryptedContent) { - givenEncryptedMessage(roomReply.message as RoomEvent.Encrypted, decryptsTo) - givenEncryptedMessage(roomReply.replyingTo as RoomEvent.Encrypted, decryptsTo) - } -} - -private fun RoomEvent.Encrypted.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1( - this.cipherText, - this.deviceId, - this.senderKey, - this.sessionId, -) - -private fun RoomEvent.Encrypted.toText(text: String) = RoomEvent.Message( - this.eventId, - this.utcTimestamp, - content = RichText.of(text), - this.author, - this.meta, - this.edited, -) \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EphemeralEventsUseCaseTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EphemeralEventsUseCaseTest.kt deleted file mode 100644 index d7f81cc..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EphemeralEventsUseCaseTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.sync.SyncService -import app.dapk.st.matrix.sync.internal.request.ApiEphemeral -import fake.FakeRoomMembersService -import fixture.aRoomId -import fixture.aRoomMember -import fixture.aUserCredentials -import internalfixture.anApiEphemeral -import internalfixture.anApiSyncRoom -import internalfixture.anEphemeralTypingEvent -import kotlinx.coroutines.test.runTest -import org.junit.Test -import test.TestSharedFlow - -private val A_ROOM_ID = aRoomId() -private val A_ROOM_MEMBER = aRoomMember() - -internal class EphemeralEventsUseCaseTest { - - private val fakeRoomMembersService = FakeRoomMembersService() - private val testFlow = TestSharedFlow>() - - private val ephemeralEventsUseCase = EphemeralEventsUseCase(fakeRoomMembersService, testFlow) - - @Test - fun `given no ephemeral events to process then does nothing`() = runTest { - val roomToProcess = aRoomToProcess() - - ephemeralEventsUseCase.processEvents(roomToProcess) - - testFlow.assertNoValues() - } - - @Test - fun `given known member is typing then emits typing`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_ROOM_MEMBER.id, A_ROOM_MEMBER) - val roomToProcess = aRoomToProcess(anApiEphemeral(listOf(anEphemeralTypingEvent(listOf(A_ROOM_MEMBER.id))))) - - ephemeralEventsUseCase.processEvents(roomToProcess) - - testFlow.assertValues( - listOf(SyncService.SyncEvent.Typing(A_ROOM_ID, listOf(A_ROOM_MEMBER))) - ) - } - - @Test - fun `given unknown member is typing then emits empty`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_ROOM_MEMBER.id, null) - val roomToProcess = aRoomToProcess(anApiEphemeral(listOf(anEphemeralTypingEvent(listOf(A_ROOM_MEMBER.id))))) - - ephemeralEventsUseCase.processEvents(roomToProcess) - - testFlow.assertValues(listOf(SyncService.SyncEvent.Typing(A_ROOM_ID, emptyList()))) - } - - @Test - fun `given member stops typing then emits empty`() = runTest { - fakeRoomMembersService.givenNoMembers(A_ROOM_ID) - val roomToProcess = aRoomToProcess(anApiEphemeral(listOf(anEphemeralTypingEvent(userIds = emptyList())))) - - ephemeralEventsUseCase.processEvents(roomToProcess) - - testFlow.assertValues(listOf(SyncService.SyncEvent.Typing(A_ROOM_ID, emptyList()))) - } -} - -private fun aRoomToProcess(ephemeral: ApiEphemeral? = null) = RoomToProcess( - A_ROOM_ID, - anApiSyncRoom(ephemeral = ephemeral), - directMessage = null, - userCredentials = aUserCredentials(), - heroes = null, -) diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt deleted file mode 100644 index 0073e7c..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.RichText -import fake.FakeRoomStore -import fixture.aMatrixRoomMessageEvent -import fixture.anEventId -import internalfixture.aTimelineTextEventContent -import internalfixture.anApiTimelineTextEvent -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private val AN_EVENT_ID = anEventId() -private val A_TIMELINE_EVENT = anApiTimelineTextEvent(AN_EVENT_ID, content = aTimelineTextEventContent(body = "timeline event")) -private val A_ROOM_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = RichText.of("previous room event")) -private val A_PERSISTED_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = RichText.of("persisted event")) - -class EventLookupUseCaseTest { - - private val fakeRoomStore = FakeRoomStore() - - private val eventLookupUseCase = EventLookupUseCase(fakeRoomStore) - - @Test - fun `given all lookup sources fail then returns null results`() = runTest { - fakeRoomStore.givenEvent(AN_EVENT_ID, result = null) - - val result = eventLookupUseCase.lookup( - AN_EVENT_ID, - DecryptedTimeline(emptyList()), - DecryptedRoomEvents(emptyList()) - ) - - result shouldBeEqualTo LookupResult(null, null) - } - - @Test - fun `when looking up event then prioritises timeline result first`() = runTest { - fakeRoomStore.givenEvent(AN_EVENT_ID, result = A_PERSISTED_EVENT) - - val result = eventLookupUseCase.lookup( - AN_EVENT_ID, - DecryptedTimeline(listOf(A_TIMELINE_EVENT)), - DecryptedRoomEvents(listOf(A_ROOM_EVENT)) - ) - - result shouldBeEqualTo LookupResult(apiTimelineEvent = A_TIMELINE_EVENT, null) - } - - @Test - fun `given no timeline event when looking up event then returns previous room result`() = runTest { - fakeRoomStore.givenEvent(AN_EVENT_ID, result = A_PERSISTED_EVENT) - - val result = eventLookupUseCase.lookup( - AN_EVENT_ID, - DecryptedTimeline(emptyList()), - DecryptedRoomEvents(listOf(A_ROOM_EVENT)) - ) - - result shouldBeEqualTo LookupResult(apiTimelineEvent = null, roomEvent = A_ROOM_EVENT) - } - - @Test - fun `given no timeline or previous room event when looking up event then returns persisted room result`() = runTest { - fakeRoomStore.givenEvent(AN_EVENT_ID, result = A_PERSISTED_EVENT) - - val result = eventLookupUseCase.lookup( - AN_EVENT_ID, - DecryptedTimeline(emptyList()), - DecryptedRoomEvents(emptyList()) - ) - - result shouldBeEqualTo LookupResult(apiTimelineEvent = null, roomEvent = A_PERSISTED_EVENT) - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichTextMessageParserTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichTextMessageParserTest.kt deleted file mode 100644 index 54c8369..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichTextMessageParserTest.kt +++ /dev/null @@ -1,269 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.common.RichText.Part.* -import app.dapk.st.matrix.sync.internal.sync.message.RichMessageParser -import fixture.aUserId -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -class RichTextMessageParserTest { - - private val parser = RichMessageParser() - - @Test - fun `parses plain text`() = runParserTest( - input = "Hello world!", - expected = RichText(listOf(Normal("Hello world!"))) - ) - - @Test - fun `parses strong tags`() = runParserTest( - Case( - input = """hello world""", - expected = RichText( - listOf( - Normal("hello "), - Bold("wor"), - Normal("ld"), - ) - ) - ), - ) - - @Test - fun `parses em tags`() = runParserTest( - Case( - input = """hello world""", - expected = RichText( - listOf( - Normal("hello "), - Italic("wor"), - Normal("ld"), - ) - ) - ), - ) - - @Test - fun `parses p tags`() = runParserTest( - input = "

          Hello world!

          foo bar

          after paragraph", - expected = RichText(listOf(Normal("Hello world!\nfoo bar\nafter paragraph"))) - ) - - @Test - fun `parses nesting within p tags`() = runParserTest( - input = "

          Hello world!

          ", - expected = RichText(listOf(Bold("Hello world!"))) - ) - - @Test - fun `replaces quote entity`() = runParserTest( - input = "Hello world! "foo bar"", - expected = RichText(listOf(Normal("Hello world! \"foo bar\""))) - ) - - @Test - fun `replaces ampersand entity`() = runParserTest( - input = "Hello & world!", - expected = RichText(listOf(Normal("Hello & world!"))) - ) - - @Test - fun `replaces apostrophe entity`() = runParserTest( - Case( - input = "Hello world! foo's bar", - expected = RichText(listOf(Normal("Hello world! foo's bar"))) - ), - Case( - input = "Hello world! foo's bar", - expected = RichText(listOf(Normal("Hello world! foo's bar"))) - ), - ) - - @Test - fun `replaces people`() = runParserTest( - input = "Hello <@my-name:a-domain.foo>!", - expected = RichText(listOf(Normal("Hello "), Person(aUserId("@my-name:a-domain.foo"), "@my-name:a-domain.foo"), Normal("!"))) - ) - - @Test - fun `replaces matrixdotto with person`() = runParserTest( - input = """Hello a-name: world""", - expected = RichText(listOf(Normal("Hello "), Person(aUserId("@a-name:foo.bar"), "@a-name"), Normal(": world"))) - ) - - @Test - fun `parses header tags`() = runParserTest( - Case( - input = "

          hello

          ", - expected = RichText(listOf(Bold("hello"))) - ), - Case( - input = "

          hello

          text after title", - expected = RichText(listOf(Bold("hello"), Normal("\ntext after title"))) - ), - Case( - input = "

          hello

          ", - expected = RichText(listOf(Bold("hello"))) - ), - Case( - input = "

          hello

          ", - expected = RichText(listOf(Bold("hello"))) - ), - Case( - input = "

          1

          \n

          1

          \n

          1

          \n", - expected = RichText(listOf(Bold("1"), Normal("\n\n"), Bold("1"), Normal("\n\n"), Bold("1"))) - ), - ) - - @Test - fun `replaces br tags`() = runParserTest( - input = "Hello world!
          next line
          another line", - expected = RichText(listOf(Normal("Hello world!\nnext line\nanother line"))) - ) - - @Test - fun `parses blockquote tags`() = runParserTest( - input = "
          \n

          hello world

          \n
          \n", - expected = RichText(listOf(Normal("> "), Bold("hello"), Normal(" "), Italic("world"))) - ) - - @Test - fun `parses lists`() = runParserTest( - Case( - input = "
          • content in list item
          • another item in list
          ", - expected = RichText(listOf(Normal("- content in list item\n- another item in list"))) - ), - Case( - input = "
          1. content in list item
          2. another item in list
          ", - expected = RichText(listOf(Normal("1. content in list item\n2. another item in list"))) - ), - Case( - input = """
          1. content in list item
          2. another item in list
          """, - expected = RichText(listOf(Normal("5. content in list item\n6. another item in list"))) - ), - Case( - input = """
          1. content in list item
          2. another item in list
          3. another change
          4. without value
          """, - expected = RichText(listOf(Normal("3. content in list item\n4. another item in list\n10. another change\n11. without value"))) - ), - ) - - - @Test - fun `parses nested lists`() = runParserTest( - input = """ -
            -
          • first item -
              -
            • nested item
            • -
            -
          • -
          - """.trimIndent().lines().joinToString("") { it.trim() }, - expected = RichText(listOf(Normal("- first item\n- nested item"))) - ) - - @Test - fun `parses urls`() = runParserTest( - Case( - input = "https://google.com", - expected = RichText(listOf(Link("https://google.com", "https://google.com"))) - ), - Case( - input = "https://google.com. after link", - expected = RichText(listOf(Link("https://google.com", "https://google.com"), Normal(". after link"))) - ), - Case( - input = "ending sentence with url https://google.com.", - expected = RichText(listOf(Normal("ending sentence with url "), Link("https://google.com", "https://google.com"), Normal("."))) - ), - Case( - input = "https://google.com
          html after url", - expected = RichText(listOf(Link("https://google.com", "https://google.com"), Normal("\nhtml after url"))) - ), - ) - - @Test - fun `removes reply fallback`() = runParserTest( - input = """ - -
          - Original message -
          -
          - Reply to message - """.trimIndent(), - expected = RichText(listOf(Normal("Reply to message"))) - ) - - @Test - fun `removes text fallback`() = runParserTest( - input = """ - > <@user:domain.foo> Original message - > Some more content - - Reply to message - """.trimIndent(), - expected = RichText(listOf(Normal("Reply to message"))) - ) - - @Test - fun `parses styling text`() = runParserTest( - input = "hello world", - expected = RichText(listOf(Italic("hello"), Normal(" "), Bold("world"))) - ) - - @Test - fun `parses invalid tags text`() = runParserTest( - input = ">> ><>> << more content", - expected = RichText(listOf(Normal(">> ><>> << more content"))) - ) - - @Test - fun `parses 'a' tags`() = runParserTest( - Case( - input = """hello world a link! more content.""", - expected = RichText( - listOf( - Normal("hello world "), - Link(url = "www.google.com", label = "a link!"), - Normal(" more content."), - ) - ) - ), - Case( - input = """www.google.comwww.bing.com""", - expected = RichText( - listOf( - Link(url = "www.google.com", label = "www.google.com"), - Link(url = "www.bing.com", label = "www.bing.com"), - ) - ) - ), - ) - - private fun runParserTest(vararg cases: Case) { - val errors = mutableListOf() - cases.forEach { - runCatching { runParserTest(it.input, it.expected) }.onFailure { errors.add(it) } - } - if (errors.isNotEmpty()) { - throw CompositeThrowable(errors) - } - } - - private fun runParserTest(input: String, expected: RichText) { - val result = parser.parse(input) - - result shouldBeEqualTo expected - } -} - -private data class Case(val input: String, val expected: RichText) - -class CompositeThrowable(inner: List) : Throwable() { - init { - inner.forEach { addSuppressed(it) } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt deleted file mode 100644 index 1cff200..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt +++ /dev/null @@ -1,338 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RichText -import app.dapk.st.matrix.common.asString -import app.dapk.st.matrix.sync.RoomEvent -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 -import fake.FakeErrorTracker -import fake.FakeRoomMembersService -import fixture.* -import internalfixture.* -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private val A_ROOM_ID = aRoomId() -private val A_SENDER = aRoomMember() -private val EMPTY_LOOKUP = FakeLookup(LookupResult(apiTimelineEvent = null, roomEvent = null)) -private val A_TEXT_EVENT_MESSAGE = RichText.of("a text message") -private val A_REPLY_EVENT_MESSAGE = RichText.of("a reply to another message") -private val A_TEXT_EVENT = anApiTimelineTextEvent( - senderId = A_SENDER.id, - content = aTimelineTextEventContent(body = A_TEXT_EVENT_MESSAGE.asString()) -) -private val A_TEXT_EVENT_WITHOUT_CONTENT = anApiTimelineTextEvent( - senderId = A_SENDER.id, - content = aTimelineTextEventContent(body = null) -) -private val A_USER_CREDENTIALS = aUserCredentials() - -internal class RoomEventCreatorTest { - - private val fakeRoomMembersService = FakeRoomMembersService() - - private val richMessageParser = RichMessageParser() - private val roomEventCreator = RoomEventCreator( - fakeRoomMembersService, FakeErrorTracker(), RoomEventFactory(fakeRoomMembersService, richMessageParser), - richMessageParser - ) - - @Test - fun `given Megolm encrypted event then maps to encrypted room message`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val megolmEvent = anEncryptedApiTimelineEvent(senderId = A_SENDER.id, encryptedContent = aMegolmApiEncryptedContent()) - - val result = with(roomEventCreator) { megolmEvent.toRoomEvent(A_ROOM_ID) } - - result shouldBeEqualTo anEncryptedRoomMessageEvent( - eventId = megolmEvent.eventId, - utcTimestamp = megolmEvent.utcTimestamp, - author = A_SENDER, - encryptedContent = megolmEvent.encryptedContent.toMegolm(), - ) - } - - @Test - fun `given Olm encrypted event then maps to null`() = runTest { - val olmEvent = anEncryptedApiTimelineEvent(encryptedContent = anOlmApiEncryptedContent()) - - val result = with(roomEventCreator) { olmEvent.toRoomEvent(A_ROOM_ID) } - - result shouldBeEqualTo null - } - - @Test - fun `given unknown encrypted event then maps to null`() = runTest { - val olmEvent = anEncryptedApiTimelineEvent(encryptedContent = anUnknownApiEncryptedContent()) - - val result = with(roomEventCreator) { olmEvent.toRoomEvent(A_ROOM_ID) } - - result shouldBeEqualTo null - } - - @Test - fun `given text event then maps to room message`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - - val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } - - result shouldBeEqualTo aMatrixRoomMessageEvent( - eventId = A_TEXT_EVENT.id, - utcTimestamp = A_TEXT_EVENT.utcTimestamp, - content = A_TEXT_EVENT_MESSAGE, - author = A_SENDER, - ) - } - - @Test - fun `given text event without body then maps to empty room message`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - - val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } - - result shouldBeEqualTo aMatrixRoomMessageEvent( - eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id, - utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp, - content = RichText(emptyList()), - author = A_SENDER, - ) - } - - @Test - fun `given edited event with no relation then maps to new room message`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val editEvent = anApiTimelineTextEvent().toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE.asString()) - - val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } - - result shouldBeEqualTo aMatrixRoomMessageEvent( - eventId = editEvent.id, - utcTimestamp = editEvent.utcTimestamp, - content = RichText.of(editEvent.asTextContent().body!!.trimStart()), - author = A_SENDER, - edited = true - ) - } - - @Test - fun `given edited event which relates to a timeline event then updates existing message`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = anApiTimelineTextEvent(utcTimestamp = 0) - val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE.asString()) - val lookup = givenLookup(originalMessage) - - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - - result shouldBeEqualTo aMatrixRoomMessageEvent( - eventId = originalMessage.id, - utcTimestamp = editedMessage.utcTimestamp, - content = A_TEXT_EVENT_MESSAGE, - author = A_SENDER, - edited = true - ) - } - - @Test - fun `given edited event which relates to a room event then updates existing message`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = aMatrixRoomMessageEvent() - val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE.asString()) - val lookup = givenLookup(originalMessage) - - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - - result shouldBeEqualTo aMatrixRoomMessageEvent( - eventId = originalMessage.eventId, - utcTimestamp = originalMessage.utcTimestamp, - content = A_TEXT_EVENT_MESSAGE, - author = A_SENDER, - edited = true - ) - } - - @Test - fun `given edited event which relates to a room reply event then only updates message`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = aRoomReplyMessageEvent(message = aMatrixRoomMessageEvent()) - val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE.asString()) - val lookup = givenLookup(originalMessage) - - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - - result shouldBeEqualTo aRoomReplyMessageEvent( - replyingTo = originalMessage.replyingTo, - message = aMatrixRoomMessageEvent( - eventId = originalMessage.eventId, - utcTimestamp = originalMessage.utcTimestamp, - content = A_TEXT_EVENT_MESSAGE, - author = A_SENDER, - edited = true - ), - ) - } - - @Test - fun `given edited event is older than related known timeline event then ignores edit`() = runTest { - val originalMessage = anApiTimelineTextEvent(utcTimestamp = 1000) - val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE.asString()) - val lookup = givenLookup(originalMessage) - - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - - result shouldBeEqualTo null - } - - @Test - fun `given edited event is older than related room event then ignores edit`() = runTest { - val originalMessage = aMatrixRoomMessageEvent(utcTimestamp = 1000) - val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE.asString()) - val lookup = givenLookup(originalMessage) - - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - - result shouldBeEqualTo null - } - - @Test - fun `given reply event with no relation then maps to new room message using the full body`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val replyEvent = anApiTimelineTextEvent().toReplyEvent(messageContent = A_TEXT_EVENT_MESSAGE.asString()) - - println(replyEvent.content) - val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } - - result shouldBeEqualTo aMatrixRoomMessageEvent( - eventId = replyEvent.id, - utcTimestamp = replyEvent.utcTimestamp, - content = RichText.of(replyEvent.asTextContent().body!!), - author = A_SENDER, - ) - } - - @Test - fun `given reply event which relates to a timeline event then maps to reply`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = anApiTimelineTextEvent(content = aTimelineTextEventContent(body = "message being replied to")) - val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE.asString()) - val lookup = givenLookup(originalMessage) - - val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - - result shouldBeEqualTo aRoomReplyMessageEvent( - replyingTo = aMatrixRoomMessageEvent( - eventId = originalMessage.id, - utcTimestamp = originalMessage.utcTimestamp, - content = RichText.of(originalMessage.asTextContent().body!!), - author = A_SENDER, - ), - message = aMatrixRoomMessageEvent( - eventId = replyMessage.id, - utcTimestamp = replyMessage.utcTimestamp, - content = A_REPLY_EVENT_MESSAGE, - author = A_SENDER, - ), - ) - } - - @Test - fun `given reply event which relates to a room event then maps to reply`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = aMatrixRoomMessageEvent() - val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE.asString()) - val lookup = givenLookup(originalMessage) - - val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - - result shouldBeEqualTo aRoomReplyMessageEvent( - replyingTo = originalMessage, - message = aMatrixRoomMessageEvent( - eventId = replyMessage.id, - utcTimestamp = replyMessage.utcTimestamp, - content = A_REPLY_EVENT_MESSAGE, - author = A_SENDER, - ), - ) - } - - @Test - fun `given reply event which relates to another room reply event then maps to reply with the reply's message`() = runTest { - fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = aRoomReplyMessageEvent() - val replyMessage = (originalMessage.message as RoomEvent.Message).toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE.asString()) - val lookup = givenLookup(originalMessage) - - val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - - result shouldBeEqualTo aRoomReplyMessageEvent( - replyingTo = originalMessage.message, - message = aMatrixRoomMessageEvent( - eventId = replyMessage.id, - utcTimestamp = replyMessage.utcTimestamp, - content = A_REPLY_EVENT_MESSAGE, - author = A_SENDER, - ), - ) - } - - private fun givenLookup(event: ApiTimelineEvent.TimelineMessage): suspend (EventId) -> LookupResult { - return { - if (it == event.id) LookupResult(event, roomEvent = null) else throw IllegalArgumentException("unexpected id: $it") - } - } - - private fun givenLookup(event: RoomEvent): suspend (EventId) -> LookupResult { - return { - if (it == event.eventId) LookupResult(apiTimelineEvent = null, roomEvent = event) else throw IllegalArgumentException("unexpected id: $it") - } - } -} - -private fun ApiTimelineEvent.TimelineMessage.toEditEvent(newTimestamp: Long, messageContent: String) = this.copy( - id = anEventId("a-new-event-id"), - utcTimestamp = newTimestamp, - content = aTimelineTextEventContent( - body = " * $messageContent", - relation = anEditRelation(this.id), - ) -) - -private fun RoomEvent.Message.toEditEvent(newTimestamp: Long, messageContent: String) = anApiTimelineTextEvent( - id = anEventId("a-new-event-id"), - utcTimestamp = newTimestamp, - content = aTimelineTextEventContent( - body = " * $messageContent", - relation = anEditRelation(this.eventId), - ) -) - -private fun ApiTimelineEvent.TimelineMessage.toReplyEvent(messageContent: String) = anApiTimelineTextEvent( - id = anEventId("a-new-event-id"), - content = aTimelineTextEventContent( - body = "${this.content} $messageContent", - formattedBody = "${this.content}$messageContent", - relation = aReplyRelation(this.id), - ) -) - -private fun RoomEvent.Message.toReplyEvent(messageContent: String) = anApiTimelineTextEvent( - id = anEventId("a-new-event-id"), - content = aTimelineTextEventContent( - body = "${this.content} $messageContent", - formattedBody = "${this.content}$messageContent", - relation = aReplyRelation(this.eventId), - ) -) - -private fun ApiEncryptedContent.toMegolm(): RoomEvent.Encrypted.MegOlmV1 { - require(this is ApiEncryptedContent.MegOlmV1) - return aMegolmV1(this.cipherText, this.deviceId, this.senderKey, this.sessionId) -} - -private class FakeLookup(private val result: LookupResult) : suspend (EventId) -> LookupResult { - override suspend fun invoke(p1: EventId) = result -} - -private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt deleted file mode 100644 index 2b9e052..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.asString -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomState -import fake.FakeMatrixLogger -import fake.FakeRoomDataSource -import fixture.* -import internalfake.FakeRoomEventsDecrypter -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import test.expect - -private val A_ROOM_ID = aRoomId() - -private object ARoom { - val MESSAGE_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 0) - val ENCRYPTED_EVENT = anEncryptedRoomMessageEvent(utcTimestamp = 1) - val DECRYPTED_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 2) - val PREVIOUS_STATE = RoomState(aMatrixRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_EVENT)) - val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT) - val NEW_STATE = RoomState(aMatrixRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS) -} - -private val A_USER_CREDENTIALS = aUserCredentials() - -internal class RoomRefresherTest { - - private val fakeRoomDataSource = FakeRoomDataSource() - private val fakeRoomEventsDecrypter = FakeRoomEventsDecrypter() - - private val roomRefresher = RoomRefresher( - fakeRoomDataSource.instance, - fakeRoomEventsDecrypter.instance, - FakeMatrixLogger(), - ) - - @Test - fun `given no existing room when refreshing then does nothing`() = runTest { - fakeRoomDataSource.givenNoCachedRoom(A_ROOM_ID) - - val result = roomRefresher.refreshRoomContent(aRoomId(), A_USER_CREDENTIALS) - - result shouldBeEqualTo null - fakeRoomDataSource.verifyNoChanges() - } - - @Test - fun `given existing room when refreshing then processes existing state`() = runTest { - fakeRoomDataSource.expect { it.instance.persist(RoomId(any()), any(), any()) } - fakeRoomDataSource.givenRoom(A_ROOM_ID, ARoom.PREVIOUS_STATE) - fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, ARoom.PREVIOUS_STATE.events, ARoom.DECRYPTED_EVENTS) - - val result = roomRefresher.refreshRoomContent(aRoomId(), A_USER_CREDENTIALS) - - fakeRoomDataSource.verifyRoomUpdated(ARoom.PREVIOUS_STATE, ARoom.NEW_STATE) - result shouldBeEqualTo ARoom.NEW_STATE - } -} - -private fun RoomEvent.Message.asLastMessage() = aLastMessage( - this.content.asString(), - this.utcTimestamp, - this.author, -) diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt deleted file mode 100644 index d8a1d4a..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt +++ /dev/null @@ -1,104 +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.RoomEvent -import app.dapk.st.matrix.sync.internal.request.ApiSyncRoom -import fixture.* -import internalfake.FakeEventLookup -import internalfake.FakeRoomEventCreator -import internalfake.FakeRoomEventsDecrypter -import internalfake.FakeSyncEventDecrypter -import internalfixture.* -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test - -private val A_ROOM_ID = aRoomId() -private val ANY_LOOKUP_RESULT = LookupResult(anApiTimelineTextEvent(), roomEvent = null) -private val AN_ENCRYPTED_TIMELINE_EVENT = anEncryptedApiTimelineEvent() -private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent() -private val A_MESSAGE_ROOM_EVENT = aMatrixRoomMessageEvent(anEventId("a-message")) -private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message")) -private val A_LOOKUP_EVENT_ID = anEventId("lookup-id") -private val A_USER_CREDENTIALS = aUserCredentials() - -class TimelineEventsProcessorTest { - - private val fakeRoomEventsDecrypter = FakeRoomEventsDecrypter() - private val fakeSyncEventDecrypter = FakeSyncEventDecrypter() - private val fakeRoomEventCreator = FakeRoomEventCreator() - private val fakeEventLookup = FakeEventLookup() - - private val timelineEventsProcessor = TimelineEventsProcessor( - fakeRoomEventCreator.instance, - fakeRoomEventsDecrypter.instance, - fakeSyncEventDecrypter.instance, - fakeEventLookup.instance, - ) - - @Test - fun `given a room with no events then returns empty`() = runTest { - val previousEvents = emptyList() - val roomToProcess = aRoomToProcess() - fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents) - fakeSyncEventDecrypter.givenDecrypts(roomToProcess.apiSyncRoom.timeline.apiTimelineEvents) - - val result = timelineEventsProcessor.process(roomToProcess, previousEvents) - - result shouldBeEqualTo (emptyList() to emptyList()) - } - - @Test - fun `given encrypted and text timeline events when processing then maps to room events`() = runTest { - val previousEvents = listOf(aMatrixRoomMessageEvent(eventId = anEventId("previous-event"))) - val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT) - val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents))) - fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents) - fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents) - fakeEventLookup.givenLookup(A_LOOKUP_EVENT_ID, DecryptedTimeline(newTimelineEvents), DecryptedRoomEvents(previousEvents), ANY_LOOKUP_RESULT) - fakeRoomEventCreator.givenCreates(A_ROOM_ID, AN_ENCRYPTED_TIMELINE_EVENT, AN_ENCRYPTED_ROOM_EVENT) - fakeRoomEventCreator.givenCreatesUsingLookup( - A_USER_CREDENTIALS, - A_ROOM_ID, - A_LOOKUP_EVENT_ID, - A_TEXT_TIMELINE_EVENT, - A_MESSAGE_ROOM_EVENT, - ANY_LOOKUP_RESULT - ) - - val result = timelineEventsProcessor.process(roomToProcess, previousEvents) - - val expectedNewRoomEvents = listOf(AN_ENCRYPTED_ROOM_EVENT, A_MESSAGE_ROOM_EVENT) - result shouldBeEqualTo (expectedNewRoomEvents to expectedNewRoomEvents + previousEvents) - } - - @Test - fun `given unhandled timeline events when processing then ignores events`() = runTest { - val previousEvents = emptyList() - val newTimelineEvents = listOf( - anEncryptionApiTimelineEvent(), - aRoomAvatarApiTimelineEvent(), - aRoomCreateApiTimelineEvent(), - aRoomMemberApiTimelineEvent(), - aRoomNameApiTimelineEvent(), - aRoomTopicApiTimelineEvent(), - anIgnoredApiTimelineEvent() - ) - val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents))) - fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents) - fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents) - - val result = timelineEventsProcessor.process(roomToProcess, previousEvents) - - result shouldBeEqualTo (emptyList() to emptyList()) - } -} - -internal fun aRoomToProcess( - roomId: RoomId = aRoomId(), - apiSyncRoom: ApiSyncRoom = anApiSyncRoom(), - directMessage: UserId? = null, - userCredentials: UserCredentials = aUserCredentials(), -) = RoomToProcess(roomId, apiSyncRoom, directMessage, userCredentials, heroes = null) diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessorTest.kt deleted file mode 100644 index 28d2be1..0000000 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessorTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package app.dapk.st.matrix.sync.internal.sync - -import app.dapk.st.matrix.common.RoomId -import fake.FakeMatrixLogger -import fake.FakeRoomStore -import fixture.* -import kotlinx.coroutines.test.runTest -import org.junit.Test -import test.expect - -private val A_ROOM_OVERVIEW = aMatrixRoomOverview() -private val A_ROOM_MESSAGE_FROM_OTHER = aMatrixRoomMessageEvent( - eventId = anEventId("a-new-message-event"), - author = aRoomMember(id = aUserId("a-different-user")) -) - -internal class UnreadEventsProcessorTest { - - private val fakeRoomStore = FakeRoomStore() - - private val unreadEventsProcessor = UnreadEventsProcessor( - fakeRoomStore, - FakeMatrixLogger() - ) - - @Test - fun `given initial sync when processing unread then does mark any events as unread`() = runTest { - unreadEventsProcessor.processUnreadState( - isInitialSync = true, - overview = aMatrixRoomOverview(), - previousState = null, - newEvents = emptyList(), - selfId = aUserId() - ) - - fakeRoomStore.verifyNoUnreadChanges() - } - - @Test - fun `given read marker has changed when processing unread then marks room read`() = runTest { - fakeRoomStore.expect { it.markRead(RoomId(any())) } - - unreadEventsProcessor.processUnreadState( - isInitialSync = false, - overview = A_ROOM_OVERVIEW.copy(readMarker = anEventId("an-updated-marker")), - previousState = A_ROOM_OVERVIEW, - newEvents = emptyList(), - selfId = aUserId() - ) - - fakeRoomStore.verifyRoomMarkedRead(A_ROOM_OVERVIEW.roomId) - } - - @Test - fun `given new events from other users when processing unread then inserts events as unread`() = runTest { - fakeRoomStore.expect { it.insertUnread(RoomId(any()), any()) } - - unreadEventsProcessor.processUnreadState( - isInitialSync = false, - overview = A_ROOM_OVERVIEW, - previousState = null, - newEvents = listOf(A_ROOM_MESSAGE_FROM_OTHER), - selfId = aUserId() - ) - - fakeRoomStore.verifyInsertsEvents(A_ROOM_OVERVIEW.roomId, listOf(A_ROOM_MESSAGE_FROM_OTHER.eventId)) - } -} diff --git a/matrix/services/sync/src/test/kotlin/internalfake/FakeEventLookup.kt b/matrix/services/sync/src/test/kotlin/internalfake/FakeEventLookup.kt deleted file mode 100644 index 84cb9bc..0000000 --- a/matrix/services/sync/src/test/kotlin/internalfake/FakeEventLookup.kt +++ /dev/null @@ -1,17 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.sync.internal.sync.DecryptedRoomEvents -import app.dapk.st.matrix.sync.internal.sync.DecryptedTimeline -import app.dapk.st.matrix.sync.internal.sync.EventLookupUseCase -import app.dapk.st.matrix.sync.internal.sync.LookupResult -import io.mockk.coEvery -import io.mockk.mockk - -internal class FakeEventLookup { - val instance = mockk() - - fun givenLookup(eventId: EventId, timeline: DecryptedTimeline, previousEvents: DecryptedRoomEvents, result: LookupResult) { - coEvery { instance.lookup(eventId, timeline, previousEvents) } returns result - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventCreator.kt b/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventCreator.kt deleted file mode 100644 index 4d0ee6b..0000000 --- a/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventCreator.kt +++ /dev/null @@ -1,41 +0,0 @@ -package internalfake - -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.RoomEvent -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent -import app.dapk.st.matrix.sync.internal.sync.LookupResult -import app.dapk.st.matrix.sync.internal.sync.RoomEventCreator -import io.mockk.coEvery -import io.mockk.mockk -import io.mockk.slot -import kotlinx.coroutines.runBlocking - -internal class FakeRoomEventCreator { - val instance = mockk() - - fun givenCreates(roomId: RoomId, event: ApiTimelineEvent.Encrypted, result: RoomEvent) { - coEvery { with(instance) { event.toRoomEvent(roomId) } } returns result - } - - fun givenCreatesUsingLookup( - userCredentials: UserCredentials, - roomId: RoomId, - eventIdToLookup: EventId, - event: ApiTimelineEvent.TimelineMessage, - result: RoomEvent, - lookupResult: LookupResult - ) { - val slot = slot LookupResult>() - coEvery { with(instance) { event.toRoomEvent(userCredentials, roomId, capture(slot)) } } answers { - runBlocking { - if (slot.captured.invoke(eventIdToLookup) == lookupResult) { - result - } else { - throw IllegalStateException() - } - } - } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventsDecrypter.kt b/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventsDecrypter.kt deleted file mode 100644 index 773832d..0000000 --- a/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventsDecrypter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter -import io.mockk.coEvery -import io.mockk.mockk - -internal class FakeRoomEventsDecrypter { - val instance = mockk() - - fun givenDecrypts(userCredentials: UserCredentials, previousEvents: List, result: List = previousEvents) { - coEvery { instance.decryptRoomEvents(userCredentials, previousEvents) } returns result - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/internalfake/FakeSyncEventDecrypter.kt b/matrix/services/sync/src/test/kotlin/internalfake/FakeSyncEventDecrypter.kt deleted file mode 100644 index 1006f76..0000000 --- a/matrix/services/sync/src/test/kotlin/internalfake/FakeSyncEventDecrypter.kt +++ /dev/null @@ -1,14 +0,0 @@ -package internalfake - -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent -import app.dapk.st.matrix.sync.internal.room.SyncEventDecrypter -import io.mockk.coEvery -import io.mockk.mockk - -internal class FakeSyncEventDecrypter { - val instance = mockk() - - fun givenDecrypts(events: List, result: List = events) { - coEvery { instance.decryptTimelineEvents(events) } returns result - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/internalfixture/ApiSyncRoomFixture.kt b/matrix/services/sync/src/test/kotlin/internalfixture/ApiSyncRoomFixture.kt deleted file mode 100644 index 8e069ea..0000000 --- a/matrix/services/sync/src/test/kotlin/internalfixture/ApiSyncRoomFixture.kt +++ /dev/null @@ -1,119 +0,0 @@ -package internalfixture - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.internal.request.* -import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent.RoomMember.Content.Membership -import fixture.* - -internal fun anApiSyncRoom( - timeline: ApiSyncRoomTimeline = anApiSyncRoomTimeline(), - state: ApiSyncRoomState = anApiSyncRoomState(), - accountData: ApiAccountData? = null, - ephemeral: ApiEphemeral? = null, -) = ApiSyncRoom(timeline, state, accountData, ephemeral) - -internal fun anApiSyncRoomTimeline( - apiTimelineEvents: List = emptyList(), -) = ApiSyncRoomTimeline(apiTimelineEvents) - -internal fun anApiSyncRoomState( - stateEvents: List = emptyList(), -) = ApiSyncRoomState(stateEvents) - -internal fun anApiEphemeral( - events: List = emptyList() -) = ApiEphemeral(events) - -internal fun anEphemeralTypingEvent( - userIds: List = emptyList(), -) = ApiEphemeralEvent.Typing(ApiEphemeralEvent.Typing.Content(userIds)) - -internal fun anApiTimelineTextEvent( - id: EventId = anEventId(), - senderId: UserId = aUserId(), - content: ApiTimelineEvent.TimelineMessage.Content = aTimelineTextEventContent(), - utcTimestamp: Long = 0L, - decryptionStatus: ApiTimelineEvent.DecryptionStatus? = null -) = ApiTimelineEvent.TimelineMessage(id, senderId, content, utcTimestamp, decryptionStatus) - -internal fun aTimelineTextEventContent( - body: String? = null, - formattedBody: String? = null, - relation: ApiTimelineEvent.TimelineMessage.Relation? = null, -) = ApiTimelineEvent.TimelineMessage.Content.Text(body, formattedBody, relation) - -internal fun anEditRelation(originalId: EventId) = ApiTimelineEvent.TimelineMessage.Relation( - relationType = "m.replace", - inReplyTo = null, - eventId = originalId, -) - -internal fun aReplyRelation(originalId: EventId) = ApiTimelineEvent.TimelineMessage.Relation( - relationType = null, - eventId = null, - inReplyTo = ApiTimelineEvent.TimelineMessage.InReplyTo(originalId), -) - -internal fun anEncryptedApiTimelineEvent( - senderId: UserId = aUserId(), - encryptedContent: ApiEncryptedContent = aMegolmApiEncryptedContent(), - eventId: EventId = anEventId(), - utcTimestamp: Long = 0L, -) = ApiTimelineEvent.Encrypted(senderId, encryptedContent, eventId, utcTimestamp) - -internal fun anEncryptionApiTimelineEvent( - algorithm: AlgorithmName = AlgorithmName("an-algorithm"), - rotationMs: Long? = null, - rotationMessages: Long? = null, -) = ApiTimelineEvent.Encryption(ApiTimelineEvent.Encryption.Content(algorithm, rotationMs, rotationMessages)) - -internal fun aRoomAvatarApiTimelineEvent( - eventId: EventId = anEventId(), - url: MxUrl? = null -) = ApiTimelineEvent.RoomAvatar(eventId, ApiTimelineEvent.RoomAvatar.Content(url)) - -internal fun aRoomCreateApiTimelineEvent( - eventId: EventId = anEventId(), - utcTimestamp: Long = 0L, - type: String? = null -) = ApiTimelineEvent.RoomCreate(eventId, utcTimestamp, ApiTimelineEvent.RoomCreate.Content(type)) - -internal fun aRoomMemberApiTimelineEvent( - eventId: EventId = anEventId(), - senderId: UserId = aUserId(), - displayName: String? = null, - membership: Membership = Membership("join"), - avatarUrl: MxUrl? = null, -) = ApiTimelineEvent.RoomMember(eventId, ApiTimelineEvent.RoomMember.Content(displayName, membership, avatarUrl), senderId) - -internal fun aRoomNameApiTimelineEvent( - eventId: EventId = anEventId(), - name: String = "a-room-name" -) = ApiTimelineEvent.RoomName(eventId, ApiTimelineEvent.RoomName.Content(name)) - -internal fun aRoomTopicApiTimelineEvent( - eventId: EventId = anEventId(), - topic: String = "a-room-topic" -) = ApiTimelineEvent.RoomTopic(eventId, ApiTimelineEvent.RoomTopic.Content(topic)) - -internal fun anIgnoredApiTimelineEvent() = ApiTimelineEvent.Ignored - -internal fun aMegolmApiEncryptedContent( - cipherText: CipherText = aCipherText(), - deviceId: DeviceId = aDeviceId(), - senderKey: String = "a-sender-key", - sessionId: SessionId = aSessionId(), - relation: ApiTimelineEvent.TimelineMessage.Relation? = null, -) = ApiEncryptedContent.MegOlmV1(cipherText, deviceId, senderKey, sessionId, relation) - -internal fun anOlmApiEncryptedContent( - cipherText: Map = mapOf(aCurve25519() to aCipherTextInfo()), - senderKey: Curve25519 = aCurve25519(), -) = ApiEncryptedContent.OlmV1(cipherText, senderKey) - -internal fun aCipherTextInfo( - body: CipherText = aCipherText(), - type: Int = 0, -) = ApiEncryptedContent.CipherTextInfo(body, type) - -internal fun anUnknownApiEncryptedContent() = ApiEncryptedContent.Unknown \ No newline at end of file diff --git a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeFilterStore.kt b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeFilterStore.kt deleted file mode 100644 index b3558df..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeFilterStore.kt +++ /dev/null @@ -1,13 +0,0 @@ -package fake - -import app.dapk.st.matrix.sync.FilterStore -import io.mockk.coEvery -import io.mockk.mockk - -class FakeFilterStore : FilterStore by mockk() { - - fun givenCachedFilter(key: String, filterIdValue: String?) { - coEvery { read(key) } returns filterIdValue - } - -} diff --git a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeMessageDecrypter.kt b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeMessageDecrypter.kt deleted file mode 100644 index 9db39e0..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeMessageDecrypter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package fake - -import app.dapk.st.matrix.common.EncryptedMessageContent -import app.dapk.st.matrix.sync.internal.room.MessageDecrypter -import io.mockk.coEvery -import io.mockk.mockk -import test.delegateReturn - -class FakeMessageDecrypter : MessageDecrypter by mockk() { - - fun givenDecrypt(content: EncryptedMessageContent) = coEvery { decrypt(content) }.delegateReturn() -} \ No newline at end of file diff --git a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomDataSource.kt b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomDataSource.kt deleted file mode 100644 index 85d2de1..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomDataSource.kt +++ /dev/null @@ -1,28 +0,0 @@ -package fake - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomState -import app.dapk.st.matrix.sync.internal.sync.RoomDataSource -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk - -class FakeRoomDataSource { - val instance = mockk() - - fun givenNoCachedRoom(roomId: RoomId) { - coEvery { instance.read(roomId) } returns null - } - - fun givenRoom(roomId: RoomId, roomState: RoomState?) { - coEvery { instance.read(roomId) } returns roomState - } - - fun verifyNoChanges() { - coVerify(exactly = 0) { instance.persist(RoomId(any()), any(), any()) } - } - - fun verifyRoomUpdated(previousEvents: RoomState?, newState: RoomState) { - coVerify { instance.persist(newState.roomOverview.roomId, previousEvents, newState) } - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomMembersService.kt b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomMembersService.kt deleted file mode 100644 index 79def2e..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomMembersService.kt +++ /dev/null @@ -1,19 +0,0 @@ -package fake - -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.sync.RoomMembersService -import io.mockk.coEvery -import io.mockk.mockk - -class FakeRoomMembersService : RoomMembersService by mockk() { - - fun givenMember(roomId: RoomId, userId: UserId, roomMember: RoomMember?) { - coEvery { find(roomId, listOf(userId)) } returns (roomMember?.let { listOf(it) } ?: emptyList()) - } - - fun givenNoMembers(roomId: RoomId) { - coEvery { find(roomId, emptyList()) } returns emptyList() - } -} \ No newline at end of file diff --git a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomStore.kt b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomStore.kt deleted file mode 100644 index 9a6bb1e..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeRoomStore.kt +++ /dev/null @@ -1,47 +0,0 @@ -package fake - -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview -import app.dapk.st.matrix.sync.RoomStore -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.Flow -import test.delegateReturn - -class FakeRoomStore : RoomStore by mockk() { - - fun verifyNoUnreadChanges() { - coVerify(exactly = 0) { insertUnread(RoomId(any()), any()) } - coVerify(exactly = 0) { markRead(RoomId(any())) } - } - - fun verifyRoomMarkedRead(roomId: RoomId) { - coVerify { markRead(roomId) } - } - - fun verifyInsertsEvents(roomId: RoomId, events: List) { - coVerify { insertUnread(roomId, events) } - } - - fun givenEvent(eventId: EventId, result: RoomEvent?) { - coEvery { findEvent(eventId) } returns result - } - - fun givenUnreadEvents(unreadEvents: Flow>>) { - every { observeUnread() } returns unreadEvents - } - - fun givenUnreadEvents() = every { observeUnread() }.delegateReturn() - fun givenUnreadByCount() = every { observeUnreadCountById() }.delegateReturn() - - fun givenNotMutedUnreadEvents(unreadEvents: Flow>>) { - every { observeNotMutedUnread() } returns unreadEvents - } - - fun givenMuted() = every { observeMuted() }.delegateReturn() - -} \ No newline at end of file diff --git a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt deleted file mode 100644 index d0acc50..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt +++ /dev/null @@ -1,16 +0,0 @@ -package fake - -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.SyncService -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import test.delegateReturn - -class FakeSyncService : SyncService by mockk() { - fun givenStartsSyncing() = every { startSyncing() }.returns(flowOf(Unit)) - fun givenRoom(roomId: RoomId) = every { room(roomId) }.delegateReturn() - fun givenEvents(roomId: RoomId? = null) = every { events(roomId) }.delegateReturn() - fun givenInvites() = every { invites() }.delegateReturn() - fun givenOverview() = every { overview() }.delegateReturn() -} diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt deleted file mode 100644 index 0d20981..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt +++ /dev/null @@ -1,51 +0,0 @@ -package fixture - -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.sync.MessageMeta -import app.dapk.st.matrix.sync.RoomEvent - -fun aMatrixRoomMessageEvent( - 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 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 = aMatrixRoomMessageEvent(), - replyingTo: RoomEvent = aMatrixRoomMessageEvent(eventId = anEventId("in-reply-to-id")), -) = RoomEvent.Reply(message, replyingTo) - -fun anEncryptedRoomMessageEvent( - eventId: EventId = anEventId(), - utcTimestamp: Long = 0L, - author: RoomMember = aRoomMember(), - meta: MessageMeta = MessageMeta.FromServer, - encryptedContent: RoomEvent.Encrypted.MegOlmV1 = aMegolmV1(), - edited: Boolean = false, -) = RoomEvent.Encrypted(eventId, utcTimestamp, author, meta, edited, encryptedContent) - -fun aMegolmV1( - cipherText: CipherText = CipherText("a-cipher"), - deviceId: DeviceId = aDeviceId(), - senderKey: String = "a-sender-key", - sessionId: SessionId = aSessionId(), -) = RoomEvent.Encrypted.MegOlmV1(cipherText, deviceId, senderKey, sessionId) - -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) \ No newline at end of file diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt deleted file mode 100644 index 03c019f..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt +++ /dev/null @@ -1,25 +0,0 @@ -package fixture - -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 app.dapk.st.matrix.sync.LastMessage -import app.dapk.st.matrix.sync.RoomOverview - -fun aMatrixRoomOverview( - roomId: RoomId = aRoomId(), - roomCreationUtc: Long = 0L, - roomName: String? = null, - roomAvatarUrl: AvatarUrl? = null, - lastMessage: LastMessage? = null, - isGroup: Boolean = false, - readMarker: EventId? = null, - isEncrypted: Boolean = false, -) = RoomOverview(roomId, roomCreationUtc, roomName, roomAvatarUrl, lastMessage, isGroup, readMarker, isEncrypted) - -fun aLastMessage( - content: String = "last-message-content", - utcTimestamp: Long = 0L, - author: RoomMember = aRoomMember(), -) = LastMessage(content, utcTimestamp, author) \ No newline at end of file diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt deleted file mode 100644 index 511bcf4..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt +++ /dev/null @@ -1,10 +0,0 @@ -package fixture - -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview -import app.dapk.st.matrix.sync.RoomState - -fun aMatrixRoomState( - roomOverview: RoomOverview = aMatrixRoomOverview(), - events: List = listOf(aMatrixRoomMessageEvent()), -) = RoomState(roomOverview, events) \ No newline at end of file diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/SyncServiceFixtures.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/SyncServiceFixtures.kt deleted file mode 100644 index 24b1cba..0000000 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/SyncServiceFixtures.kt +++ /dev/null @@ -1,2 +0,0 @@ -package fixture - diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..39a2b6e --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} diff --git a/screen-state b/screen-state new file mode 160000 index 0000000..337a65b --- /dev/null +++ b/screen-state @@ -0,0 +1 @@ +Subproject commit 337a65b27b9911205e52a87c075be4bbf70a557d diff --git a/settings.gradle b/settings.gradle index bd5242f..17580aa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,11 +1,21 @@ -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - apply from: "dependencies.gradle" +pluginManagement { + includeBuild 'tools/conventions' repositories { - Dependencies._repositories.call(it) + gradlePluginPortal() + google() } } +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + apply from: "gradle/repositories.gradle" + applyRepositories(it) +} + rootProject.name = "SmallTalk" + +includeBuild 'screen-state' +includeBuild 'chat-engine' + include ':app' include ':design-library' @@ -31,31 +41,10 @@ include ':domains:android:push' include ':domains:android:viewmodel-stub' include ':domains:android:viewmodel' include ':domains:store' -include ':domains:olm-stub' -include ':domains:olm' -include ':domains:state' include ':domains:firebase:crashlytics' include ':domains:firebase:crashlytics-noop' include ':domains:firebase:messaging' include ':domains:firebase:messaging-noop' -include ':matrix:matrix' -include ':matrix:common' -include ':matrix:matrix-http' -include ':matrix:matrix-http-ktor' -include ':matrix:services:auth' -include ':matrix:services:sync' -include ':matrix:services:room' -include ':matrix:services:push' -include ':matrix:services:message' -include ':matrix:services:device' -include ':matrix:services:crypto' -include ':matrix:services:profile' - -include ':core' - -include ':test-harness' - -include ':chat-engine' -include ':matrix-chat-engine' \ No newline at end of file +include ':core' \ No newline at end of file diff --git a/test-harness/build.gradle b/test-harness/build.gradle deleted file mode 100644 index f851278..0000000 --- a/test-harness/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - id 'kotlin' - id 'org.jetbrains.kotlin.plugin.serialization' -} - -test { - useJUnitPlatform() -} - -dependencies { - kotlinTest(it) - testImplementation 'app.cash.turbine:turbine:0.12.1' - - testImplementation Dependencies.mavenCentral.kotlinSerializationJson - - testImplementation project(":core") - testImplementation project(":domains:store") - testImplementation project(":domains:olm") - - testImplementation project(":matrix:matrix") - testImplementation project(":matrix:matrix-http-ktor") - testImplementation project(":matrix:services:auth") - testImplementation project(":matrix:services:sync") - testImplementation project(":matrix:services:room") - testImplementation project(":matrix:services:push") - testImplementation project(":matrix:services:message") - testImplementation project(":matrix:services:device") - testImplementation project(":matrix:services:crypto") - - testImplementation rootProject.files("external/jolm.jar") - testImplementation 'org.json:json:20220924' - - testImplementation Dependencies.mavenCentral.ktorJava - testImplementation Dependencies.mavenCentral.sqldelightInMemory -} diff --git a/test-harness/src/test/kotlin/SmokeTest.kt b/test-harness/src/test/kotlin/SmokeTest.kt deleted file mode 100644 index 488680c..0000000 --- a/test-harness/src/test/kotlin/SmokeTest.kt +++ /dev/null @@ -1,218 +0,0 @@ -import app.dapk.st.matrix.auth.AuthService -import app.dapk.st.matrix.auth.authService -import app.dapk.st.matrix.common.HomeServerUrl -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.crypto.ImportResult -import app.dapk.st.matrix.crypto.Verification -import app.dapk.st.matrix.crypto.cryptoService -import app.dapk.st.matrix.room.roomService -import app.dapk.st.matrix.sync.syncService -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeInstanceOf -import org.amshove.kluent.shouldNotBeEqualTo -import org.junit.jupiter.api.MethodOrderer -import org.junit.jupiter.api.Order -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestMethodOrder -import test.* -import java.nio.file.Paths -import java.util.* - -private const val HTTPS_TEST_SERVER_URL = "https://localhost:8080/" - -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -class SmokeTest { - - @Test - @Order(1) - fun `can register accounts`() = runTest { - SharedState._alice = createAndRegisterAccount("alice") - SharedState._bob = createAndRegisterAccount("bob") - } - - @Test - @Order(2) - fun `can login`() = runTest { - login(SharedState.alice) - login(SharedState.bob) - } - - @Test - @Order(3) - fun `can create and join rooms`() = flowTest { - val alice = TestMatrix(SharedState.alice, includeLogging = true).also { it.loginWithInitialSync() } - - val roomId = alice.client.roomService().createDm(SharedState.bob.roomMember.id, encrypted = true) - alice.client.syncService().startSyncing().collectAsync { - alice.expectRoom(roomId) - } - - val bob = TestMatrix(SharedState.bob, includeLogging = true).also { it.loginWithInitialSync() } - bob.client.syncService().startSyncing().collectAsync { - bob.expectInvite(roomId) - bob.client.roomService().joinRoom(roomId) - bob.expectRoom(roomId) - } - - SharedState._sharedRoom = roomId - } - - @Test - @Order(4) - fun `can send and receive clear text messages`() = testTextMessaging(isEncrypted = false) - - @Test - @Order(5) - fun `can send and receive encrypted text messages`() = testTextMessaging(isEncrypted = true) - - @Test - @Order(6) - fun `can send and receive clear image messages`() = testAfterInitialSync { alice, bob -> - val testImage = loadResourceFile("test-image.png") - alice.sendImageMessage(SharedState.sharedRoom, testImage, isEncrypted = false) - bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember) - } - - @Test - @Order(7) - fun `can send and receive encrypted image messages`() = testAfterInitialSync { alice, bob -> - val testImage = loadResourceFile("test-image2.png") - alice.sendImageMessage(SharedState.sharedRoom, testImage, isEncrypted = true) - bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember) - } - - @Test - @Order(8) - fun `can request and verify devices`() = testAfterInitialSync { alice, bob -> - alice.client.cryptoService().verificationAction(Verification.Action.Request(bob.userId(), bob.deviceId())) - alice.client.cryptoService().verificationState().automaticVerification(alice).expectAsync { it == Verification.State.Done } - bob.client.cryptoService().verificationState().automaticVerification(bob).expectAsync { it == Verification.State.Done } - - waitForExpects() - } - - @Test - fun `can import E2E room keys file`() = runTest { - val ignoredUser = TestUser("ignored", RoomMember(UserId("ignored"), null, null), "ignored", "ignored") - val cryptoService = TestMatrix(ignoredUser, includeLogging = true).client.cryptoService() - val stream = loadResourceStream("element-keys.txt") - - val result = with(cryptoService) { - stream.importRoomKeys(password = "aaaaaa").first { it is ImportResult.Success } - } - - result shouldBeEqualTo ImportResult.Success( - roomIds = setOf(RoomId(value = "!qOSENTtFUuCEKJSVzl:matrix.org")), - totalImportedKeysCount = 28, - ) - } - - private fun testTextMessaging(isEncrypted: Boolean) = testAfterInitialSync { alice, bob -> - val message = "from alice to bob".from(SharedState.alice.roomMember) - alice.sendTextMessage(SharedState.sharedRoom, message.content, isEncrypted) - bob.expectTextMessage(SharedState.sharedRoom, message) - - val message2 = "from bob to alice".from(SharedState.bob.roomMember) - bob.sendTextMessage(SharedState.sharedRoom, message2.content, isEncrypted) - alice.expectTextMessage(SharedState.sharedRoom, message2) - - // Needs investigation - val aliceSecondDevice = testMatrix(SharedState.alice, isTemp = true, withLogging = true).also { it.newlogin() } - aliceSecondDevice.client.syncService().startSyncing().collectAsync { - aliceSecondDevice.client.proxyDeviceService().waitForOneTimeKeysToBeUploaded() - - val message3 = "from alice to bob and alice's second device".from(SharedState.alice.roomMember) - alice.sendTextMessage(SharedState.sharedRoom, message3.content, isEncrypted) - aliceSecondDevice.expectTextMessage(SharedState.sharedRoom, message3) - bob.expectTextMessage(SharedState.sharedRoom, message3) - - val message4 = "from alice's second device to bob and alice's first device".from(SharedState.alice.roomMember) - aliceSecondDevice.sendTextMessage(SharedState.sharedRoom, message4.content, isEncrypted) - alice.expectTextMessage(SharedState.sharedRoom, message4) - bob.expectTextMessage(SharedState.sharedRoom, message4) - } - } -} - -private suspend fun createAndRegisterAccount(testUsername: String): TestUser { - val aUserName = "${UUID.randomUUID()}" - val userId = UserId("@$aUserName:localhost:8080") - val aUser = TestUser("aaaa11111zzzz", RoomMember(userId, aUserName, null), HTTPS_TEST_SERVER_URL, testUsername) - - val result = TestMatrix(aUser, includeLogging = true, includeHttpLogging = true) - .client - .authService() - .register(aUserName, aUser.password, homeServer = HTTPS_TEST_SERVER_URL) - - result.accessToken shouldNotBeEqualTo null - result.homeServer shouldBeEqualTo HomeServerUrl(HTTPS_TEST_SERVER_URL) - result.userId shouldBeEqualTo userId - return aUser -} - -private suspend fun login(user: TestUser) { - val testMatrix = TestMatrix(user, includeLogging = true) - val result = testMatrix - .client - .authService() - .login(AuthService.LoginRequest(userName = user.roomMember.id.value, password = user.password, serverUrl = null)) - - result shouldBeInstanceOf AuthService.LoginResult.Success::class.java - (result as AuthService.LoginResult.Success).userCredentials.let { credentials -> - credentials.accessToken shouldNotBeEqualTo null - credentials.homeServer shouldBeEqualTo HomeServerUrl(HTTPS_TEST_SERVER_URL) - credentials.userId shouldBeEqualTo user.roomMember.id - - testMatrix.saveLogin(credentials) - } -} - -object SharedState { - - val alice: TestUser - get() = _alice!! - var _alice: TestUser? = null - set(value) { - field = value!! - TestUsers.users.add(value) - } - - val bob: TestUser - get() = _bob!! - var _bob: TestUser? = null - set(value) { - field = value!! - TestUsers.users.add(value) - } - - val sharedRoom: RoomId - get() = _sharedRoom!! - var _sharedRoom: RoomId? = null -} - -data class TestUser(val password: String, val roomMember: RoomMember, val homeServer: String, val testName: String) -data class TestMessage(val content: String, val author: RoomMember) - -fun String.from(roomMember: RoomMember) = TestMessage("$this - ${UUID.randomUUID()}", roomMember) - -fun testAfterInitialSync(block: suspend MatrixTestScope.(TestMatrix, TestMatrix) -> Unit) { - restoreLoginAndInitialSync(TestMatrix(SharedState.alice, includeLogging = true), TestMatrix(SharedState.bob, includeLogging = false), block) -} - -private fun Flow.automaticVerification(testMatrix: TestMatrix) = this.onEach { - when (it) { - is Verification.State.WaitingForMatchConfirmation -> testMatrix.client.cryptoService().verificationAction(Verification.Action.AcknowledgeMatch) - else -> { - // do nothing - } - } -} - -private fun loadResourceStream(name: String) = Thread.currentThread().contextClassLoader.getResourceAsStream(name)!! -private fun loadResourceFile(name: String) = Paths.get(Thread.currentThread().contextClassLoader.getResource(name)!!.toURI()).toFile() diff --git a/test-harness/src/test/kotlin/io/ktor/client/engine/java/AJava.kt b/test-harness/src/test/kotlin/io/ktor/client/engine/java/AJava.kt deleted file mode 100644 index b08a5d8..0000000 --- a/test-harness/src/test/kotlin/io/ktor/client/engine/java/AJava.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.ktor.client.engine.java - -import io.ktor.client.* -import io.ktor.client.engine.* -import io.ktor.util.* -import java.security.SecureRandom -import java.security.cert.X509Certificate -import javax.net.ssl.SSLContext -import javax.net.ssl.X509TrustManager - -/** - * Work around to disable SSL for local syanpse testing - * Ktor loads http engines by name, hence the JavaHttpEngineContainer name being 1 - */ -@OptIn(InternalAPI::class) -object TestJava : HttpClientEngineFactory { - init { - System.getProperties().setProperty("jdk.internal.httpclient.disableHostnameVerification", "true") - } - - override fun create(block: JavaHttpConfig.() -> Unit): HttpClientEngine { - val config = JavaHttpConfig().apply(block).also { - it.config { - val apply = SSLContext.getInstance("TLS").apply { - init(null, arrayOf(TrustAllX509TrustManager()), SecureRandom()) - } - sslContext(apply) - } - } - return JavaHttpEngine(config) - } -} - -@Suppress("KDocMissingDocumentation") -class JavaHttpEngineContainer : HttpClientEngineContainer { - override val factory: HttpClientEngineFactory<*> = TestJava - - override fun toString(): String = "1" -} - -private class TrustAllX509TrustManager : X509TrustManager { - override fun getAcceptedIssuers(): Array? = null - override fun checkClientTrusted(certs: Array?, authType: String?) {} - override fun checkServerTrusted(certs: Array?, authType: String?) {} -} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/Test.kt b/test-harness/src/test/kotlin/test/Test.kt deleted file mode 100644 index 731927c..0000000 --- a/test-harness/src/test/kotlin/test/Test.kt +++ /dev/null @@ -1,232 +0,0 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) - -package test - -import TestMessage -import TestUser -import app.dapk.st.core.extensions.ifNull -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.asString -import app.dapk.st.matrix.crypto.MatrixMediaDecrypter -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.message.messageService -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.syncService -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.util.cio.* -import io.ktor.utils.io.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.fail -import org.amshove.kluent.shouldBeEqualTo -import java.io.ByteArrayOutputStream -import java.io.File -import java.math.BigInteger -import java.security.MessageDigest -import java.util.* - -fun flowTest(block: suspend MatrixTestScope.() -> Unit) { - runTest { - val testScope = MatrixTestScope(this) - block(testScope) - } -} - -fun restoreLoginAndInitialSync(m1: TestMatrix, m2: TestMatrix, testBody: suspend MatrixTestScope.(TestMatrix, TestMatrix) -> Unit) { - runTest { - println("restore login 1") - m1.restoreLogin() - println("restore login 2") - m2.restoreLogin() - val testHelper = MatrixTestScope(this) - testHelper.testMatrix(m1) - testHelper.testMatrix(m2) - with(testHelper) { - combine(m1.client.syncService().startSyncing(), m2.client.syncService().startSyncing()) { _, _ -> }.collectAsync { - m1.client.syncService().overview().first() - m2.client.syncService().overview().first() - testBody(testHelper, m1, m2) - } - } - testHelper.release() - } -} - -suspend fun Flow.collectAsync(scope: CoroutineScope, block: suspend () -> Unit) { - val work = scope.async { - withContext(Dispatchers.IO) { - collect() - } - } - block() - work.cancelAndJoin() -} - -class MatrixTestScope(private val testScope: TestScope) { - - private val inProgressExpects = mutableListOf>() - private val inProgressInstances = mutableListOf() - - suspend fun Flow.collectAsync(block: suspend () -> Unit) { - collectAsync(testScope, block) - } - - suspend fun delay(amountMs: Long) { - withContext(Dispatchers.Unconfined) { - kotlinx.coroutines.delay(amountMs) - } - } - - suspend fun Flow.expectAsync(matcher: (T) -> Boolean) { - val flow = this - - inProgressExpects.add(testScope.async(Dispatchers.Unconfined) { - flow.first { matcher(it) } - }) - } - - suspend fun waitForExpects() { - inProgressExpects.awaitAll() - } - - suspend fun Flow.expect(matcher: (T) -> Boolean) { - val flow = this - - val collected = mutableListOf() - val work = testScope.async { - flow.onEach { - collected.add(it) - }.first { matcher(it) } - } - withContext(Dispatchers.Unconfined) { - withTimeoutOrNull(5000) { work.await() } - }.ifNull { - fail("found no matches in $collected") - } - } - - suspend fun Flow.assert(expected: T) { - val flow = this - - val collected = mutableListOf() - val work = testScope.async { - flow.onEach { - println("found: $it") - collected.add(it) - }.first { it == expected } - } - withContext(Dispatchers.IO) { - withTimeoutOrNull(5000) { work.await() } - } - collected.lastOrNull() shouldBeEqualTo expected - } - - suspend fun TestMatrix.expectRoom(roomId: RoomId) { - this.client.syncService().overview() - .expect { it.any { it.roomId == roomId } } - } - - suspend fun TestMatrix.expectInvite(roomId: RoomId) { - this.client.syncService().invites() - .expect { it.any { it.roomId == roomId } } - } - - suspend fun TestMatrix.expectTextMessage(roomId: RoomId, message: TestMessage) { - println("expecting ${message.content}") - this.client.syncService().room(roomId) - .map { it.events.filterIsInstance().map { TestMessage(it.content.asString(), it.author) }.firstOrNull() } - .assert(message) - } - - suspend fun TestMatrix.expectImageMessage(roomId: RoomId, image: File, author: RoomMember) { - println("expecting ${image.absolutePath} from ${author.displayName}") - this.client.syncService().room(roomId) - .map { - it.events.filterIsInstance().map { - val output = File(image.parentFile.absolutePath, "output.png") - HttpClient().request(it.imageMeta.url).bodyAsChannel().copyAndClose(output.writeChannel()) - val md5Hash = when (val keys = it.imageMeta.keys) { - null -> output.readBytes().md5Hash() - else -> { - val byteStream = ByteArrayOutputStream() - MatrixMediaDecrypter(this.base64).decrypt(output.inputStream(), keys.k, keys.iv).collect { - byteStream.write(it) - } - byteStream.toByteArray().md5Hash() - } - } - - md5Hash to it.author - }.firstOrNull() - } - .assert(image.readBytes().md5Hash() to author) - } - - suspend fun TestMatrix.sendTextMessage(roomId: RoomId, content: String, isEncrypted: Boolean) { - println("sending $content") - this.client.messageService().scheduleMessage( - MessageService.Message.TextMessage( - content = MessageService.Message.Content.TextContent(body = RichText.of(content)), - roomId = roomId, - sendEncrypted = isEncrypted, - localId = "local.${UUID.randomUUID()}", - timestampUtc = System.currentTimeMillis(), - ) - ) - } - - suspend fun TestMatrix.sendImageMessage(roomId: RoomId, file: File, isEncrypted: Boolean) { - println("sending ${file.name}") - this.client.messageService().scheduleMessage( - MessageService.Message.ImageMessage( - content = MessageService.Message.Content.ImageContent( - uri = file.absolutePath, - meta = JavaImageContentReader().meta(file.absolutePath).let { - MessageService.Message.Content.ImageContent.Meta( - height = it.height, - width = it.width, - size = it.size, - fileName = it.fileName, - mimeType = it.mimeType, - ) - } - ), - roomId = roomId, - sendEncrypted = isEncrypted, - localId = "local.${UUID.randomUUID()}", - timestampUtc = System.currentTimeMillis(), - ) - ) - } - - suspend fun TestMatrix.loginWithInitialSync() { - this.restoreLogin() - client.syncService().startSyncing().collectAsync(testScope) { - client.syncService().overview().first() - } - } - - fun testMatrix(user: TestUser, isTemp: Boolean, withLogging: Boolean = false) = TestMatrix( - user, - temporaryDatabase = isTemp, - includeLogging = withLogging - ).also { inProgressInstances.add(it) } - - fun testMatrix(testMatrix: TestMatrix) = inProgressInstances.add(testMatrix) - - suspend fun release() { - inProgressInstances.forEach { it.release() } - } -} - -private fun ByteArray.md5Hash(): String { - val md = MessageDigest.getInstance("MD5") - val bigInt = BigInteger(1, md.digest(this)) - return String.format("%032x", bigInt) -} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/TestExtensions.kt b/test-harness/src/test/kotlin/test/TestExtensions.kt deleted file mode 100644 index 0e2beaa..0000000 --- a/test-harness/src/test/kotlin/test/TestExtensions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package test - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.last -import kotlinx.coroutines.flow.take - -fun Any.print() = this.also { println(this) } - -suspend fun Flow.collectItem(count: Int): T { - return this.take(count).last() -} - -fun T.unit() = Unit - diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt deleted file mode 100644 index 088340d..0000000 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ /dev/null @@ -1,392 +0,0 @@ -package test - -import TestUser -import app.dapk.st.core.Base64 -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.SingletonFlows -import app.dapk.st.domain.StoreModule -import app.dapk.st.matrix.MatrixClient -import app.dapk.st.matrix.auth.AuthService -import app.dapk.st.matrix.auth.authService -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.DeviceService -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.RoomMessenger -import app.dapk.st.matrix.room.installRoomService -import app.dapk.st.matrix.room.internal.SingleRoomStore -import app.dapk.st.matrix.room.roomService -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.OlmPersistenceWrapper -import app.dapk.st.olm.OlmWrapper -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.serialization.json.Json -import org.amshove.kluent.fail -import test.impl.InMemoryDatabase -import test.impl.InMemoryPreferences -import test.impl.InstantScheduler -import test.impl.PrintingErrorTracking -import java.io.File -import java.time.Clock -import javax.imageio.ImageIO -import kotlin.coroutines.resume - -object TestUsers { - - val users = mutableSetOf() - -} - -class TestMatrix( - private val user: TestUser, - temporaryDatabase: Boolean = false, - includeHttpLogging: Boolean = false, - includeLogging: Boolean = false, -) { - - private val errorTracker = PrintingErrorTracking(prefix = user.testName) - private val logger: MatrixLogger = { tag, message -> - if (includeLogging) { - val messageWithIdReplaceByName = TestUsers.users.fold(message) { acc, user -> acc.replace(user.roomMember.id.value, "*${user.testName}") } - println("${user.testName} $tag $messageWithIdReplaceByName") - } - } - - private val preferences = InMemoryPreferences() - private val database = when (temporaryDatabase) { - true -> InMemoryDatabase.temp() - false -> InMemoryDatabase.realInstance(user.roomMember.id.value) - } - private val coroutineDispatchers = CoroutineDispatchers( - Dispatchers.Unconfined, - main = Dispatchers.Unconfined, - global = CoroutineScope(Dispatchers.Unconfined) - ) - - private val storeModule = StoreModule( - database = database, - preferences = preferences, - errorTracker = errorTracker, - credentialPreferences = preferences, - databaseDropper = { - // do nothing - }, - coroutineDispatchers = coroutineDispatchers - ) - val base64 = JavaBase64() - - val client = MatrixClient( - KtorMatrixHttpClientFactory( - storeModule.credentialsStore(), - includeLogging = includeHttpLogging, - ), - logger - ).also { - it.install { - installAuthService(storeModule.credentialsStore()) - installEncryptionService(storeModule.knownDevicesStore()).proxy { - ProxyDeviceService(it) - } - - val olmAccountStore = OlmPersistenceWrapper(storeModule.olmStore(), base64) - val olm = OlmWrapper( - olmStore = olmAccountStore, - singletonFlows = SingletonFlows(coroutineDispatchers), - jsonCanonicalizer = JsonCanonicalizer(), - deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()), - errorTracker = errorTracker, - logger = logger, - clock = Clock.systemUTC(), - coroutineDispatchers = coroutineDispatchers, - ) - installCryptoService( - storeModule.credentialsStore(), - olm, - roomMembersProvider = { services -> - RoomMembersProvider { - services.roomService().joinedMembers(it).map { it.userId } - } - }, - base64 = base64, - coroutineDispatchers = coroutineDispatchers, - ) - - installMessageService( - localEchoStore = storeModule.localEchoStore, - backgroundScheduler = InstantScheduler(it), - imageContentReader = JavaImageContentReader(), - messageEncrypter = { - val cryptoService = it.cryptoService() - MessageEncrypter { message -> - val result = cryptoService.encrypt( - roomId = message.roomId, - credentials = storeModule.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( - storeModule.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 = { storeModule.overviewStore().removeInvites(listOf(it)) }, - singleRoomStore = singleRoomStoreAdapter(storeModule.roomStore()) - ) - - installSyncService( - storeModule.credentialsStore(), - storeModule.overviewStore(), - storeModule.roomStore(), - storeModule.syncStore(), - storeModule.filterStore(), - deviceNotifier = { services -> - val encryptionService = services.deviceService() - val cryptoService = services.cryptoService() - DeviceNotifier { userIds, syncToken -> - encryptionService.updateStaleDevices(userIds) - cryptoService.updateOlmSession(userIds, syncToken) - } - }, - messageDecrypter = { serviceProvider -> - MessageDecrypter { - serviceProvider.cryptoService().decrypt(it) - } - }, - keySharer = { serviceProvider -> - KeySharer { sharedRoomKeys -> - serviceProvider.cryptoService().importRoomKeys(sharedRoomKeys) - } - }, - verificationHandler = { services -> - val cryptoService = services.cryptoService() - VerificationHandler { apiEvent -> - logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $apiEvent") - 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.VerificationAccept -> Verification.Event.Accepted( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.method, - apiEvent.content.protocol, - apiEvent.content.hash, - apiEvent.content.code, - apiEvent.content.short, - apiEvent.content.transactionId, - ) - - is ApiToDeviceEvent.VerificationCancel -> 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, - syncConfig = SyncConfig(loopTimeout = 500, allowSharedFlows = false) - ) - installPushService(storeModule.credentialsStore()) - } - } - - suspend fun newlogin() { - val result = client.authService() - .login(AuthService.LoginRequest(user.roomMember.id.value, user.password, null)) - - if (result !is AuthService.LoginResult.Success) { - fail("Login failed: $result") - } - } - - suspend fun restoreLogin() { - val userId = this@TestMatrix.user.roomMember.id - val json = TestPersistence(prefix = "").readJson("credentials-${userId.value}.json")!! - val credentials = Json.decodeFromString(UserCredentials.serializer(), json) - storeModule.credentialsStore().update(credentials) - logger.matrixLog("restored: ${credentials.userId} : ${credentials.deviceId}") - } - - suspend fun saveLogin(result: UserCredentials) { - val userId = result.userId - TestPersistence(prefix = "").put("credentials-${userId.value}.json", UserCredentials.serializer(), result) - } - - suspend fun deviceId() = storeModule.credentialsStore().credentials()!!.deviceId - suspend fun userId() = storeModule.credentialsStore().credentials()!!.userId - suspend fun credentials() = storeModule.credentialsStore().credentials()!! - - suspend fun release() { - coroutineDispatchers.global.waitForCancel() - coroutineDispatchers.io.waitForCancel() - coroutineDispatchers.main.waitForCancel() - } -} - -private suspend fun CoroutineDispatcher.waitForCancel() { - if (this.isActive) { - this.job.cancelAndJoin() - } -} - -private suspend fun CoroutineScope.waitForCancel() { - if (this.isActive) { - this.coroutineContext.job.cancelAndJoin() - } -} - -class JavaBase64 : Base64 { - override fun encode(input: ByteArray): String { - return java.util.Base64.getEncoder().encode(input).toString(Charsets.UTF_8) - } - - override fun decode(input: String): ByteArray { - return java.util.Base64.getDecoder().decode(input) - } -} - -class JavaImageContentReader : ImageContentReader { - - override fun meta(uri: String): ImageContentReader.ImageContent { - val file = File(uri) - val size = file.length() - val image = ImageIO.read(file) - return ImageContentReader.ImageContent( - height = image.height, - width = image.width, - size = size, - mimeType = "image/${file.extension}", - fileName = file.name, - ) - } - - override fun inputStream(uri: String) = File(uri).inputStream() - -} - -class ProxyDeviceService(private val deviceService: DeviceService) : DeviceService by deviceService { - - private var oneTimeKeysContinuation: (() -> Unit)? = null - - override suspend fun uploadOneTimeKeys(oneTimeKeys: DeviceService.OneTimeKeys) { - deviceService.uploadOneTimeKeys(oneTimeKeys) - oneTimeKeysContinuation?.invoke()?.also { oneTimeKeysContinuation = null } - } - - suspend fun waitForOneTimeKeysToBeUploaded() { - suspendCancellableCoroutine { continuation -> - oneTimeKeysContinuation = { continuation.resume(Unit) } - } - } - -} - -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() -} - -fun MatrixClient.proxyDeviceService() = this.deviceService() as ProxyDeviceService \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/TestPersistence.kt b/test-harness/src/test/kotlin/test/TestPersistence.kt deleted file mode 100644 index c28a578..0000000 --- a/test-harness/src/test/kotlin/test/TestPersistence.kt +++ /dev/null @@ -1,72 +0,0 @@ -package test - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.Json -import java.io.ByteArrayInputStream -import java.io.File -import java.io.ObjectInputStream -import java.io.Serializable -import java.util.* - -class TestPersistence( - prefix: String, -) { - - private val dir = File("build/st-test-persistence/${prefix}") - - init { - dir.mkdirs() - } - - suspend fun put(fileName: String, serializer: KSerializer, value: T) { - writeFile(fileName, Json.encodeToString(serializer, value)) - } - - suspend fun readOrPut(fileName: String, serializer: KSerializer, storeAction: suspend () -> T): T { - val file = File(dir, fileName) - - return if (file.exists()) { - Json.decodeFromString(serializer, file.readText()).also { - println("restored $it from $fileName") - } - } else { - storeAction().also { result -> - writeFile(fileName, Json.encodeToString(serializer, result)) - } - } - } - - suspend fun read(fileName: String): T? { - val file = File(dir, fileName) - - return if (file.exists()) { - val text = file.readBytes() - val decoded = Base64.getDecoder().decode(text) - ObjectInputStream(ByteArrayInputStream(decoded)).use { - it.readObject() as T - }.also { - println("restored $it from $fileName") - } - } else { - null - } - } - - suspend fun readJson(fileName: String): String? { - val file = File(dir, fileName) - - return if (file.exists()) { - file.readText() - } else { - null - } - } - - private fun writeFile(name: String, jsonContent: String) { - ensureFile(name).writeText(jsonContent) - } - - private fun ensureFile(fileName: String) = File(dir, fileName).also { - it.parentFile?.mkdirs() - } -} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/impl/InMemoryDatabase.kt b/test-harness/src/test/kotlin/test/impl/InMemoryDatabase.kt deleted file mode 100644 index 887b1e0..0000000 --- a/test-harness/src/test/kotlin/test/impl/InMemoryDatabase.kt +++ /dev/null @@ -1,29 +0,0 @@ -package test.impl - -import app.dapk.db.DapkDb -import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver -import java.io.File - -object InMemoryDatabase { - - fun realInstance(id: String): DapkDb { - val dbDir = "build/smalltalk-test-persistence" - val dbPath = "$dbDir/test-$id.db" - return DapkDb(JdbcSqliteDriver( - url = "jdbc:sqlite:${File(dbPath).absolutePath}", - ).also { - if (!File(dbPath).exists()) { - File(dbDir).mkdirs() - DapkDb.Schema.create(it) - } - }) - } - - fun temp(): DapkDb { - val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) - return DapkDb(driver).also { - DapkDb.Schema.create(driver) - } - } - -} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/impl/InMemoryPreferences.kt b/test-harness/src/test/kotlin/test/impl/InMemoryPreferences.kt deleted file mode 100644 index 3390155..0000000 --- a/test-harness/src/test/kotlin/test/impl/InMemoryPreferences.kt +++ /dev/null @@ -1,23 +0,0 @@ -package test.impl - -import app.dapk.st.core.Preferences - -class InMemoryPreferences : Preferences { - - private val prefs = mutableMapOf() - - override suspend fun store(key: String, value: String) { - prefs[key] = value - } - - override suspend fun readString(key: String): String? = prefs[key] - - override suspend fun remove(key: String) { - prefs.remove(key) - } - - override suspend fun clear() { - prefs.clear() - } - -} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/impl/InstantScheduler.kt b/test-harness/src/test/kotlin/test/impl/InstantScheduler.kt deleted file mode 100644 index 2cdf443..0000000 --- a/test-harness/src/test/kotlin/test/impl/InstantScheduler.kt +++ /dev/null @@ -1,21 +0,0 @@ -package test.impl - -import app.dapk.st.matrix.MatrixClient -import app.dapk.st.matrix.MatrixTaskRunner -import app.dapk.st.matrix.message.BackgroundScheduler -import kotlinx.coroutines.runBlocking - -class InstantScheduler(private val matrixClient: MatrixClient) : BackgroundScheduler { - - override fun schedule(key: String, task: BackgroundScheduler.Task) { - runBlocking { - matrixClient.run( - MatrixTaskRunner.MatrixTask( - task.type, - task.jsonPayload.value, - ) - ) - } - } - -} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/impl/PrintingErrorTracking.kt b/test-harness/src/test/kotlin/test/impl/PrintingErrorTracking.kt deleted file mode 100644 index 4f69ca7..0000000 --- a/test-harness/src/test/kotlin/test/impl/PrintingErrorTracking.kt +++ /dev/null @@ -1,10 +0,0 @@ -package test.impl - -import app.dapk.st.core.extensions.ErrorTracker - -class PrintingErrorTracking(private val prefix: String) : ErrorTracker { - override fun track(throwable: Throwable, extra: String) { - println("$prefix ${throwable.message}") - throwable.printStackTrace() - } -} \ No newline at end of file diff --git a/test-harness/src/test/resources/element-keys.txt b/test-harness/src/test/resources/element-keys.txt deleted file mode 100644 index 705470f..0000000 --- a/test-harness/src/test/resources/element-keys.txt +++ /dev/null @@ -1,176 +0,0 @@ ------BEGIN MEGOLM SESSION DATA----- -AUbSvWJ63z3psDZYYkNws81PR0C9+ls7NRK8BSge0kAOAAehIAPm4olji/OSyZh32QBCxoGCGjZS/RD3QT1ScWtlw+aAufY79DZFwODOjbI4i9C+ODhEZ53dGr2pWSty -QhhG7ZuDo9ZgA/v8RUe89J3Szf2QDgxeQS8gJ3u6CAh7t7/eIQo4hbV/Q+hVls/6JW3z1WfxRL1mapAS90mztUITK1ssWBlJr1PNizZfq7xfYjUa0CQyezbM14IMDVBd -TlRuxNkH9gTkLshNviMnrwAy5WJZrYGn2OnFhzUc7oaJYwM45JG/C6Ap5W06dRVau3fyMK8bg0g0DUEUOaoUXtrGZWHcx/jAY+kxPS1ziYhWUmp6yTjyJpALL2XpSfl1 -K6yLyLLgK6h6fMH8B+2ePPcqJ4l1I1s5o2Rzb2JJIOxpp6+5dtBgNRRx5BZ5kf7EObWLG3vJEfwhhtipYa6dIZ/1m9zLAaDLhlWhzSAaJgXhg2O5c+N7reZfA7iDhMs/ -KndqAeTrUeoE8glTJ0QJdJEsYzJLov040RH1dZqPgdKG7KgAqmaSand8AkmKskYUCUjDM85Ge9QyxgHFZiwTY70OMAnUvucNplUXHG86uOCVbBjmzVDJJvPiJ0eDy8pV -VOhQeUf7LVWwgEb3mwfRBKJIpCXA5o/XL4WBqfKAfA0MN89YoxcTFxo8eUwi3aZxnsM7QwnQqnSukCWbXHdJn9Y3CXUZwZPgJNM3olwCeyBYJbIcAz9Te/vvhOQULwGx -pC5axvHjx2S4WJGJMWRjQ7odUcjvUB94Pjvu5HsstVRiTCu7OOrRiil7KdhIasEU2uDdHjOvlIf8r4TiJ9Z71+3VnavJjQvD4kY7LEqMKh8lJJolk9ikN9fALnGrjykn -K1yLyTksEpH/spAsr1A2ICMgdTTxhKyAk/aEKystgwlzfEjqlybXAUrwEqmu2ehXuUTdIVRf/5CoT1DSmkXws2xsJaEo4jBzqAi9X3+THbaTPb1JEE33hhum/Vbxg4AA -na5TG3tSMeQN7FfP+xksev3yYfkTGcZL5uyK7lZwyZOmt8N6DRl/zW9byCtx8cosItv3KirpKimFNnmk+e6JlQbb3RGh9DduiMHxqi3IPK4Zvm+tmgnpj5hm5BavPDkT -MhMIkJPgscoVBMQYerIJjTgzOobopJbpxcD3WPcnmHBarYWlneu95t5fySBMZXke8T7b1n3SBWSig+ygHLbmQiWJ7q89Vgr6kEcE9rjg89rA4kqJpK3l+60sJzKFGP88 -6qlnSkVZG+hFcidYixJeIUE2/TkErG/CWnAUucbGfqay+13wVBB08Hvk8NlrjzS1C/SJhe11JZhAqZkHnDNmxPGvElbCPj+l60sNrS+4AYO4sjoURcREBLOjaIAJ7B65 -O3xFvUFycyINMGjmeVHhnbtqN5viR6NzNb4cgbbx9kIXMvBJtkbb47TAl/OGGteyP79ofUOAh9+B4F56P1gQ6+l7rzTyr5d97j0dsfeOMpiJRohNj7Tvt5tBr99aWjlQ -8fUslTkZzkX61194kR7gyAacbUWlHkPpk+EwQAd92R5XksInCMzh0PBh9NW+OR4HtXdtZFpjFZmSPMrBXP7qysCuZP9bTjbHGFV3SwL5gKpmiylQK1/kl4Dp9QnvN3HM -tqKDUy1bOYmLh6fZhlFkA/yJDlhJV7QIjQ0m9iAyFo4JA4xBvl4fpUGWpgqtlPqrXZS+Jk2VVYp2821aYCsY8RiTbaK/e4r8TnPs9VgoPHNSZ3n/FIeGqilGH5RgC27Y -vE38Uo4MGne1h3xnR7bAQIo4e0LWp4u/88wM7Y/pDtTrIMC24DuYg3dMcf5xya/fWA4eIWMEDHo2PDl9hv8bMEWaGkK3VuSWNU0d5XLjXfWLqqDrfrsqGfNMfywpJpd6 -oNYNfU/pnb4AOBeFrt3JUC77InZQH688jhLPwAC6rrLzX6tHzKivKKbwjLO9v/Lkv+JdH4K1AV8Rpw4P2Y60QcZ2UTO22KoNy2ZcM+gpvpCpH0UeUt1kALDRe9Di/4bL -DLWvD88y5vlJQAs50bpTQoNEceGXeUwZz/ZmapVS+1wfDLo1Pui4Zu7Acj4avXqFr9pBPmNUFvGYjivUrvE6XcFvnQHip6fLCWblJJZuqycCZ+pDdW9yiQUnRS46+SRZ -i879NTwCra2+TP3wckoKWGFlvMDCtzqDBrucqV7qqSB7DK+PT4c7zw3qvuoZJns0bferiaV6YhMTIzNB0Z0fDRc6bLBmdSfsh/dVcVIcYHFOzws+nPv7QNtrbVUkkWFP -Tv8NKaFSAppIj6zxce4UVJTenefsJ7e33NknWIF0g2/i6D+ib5Wq90i75J4D/Xt6T9RktBjRJtDC3VFUIPx/Bg4uXsF9dP6djoKBeqMb9hHwkgE9c1CD9pDBYosT7Svs -dB8wNKVwTvLCMt0DJBOIf+FfzltOSViFxIGEFGXM6bwJeBRd/v5gd1tbn3puEZ9GrC1g2+WXNzC6f/jL7rZFvZyukVmh/3hl6mA9xCKvXE1Q9lEUh0R8khdOCa7b9SzP -hbNuVZS4fLDNWjYVzApHOSZ9UhVnxbKHFz+iZhHE9/02MEwgXYEYXNqWdXfYSm81+zXEJqHpJcHO57Gw4C1Ce1GFuU4BAuYzlv6vjIzHWMc/E0FsQ7dhHYEkuDicBl21 -YPDA0IG2BLyXJRSJMKaweNOGudhPflyyZGKwjJmly1ibB0a3iaZ9tZn6Es7H2wyXm+m+1G5Fsgoc69Xy6bKMJiOJyOEkX4PkG8kDgGg9siD3usTz4JZP/HN8hBtLPVQq -n8gQdNaCbcLNB3vo56eFVqhyy26EF6QQpFkOjBNYFCaEv0YosRHn498Qqj4r7zhUajePhR2i/tjGTGSpDBFhbZ4Tlepvfga1UVEv0asphvlbuZTMvMxY4i2H8ACQrBRK -CC7YEJL4AzupHDsht49nhVRr/e3NO3T45TH00Za2QXiw7c0JuTspWnJZ8N25cKU88zi9cs8HzDRvBkzeBtF8LGhnjMo/9KfsREYAtF0kyE2s7F+0ANo8zCWeeyNJUaip -3++UM6D99/cywwDhmaMbMeyNEtrgvtNq/lhx+YqADl1eMx3tDuMxPcu5uqYEXOuCl06ipysgumfNo7YszFi57XiYxjs2AL1HR/6YDmQpbo1B8XiKWLCOc/Y/1EGDoFcD -mHRz65ZCzESku6XJbQp1AuBJxGluW+Ag97d0yEKqpQLggC4ELSHGBjixcIcF3fAoqMOewqJCr8ht57DTlFreyAkQXQ9d/51Eh2VWYzJeJKWdKZmkANzpTtGL36O/fEEb -Gy75IJEEhsMaNihhHUnVvm/90pTzxFbkvoZWwBfPUsY0cyCycG1wiQffzI3Y3YhSdrJPlbnJzRSCKenDoknPADF6GmRCMoUwX4pKl1f6dcejlpZrdEittzuv+GLegc87 -5Zc0F1znHxCw7pH+t5I8gnXjH2ZhR/A7/GXOXYVnu6vueXO/Yt5u0mIJSeD/qqUMqWvdvturv6PktTLLlv6sU1y0Fd6CGi1CrEzq+Fmn7OLqB04KxuV4FkklIsfZwZSU -rFa7eAYr/ZOfZ9ozDfo2wy+s2N7awzdOVWIiBrjEElQGUOu0C+0KJ2B4hMAHGacLYHKUpq8RhigfnExOxF3iVJLX9JnYaAOo0NRfCwX/KTAz+AzOLCDh9dI6nE5SO0VQ -hl8MRI65NMxSztgAdElh0Q5v8hXw/pE6odZbj73tkE2YNi7RlktVPrheoRh7UqdNLpXKCvduZUH3Bjmd8CbOx3EGGhM6bOgWsuZ35pTfg+izQjZK9hMSSq0tLdXSwxfh -W2Ra3lLCpwqOjdA5gxGHbqUPvHGmkX0YvBGMjAjGgxO5g2AflcozLxXbmIJ8lneqqX48US7wwhqxEj5llB5E2da6OD22F5QG1r1BNl1rSeQeH1agK+vrZZ/4tmroNf/H -vdy++5+adiEJito7KAVRvLZ3fBOZLhSzGOBJsGILrVV02+3/6Dyd/ZTiOrHcYQGo9mZ9YS6oeyjIiEWSyQczyb+D+/jWfmPOgFSDRvzuYkqswHLf7bGIfAKpov/ljmcs -cJoar2mYEpJqXcyjjnEhVVLzThng1BAPhXflQje5H7Aa5fRQTD452Fs41MtjgHBd1SuqhWheIEAU0fhwCiBbt/4SxB5HWaoazOF+jYjGWIpXDQ30cwL5WL68NNPQzlxJ -4aA4LiLsbvReV57J9tjIqC3jHwciMOhjyUXCn4CWGP+Ufk9MZNw56X8K7DZXa0g/TpIcsdJbClPdysKWyXqUpjg5WBI5sJ0G5m2uBJI8Jdtd42Tv1zYCshH/Rts1Vrni -zaRQknWxqil42Zr6t/pQthMI9yYXycY1Ey3USW4o/eE0xkSh54yAHCdPMEzEkrh6GuwPNSmchALZVotMkkft+x4AAFjc9KF6X/Q1VXmVLbwqWuTj4o3Flip6A/9u62Mn -dSwlbI80zv2Yqalm4GdPVvZTC/KF+gl9sVRZuTmMBcp6x5ftRdLKjU5lwyle2fMwKrgwyFa38PD9mn/8PybEzic0fV70z1K1ijRzP+Mg/O1BGJjDADlV7jKcdDrCvpuI -pqr7D8ByOnQcW6YfZXOwkrs4PySnm6WUxTFm9lmaSjrP1/YLOLG3H/V4mfsXvCZWQ0C0/AHpbiwzmaonR8HO8/5X32uRfM33PtBV+cOKSZWnTDd3kqShK624kbIRX1cS -y6NHnJ5JSBVpt4E1gWt9Fj8r1DR8LBkk/FvgDQYIY1IvCViUv/IN5RIVmI9m0XQqi0Cd7JCgD/EUa/J4GuQlrTRueqDg7fM5hWmvs5Y1JDqOkA1MnPDns2l8xiH6OecZ -V2kpTkKQGGpmKkeo5OpIY67ogQOKYH1Pzy9igBnQneJ6MM/TET4ETM8s8CxRWRmex2oS6JjXdkyRVJvuE5rnDbZ0NokgaEBOm/2XEjWzYD/n0bPkf6jbnxiOYHbNebdt -zZF9RZdjRFZph1CmxPyKSeBlUXWH+jhEMn4pj5Sc6G8zcvmWqgetQuaWdyKj0AVelfEkcD1J7jj7v2e9vlHj/agoJ9Q2gqJX+xwawddoLsTBZWUwb76EfcGislnHWXhb -KrTsYYbcUDRjZ3pGbPl4jUVoqzKPpEkGXhibe+IXwE9XY9dSRzmtm1NOIhKHhsWFCR7irskwUYdrSkF1wyk5SZ5S5g/H1NLAPHng2/DfX3Srsbo4VIdXFZUccdJVEpvd -albBjte2kbpAIKHADiuQvEZYabPj51A6kBTqM7KEIfPUP1BWshy45cnhemkIBm/nCPEwz1KgaPksULj3Ky32ITlEICYu51KrzR0UGkd0GvSL8rhGTs9WJRwKwlkAhSp1 -8arrMelkAdQ75BZMv5M1zp3KCdYUpKpVh45IuM3e6zzTfazdDzwpTq6irHutsSYbHn37dkRCHEq4TvECPgAI0Mx2wwVT28c5ciK5DNNjL/CeuNR0jPR/lZu1OYZUGhwB -ynJoCVGXL4ajQRTHa+Jm/92WpZJZFlPkK7BwDR0PyJQj157zMCQ860oxWolsUHETRrSi1wGxmfpsowSVmH4+Er+/oCe8qWAMdzNnSaKXN7qvg33UeGaPhKph0xl+HkBA -DE5ob5HUaSPxRcPn+Da7CDnWEIAx+kxjm3Vr0fzM/38gTULuTMUb5vK/d+UlD+YvPFlvZ0Bu3RJACgPtHGzs96pGcD48Lv1vIXWa4UaEK9RwvnhunI23k99pZdEQsrH5 -c/ZqomFA6FN3kMpANWWKiRHgmhUeYuLgHiBzl+nnnuZmqPrghXV6jOTraIKKcY4KiexsZ51bM/xSkJd6jWpS4/i8rdnbBTLCxjEd5sH6XMiBak9AcrQfhX9h6hCophxY -6HITWFUpk2yok1zeRVwTHf+TxgbLN6WwdeNS0r9L2Kuf1bKKCNpyIJpVIJtyudw1lkfdbrCVn6O/hS3JFi6oZZtme/VNn3gn2wu0Wr58TE7SCrdP/67Vpleg8ssxI1Vf -x4l7talNFc307+ILjeOlZN4ftLgQ3gH6tqKhds0aPFS9vS1gKZTOa9gJU9x3f5HxhaPorzmH95B0hYu0bCtZQVMNJTTjdAs59oD+8SHXepVqFRr1Q3X9ag+CtOVDmlwj -pmzTbyMAveteAksLBAxZfjKjUdr1tBzMzEOP0vVKt8lyp6z0fjxocEQFt395BTJrlA4hKBvejrAUWPSFiJ4pmV3+t+983y3vcoeS+6CwKGACGbkkDFc/Yv5Gxx4IbrUr -FsCauDpk0Ff0Zn6VClGhK1sz7Tz85OTsGnNoXn4c+ZVdOOePKo6skUniBHrs7QObVAJrixu6Byc+Ah5voC6PaUUYyM5mdGeA30YYCikEPsbKZ38TiO8j3/C4GEU20LhF -B4HMAPpX5aGo4uca7ywqJ/qpv3y4New67HRrZkxd96X82qa0DOvzCZr0oBVa2JLlqPTrqjGNXNKDiK8f9sC2Eccm1T8k6U1JEnbEkhP/PerTR5ZkbP4ED4f50fM0JZb7 -rMhkrroviyEec8xwjeLfidBAuBQdDeaj3LshrWTf7eN5t7TxCCtpP+tw+Ui6goU3cwLEQzPi2DSco0HUHVqVUKUYqeqzF2Dt5FpO+p/2TikgbEM/azI1djdFnkyHGZfo -dekFIciG0L7VuEgRZUYrHEGxcjTPZk2KQCfaxOi/g7Ns+ieabk25zxoqKcx+ztYtRr2Zylv6tXLAk03xm5muTSWmM3uqJNZMcZPsiiBAnANtq391VqnLj1/RtJC6dBhE -yI5IGXEd7eHNInoyqmI1LnXnQNAif3Y3O0HokQquqGJqn6yv17Ax4TyITXzsjTP1Qqpwzh0QuS68Oe7LnyIILoeasl9mj+OHQpEuKQi+8S9stEk4tdFOrkjVtOvCQTsh -IGb3I7LOgH/yEGc8EMQNk4le6UNB29cVgCRxwuoRXvMRSK5ma00WeeQDxrEn2uC5CO6xZ0Y7ZyE8NLYZpxQEXAoWlLEp0D2p26VL+FYaQ4E8lj2rPxz9BU1oFglVxxOm -5azp3vr/HKEuUQIvL7T4+q/wFdjJh4EvZG60goNnl3fTLq3T/IS6GbOdhFOCrjAqee3D8DQD2o0uFzJysKSypFEl2yTilSGwAq3NmUZ0eSUFNGziGoOTA+pS2dyhljpn -Go/Ed3vzwghz+R5FB2a+uJUEYY/D+kQRypXakBFOIIXBI0m2ub+pbbeO00oefwWwhu4tDuYAYzvkPVmhC8pRn5uBC6N5yEHpQpW7inSQBTc7S+Z7Idls9JBa1FzBHWcr -HJ84kvtrJ1LyOcBwtGuK68Vh1ONkp2fOoLbThPO+c/aN7VWd5Qja052R5dQH5zTLDqHi5FApD169f8MklPX1OeiEZAAosuspv5rv7XiJMH3XyYK0Sm56yH9vEh57i6ym -Ujo4UZEen2rCoRhKFj6VeD/LtJYPZPmamu15H2z0zIVfVMZtgatJMoEo9ZgZv7IbmWDinahT3l20QHAWOTRdawAO2cHGUfpldK+NuizP2Iu1xDxGMc2on1qtH7BjbDUl -XRQMz/pWwwjl/1THUR3B2yoCeOg0edRs9L0jNIMxEw9GGr5qWzyNkCf6qR9/U0jPotyD7gFhQUAuyme6ZUvHNUNQleh/Wy7vol8gKZ+MKBW27s8v0+jvzIU+/HrqV2eR -Oh7kthI4T6eeQbqD7n1Powx4Y+9VP1lKe0c7PQaZIJMEldIhUMGxYUqEIpG+z2cQevVhk5r4ajcAKfTZur2DiJoWxBk++T6E7EiWOSA+rgYah7KeOmD9kxsj8Q5TZaSL -gVOBewENTmqrwFVKzCBUxzUQMj8bGRzDJYYlfBmpQqvQ0L64ZucqCrtnNN9zptsifXVhtOigoYFdzk1yhYRFh7jDiktbO1LkEE5y1L7dmlggL88MgR+A/tEh5MfRz/zA -sftKer8NlFVrzxMqjPFtvLycHEz/o4JSK12OZ3b/tp5Bdhs3W2Kwv5t4N+vDRvgS4oiCBonWj72MxJyP+OL2sYDKn/Va6+VPigfEGBM9TjZ6qLjkHRCeogRZD1ig1RAS -bpsWD41NUt0Xfhw73jc9Yn+SMVk9y81QD9gQo6bEPHFZJnArosrEW8zNJ3Jl9wkIFXkqWuizdXSIvNi4owTlF8AHwIy2/wyu1550pV5DSu03sHA5lWcG0iQYF4J3qjzd -s6Pjz0PSziweMeaaIYyrU0x7iOyhnAe4pBVf5SS+B+GRFqyJNu6KiSZkFVZPTZqoM8y7FlWe5TulqwWk+tXGWzku4oHyjTjsjoizSCivxfTRxzjfHknSkNZhYgohUH/3 -MoFlsxt6DwS0QIctKK3p8WwpvCp0PneQlXNBCgALAw+tBC3knQoHZYzlicJFBXuLyhxhMTkQ+8RUb25bsP5gTaa+jYf+8jZ5qtDaPKFkBdikBAi4UCv8jktzB++w+P3R -XMCD65rwgBGtYUqt7PLDLXhCvo5jF+E/UkuEGQKENwk7cv4Q+gEuY1PQy7pHO53pkwXnAh3JPARVPMzi8kHv7HiCPwRiqSyrwzO/YNxHxx2Cd89M0TwhDB50iuBrNrTe -shxXlWOjY+Vrg35dGyoszG4rCnZqERdqY3psPWVawzarVvld3nM8AmfArxVOrtw4TH6iGipYi6LEbhHBXJ43N6057EMdzpqyKXuB7mM87nc838x1dMyLynoSJ0+14Oqp -8SpNwiSFZxtfc002YeChRmVK3AKSD2dgLHVdyHwvWGhkrVEG6NLQUzDkHg6anKdjlUQGyZXiX/BAbuOZTVavoLw2ESF1MWumf+YGfDgGkq4MqKxEm0rVqbbaUW05F+nY -2Ep2bTG+WdcaLjiEujjSjlKVfre+Q3gQb9BEhnLCfbGsk1YMyzXfsmJT9RoLgeB6I5aqIq3p3wKdfgwgESusumI7nQUpsnBKdOIAZMkJnrR73bXLZQdMFUqTaevp1LW/ -RsDpAe6LOP7SOlOJsTKsyK8kAVyR7HphdHzxT//zqSd+56jKvGQlZEgQ1Hripz1v/+p3EgFdHNMGS/1/rFQcQDKi4U7uk63elh4cmUv7MDaPkLmxPyDviJQVs0GENBPA -vr9balIK60eG1FOue3HOuyzWOt4g6yVUAhTr+9DH38H/L06gjv3kb0A7W1r0iku/3GhAoXV7V4//4JITVpUCgtHhyT3MRTIRE3J0TkDgPdjJlhLyClUQRZJ6OKpqRIRK -nydclbk3NJyMRxcbywDl1jY4dx1N8HvrQtH+U1C6zmSFMSr8p6xBylD9Yl0KD0NGKRK9tVSY0vIH/twZ3tBzKnQMvy1PZqRrzJMZHBaYj9wSSYh3K0kB9SX+TbiAtf2B -vuaeAeCuG3RKM5hK/bCs1oE3tY2TUi6KHQSKFKZ3uW3nIJ3+x2fOMcjQbsYUhihg9RhKig7ljakUjh5w4yvpIGKm2uhzxR5dJCl5WccJcFp+0vA0qjioxrsMAZ1Hw/1t -vzAmUOw9/5n+FAwdAXzoqQQndREC1juuI17Spa2wJtTo6kAOwD5D885MmxxBbfYCSNjWhm9ywD6b3tNIw9bXqxdTF2vJ9UtoZlyMniDHbRYCkfDF1u0Gs15X1JNguy1L -FB4Fiqu6FZobkr5Ss2r6osStAdSwBCmpHqpEmQBbSd2mVaL3JuMnp1SfynB8mkJ6TyEFseB8wsZGzYPayxCaj1zCkNVQ1oGONmu3O4Me3kAQPdCt3iLLnADQ/p9K2g/1 -TyaskIFULh54RuhVLpYzf7x42mKWbDbNQgy0NR0Ac14BzLYfOEGxA32yJTarEIUCSc3zuMHgjpDvKgdYqTexhQGALpw/b+5G10JSOP0aVQttopoPP4UkLXTRS03gcTgG -LMbOgX1oikOiiVYsO7F5ehpmw4o307P4ZzOYvJmV5pkc05aDSxnInueBkVSxpblZJUdgYxedFf7pLIuzpA8PAIJnDe6ucgu1mOxzCfz3s5CERjbc578+zbSeUKw0L594 -9U/rz0zX7+kzaxdaLlxsH83tbXZ8LRRJjUEYDOUdIUAWFZKNhcXtcUb84lhsmDXXP4YC4hghPsMInqH4nxMSyxP64LDaDlp7rPex5ZsNrVHhmD/43jW1aPS2qrrrDDlx -FcqCCI88jwnTm4D1xSLVuaEdbXpXbfV5a8YKJyyuD3LkqB5I8DNvbZbishi8WsrOSIw2zp9pqWB0yWoZzQJ7Z6/Pxzbyf03AJApuyUxCwm4h1/GwMkCMYFK7Oww8dnTg -oG+jL/v+DJ9OLB8oRnyIF8vC5K0wrR5W5tDoFdOrghcu4lGcQeUm/0L5HqrmeibBy7H8gIpt6mlE/FQXr2hLFI6WcNX0VrHcen/Nx+Lv9U9fMRHfSYGYc6YfPkn1RZR8 -N5groa30YsCy3hvzSUjO+9c/bUPfM7n+1RQtw4fVOgxOje+cSusG3vp949o+OYfNbkeyVxdiHX2WMW5vXDKs7iU0kqxPJm0FfnA8tYv9BvMnJd8opeNJpm79HjPKTlaL -E2OssrnWHKWkI1kooEi/FKSTy/GT39LWO3nJOTaBodUICSyd9aq9gWkvVSqgVc9Ks0vqMgEoA986XyD5hqehZRL1rO0B/uJAFkKJl+QV542I76JqHF1fAVXiKnmz83Wt -7A5E1EhLri8XxpPYhlNKqvILRG69NddJOxtGM97joIH+zE4mUUJM/Q2BjPoh3VIhd7OmH9PIZIY6Wt/tmUm5+z6bvRJxViIfzXNprqg6d0/VkkrwiiPxgQutUXETsvWx -9uqkRzCCfe5qtfIOcGvNb1OnnyKk2rXckBBsRq/kaBfy/BO1fAx7qKsRmePzUdEk+6NJ1XzEE22BluI3GAZMjxKXUtdUnkexejkmmIOwDbubfYQbF5NM35wbOdh+8wUH -MYcF38jAEO5LKAze6x14BccueUctpQH8EGXxOdX/Wbnzjb2LcQXPaHpYz6GQHh4MXMxpMb7LFV+CC0jZ5CVZIgA+1BIUOeBizlTbpX/sSL43h3oEyeomnwA7Q7s41bN2 -Sl4yKKh/Z3wr2AzuB/WCH9Sln9t/Mmsq+/9DFVQxHKGdJXT5DcDU5i47JheWTQUczFA3mjT8HiVwItauzezUtFl9mFML4guqFMp8okNmq8zw786G3d+UePD/GHcvQsjX -P65BJPDVemaiOMhFMVi+nXeNneUoKimyOcH5Fjfx008QzaXzuHonTtoGbQna2H0GDaRBC4uYxhCp7syMoyFIuU34zsibsOP0CT4WdpVlgxbf9Q81A32tYWI5PxdlT4r0 -s9nTCiJMgZ0+gRereGl7eB7j2cExibM334Lj2/whfoCSucyIHfp4ngnRRfeZ0xk0QRDtm1yiG+4Uw4OJQIzQaB9le11c9sgEsaFsidiAs0X/xeem8BTzTIv+/V1rxV8A -SqjH0hfXGdFS3Fu/Xyh3jb8206ldcrhUzTiCzVl1aNkrSVsxUzviZFDD+/K5lB1w+tn6HzLmGotGANxNlSiq0trDMYxQ2RdrNtnXuAGBC1e1iHOyqMBbUUMUCkKEfL2b -/hZevNx5CVrYB5nhBMd7wHkINquiaqm3/iKcpQYgIos4JW+W9vWdsf5RO/QM085xxPIRrWoug/4ovHpZz7Cy0Cd95AkG+WmhReMN8fHnEJ3xijkkau9d3dHT0R108QsY -C3IsRHJI6IMfgOwvOG7MNXb9G+1Q890lQ9VANcxrrkLeaHbOGFOP2xnuDSiAuniCzunV3ROhCzFHorQxXeMUOyeyyqPWOIlyIt471LD7m6UymLXyWSPfGyB6TlKaiBEC -sA2IqRellsVDUurKV9ZlZESwCddTr9k07s8JNNsF4I+IzOQwfjXfidqH1aBjRtviMCupz2/oZ+Ejl21HxkKY9jGAkRdRX6CzzIsAnIu5ssxTvTcEU4DPsF88ovgGV3/R -Igj5lkwZ82mipk7q5fi35P/xgeza+NljEjN2KfF1Py2cKn2WFP5uA/HLonaGTJFnGzAMu7zaQ6XKmVfWyHTJBYUpp5HdcZYafgzZNb0cmrmeRJIaIe3BV8ZfzVCE8y8M -CfuWO0RMtBvVlvPETtit2hzWAM9IGpGHROHe4IxL37xP1tbpInhmk0BiaEBd5+sD4otiX/ijLNV+yN94SO91HeODdngp+bggWyYFNYkUkhIMX45dR1FDElpPUVijASen -fjHaRLWjLqXs+4Ff1VRANBq9WjfYmSGpSw2GRksVjAPOo8RU9pKiXNR2v9KkfuNmpu5Zrl8WqiojmfQTg0SLKRE5u8nppbN7FpU/Kxy9EnKATP0ydr8sWgOqCvFOF6SU -gARHqhcxDuxOkU3z7t9Fep5XR1NEdxmIGXoNfDC8pjU1MvTaSMpjz0JjwwCw0/XMg6FXLMJN6zpX1JXFdX3wT9rrzO4GB9gjY96Qtw1BJL/MhWbUNRcEV3COB0KKckHJ -9Bd2OaJjyaQZLn3hRFzlvy1ZzSDBS0ll84L/bnDV1Sq0iEgbTMVtSayI78HCwNQuFaM0Cq+d9mg8Zm2AGwMVkSbLG2YNXZAvFGsccNkTbXPCmsPBxh9QkaxiUkYVYjCI -/zc4VYAK/WtRU/43CH8SLMHknkZ+RciDagN0djkIMKCzIrodddw37OgL6DFpLQnaUies4GgyC2etMLm4uLo4cy9Cwubh190x9jUtlLXuGSfuXcBEjxUn/FFxdErJ/mRK -BWy4ZJ/iniUvqkbnkrA6Zq0VRH9K45hRQGb5KAxyDMB+EEa9kwMQ9l+rJ17l0Jd//kKtaHPC3Ql67X2fB15oqlFctOIrDGzUDW/NbRImXHQwyQkpsCVNOvsfeffo2T9c -9jCeCrwfjOpOvXMfehdMFuJcn09vCbosPFuKhRjAt8hLYdjkbrfgHwyL+es7zHEkbA6PmVFFztMI5XkbyaKlGxn2ADdN0wDdXjEN8e4SPTp4EPHkNjsQeGDHYS5IcUd6 -GzAPl1afOqCcPPCHW5r3YfI+/E8aWEn7QkcnEMx1zi5tngbINgzLbpbVTstQEhk9QyQxCi9A4t9sLDPaBnMqOzXBheECdoqq84GOo9WizFvcLyAnV9X16OG2EafDN6Tj -zve/Mhut7EAKfFg9nlCl1tsQBkP2oTlW+GSQX8NTtvftKIPzfN3AL/xbV8otdZTqOQshmJZ/gM/9UXiITd3VmNqs5Rjptpw/fz9LFwFq8xK8oagK7DZozofyZ1gxOD4P -786CyIL/o7wYlMEIH3hOwJSk/cTh5sXWmigO3l4IED7fVJJZMhW5K2cAtvkHQsCbQeks7WRLt/xVdihnKG34z7umlV0ChzgtVayovbPc/8+dF/rZTIuacwMft8EKMv6i -zuG++j8qDQutWEBuUzTK/OvvtzAW3jg31mPxIsvrtHaGr32xoqFTIScUY4BjqVQQVmiJ36LwNZf2AQRxP/oSm5J2xnZNxIowAW5tweZj+d54M3UyMi5Mblnl/tK7cCPu -B+qAZq+ZAoyWMmmjsFGTsuk3g1CEieisUjDX4uXglxJc5dlmYoLVdJ6EInVPu4GD7Ogh7pQA3tdSgRNLEvUZxOjKRjPsk766Ria/EnXXiLOGlawNPxBo/pw21oiaZGIG -NPlYU0ud7DzGnVtjJwMzpQsySWL2DjeKUNfkNCuozK69OE9OZSiYEntZiXQNp4IgZveJyl0AOY+MaOQtIx3Nt8d91j+kttOUrOSShoxY5RaNv4kK1s0rYWeACQkMGBNH -DRFUnfaXmjR9d3Hu2VBSud9T+jDQFjtaE2BRkMhAAM6gCmyG2Z7Q/lwZG2zBLcTqFnnzCtLM+ZsTk0JwgCKf+W4Sa5H2gTtpIKUN6gRU25wXQy+rjY/cVdFS0f8y9UM9 -eMfIl4H1p7Ylxv/9UCzhiJX998L0Rla/FL/OHtMwZ9xFJPFfkUG7A+4SswL0G2bsk+v37/id6byzr/aY8tp2VHI4BoBItiQIgaQJ6fen3J67ir8zg9rfu94PR32fFNa0 -umleJZJ5VuaJBU/tYvPm3RZAj8+PUWSXEw7yWexan3hQ3dIXIh/aYEpYo22Yxx75Cpd7OyXEDXhsXfDAgajlJF3p/FUeMhdXgHnEag2NcRoVL6VR55jr5HtpNSFWo9z7 -woQIDte1q59DArkKtMMT1dsihk7C1XXuj/v3CqMwerAywD7S0qyC6RdQMAoWBlFlAFFXdC8eSrRetJFFo91CJx7iCgZk0EZmHkzFsQXqANgFY/DQ2clQvkGhQA7pXJnV -ffrWBqlgIo6vQOmN0boA6eBs4DYTe6xSzETOnUkr4wXdgjQSXeOButhr6L+jFX2kQI4Aso68TX+S4yIpfB3gsLL6iynaX7/1Fu//yCCkeZHVixe0e2C3VU3Z2qJdjux9 -IP6xobjmyU9A9w6T81MsDlBBxzvm1Sp6Yg62byzNCUWUv947yc7TKTqe2qjIFLSokOGyJ77jqfZRftgK4+GhzJRx9ws3j9YobYB8yqP+ud/yk/gKdxdtC4rqVIvMjbEw -bLfGNMd9BqHi0X3GYQR2L0+zltNqgT5EVuFwIkPMwb8bU91J9aFKpsjwM9QL1OYWigdSTarJ9QXxB6+tF2M3UTsec2Py2WQUYPt+WwUZjKl9EkCd9l5HU0yhKYWpJfJs -ay2B94p1yrIxK0SH8ru4KVSmypdWA/oOqzapGMW8Eo22LMjNhSDk827e7yAljmZ0hJdNjGhGhBdnKxApAaJQy/bWM97KaqhVmSaQ9tO04J/4u2+Mu11QuqJROx7muC8U -2dtfmtpr9RVrmoNn5UTM3dnAf4QeS6p2lthhZBNRMsjGQCreF6EJngXoO0xPWuvgIXoveOEUvYveOaiuMHlI0G0qqbgq2dRJ0MTxrshWuysQDFj2IArWD734fi2B3xY4 -JDiv2DWpqLbO1PEUV6miasZ1apDYDPcH1j+6z0UgphgdkavNZ8/te4uhbIzCtpqSTuevLzVvSFgemiYxHMTYuxFVwk8L2wwGtMEWASaGMZzxiu7fvtiSnRKzNQp6Ax7t -YisE0EL7KaZO9Uw43ULwOE38mrmlkhFmihLqu0rMCYPU5CzigMPoc+NuRQdfntnkZPgsA/9KEJTkgiCvY8agZXZTpUW9HfNlJpCwT19lC/E4L6qJ/trTgy72r4MO3FQj -xYGY+63/YNuwPQ1vSWOFINHaw45tS0e54cE4xGtUKo4LGYhLsM5WpNpQeryzsqw80j7zzmNunQjwRA+GvvRYew4VJQe/wO+iefL+KgTHoob8amfO2Gft5o4LQDtk1vyp -1h33GVq6o5oS8Zht7tSALoOcJ1KCZqL4IRnDbOMmtB35mDfSmdFqr/qQI7gdGhppBlcTFRlMdmEa7F8bePUWBHWxwZeezizg7TPau5BYkR/XW0uep7Dw8pgdXCoR0mZI -lTI8qXhDAb0ehNhZrk8DVnCnEEjwVtq1cQvVGSk3tkFnp38SE8S3iVNdF90gVlZepX0mxDqR3b/3aGxlE9n3W4kL0Yq7By5sRdFdG6s/uEQ/vO8tPaB46kzZbfODWXpN -8fGhQhFGmE9Y0/ILCk8k2Aigh00tv0qzcH982fBmWLii5IVhvictHTVsfWNX+lkO7Vok6Olpuu5h+/t2El+OpFDQPKig5p58+cAOD5mKN5Gvv0+gpuBHk1uFHMebpfWc -wwI98GbSjlvtzv4hmngU3Yn18Q4lch9xzwOEaHEolyanezdFvWa4W3ep9Qhy0N8Z9W4P067I59kLNQBJ9rzqe3iQNa+CZwFqK5RcfPawvLN6zBkCFtyUpxmd5Z5cDdyR -+RJTCWeOzX3WlO0EGdOem9OeeYuyATnOsdPFegxEX8LXSWNQobkUZTeY+TFgkEU1msOURGY/kFJg1a7cCLypq4J9rTO40tiN9dmQ7xJ+jTX0xFCCbgVsdTaKQrDVHqhc -/zKxG0s1fuiiISZfBhassLaW5IiEM1WgzxPRwKhjIQh8JiEBbr1/GvjBNP+NYNLEOW+GGDB4+aultg9iLDaJEX1nPCTmtsHGpecahalHOOUjGkjWmblR4RYQFiunHqYT -Ia92ImikYWe/z+aenj2jdEmzWO6ntTdqMLeODOTCvhz2BjQ9vp7EakFyZ32+WQOWxwe/NwYYs3ketxxWNP9tBjGMXQ6GwEMqD+xUtUf48P8xkRqqopmcuB4jdkM/CzHj -VbE6JqQW4uNd1g/xLmudSx8q66RVPuI9g7pf8ogFBMJdgUZ1VEMAWXDD8np3dK5GxWGu7xOyyzYvvsut/Rn2KerOJxJcOvAQmu2lF/JyxagzC2ofMSrzkxOIz3F+Ugo8 -02aOkV1Jes7ulSbryJOKP1SOxBCfDHqby0wsAd3x5bDmreE1ykgVemXwKRmdo2CESQgqi4UIBQsXj2syp+m8Sk08+kSY1qRlNFS0YKXOLDDu2/vDN1FaW7vWxAMBW/zq -HTd2yJBFJ9A9yKZm/xuQh7HwW4SbCvYQNNGHcRV/lics4iUIek2SwFX16PVsMzA/06xdj2pXmskHVvv7r8ow0fJh8VmLpJxzW6/2sBZdIYEven7yASx0qyIinL83ovvO -tgIi84OAONUGsFd3gk+x25YH9BzJYnyRvLGIdVqSBWl3lxJUejZob5ipGMFKMza7IcBE08rDI9BNgjI0WJOAscBk77zY+1sMsXPVkIdmIjg48RI+8uf2Hv8AEk2/JlXu -Nw3UilSXjdRzoph1GD0F6qUCyEiZ453EwgJdVZ9q2PGjRpnI3JxfTBG98aXWoY1w6S8SldfiD1whpKhptHF/+cE94Skgzci6WD6nj5bCjDr7oLzvND1pDab4k+61T26i -Rd56ZiqyE5OUZ5uVw1vpwUNG7JqH1T5x0uqIYYidc1EZ/2gxLEOzUNh2qngMQ5wH/gBRnLtDfao0tfBk7D23EnXcHtxNDHAG0J9bgFWtxZLV9g1XQffojB2WKdppLPqz -pBi7xqOMh4lbxu6nD8UhpzvVShAk2mP+KgN1je9hlgOTCOXue3MXrm11ccDUPcR/Qy+cb6jNRtBTcsjdWhlnmq8ibZ4jnsXRZmFs4AJvZ6eVxHcxFt0rYOwUSRde3c/e -tlULZDhJw488apCgShpaM2PMoG4G+wZcew6gpgTiMD/HBPaeAevGLAOp+A3diNQOa7WgVM1Jt0d9UaZrD8PZDrKVfkTaNdARqjLRcG5nTPt5Fil+LW6wW3ONUSmsCSdU -2uvqKTH96TSp5I1AquDIQOwrEbHgpzpxyiGxp4jMpqAKyH2gSOE4Q0pcFKnMAiILep4D/mZ5Veofhn3LTCN3lIbKHVbcvAY05zoZQNLbTh29wSOZEgyQ+rw+AjCBnlTx -p4bovMZ7ccf0PL6QlfZREP0H4CBVDnb2V1iwkte1y+LD55o59DqFaPQilPSACZNsw4EP4xEA14U4ogt/6CDPem614UqecdIubDyyHvZTGXFguOs2MJU3/wUf53tAOiyo -x9fZKTDO3/NdNMTwpoRMAQBgSz5lVXCnAyXBXrEvgoEvkdiqVt6qqk/Jw4qksy/ygVNhwaBbkTVqF2FYrdZyghVbkWgMdxmM+abVuI8qarDYt/mnz0iWU7cSIiYXR9sM -OIhAO5UKh+yp0AICo3D123vjbMFjmruNK6y0Fyy4U5Nrq0ZSz+cTrnljkugdztBZTwryLIG7Gn1x5MLgZw9zzdfFyhl6Z1b7NOcVZO1S5Kl3ZSNKYVBJKl9xM5wWKDWo -DpNvcJL4Y+38kVbIgp3widFbGp6YNxqajgsHDcXcHCokEawUiJuwpA+/HG0NTUESo+qanQWR+8M706aZJir7PmHln0Pdb849Pf1Mn9ptvGhKN7PqVAreGDPZVf3EsglP -ugr1WMhT0BDHfPe+0bk4GRv0ySRkFJPKB6U8RUCcCYOn1BIzjM1O2tM6AhQOp+sfn9CR5UKmd/wx3kvQld2QRFVjy8hxj/nfPFaWhj1NmGuANm+y2dDzZjudyjV0dpiV -ppC5Kzm8on9K+YYZpQof0/MVl773xIO++O/nY2qZz9k9ZbszZFgF6JOAz/GinfK9UoP9K9nvzmhrrbkHGNDpVLGctfVZ9anqOj4ZLXghHWcRFsxZqc4UUKGLLz27uLjl -DW4gABzbgOY5d1Ehlrhqt3pPHkAQyAtL3qXOpbhw742RQXC4ivQLIdzWHXVLXbxj3kNYe/DUsE7ZZUpEYQM8NRQL9HGl1QP3ctkTVIvBiWy5jBOcNsXvlAJjaHYN2E+W -jNKSr97u6okc5vEVu9nPIMZmT7CBc1TgR1S0HdQNNY4y9PVI18HjTDtD/c/HWW9vbncwePWsVbzf8teisw/Ljs+4CreAzQoa4OTYmHn8KoIJ9dQ+O/08/2o80JdljyQP -EfrHPEyz4d3bujkGrWDZI9Frp6xpCDMwKNALkIFhZIvi2MtzD2TNX0r2Ve8FxEucATfvghTwBFpYm0vB5MJFEDvRdTVuetVev7kwOrDUTDCwv4XZ+rjO9Z1G6D89V2QB -FQiv0yWPqc26fGw7q2W/6pUlzAf+pjcSLsOq702TnZhimiFn/MVcpVOHHX1rDOCo33PB5Ssm+gzRDuSqkwhsKVUZP0MJk7/C7N2ng/NRw7JBMwnM0OCo2FC5GQjYPLil -qApZ+KUD3m2goQKGxabVCFYm0QB4hUONdLDr+p9Do9Z+Nq1YkSCfh4HrLynOfObF3m524kXDGWqFDLVoMtH1RQ3E4IG6shkIf+M0vq9T7km/lVGNKbIsUD0o02nEKcN+ -Ymm0IDISwPG0tJc2lZyoQQhJAvbQG/9yIyPAv6Z0u1s1eyF5MKP5DbC+b7lbbmjOq92v2+TF0VUJCO9NgO6+fqeNigR8UORys2uWhMh4wIdcXEpC8GeLAxzFVjcsi2IM -2oAqMujmouBM3Z3VQKLoPC6MWxgKrNvBvKHiL3nOc1HbxFhQlzf3VqrfCdimC4+FRFUgXZI0+gyOALunf0YIHgSaDPmI29/Hh6dAmTLeVCoYWcQ6gTZjq6jNcokQ/2Li -9TcleGyp2YRaP0AT3atnc6QAkAtFDzEH2L51lWlZ2HzejKpA/nUQ+S3wbqmg0PvJtEX6/pxYMWcoZ+9X1+8i2h3rYdJPbtsLYfcKqQmwoiN3vpYOL5jb9M7jx2aZrI2w -pe3/oArlADAjEUo4J5gMznnL6nNZlEdGPQsEjtZYj2ZKoUM0Pk6bmX36qDjGt9UL1iymVeSB0/w04m2hwMbbTPj6PlXXiH7y1PX35IHKWRgPSKXYg4teY4VFkNIktxey -F+pkZD8KLHDnrB36UByWZecxYjLcMBV8dzsPVeEAx9W+EC2lLXfjtt/KsOYb1c3CKsTsVwl5E4sNyxcWyGCIcUcI3csomnvaLSbvKQP/OxczuHLOOnyC7/iiAbGA+aHD -E+k7g+c+9xnL9dHFx6aN5oZ3QTUj+M9tH1uHIA+EXWE/fH83JuK1tbLT+KwFCqpUsLejZoV4ROPSFSeQs3MjyNUTxbKNpcXiKkLd0vkwpT7ribKlJzVX+rfUcKCtvmNR -I9Bl7k+YKn18+gs8it2PlWL2ILY8eylXXs7Qx6qdtg4dG+cwRYXihlYOzauaaGOIyIULPBN4C30X+s77fZG++PXGDjmIf066/CLSMs8mNULhPlipDTma7Q/VQij8WIrP -iGEjSYukRnBg3QoQ5M7YgnPInUqdrUTd4Oi2yruXh5QF256/odaRzvOTFD6xMcIIzTPLAAJx+WLAxckDKnSQsQU6oOY/8CVTe/B3OJWnWVSx1joLPuDbyjeh+GBVAe/k -eNff+w1qx0aF4kQ46FmKyQkMUtE5kTW+MWPf9ag8S1n6x+ZrdrGzBVPxm+pUEl9rZAYLp7VrU90+Oo4UYHc004b0YEOg0s9EZqLxiUUT/N1L9Bu7bQU4XHNCUhf9PJ1z -pcMOaGbl3ZPggss5bGRa5Mcdv8x7E9JkXU0rSGoT+XI2dEtriCjCnDHEpDn1cQmxfc99gt/6A2UBIuP/5uPO6Sc7MG5E97fJGRic2NPy1YFD0OlUOAWZQaMZ6MHAohKK -voF+0p/MWi+CL/bA/SoH+AskQIRLEcezedRMRPSmAU8mn6rvuj3id3Ey7BIN76KMB8tgGZ7dnz6kShFWYSRcmCF7iDIOJcwYHpmWVzjNh+HdpoVQgZhcb19t6u/zB1f0 -+q+D+HFy9d2+6OvoGUhMGMWejKwrYmI9yB1JNOgQ9USXRgKc6+XZGL0O523SSBCle3VQhKXCXYwLk5cRZL+lKdatSvBXYvo4ClIL816IPf55LOpz1mZ2DbbjE+ERHWBz -JNGaQ4qQO3fKI1zI4O6hZKpXO++rpvOhCwJo0giSFjkw3ILxSvGkbGshclxcfvGvZIKViDgZBBsNfcfaRyeKrYbDuCkK0ltruUkOwG2sQ54sNkJZzQgmPoYC9AdfvJD7 -TX4kYGm/LDQCI55bRjlXh27Axw8oLXbgQSlnxw3JH4L1pmcio4udbNcGyZXZCfe+gmZjGpChyiHi8URCLO5NC+JxKG1wDRnuWRMIe/UXuK2WgY2FI8+1+axXV+aU/WnE -tI0CRARv97MzT3w9A/ODIj+5MHfLKnMC5iLbH89l6GBKZvSPJPmDsaQRLhQNFoTXeeTzkZxQ0maP6rm50mD6IeEtnEUO4YwMktapl1nLTS/zvsBoGwXXcVtOP1YdEQ1t -PCBYFCL4H6doJuVXORgQQn1Ddg01D3INtenhA+v7f64ddDN76vgmf8FlVQxwoUi8kn6/jj4Ro2M7kIFM90nBgl5d0gigujnHhgEUIHAwAeJXugXPp9vGhjx+vuXHIIBI -FjwYr3s81YYOu4HlAzNwlZzKrYNtoQiXuLjPLuCrqnu3fTKbAoexNXtCVXvYk+r/d6+1dkpbP2YM1hm8jrAlYfXuNXG5RRrMkW0LWrppIoArgqSwnHq2Vs2fOt91dVu4 -DT93HSEASwTu0t+QEoVOv70AJsPI+Na4pzdL1SxH+uq8i2xO8oBXxon5G5uTD6FcRBDdPmCHcjFbW3J/eOeZj3x2AZacpH+rw/MBCXxxvo8AN7oERuTlH6FsKBBDPhBL -hOJdVcLYBIB38mgkh4fY7KkS5QYR+LCaVeS4QXQvzj1O13nktuSiAeeOFkbJaxlWNY4Cco8Vnctcvj98xq/adC12wTwmQPBPOBD7mgR8KQ4vx9hvgErr0TJPOJxpqqUX -lgHQVGGVS41srIguaQqfzxH+v6/C7HRX/Ae9IR7qz1j32CA1jEWqdYnqS7AgR315m52i7nkkg5BNSImeb9KXUkracspjpavBvmXThAti6gs1UNXwU2q9ydPC/CbnqQ+i -Rv3in4nMBhfCp1xCBRwUHkxrsFmKs01U4W1ROFj6BJcYzTGSAL6wHzCHL+fB6T+j8FQaVn7c5LLVJA1wz9c2Ojrp2C+XElgkdBW/D4KuccYp1FAzvghmVblQF4bnTMoq -LJaPtJ7Jf0XxCp5l6GO7pYBr4W9tgAHCdi+EwvH6HcU89Nng4at/a9lFzE1Xt1AWyBg01HwwwYYLLqIRH5NW3vh/J33oGzAemATBA15y7DvUUuhBpNzAtRpOwKIPWj1x -xvnDLXYwrn1SaQ5+nGPAkbK5Mi3tadrp7L43sgrxeHNuIHHM/688u7QebMbfepNKdkjhZBPhy34hepc92O2wDLH6+YJmgqS+Jo/XZsyTpi1inZzs6sb/Piyme9sWb0KW -WbTXR2Zss8RdYGcV+gWWKJJrks/Aa9TFn5iwNxOtRmnS4QjoBGS91yyvouII4zVfcw8Lnw9Kd4NLqPvOZmr2FmgaIzEjf7LTBdPok9nkIu0s3P8eD6wqAnm73A4K5Lml -uCium/bfnZul3Y4n8ZvdEhfQwerOJQ/lvsgWSjFUYBUWR/CgzN746ckXgWW1NylZgudmNt5otQLxtTFT0pAsvWlMIQ2ST3DR79wCh5pp9UTvtcPl5Ojff9haNlrxnfYl -wNfmtukAu0XBWoobDyPKqYpWMry1gRMoaGQcm1dFOOCyURX8VZWBPYYtcojzZqwJI/pghZ0pmxx7mo+4PiU+SdQwFX2ix92cuNscNrs10kL3EBxThEVMWip7LslyWfSv -GnTvRo5CmvUnjJGNAkLz6RPCg62wZXsw5W0UnV7mkr0hNsOgzrkoHpteyMBzEXYKi96nP27LNOIuDje70rTqFXhku7IOw/t46tm/yGvACNVin8kTUu9ys3Or9wzdZ1E4 -j4svVLr4e7uYGfaJeDOc4/XaYhXWZAZc28PU0knV3EHq/cyH/TMWTd//lxp+5l7+BstWOME8RbudBxUN8jB1DXPD ------END MEGOLM SESSION DATA----- diff --git a/test-harness/src/test/resources/test-image.png b/test-harness/src/test/resources/test-image.png deleted file mode 100644 index 3127893..0000000 Binary files a/test-harness/src/test/resources/test-image.png and /dev/null differ diff --git a/test-harness/src/test/resources/test-image2.png b/test-harness/src/test/resources/test-image2.png deleted file mode 100644 index 56b0161..0000000 Binary files a/test-harness/src/test/resources/test-image2.png and /dev/null differ diff --git a/tools/beta-release/app.js b/tools/beta-release/app.js index 9ee62da..472f78a 100644 --- a/tools/beta-release/app.js +++ b/tools/beta-release/app.js @@ -135,14 +135,18 @@ const createBranch = async (github, branchName, fromBranch) => { const incrementVersionFile = async (github, branchName) => { const versionFile = await readVersionFile(github, branchName) - const [date, rc] = versionFile.content.name.split("-V") - const today = new Date().toLocaleDateString("en-GB") + const [date, rc] = versionFile.content.name.split(".1") + const today = new Date() + const month = (today.getMonth() + 1).toString().padStart(2, '0') + const day = (today.getDay() + 1).toString().padStart(2, '0') + const year = today.getFullYear().toString().slice(-2) + const todayFormatted = `${year}/${month}/${day}` let updatedVersionName = undefined - if (today == date) { - updatedVersionName = `${date}-V${parseInt(rc) + 1}` + if (todayFormatted == date) { + updatedVersionName = `${date}.${parseInt(rc) + 1}` } else { - updatedVersionName = `${today}-V1` + updatedVersionName = `${todayFormatted}.1` } const updatedVersionFile = { diff --git a/tools/beta-release/package-lock.json b/tools/beta-release/package-lock.json index efeda28..1d93d5f 100644 --- a/tools/beta-release/package-lock.json +++ b/tools/beta-release/package-lock.json @@ -9,8 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@googleapis/androidpublisher": "^3.0.0", - "matrix-js-sdk": "^19.7.0", + "@googleapis/androidpublisher": "^4.0.0", + "matrix-js-sdk": "^23.0.0", "request": "^2.88.2" } }, @@ -26,32 +26,26 @@ } }, "node_modules/@googleapis/androidpublisher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@googleapis/androidpublisher/-/androidpublisher-3.0.0.tgz", - "integrity": "sha512-r4JfmLlcu/VI4hObZuQ8RW5OeWRFtOxqE9xU8C2GAp3GTu2ZcemEICu69xy/rchpzMNQY4lrr8WiqUG1LE1L5Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/androidpublisher/-/androidpublisher-4.0.0.tgz", + "integrity": "sha512-DH8RwdIETMG8TGRrplBb1GreyeyOO+M+m8B9qBZbO88tc3D3JIGjyn+pu4n0nys7IAHTNX59l9zr+9P2dxZqeg==", "dependencies": { - "googleapis-common": "^5.0.1" + "googleapis-common": "^6.0.3" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" } }, + "node_modules/@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -158,21 +152,13 @@ } }, "node_modules/bignumber.js": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", - "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", "engines": { "node": "*" } }, - "node_modules/browser-request": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", - "integrity": "sha512-YyNI4qJJ+piQG6MMEuo7J3Bzaqssufx04zpEKYfSrl/1Op59HWali9zMtBpXnkmqMcOuWJPZvudrm9wISmnCbg==", - "engines": [ - "node" - ] - }, "node_modules/bs58": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", @@ -279,12 +265,12 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "engines": { - "node": ">=6" + "node": ">=0.8.x" } }, "node_modules/extend": { @@ -311,9 +297,9 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-text-encoding": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.4.tgz", - "integrity": "sha512-x6lDDm/tBAzX9kmsPcZsNbvDs3Zey3+scsxaZElS8xWLgUMAg/oFLeewfUz0mu1CblHhhsu15jGkraldkFh8KQ==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" }, "node_modules/forever-agent": { "version": "0.6.1", @@ -342,30 +328,29 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/gaxios": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", - "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", "dependencies": { - "abort-controller": "^3.0.0", "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", "node-fetch": "^2.6.7" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/gcp-metadata": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", - "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.1.0.tgz", + "integrity": "sha512-QVjouEXvNVG/nde6VZDXXFTB02xQdztaumkWCHUff58qsdCS05/8OPh68fQ2QnArfAzZTwfEc979FHSHsU8EWg==", "dependencies": { - "gaxios": "^4.0.0", + "gaxios": "^5.0.0", "json-bigint": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/get-intrinsic": { @@ -390,28 +375,28 @@ } }, "node_modules/google-auth-library": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", - "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", "jws": "^4.0.0", "lru-cache": "^6.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/google-p12-pem": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", - "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", "dependencies": { "node-forge": "^1.3.1" }, @@ -419,36 +404,36 @@ "gp12-pem": "build/src/bin/gp12-pem.js" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" } }, "node_modules/googleapis-common": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.1.0.tgz", - "integrity": "sha512-RXrif+Gzhq1QAzfjxulbGvAY3FPj8zq/CYcvgjzDbaBNCD6bUl+86I7mUs4DKWHGruuK26ijjR/eDpWIDgNROA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz", + "integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==", "dependencies": { "extend": "^3.0.2", - "gaxios": "^4.0.0", - "google-auth-library": "^7.14.0", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", "qs": "^6.7.0", "url-template": "^2.0.8", - "uuid": "^8.0.0" + "uuid": "^9.0.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=12.0.0" } }, "node_modules/gtoken": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", - "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", "dependencies": { - "gaxios": "^4.0.0", - "google-p12-pem": "^3.1.3", + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", "jws": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" } }, "node_modules/har-schema": { @@ -626,29 +611,47 @@ } }, "node_modules/matrix-events-sdk": { - "version": "0.0.1-beta.7", - "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz", - "integrity": "sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==" + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", + "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "node_modules/matrix-js-sdk": { - "version": "19.7.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-19.7.0.tgz", - "integrity": "sha512-mFN1LBmEpYHCH6II1F8o7y8zJr0kn1yX7ga7tRXHbLJAlBS4bAXRsEoAzdv6OrV8/dS325JlVUYQLHFHQWjYxg==", + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-23.0.0.tgz", + "integrity": "sha512-FbbVxbH+K9ekec4hfnAD64KaP1X4DRQw1htw0qnx761saJEW3rQc1bTQtEyKFi4dZz7XaFYbspVUXeWkVqnqew==", "dependencies": { "@babel/runtime": "^7.12.5", "another-json": "^0.2.0", - "browser-request": "^0.3.3", "bs58": "^5.0.0", "content-type": "^1.0.4", "loglevel": "^1.7.1", - "matrix-events-sdk": "^0.0.1-beta.7", + "matrix-events-sdk": "0.0.1", + "matrix-widget-api": "^1.0.0", "p-retry": "4", "qs": "^6.9.6", - "request": "^2.88.2", - "unhomoglyph": "^1.0.6" + "sdp-transform": "^2.14.1", + "unhomoglyph": "^1.0.6", + "uuid": "7" }, "engines": { - "node": ">=12.9.0" + "node": ">=16.0.0" + } + }, + "node_modules/matrix-js-sdk/node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/matrix-widget-api": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz", + "integrity": "sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA==", + "dependencies": { + "@types/events": "^3.0.0", + "events": "^3.2.0" } }, "node_modules/mime-db": { @@ -847,6 +850,14 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sdp-transform": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", + "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==", + "bin": { + "sdp-verify": "checker.js" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -936,9 +947,9 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { "uuid": "dist/bin/uuid" } @@ -986,26 +997,23 @@ } }, "@googleapis/androidpublisher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@googleapis/androidpublisher/-/androidpublisher-3.0.0.tgz", - "integrity": "sha512-r4JfmLlcu/VI4hObZuQ8RW5OeWRFtOxqE9xU8C2GAp3GTu2ZcemEICu69xy/rchpzMNQY4lrr8WiqUG1LE1L5Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/androidpublisher/-/androidpublisher-4.0.0.tgz", + "integrity": "sha512-DH8RwdIETMG8TGRrplBb1GreyeyOO+M+m8B9qBZbO88tc3D3JIGjyn+pu4n0nys7IAHTNX59l9zr+9P2dxZqeg==", "requires": { - "googleapis-common": "^5.0.1" + "googleapis-common": "^6.0.3" } }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1082,14 +1090,9 @@ } }, "bignumber.js": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", - "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==" - }, - "browser-request": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", - "integrity": "sha512-YyNI4qJJ+piQG6MMEuo7J3Bzaqssufx04zpEKYfSrl/1Op59HWali9zMtBpXnkmqMcOuWJPZvudrm9wISmnCbg==" + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==" }, "bs58": { "version": "5.0.0", @@ -1174,10 +1177,10 @@ "safe-buffer": "^5.0.1" } }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "extend": { "version": "3.0.2", @@ -1200,9 +1203,9 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-text-encoding": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.4.tgz", - "integrity": "sha512-x6lDDm/tBAzX9kmsPcZsNbvDs3Zey3+scsxaZElS8xWLgUMAg/oFLeewfUz0mu1CblHhhsu15jGkraldkFh8KQ==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" }, "forever-agent": { "version": "0.6.1", @@ -1225,11 +1228,10 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gaxios": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", - "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", "requires": { - "abort-controller": "^3.0.0", "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", @@ -1237,11 +1239,11 @@ } }, "gcp-metadata": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", - "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.1.0.tgz", + "integrity": "sha512-QVjouEXvNVG/nde6VZDXXFTB02xQdztaumkWCHUff58qsdCS05/8OPh68fQ2QnArfAzZTwfEc979FHSHsU8EWg==", "requires": { - "gaxios": "^4.0.0", + "gaxios": "^5.0.0", "json-bigint": "^1.0.0" } }, @@ -1264,49 +1266,49 @@ } }, "google-auth-library": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", - "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", "jws": "^4.0.0", "lru-cache": "^6.0.0" } }, "google-p12-pem": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", - "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", "requires": { "node-forge": "^1.3.1" } }, "googleapis-common": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.1.0.tgz", - "integrity": "sha512-RXrif+Gzhq1QAzfjxulbGvAY3FPj8zq/CYcvgjzDbaBNCD6bUl+86I7mUs4DKWHGruuK26ijjR/eDpWIDgNROA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz", + "integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==", "requires": { "extend": "^3.0.2", - "gaxios": "^4.0.0", - "google-auth-library": "^7.14.0", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", "qs": "^6.7.0", "url-template": "^2.0.8", - "uuid": "^8.0.0" + "uuid": "^9.0.0" } }, "gtoken": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", - "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", "requires": { - "gaxios": "^4.0.0", - "google-p12-pem": "^3.1.3", + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", "jws": "^4.0.0" } }, @@ -1443,26 +1445,43 @@ } }, "matrix-events-sdk": { - "version": "0.0.1-beta.7", - "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz", - "integrity": "sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==" + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", + "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "matrix-js-sdk": { - "version": "19.7.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-19.7.0.tgz", - "integrity": "sha512-mFN1LBmEpYHCH6II1F8o7y8zJr0kn1yX7ga7tRXHbLJAlBS4bAXRsEoAzdv6OrV8/dS325JlVUYQLHFHQWjYxg==", + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-23.0.0.tgz", + "integrity": "sha512-FbbVxbH+K9ekec4hfnAD64KaP1X4DRQw1htw0qnx761saJEW3rQc1bTQtEyKFi4dZz7XaFYbspVUXeWkVqnqew==", "requires": { "@babel/runtime": "^7.12.5", "another-json": "^0.2.0", - "browser-request": "^0.3.3", "bs58": "^5.0.0", "content-type": "^1.0.4", "loglevel": "^1.7.1", - "matrix-events-sdk": "^0.0.1-beta.7", + "matrix-events-sdk": "0.0.1", + "matrix-widget-api": "^1.0.0", "p-retry": "4", "qs": "^6.9.6", - "request": "^2.88.2", - "unhomoglyph": "^1.0.6" + "sdp-transform": "^2.14.1", + "unhomoglyph": "^1.0.6", + "uuid": "7" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, + "matrix-widget-api": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz", + "integrity": "sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA==", + "requires": { + "@types/events": "^3.0.0", + "events": "^3.2.0" } }, "mime-db": { @@ -1597,6 +1616,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sdp-transform": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", + "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==" + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -1669,9 +1693,9 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" }, "verror": { "version": "1.10.0", diff --git a/tools/beta-release/package.json b/tools/beta-release/package.json index b826e45..f45104d 100644 --- a/tools/beta-release/package.json +++ b/tools/beta-release/package.json @@ -6,8 +6,8 @@ "type": "module", "private": true, "dependencies": { - "@googleapis/androidpublisher": "^3.0.0", - "matrix-js-sdk": "^19.7.0", + "@googleapis/androidpublisher": "^4.0.0", + "matrix-js-sdk": "^23.0.0", "request": "^2.88.2" } } diff --git a/tools/beta-release/release.js b/tools/beta-release/release.js index e054710..f48deb9 100644 --- a/tools/beta-release/release.js +++ b/tools/beta-release/release.js @@ -1,10 +1,8 @@ import * as google from '@googleapis/androidpublisher' import * as fs from "fs" import * as http from 'https' -import matrixcs, * as matrix from 'matrix-js-sdk' -import request from 'request' +import * as matrix from 'matrix-js-sdk' import * as url from 'url' -matrixcs.request(request) const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); diff --git a/tools/conventions b/tools/conventions new file mode 160000 index 0000000..730feff --- /dev/null +++ b/tools/conventions @@ -0,0 +1 @@ +Subproject commit 730feff287862141dc7e3924821f57027b6c312b diff --git a/version.json b/version.json index 5d6abcc..d662d8c 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "code": 28, - "name": "07/11/2022-V1" + "code": 29, + "name": "23/01/01.1" } \ No newline at end of file