Merge pull request #306 from ouchadam/release-candidate
[Auto] Release Candidate
This commit is contained in:
commit
a8345f9a7c
|
@ -1,7 +0,0 @@
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: gradle
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
open-pull-requests-limit: 3
|
|
|
@ -16,7 +16,10 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
|
|
|
@ -13,8 +13,11 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-java@v2
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
|
|
|
@ -28,14 +28,14 @@ jobs:
|
||||||
id: size
|
id: size
|
||||||
|
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
uses: peter-evans/find-comment@v1
|
uses: peter-evans/find-comment@v2
|
||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ steps.size.outputs.PR_NUMBER }}
|
issue-number: ${{ steps.size.outputs.PR_NUMBER }}
|
||||||
comment-author: 'github-actions[bot]'
|
comment-author: 'github-actions[bot]'
|
||||||
body-includes: APK Size
|
body-includes: APK Size
|
||||||
- name: Publish size to PR
|
- name: Publish size to PR
|
||||||
uses: peter-evans/create-or-update-comment@v1
|
uses: peter-evans/create-or-update-comment@v2
|
||||||
with:
|
with:
|
||||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
issue-number: ${{ steps.size.outputs.PR_NUMBER }}
|
issue-number: ${{ steps.size.outputs.PR_NUMBER }}
|
||||||
|
|
|
@ -15,8 +15,11 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-java@v2
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
|
|
|
@ -15,7 +15,9 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -16,32 +16,19 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-java@v2
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
- uses: gradle/gradle-build-action@v2
|
- 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
|
- 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:
|
with:
|
||||||
files: ./build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
verbose: true
|
||||||
|
files: ./build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
|
@ -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
|
|
|
@ -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
|
27
README.md
27
README.md
|
@ -28,16 +28,16 @@
|
||||||
- Importing of E2E room keys from Element clients
|
- Importing of E2E room keys from Element clients
|
||||||
- [UnifiedPush](https://unifiedpush.org/)
|
- [UnifiedPush](https://unifiedpush.org/)
|
||||||
- FOSS variant
|
- FOSS variant
|
||||||
|
- Minimal HTML formatting
|
||||||
|
- Invitations
|
||||||
|
- Image attachments
|
||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
|
|
||||||
- Device verification (technically supported but has no UI)
|
- Device verification (technically supported but has no UI)
|
||||||
- Invitations (technically supported but has no UI)
|
|
||||||
- Room history
|
- Room history
|
||||||
- Message media
|
|
||||||
- Cross signing
|
- Cross signing
|
||||||
- Google drive backups
|
- Google drive backups
|
||||||
- Markdown subset (bold, italic, blocks)
|
|
||||||
- Changing user name/avatar
|
- Changing user name/avatar
|
||||||
- Room settings and information
|
- Room settings and information
|
||||||
- Exporting E2E room keys
|
- 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
|
#### Join the conversation @ https://matrix.to/#/#small-talk:iswell.cool
|
||||||
|
|
100
app/build.gradle
100
app/build.gradle
|
@ -1,76 +1,27 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id "st-application-conventions"
|
||||||
id 'kotlin-android'
|
alias libs.plugins.firebase.crashlytics apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCommonAndroidParameters(project)
|
applyCrashlyticsIfRelease()
|
||||||
applyCrashlyticsIfRelease(project)
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
ndkVersion "25.0.8141415"
|
namespace "app.dapk.st"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.dapk.st"
|
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 {
|
buildTypes {
|
||||||
debug {
|
|
||||||
versionNameSuffix = " [debug]"
|
|
||||||
matchingFallbacks = ['release']
|
|
||||||
signingConfig.storeFile rootProject.file("tools/debug.keystore")
|
|
||||||
}
|
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||||
'proguard/app.pro',
|
'proguard/app.pro',
|
||||||
"proguard/serializationx.pro",
|
"proguard/serializationx.pro",
|
||||||
"proguard/olm.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 {
|
dependencies {
|
||||||
coreLibraryDesugaring Dependencies.google.jdkLibs
|
coreLibraryDesugaring libs.android.desugar
|
||||||
|
|
||||||
implementation project(":features:home")
|
implementation project(":features:home")
|
||||||
implementation project(":features:directory")
|
implementation project(":features:directory")
|
||||||
|
@ -82,38 +33,37 @@ dependencies {
|
||||||
implementation project(":features:navigator")
|
implementation project(":features:navigator")
|
||||||
implementation project(":features:share-entry")
|
implementation project(":features:share-entry")
|
||||||
|
|
||||||
implementation project(':domains:store')
|
|
||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:core")
|
implementation project(":domains:android:core")
|
||||||
implementation project(":domains:android:tracking")
|
implementation project(":domains:android:tracking")
|
||||||
implementation project(":domains:android:push")
|
implementation project(":domains:android:push")
|
||||||
implementation project(":domains:android:work")
|
implementation project(":domains:android:work")
|
||||||
implementation project(":domains:android:imageloader")
|
implementation project(":domains:android:imageloader")
|
||||||
implementation project(":domains:olm")
|
implementation project(":domains:store")
|
||||||
|
|
||||||
firebase(it, "messaging")
|
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(":core")
|
||||||
|
|
||||||
implementation project(":chat-engine")
|
implementation "chat-engine:chat-engine"
|
||||||
implementation project(":matrix-chat-engine")
|
implementation "chat-engine:matrix-chat-engine"
|
||||||
|
implementation "chat-engine.matrix:store"
|
||||||
|
|
||||||
implementation Dependencies.google.androidxComposeUi
|
implementation libs.ktor.android
|
||||||
implementation Dependencies.mavenCentral.ktorAndroid
|
implementation libs.sqldelight.android
|
||||||
implementation Dependencies.mavenCentral.sqldelightAndroid
|
implementation libs.matrix.olm
|
||||||
implementation Dependencies.mavenCentral.matrixOlm
|
|
||||||
|
|
||||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
implementation libs.kotlin.serialization
|
||||||
debugImplementation Dependencies.mavenCentral.leakCanary
|
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/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="app.dapk.st">
|
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="app.dapk.st">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import app.dapk.st.core.attachAppLogger
|
||||||
import app.dapk.st.core.extensions.ResettableUnsafeLazy
|
import app.dapk.st.core.extensions.ResettableUnsafeLazy
|
||||||
import app.dapk.st.core.extensions.Scope
|
import app.dapk.st.core.extensions.Scope
|
||||||
import app.dapk.st.directory.DirectoryModule
|
import app.dapk.st.directory.DirectoryModule
|
||||||
import app.dapk.st.domain.StoreModule
|
|
||||||
import app.dapk.st.firebase.messaging.MessagingModule
|
import app.dapk.st.firebase.messaging.MessagingModule
|
||||||
import app.dapk.st.graph.AppModule
|
import app.dapk.st.graph.AppModule
|
||||||
import app.dapk.st.home.HomeModule
|
import app.dapk.st.home.HomeModule
|
||||||
|
@ -55,23 +54,20 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||||
attachAppLogger(logger)
|
attachAppLogger(logger)
|
||||||
_appLogger = logger
|
_appLogger = logger
|
||||||
|
|
||||||
onApplicationLaunch(notificationsModule, storeModule)
|
onApplicationLaunch(notificationsModule)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
|
private fun onApplicationLaunch(notificationsModule: NotificationsModule) {
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
featureModules.homeModule.betaVersionUpgradeUseCase.waitUnitReady()
|
featureModules.homeModule.betaVersionUpgradeUseCase.waitUnitReady()
|
||||||
|
featureModules.chatEngineModule.engine.preload()
|
||||||
storeModule.credentialsStore().credentials()?.let {
|
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
||||||
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
|
||||||
}
|
|
||||||
runCatching { storeModule.localEchoStore.preload() }
|
|
||||||
val notificationsUseCase = notificationsModule.notificationsUseCase()
|
val notificationsUseCase = notificationsModule.notificationsUseCase()
|
||||||
notificationsUseCase.listenForNotificationChanges(this)
|
notificationsUseCase.listenForNotificationChanges(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ProvidableModule> provide(klass: KClass<T>): T {
|
override fun <T : ProvidableModule> provide(klass: KClass<T>): T {
|
||||||
return when (klass) {
|
return when (klass) {
|
||||||
DirectoryModule::class -> featureModules.directoryModule
|
DirectoryModule::class -> featureModules.directoryModule
|
||||||
|
@ -99,7 +95,6 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||||
lazyFeatureModules.reset()
|
lazyFeatureModules.reset()
|
||||||
|
|
||||||
val notificationsModule = featureModules.notificationsModule
|
val notificationsModule = featureModules.notificationsModule
|
||||||
val storeModule = appModule.storeModule.value
|
onApplicationLaunch(notificationsModule)
|
||||||
onApplicationLaunch(notificationsModule, storeModule)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,42 +2,38 @@ package app.dapk.st.graph
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.media.ExifInterface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import app.dapk.db.DapkDb
|
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.BuildConfig
|
||||||
import app.dapk.st.SharedPreferencesDelegate
|
|
||||||
import app.dapk.st.core.*
|
import app.dapk.st.core.*
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.directory.DirectoryModule
|
import app.dapk.st.directory.DirectoryModule
|
||||||
|
import app.dapk.st.domain.MatrixStoreModule
|
||||||
import app.dapk.st.domain.StoreModule
|
import app.dapk.st.domain.StoreModule
|
||||||
|
import app.dapk.st.engine.ImageContentReader
|
||||||
import app.dapk.st.engine.MatrixEngine
|
import app.dapk.st.engine.MatrixEngine
|
||||||
import app.dapk.st.firebase.messaging.MessagingModule
|
import app.dapk.st.firebase.messaging.MessagingModule
|
||||||
import app.dapk.st.home.BetaVersionUpgradeUseCase
|
import app.dapk.st.home.BetaVersionUpgradeUseCase
|
||||||
import app.dapk.st.home.HomeModule
|
import app.dapk.st.home.HomeModule
|
||||||
import app.dapk.st.home.MainActivity
|
import app.dapk.st.home.MainActivity
|
||||||
import app.dapk.st.imageloader.ImageLoaderModule
|
import app.dapk.st.imageloader.ImageLoaderModule
|
||||||
|
import app.dapk.st.impl.*
|
||||||
import app.dapk.st.login.LoginModule
|
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.EventId
|
||||||
import app.dapk.st.matrix.common.JsonString
|
import app.dapk.st.matrix.common.JsonString
|
||||||
import app.dapk.st.matrix.common.MatrixLogger
|
import app.dapk.st.matrix.common.MatrixLogger
|
||||||
import app.dapk.st.matrix.common.RoomId
|
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.MessengerActivity
|
||||||
import app.dapk.st.messenger.MessengerModule
|
import app.dapk.st.messenger.MessengerModule
|
||||||
import app.dapk.st.messenger.gallery.ImageGalleryModule
|
import app.dapk.st.messenger.gallery.ImageGalleryModule
|
||||||
import app.dapk.st.navigator.IntentFactory
|
import app.dapk.st.navigator.IntentFactory
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
import app.dapk.st.notifications.NotificationsModule
|
import app.dapk.st.notifications.NotificationsModule
|
||||||
import app.dapk.st.olm.OlmPersistenceWrapper
|
|
||||||
import app.dapk.st.profile.ProfileModule
|
import app.dapk.st.profile.ProfileModule
|
||||||
import app.dapk.st.push.PushHandler
|
import app.dapk.st.push.PushHandler
|
||||||
import app.dapk.st.push.PushModule
|
import app.dapk.st.push.PushModule
|
||||||
|
@ -51,7 +47,6 @@ import app.dapk.st.work.WorkModule
|
||||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
internal class AppModule(context: Application, logger: MatrixLogger) {
|
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 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)
|
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
||||||
private val base64 = AndroidBase64()
|
private val base64 = AndroidBase64()
|
||||||
|
|
||||||
val storeModule = unsafeLazy {
|
val storeModule = unsafeLazy {
|
||||||
StoreModule(
|
StoreModule(
|
||||||
database = database,
|
database = stDatabase,
|
||||||
preferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", coroutineDispatchers),
|
preferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", coroutineDispatchers),
|
||||||
errorTracker = trackingModule.errorTracker,
|
|
||||||
credentialPreferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-credentials-preferences", coroutineDispatchers),
|
credentialPreferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-credentials-preferences", coroutineDispatchers),
|
||||||
databaseDropper = DefaultDatabaseDropper(coroutineDispatchers, driver),
|
databaseDropper = DefaultDatabaseDropper(coroutineDispatchers, driver),
|
||||||
coroutineDispatchers = coroutineDispatchers
|
coroutineDispatchers = coroutineDispatchers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val workModule = WorkModule(context)
|
private val workModule = WorkModule(context)
|
||||||
private val imageLoaderModule = ImageLoaderModule(context)
|
private val imageLoaderModule = ImageLoaderModule(context)
|
||||||
|
|
||||||
private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) }
|
private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) }
|
||||||
private val chatEngineModule =
|
private val chatEngineModule = ChatEngineModule(
|
||||||
ChatEngineModule(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta)
|
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)
|
val domainModules = DomainModules(chatEngineModule, trackingModule.errorTracker, context, coroutineDispatchers)
|
||||||
|
|
||||||
|
@ -133,7 +150,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||||
|
|
||||||
internal class FeatureModules internal constructor(
|
internal class FeatureModules internal constructor(
|
||||||
private val storeModule: Lazy<StoreModule>,
|
private val storeModule: Lazy<StoreModule>,
|
||||||
private val chatEngineModule: ChatEngineModule,
|
val chatEngineModule: ChatEngineModule,
|
||||||
private val domainModules: DomainModules,
|
private val domainModules: DomainModules,
|
||||||
private val trackingModule: TrackingModule,
|
private val trackingModule: TrackingModule,
|
||||||
private val coreAndroidModule: CoreAndroidModule,
|
private val coreAndroidModule: CoreAndroidModule,
|
||||||
|
@ -173,6 +190,9 @@ internal class FeatureModules internal constructor(
|
||||||
storeModule.value.applicationStore(),
|
storeModule.value.applicationStore(),
|
||||||
buildMeta,
|
buildMeta,
|
||||||
),
|
),
|
||||||
|
profileModule,
|
||||||
|
loginModule,
|
||||||
|
directoryModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val settingsModule by unsafeLazy {
|
val settingsModule by unsafeLazy {
|
||||||
|
@ -220,7 +240,7 @@ internal class FeatureModules internal constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class ChatEngineModule(
|
internal class ChatEngineModule(
|
||||||
private val storeModule: Lazy<StoreModule>,
|
private val matrixStoreModule: Lazy<MatrixStoreModule>,
|
||||||
private val trackingModule: TrackingModule,
|
private val trackingModule: TrackingModule,
|
||||||
private val workModule: WorkModule,
|
private val workModule: WorkModule,
|
||||||
private val logger: MatrixLogger,
|
private val logger: MatrixLogger,
|
||||||
|
@ -231,26 +251,22 @@ internal class ChatEngineModule(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val engine by unsafeLazy {
|
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(
|
MatrixEngine.Factory().create(
|
||||||
base64,
|
base64,
|
||||||
buildMeta,
|
|
||||||
logger,
|
logger,
|
||||||
SmallTalkDeviceNameGenerator(),
|
SmallTalkDeviceNameGenerator(),
|
||||||
coroutineDispatchers,
|
matrixCoroutineDispatchers,
|
||||||
trackingModule.errorTracker,
|
trackingModule.errorTracker.engine(),
|
||||||
imageContentReader,
|
imageContentReader,
|
||||||
BackgroundWorkAdapter(workModule.workScheduler()),
|
BackgroundWorkAdapter(workModule.workScheduler()),
|
||||||
store.memberStore(),
|
matrixStore,
|
||||||
store.roomStore(),
|
includeLogging = buildMeta.isDebug,
|
||||||
store.profileStore(),
|
|
||||||
store.syncStore(),
|
|
||||||
store.overviewStore(),
|
|
||||||
store.filterStore(),
|
|
||||||
store.localEchoStore,
|
|
||||||
store.credentialsStore(),
|
|
||||||
store.knownDevicesStore(),
|
|
||||||
OlmPersistenceWrapper(store.olmStore(), base64),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,43 +305,23 @@ internal class DomainModules(
|
||||||
val taskRunnerModule by unsafeLazy {
|
val taskRunnerModule by unsafeLazy {
|
||||||
TaskRunnerModule(TaskRunnerAdapter(chatEngineModule.engine, AppTaskRunner(chatEngineModule.engine)))
|
TaskRunnerModule(TaskRunnerAdapter(chatEngineModule.engine, AppTaskRunner(chatEngineModule.engine)))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {
|
private fun CoroutineDispatchers.engine() = app.dapk.engine.core.CoroutineDispatchers(this.io, this.main, this.global)
|
||||||
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 }
|
private fun ErrorTracker.engine(): app.dapk.engine.core.extensions.ErrorTracker {
|
||||||
BitmapFactory.decodeStream(fileStream, null, options)
|
val tracker = this
|
||||||
|
return object : app.dapk.engine.core.extensions.ErrorTracker {
|
||||||
val fileSize = contentResolver.query(androidUri, null, null, null, null)?.use { cursor ->
|
override fun track(throwable: Throwable, extra: String) = tracker.track(throwable, extra)
|
||||||
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))!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SmallTalkDeviceNameGenerator : DeviceDisplayNameGenerator {
|
private fun Preferences.engine(): app.dapk.engine.core.Preferences {
|
||||||
override fun generate(): String {
|
val prefs = this
|
||||||
val randomIdentifier = (('A'..'Z') + ('a'..'z') + ('0'..'9')).shuffled().take(4).joinToString("")
|
return object : app.dapk.engine.core.Preferences {
|
||||||
return "SmallTalk Android ($randomIdentifier)"
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
override fun encode(input: ByteArray): String {
|
||||||
return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT)
|
return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT)
|
||||||
}
|
}
|
|
@ -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))!!
|
||||||
|
}
|
|
@ -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.ChatEngine
|
||||||
import app.dapk.st.push.PushTokenPayload
|
import app.dapk.st.push.PushTokenPayload
|
|
@ -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
|
import app.dapk.st.work.WorkScheduler
|
||||||
|
|
||||||
class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : BackgroundScheduler {
|
class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : BackgroundScheduler {
|
|
@ -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.CoroutineDispatchers
|
||||||
import app.dapk.st.core.withIoContext
|
import app.dapk.st.core.withIoContext
|
|
@ -1,4 +1,4 @@
|
||||||
package app.dapk.st
|
package app.dapk.st.impl
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.ChatEngine
|
||||||
import app.dapk.st.engine.ChatEngineTask
|
import app.dapk.st.engine.ChatEngineTask
|
127
build.gradle
127
build.gradle
|
@ -1,16 +1,5 @@
|
||||||
buildscript {
|
plugins {
|
||||||
apply from: "dependencies.gradle"
|
id "st-base-conventions" apply false
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def launchTask = getGradle()
|
def launchTask = getGradle()
|
||||||
|
@ -18,7 +7,7 @@ def launchTask = getGradle()
|
||||||
.getTaskRequests()
|
.getTaskRequests()
|
||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
def isReleaseBuild = launchTask.contains("release")
|
ext.isReleaseBuild = launchTask.contains("bundlerelease") || launchTask.contains("assemblerelease")
|
||||||
ext.isDebugBuild = !isReleaseBuild
|
ext.isDebugBuild = !isReleaseBuild
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
@ -37,112 +26,17 @@ task clean(type: Delete) {
|
||||||
delete rootProject.buildDir
|
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 ->
|
ext.kotlinTest = { dependencies ->
|
||||||
dependencies.testImplementation Dependencies.mavenCentral.kluent
|
dependencies.testImplementation libs.kluent
|
||||||
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
|
dependencies.testImplementation libs.kotlin.test
|
||||||
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.7.20"
|
dependencies.testImplementation libs.mockk
|
||||||
dependencies.testImplementation Dependencies.mavenCentral.mockk
|
dependencies.testImplementation libs.kotlin.coroutines.test
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.kotlinFixtures = { dependencies ->
|
ext.kotlinFixtures = { dependencies ->
|
||||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.mockk
|
dependencies.testFixturesImplementation libs.mockk
|
||||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
|
dependencies.testFixturesImplementation libs.kluent
|
||||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
dependencies.testFixturesImplementation libs.kotlin.coroutines
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.androidImportFixturesWorkaround = { project, fixtures ->
|
ext.androidImportFixturesWorkaround = { project, fixtures ->
|
||||||
|
@ -163,7 +57,6 @@ ext.firebase = { dependencies, name ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (launchTask.contains("codeCoverageReport".toLowerCase())) {
|
if (launchTask.contains("codeCoverageReport".toLowerCase())) {
|
||||||
apply from: 'tools/coverage.gradle'
|
apply from: 'tools/coverage.gradle'
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 04178168e2107c8a11d8259d7cb3be499f55f30c
|
|
@ -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")))
|
|
||||||
}
|
|
|
@ -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<DirectoryState>
|
|
||||||
fun invites(): Flow<InviteState>
|
|
||||||
fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerPageState>
|
|
||||||
|
|
||||||
fun notificationsMessages(): Flow<UnreadNotifications>
|
|
||||||
fun notificationsInvites(): Flow<InviteNotification>
|
|
||||||
|
|
||||||
suspend fun login(request: LoginRequest): LoginResult
|
|
||||||
|
|
||||||
suspend fun me(forceRefresh: Boolean): Me
|
|
||||||
|
|
||||||
suspend fun InputStream.importRoomKeys(password: String): Flow<ImportResult>
|
|
||||||
|
|
||||||
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<RoomMember>
|
|
||||||
|
|
||||||
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<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>
|
|
||||||
|
|
||||||
data class NotificationDiff(
|
|
||||||
val unchanged: Map<RoomId, List<EventId>>,
|
|
||||||
val changedOrNew: Map<RoomId, List<EventId>>,
|
|
||||||
val removed: Map<RoomId, List<EventId>>,
|
|
||||||
val newRooms: Set<RoomId>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class InviteNotification(
|
|
||||||
val content: String,
|
|
||||||
val roomId: RoomId
|
|
||||||
)
|
|
|
@ -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<DirectoryItem>
|
|
||||||
typealias OverviewState = List<RoomOverview>
|
|
||||||
typealias InviteState = List<RoomInvite>
|
|
||||||
|
|
||||||
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<RoomMember>)
|
|
||||||
|
|
||||||
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<RoomId>, 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<RoomEvent>,
|
|
||||||
)
|
|
||||||
|
|
||||||
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<String, String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<RoomEvent> = 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<RoomMember> = listOf(aRoomMember())
|
|
||||||
) = Typing(roomId, members)
|
|
|
@ -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<RoomId, List<EventId>> = emptyMap(),
|
|
||||||
changedOrNew: Map<RoomId, List<EventId>> = emptyMap(),
|
|
||||||
removed: Map<RoomId, List<EventId>> = emptyMap(),
|
|
||||||
newRooms: Set<RoomId> = emptySet(),
|
|
||||||
) = NotificationDiff(unchanged, changedOrNew, removed, newRooms)
|
|
||||||
|
|
||||||
}
|
|
|
@ -4,9 +4,9 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api Dependencies.mavenCentral.kotlinCoroutinesCore
|
api libs.kotlin.coroutines
|
||||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
testFixturesImplementation libs.kotlin.coroutines
|
||||||
testFixturesImplementation Dependencies.mavenCentral.kluent
|
testFixturesImplementation libs.kluent
|
||||||
testFixturesImplementation Dependencies.mavenCentral.mockk
|
testFixturesImplementation libs.mockk
|
||||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
|
testFixturesImplementation libs.kotlin.coroutines.test
|
||||||
}
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
package app.dapk.st.core
|
|
||||||
|
|
||||||
interface Base64 {
|
|
||||||
fun encode(input: ByteArray): String
|
|
||||||
fun decode(input: String): ByteArray
|
|
||||||
}
|
|
|
@ -25,4 +25,8 @@ class JobBag {
|
||||||
jobs.remove(key.java.canonicalName)?.cancel()
|
jobs.remove(key.java.canonicalName)?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelAll() {
|
||||||
|
jobs.values.forEach { it.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,7 +1,13 @@
|
||||||
applyAndroidComposeLibraryModule(project)
|
plugins {
|
||||||
|
id "st-android-compose-library-conventions"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "app.dapk.st.design"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation Dependencies.mavenCentral.coil
|
implementation libs.compose.coil
|
||||||
implementation Dependencies.mavenCentral.accompanistSystemuicontroller
|
implementation libs.accompanist.systemuicontroller
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="app.dapk.st.design"/>
|
|
|
@ -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 <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?) -> Unit, graph: SpiderScope.() -> Unit) {
|
|
||||||
val pageCache = remember { mutableMapOf<Route<*>, SpiderPage<out T>>() }
|
|
||||||
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<Route<*>, @Composable (T) -> Unit>().also { computedWeb ->
|
|
||||||
val scope = object : SpiderScope {
|
|
||||||
override fun <T> item(route: Route<T>, 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 <T> item(route: Route<T>, content: @Composable SpiderItemScope.(T) -> Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpiderItemScope {
|
|
||||||
fun goBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SpiderPage<T>(
|
|
||||||
val route: Route<T>,
|
|
||||||
val label: String,
|
|
||||||
val parent: Route<*>?,
|
|
||||||
val state: T,
|
|
||||||
val hasToolbar: Boolean = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
@JvmInline
|
|
||||||
value class Route<out S>(val value: String)
|
|
|
@ -1,9 +1,10 @@
|
||||||
applyAndroidComposeLibraryModule(project)
|
plugins {
|
||||||
|
id "st-android-compose-library-conventions"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation project(":features:navigator")
|
implementation project(":features:navigator")
|
||||||
implementation project(":design-library")
|
implementation project(":design-library")
|
||||||
api project(":domains:android:core")
|
api project(":domains:android:core")
|
||||||
api project(":domains:state")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,53 +21,3 @@ inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
|
||||||
}
|
}
|
||||||
return ViewModelLazy(VM::class, { viewModelStore }, { factoryPromise })
|
return ViewModelLazy(VM::class, { viewModelStore }, { factoryPromise })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
inline fun <reified S, E> ComponentActivity.state(
|
|
||||||
noinline factory: () -> State<S, E>
|
|
||||||
): Lazy<State<S, E>> {
|
|
||||||
val factoryPromise = object : Factory {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): 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<State<S, E>>
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeyedViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
|
|
||||||
private val key: String,
|
|
||||||
private val viewModelClass: KClass<VM>,
|
|
||||||
private val storeProducer: () -> ViewModelStore,
|
|
||||||
private val factoryProducer: () -> ViewModelProvider.Factory,
|
|
||||||
) : Lazy<VM> {
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
@ -27,6 +28,10 @@ class StartScope(private val scope: CoroutineScope) {
|
||||||
fun <T> SharedFlow<T>.launch(onEach: suspend (T) -> Unit) {
|
fun <T> SharedFlow<T>.launch(onEach: suspend (T) -> Unit) {
|
||||||
this.onEach(onEach).launchIn(scope)
|
this.onEach(onEach).launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<T>.launch(onEach: suspend (T) -> Unit) {
|
||||||
|
this.onEach(onEach).launchIn(scope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EffectScope {
|
interface EffectScope {
|
||||||
|
|
|
@ -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<S, E>(
|
|
||||||
reducerFactory: ReducerFactory<S>,
|
|
||||||
eventSource: MutableSharedFlow<E>,
|
|
||||||
) : ViewModel(), State<S, E> {
|
|
||||||
|
|
||||||
private val store: Store<S> = createStore(reducerFactory, viewModelScope)
|
|
||||||
override val events: SharedFlow<E> = 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 <S, E> createStateViewModel(block: (suspend (E) -> Unit) -> ReducerFactory<S>): StateViewModel<S, E> {
|
|
||||||
val eventSource = MutableSharedFlow<E>(extraBufferCapacity = 1)
|
|
||||||
val reducer = block { eventSource.emit(it) }
|
|
||||||
return StateViewModel(reducer, eventSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State<S, E> {
|
|
||||||
fun dispatch(action: Action)
|
|
||||||
val events: SharedFlow<E>
|
|
||||||
val current: S
|
|
||||||
}
|
|
|
@ -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<out P> : Action {
|
|
||||||
data class GoTo<P : Any>(val page: SpiderPage<P>) : PageAction<P>
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface PageStateChange : Action {
|
|
||||||
data class ChangePage<P : Any>(val previous: SpiderPage<out P>, val newPage: SpiderPage<out P>) : PageAction<P>
|
|
||||||
data class UpdatePage<P : Any>(val pageContent: P) : PageAction<P>
|
|
||||||
}
|
|
||||||
|
|
||||||
data class PageContainer<P>(
|
|
||||||
val page: SpiderPage<out P>
|
|
||||||
)
|
|
||||||
|
|
||||||
interface PageReducerScope<P> {
|
|
||||||
fun <PC : Any> withPageContent(page: KClass<PC>, block: PageDispatchScope<PC>.() -> Unit)
|
|
||||||
fun rawPage(): SpiderPage<out P>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageDispatchScope<PC> {
|
|
||||||
fun ReducerScope<*>.pageDispatch(action: PageAction<PC>)
|
|
||||||
fun getPageState(): PC?
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <P : Any, S : Any> createPageReducer(
|
|
||||||
initialPage: SpiderPage<out P>,
|
|
||||||
factory: PageReducerScope<P>.() -> ReducerFactory<S>,
|
|
||||||
): ReducerFactory<Combined2<PageContainer<P>, S>> = shareState {
|
|
||||||
combineReducers(createPageReducer(initialPage), factory(pageReducerScope()))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <P : Any, S : Any> SharedStateScope<Combined2<PageContainer<P>, S>>.pageReducerScope() = object : PageReducerScope<P> {
|
|
||||||
override fun <PC : Any> withPageContent(page: KClass<PC>, block: PageDispatchScope<PC>.() -> Unit) {
|
|
||||||
val currentPage = getSharedState().state1.page.state
|
|
||||||
if (currentPage::class == page) {
|
|
||||||
val pageDispatchScope = object : PageDispatchScope<PC> {
|
|
||||||
override fun ReducerScope<*>.pageDispatch(action: PageAction<PC>) {
|
|
||||||
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 <P : Any> createPageReducer(
|
|
||||||
initialPage: SpiderPage<out P>
|
|
||||||
): ReducerFactory<PageContainer<P>> {
|
|
||||||
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<out P>)
|
|
||||||
},
|
|
||||||
|
|
||||||
change(PageStateChange.UpdatePage::class) { action, state ->
|
|
||||||
val isSamePage = state.page.state::class == action.pageContent::class
|
|
||||||
if (isSamePage) {
|
|
||||||
val updatedPageContent = (state.page as SpiderPage<Any>).copy(state = action.pageContent)
|
|
||||||
state.copy(page = updatedPageContent as SpiderPage<out P>)
|
|
||||||
} else {
|
|
||||||
state
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified PC : Any> PageReducerScope<*>.withPageContext(crossinline block: PageDispatchScope<PC>.(PC) -> Unit) {
|
|
||||||
withPageContent(PC::class) { getPageState()?.let { block(it) } }
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
plugins { id 'kotlin' }
|
plugins {
|
||||||
|
id "kotlin"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly project(":domains:android:stub")
|
compileOnly project(":domains:android:stub")
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
applyAndroidLibraryModule(project)
|
plugins {
|
||||||
|
id "st-android-library-conventions"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation Dependencies.mavenCentral.coil
|
implementation libs.compose.coil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
applyAndroidLibraryModule(project)
|
plugins {
|
||||||
apply plugin: "org.jetbrains.kotlin.plugin.serialization"
|
id "st-android-library-conventions"
|
||||||
|
alias libs.plugins.kotlin.serialization
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation "chat-engine:chat-engine"
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
implementation project(':domains:android:core')
|
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
|
implementation project(':domains:android:core')
|
||||||
|
|
||||||
firebase(it, "messaging")
|
firebase(it, "messaging")
|
||||||
|
|
||||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
implementation libs.kotlin.serialization
|
||||||
implementation Dependencies.jitPack.unifiedPush
|
implementation libs.unifiedpush
|
||||||
|
|
||||||
kotlinTest(it)
|
kotlinTest(it)
|
||||||
|
testImplementation 'chat-engine:chat-engine-test'
|
||||||
androidImportFixturesWorkaround(project, project(":core"))
|
androidImportFixturesWorkaround(project, project(":core"))
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@ import app.dapk.st.matrix.common.RoomId
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
interface PushHandler {
|
|
||||||
fun onNewToken(payload: PushTokenPayload)
|
|
||||||
fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PushTokenPayload(
|
data class PushTokenPayload(
|
||||||
@SerialName("token") val token: String,
|
@SerialName("token") val token: String,
|
||||||
@SerialName("gateway_url") val gatewayUrl: String,
|
@SerialName("gateway_url") val gatewayUrl: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface PushHandler {
|
||||||
|
fun onNewToken(payload: PushTokenPayload)
|
||||||
|
fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'kotlin'
|
id "kotlin"
|
||||||
id 'java-test-fixtures'
|
id 'java-test-fixtures'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ if (localProperties.exists()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def androidVer = androidSdkVersion
|
def androidVer = 33
|
||||||
api files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar")
|
api files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar")
|
||||||
|
|
||||||
kotlinFixtures(it)
|
kotlinFixtures(it)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
applyAndroidLibraryModule(project)
|
plugins {
|
||||||
|
id "st-android-library-conventions"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
|
|
|
@ -10,6 +10,19 @@ class CrashTrackerLogger : ErrorTracker {
|
||||||
override fun track(throwable: Throwable, extra: String) {
|
override fun track(throwable: Throwable, extra: String) {
|
||||||
Log.e("ST", throwable.message, throwable)
|
Log.e("ST", throwable.message, throwable)
|
||||||
log(AppLogTag.ERROR_NON_FATAL, "${throwable.message ?: "N/A"} extra=$extra")
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
plugins { id 'kotlin' }
|
plugins {
|
||||||
|
id "kotlin"
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'kotlin'
|
id "kotlin"
|
||||||
id 'java-test-fixtures'
|
id 'java-test-fixtures'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly project(":domains:android:viewmodel-stub")
|
compileOnly project(":domains:android:viewmodel-stub")
|
||||||
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
implementation libs.kotlin.coroutines
|
||||||
|
|
||||||
kotlinFixtures(it)
|
kotlinFixtures(it)
|
||||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
testFixturesImplementation libs.kotlin.coroutines
|
||||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
|
testFixturesImplementation libs.kotlin.coroutines.test
|
||||||
testFixturesImplementation testFixtures(project(":core"))
|
testFixturesImplementation testFixtures(project(":core"))
|
||||||
testFixturesCompileOnly project(":domains:android:viewmodel-stub")
|
testFixturesCompileOnly project(":domains:android:viewmodel-stub")
|
||||||
}
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
applyAndroidLibraryModule(project)
|
plugins {
|
||||||
|
id "st-android-library-conventions"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
plugins { id 'kotlin' }
|
plugins {
|
||||||
|
id "kotlin"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
applyAndroidLibraryModule(project)
|
plugins {
|
||||||
|
id "st-android-library-conventions"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
implementation platform(Dependencies.google.firebaseBom)
|
implementation platform(libs.firebase.bom)
|
||||||
implementation 'com.google.firebase:firebase-crashlytics'
|
implementation 'com.google.firebase:firebase-crashlytics'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
applyAndroidLibraryModule(project)
|
plugins {
|
||||||
|
id "st-android-library-conventions"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
implementation project(':matrix:common')
|
implementation "chat-engine:chat-engine"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
applyAndroidLibraryModule(project)
|
plugins {
|
||||||
|
id "st-android-library-conventions"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
implementation project(':domains:android:core')
|
implementation project(':domains:android:core')
|
||||||
implementation project(':matrix:common')
|
implementation "chat-engine:chat-engine"
|
||||||
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
implementation platform(libs.firebase.bom)
|
||||||
implementation 'com.google.firebase:firebase-messaging'
|
implementation 'com.google.firebase:firebase-messaging'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
plugins {
|
|
||||||
id 'kotlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly 'org.json:json:20220924'
|
|
||||||
}
|
|
|
@ -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<String, String> 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<String, Map<String, String>> 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<String, Map<String, String>> fallbackKey() throws OlmException {
|
|
||||||
throw new RuntimeException("stub");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void forgetFallbackKey() throws OlmException {
|
|
||||||
throw new RuntimeException("stub");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<String, String> toStringMap(JSONObject jsonObject) {
|
|
||||||
throw new RuntimeException("stub");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String, Map<String, String>> toStringMapMap(JSONObject jsonObject) {
|
|
||||||
throw new RuntimeException("stub");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Ed25519, Curve25519> {
|
|
||||||
val identityKeys = this.identityKeys()
|
|
||||||
return Ed25519(identityKeys["ed25519"]!!) to Curve25519(identityKeys["curve25519"]!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun OlmAccount.oneTimeCurveKeys(): List<Pair<String, Curve25519>> {
|
|
||||||
return this.oneTimeKeys()["curve25519"]?.map { it.key to Curve25519(it.value) } ?: emptyList()
|
|
||||||
}
|
|
|
@ -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<Long, OlmOutboundGroupSession>? {
|
|
||||||
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<Curve25519>): List<Pair<Curve25519, OlmSession>>? {
|
|
||||||
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 : Serializable> T.serialize(): String {
|
|
||||||
val baos = ByteArrayOutputStream()
|
|
||||||
ObjectOutputStream(baos).use {
|
|
||||||
it.writeObject(this)
|
|
||||||
}
|
|
||||||
return base64.encode(baos.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private fun <T : Serializable> String.deserialize(): T {
|
|
||||||
val decoded = base64.decode(this)
|
|
||||||
val baos = ByteArrayInputStream(decoded)
|
|
||||||
return ObjectInputStream(baos).use {
|
|
||||||
it.readObject() as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Long, OlmOutboundGroupSession>?
|
|
||||||
suspend fun persistOutbound(roomId: RoomId, creationTimestampUtc: Long, outboundGroupSession: OlmOutboundGroupSession)
|
|
||||||
suspend fun persistSession(identity: Curve25519, sessionId: SessionId, olmSession: OlmSession)
|
|
||||||
suspend fun readSessions(identities: List<Curve25519>): List<Pair<Curve25519, OlmSession>>?
|
|
||||||
suspend fun persist(sessionId: SessionId, inboundGroupSession: OlmInboundGroupSession)
|
|
||||||
suspend fun readInbound(sessionId: SessionId): OlmInboundGroupSession?
|
|
||||||
}
|
|
|
@ -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<SharedRoomKey>) {
|
|
||||||
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<OlmSession>
|
|
||||||
|
|
||||||
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<Throwable>()
|
|
||||||
|
|
||||||
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<Unit>(INIT_OLM).first()
|
|
||||||
|
|
||||||
override suspend fun olmSessions(devices: List<DeviceKeys>, onMissing: suspend (List<DeviceKeys>) -> List<DeviceCryptoSession>): List<DeviceCryptoSession> {
|
|
||||||
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<Curve25519, Ed25519> {
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,194 +0,0 @@
|
||||||
package app.dapk.state
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
fun <S> createStore(reducerFactory: ReducerFactory<S>, coroutineScope: CoroutineScope): Store<S> {
|
|
||||||
val subscribers = mutableListOf<(S) -> Unit>()
|
|
||||||
var state: S = reducerFactory.initialState()
|
|
||||||
return object : Store<S> {
|
|
||||||
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<S> {
|
|
||||||
fun create(scope: ReducerScope<S>): Reducer<S>
|
|
||||||
fun initialState(): S
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface Reducer<S> {
|
|
||||||
fun reduce(action: Action): S
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <S> createScope(coroutineScope: CoroutineScope, store: Store<S>) = object : ReducerScope<S> {
|
|
||||||
override val coroutineScope = coroutineScope
|
|
||||||
override fun dispatch(action: Action) = store.dispatch(action)
|
|
||||||
override fun getState(): S = store.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Store<S> {
|
|
||||||
fun dispatch(action: Action)
|
|
||||||
fun getState(): S
|
|
||||||
fun subscribe(subscriber: (S) -> Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReducerScope<S> {
|
|
||||||
val coroutineScope: CoroutineScope
|
|
||||||
fun dispatch(action: Action)
|
|
||||||
fun getState(): S
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface ActionHandler<S> {
|
|
||||||
val key: KClass<Action>
|
|
||||||
|
|
||||||
class Async<S>(override val key: KClass<Action>, val handler: suspend ReducerScope<S>.(Action) -> Unit) : ActionHandler<S>
|
|
||||||
class Sync<S>(override val key: KClass<Action>, val handler: (Action, S) -> S) : ActionHandler<S>
|
|
||||||
class Delegate<S>(override val key: KClass<Action>, val handler: ReducerScope<S>.(Action) -> ActionHandler<S>) : ActionHandler<S>
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Combined2<S1, S2>(val state1: S1, val state2: S2)
|
|
||||||
|
|
||||||
fun interface SharedStateScope<C> {
|
|
||||||
fun getSharedState(): C
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S> shareState(block: SharedStateScope<S>.() -> ReducerFactory<S>): ReducerFactory<S> {
|
|
||||||
var internalScope: ReducerScope<S>? = null
|
|
||||||
val scope = SharedStateScope { internalScope!!.getState() }
|
|
||||||
val combinedFactory = block(scope)
|
|
||||||
return object : ReducerFactory<S> {
|
|
||||||
override fun create(scope: ReducerScope<S>) = combinedFactory.create(scope).also { internalScope = scope }
|
|
||||||
override fun initialState() = combinedFactory.initialState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S1, S2> combineReducers(r1: ReducerFactory<S1>, r2: ReducerFactory<S2>): ReducerFactory<Combined2<S1, S2>> {
|
|
||||||
return object : ReducerFactory<Combined2<S1, S2>> {
|
|
||||||
override fun create(scope: ReducerScope<Combined2<S1, S2>>): Reducer<Combined2<S1, S2>> {
|
|
||||||
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<S1, S2> = Combined2(r1.initialState(), r2.initialState())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <S> createReducerScope(scope: ReducerScope<*>, state: () -> S) = object : ReducerScope<S> {
|
|
||||||
override val coroutineScope: CoroutineScope = scope.coroutineScope
|
|
||||||
override fun dispatch(action: Action) = scope.dispatch(action)
|
|
||||||
override fun getState() = state.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S> createReducer(
|
|
||||||
initialState: S,
|
|
||||||
vararg reducers: (ReducerScope<S>) -> ActionHandler<S>,
|
|
||||||
): ReducerFactory<S> {
|
|
||||||
return object : ReducerFactory<S> {
|
|
||||||
override fun create(scope: ReducerScope<S>): Reducer<S> {
|
|
||||||
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 <A : Action, S> sideEffect(klass: KClass<A>, block: suspend (A, S) -> Unit): (ReducerScope<S>) -> ActionHandler<S> {
|
|
||||||
return {
|
|
||||||
ActionHandler.Async(key = klass as KClass<Action>) { action -> block(action as A, getState()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <A : Action, S> change(klass: KClass<A>, block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S> {
|
|
||||||
return {
|
|
||||||
ActionHandler.Sync(key = klass as KClass<Action>, block as (Action, S) -> S)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <A : Action, S> async(klass: KClass<A>, block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> {
|
|
||||||
return {
|
|
||||||
ActionHandler.Async(key = klass as KClass<Action>, block as suspend ReducerScope<S>.(Action) -> Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <A : Action, S> multi(klass: KClass<A>, block: Multi<A, S>.(A) -> (ReducerScope<S>) -> ActionHandler<S>): (ReducerScope<S>) -> ActionHandler<S> {
|
|
||||||
val multiScope = object : Multi<A, S> {
|
|
||||||
override fun sideEffect(block: suspend (S) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = sideEffect(klass) { _, state -> block(state) }
|
|
||||||
override fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S> = change(klass, block)
|
|
||||||
override fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = async(klass, block)
|
|
||||||
override fun nothing() = sideEffect { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
ActionHandler.Delegate(key = klass as KClass<Action>) { action ->
|
|
||||||
block(multiScope, action as A).invoke(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Multi<A : Action, S> {
|
|
||||||
fun sideEffect(block: suspend (S) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
|
||||||
fun nothing(): (ReducerScope<S>) -> ActionHandler<S>
|
|
||||||
fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S>
|
|
||||||
fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Action
|
|
|
@ -1,20 +0,0 @@
|
||||||
package fake
|
|
||||||
|
|
||||||
import org.amshove.kluent.internal.assertEquals
|
|
||||||
|
|
||||||
class FakeEventSource<E> : (E) -> Unit {
|
|
||||||
|
|
||||||
private val captures = mutableListOf<E>()
|
|
||||||
|
|
||||||
override fun invoke(event: E) {
|
|
||||||
captures.add(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertEvents(expected: List<E>) {
|
|
||||||
assertEquals(expected, captures)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertNoEvents() {
|
|
||||||
assertEquals(emptyList(), captures)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<S, E> {
|
|
||||||
operator fun invoke(block: suspend ReducerTestScope<S, E>.() -> Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S, E> testReducer(block: ((E) -> Unit) -> ReducerFactory<S>): ReducerTest<S, E> {
|
|
||||||
val fakeEventSource = FakeEventSource<E>()
|
|
||||||
val reducerFactory = block(fakeEventSource)
|
|
||||||
return object : ReducerTest<S, E> {
|
|
||||||
override fun invoke(block: suspend ReducerTestScope<S, E>.() -> Unit) {
|
|
||||||
runReducerTest(reducerFactory, fakeEventSource, block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S, E> runReducerTest(reducerFactory: ReducerFactory<S>, fakeEventSource: FakeEventSource<E>, block: suspend ReducerTestScope<S, E>.() -> Unit) {
|
|
||||||
runTest {
|
|
||||||
val expectTestScope = ExpectTest(coroutineContext)
|
|
||||||
block(ReducerTestScope(reducerFactory, fakeEventSource, expectTestScope))
|
|
||||||
expectTestScope.verifyExpects()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReducerTestScope<S, E>(
|
|
||||||
private val reducerFactory: ReducerFactory<S>,
|
|
||||||
private val fakeEventSource: FakeEventSource<E>,
|
|
||||||
private val expectTestScope: ExpectTestScope
|
|
||||||
) : ExpectTestScope by expectTestScope, Reducer<S> {
|
|
||||||
|
|
||||||
private var invalidateCapturedState: Boolean = false
|
|
||||||
private val actionSideEffects = mutableMapOf<Action, () -> S>()
|
|
||||||
private var manualState: S? = null
|
|
||||||
private var capturedResult: S? = null
|
|
||||||
|
|
||||||
private val actionCaptures = mutableListOf<Action>()
|
|
||||||
private val reducerScope = object : ReducerScope<S> {
|
|
||||||
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<S> = 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<E>) {
|
|
||||||
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<Action>) {
|
|
||||||
assertEquals(expected, actionCaptures)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertNoDispatches() {
|
|
||||||
assertEquals(emptyList(), actionCaptures)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertNoStateChange() {
|
|
||||||
assertEquals(reducerScope.getState(), capturedResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertNoEvents() {
|
|
||||||
fakeEventSource.assertNoEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertOnlyDispatches(expected: List<Action>) {
|
|
||||||
assertDispatches(expected)
|
|
||||||
fakeEventSource.assertNoEvents()
|
|
||||||
assertNoStateChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertOnlyEvents(events: List<E>) {
|
|
||||||
fakeEventSource.assertEvents(events)
|
|
||||||
assertNoDispatches()
|
|
||||||
assertNoStateChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertNoChanges() {
|
|
||||||
assertNoStateChange()
|
|
||||||
assertNoEvents()
|
|
||||||
assertNoDispatches()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S, E> ReducerTestScope<S, E>.assertOnlyDispatches(vararg action: Action) {
|
|
||||||
this.assertOnlyDispatches(action.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S, E> ReducerTestScope<S, E>.assertDispatches(vararg action: Action) {
|
|
||||||
this.assertDispatches(action.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S, E> ReducerTestScope<S, E>.assertEvents(vararg event: E) {
|
|
||||||
this.assertEvents(event.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S, E> ReducerTestScope<S, E>.assertOnlyEvents(vararg event: E) {
|
|
||||||
this.assertOnlyEvents(event.toList())
|
|
||||||
}
|
|
|
@ -1,28 +1,22 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'kotlin'
|
id 'kotlin'
|
||||||
id 'com.squareup.sqldelight'
|
alias libs.plugins.kotlin.serialization
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization'
|
alias libs.plugins.sqldelight
|
||||||
id 'java-test-fixtures'
|
id 'java-test-fixtures'
|
||||||
}
|
}
|
||||||
|
|
||||||
sqldelight {
|
sqldelight {
|
||||||
DapkDb {
|
StDb {
|
||||||
packageName = "app.dapk.db"
|
packageName = "app.dapk.db.app"
|
||||||
}
|
}
|
||||||
linkSqlite = true
|
linkSqlite = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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 project(":core")
|
||||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
implementation "chat-engine:chat-engine"
|
||||||
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
implementation libs.kotlin.serialization
|
||||||
implementation "com.squareup.sqldelight:coroutines-extensions:1.5.4"
|
implementation libs.sqldelight.extensions
|
||||||
|
|
||||||
kotlinFixtures(it)
|
kotlinFixtures(it)
|
||||||
testImplementation(testFixtures(project(":core")))
|
testImplementation(testFixtures(project(":core")))
|
||||||
|
|
|
@ -16,6 +16,5 @@ class ApplicationPreferences(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmInline
|
data class ApplicationVersion(val value: Int)
|
||||||
value class ApplicationVersion(val value: Int)
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<DeviceId>) {
|
|
||||||
dispatchers.withIoContext {
|
|
||||||
database.deviceQueries.transaction {
|
|
||||||
deviceIds.forEach {
|
|
||||||
database.deviceQueries.insertDeviceToMegolmSession(
|
|
||||||
device_id = it.value,
|
|
||||||
session_id = sessionId.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun markOutdated(userIds: List<UserId>) {
|
|
||||||
devicesCache.updateOutdated(userIds)
|
|
||||||
database.deviceQueries.markOutdated(userIds.map { it.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun maybeConsumeOutdated(userIds: List<UserId>): List<UserId> {
|
|
||||||
return devicesCache.consumeOutdated(userIds).also {
|
|
||||||
database.deviceQueries.markIndate(userIds.map { it.value })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateDevices(devices: Map<UserId, Map<DeviceId, DeviceKeys>>): List<DeviceKeys> {
|
|
||||||
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<UserId>, sessionId: SessionId): List<DeviceKeys> {
|
|
||||||
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<UserId, MutableMap<DeviceId, DeviceKeys>> = mutableMapOf(),
|
|
||||||
private var outdatedUserIds: MutableSet<UserId> = mutableSetOf()
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun consumeOutdated(userIds: List<UserId>): List<UserId> {
|
|
||||||
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<UserId>) {
|
|
||||||
outdatedUserIds.addAll(userIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putAll(devices: Map<UserId, Map<DeviceId, DeviceKeys>>) {
|
|
||||||
devices.mapValues { it.value.toMutableMap() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun devices(): List<DeviceKeys> {
|
|
||||||
return devicesCache.values.map { it.values }.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun device(userId: UserId, deviceId: DeviceId): DeviceKeys? {
|
|
||||||
return devicesCache[userId]?.get(deviceId)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<RoomMember>) {
|
|
||||||
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<UserId>): List<RoomMember> {
|
|
||||||
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<RoomMember> {
|
|
||||||
return coroutineDispatchers.withIoContext {
|
|
||||||
database.roomMemberQueries.selectMembersByRoom(roomId.value, limit.toLong())
|
|
||||||
.executeAsList()
|
|
||||||
.map { Json.decodeFromString(RoomMember.serializer(), it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Long, String>? {
|
|
||||||
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<Curve25519>): List<Pair<Curve25519, String>>? {
|
|
||||||
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)
|
|
|
@ -1,56 +1,23 @@
|
||||||
package app.dapk.st.domain
|
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.CoroutineDispatchers
|
||||||
import app.dapk.st.core.Preferences
|
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.EventLogPersistence
|
||||||
import app.dapk.st.domain.application.eventlog.LoggingStore
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
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.CachingPreferences
|
||||||
import app.dapk.st.domain.preference.PropertyCache
|
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.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(
|
class StoreModule(
|
||||||
private val database: DapkDb,
|
private val database: StDb,
|
||||||
private val databaseDropper: DatabaseDropper,
|
private val databaseDropper: DatabaseDropper,
|
||||||
val preferences: Preferences,
|
val preferences: Preferences,
|
||||||
private val credentialPreferences: Preferences,
|
val credentialPreferences: Preferences,
|
||||||
private val errorTracker: ErrorTracker,
|
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
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()
|
private val cache = PropertyCache()
|
||||||
val cachingPreferences = CachingPreferences(cache, preferences)
|
val cachingPreferences = CachingPreferences(cache, preferences)
|
||||||
|
|
||||||
|
@ -58,11 +25,6 @@ class StoreModule(
|
||||||
|
|
||||||
fun applicationStore() = ApplicationPreferences(preferences)
|
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 ->
|
fun cacheCleaner() = StoreCleaner { cleanCredentials ->
|
||||||
if (cleanCredentials) {
|
if (cleanCredentials) {
|
||||||
credentialPreferences.clear()
|
credentialPreferences.clear()
|
||||||
|
@ -79,8 +41,4 @@ class StoreModule(
|
||||||
|
|
||||||
fun messageStore(): MessageOptionsStore = MessageOptionsStore(cachingPreferences)
|
fun messageStore(): MessageOptionsStore = MessageOptionsStore(cachingPreferences)
|
||||||
|
|
||||||
fun memberStore(): MemberStore {
|
|
||||||
return MemberPersistence(database, coroutineDispatchers)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
package app.dapk.st.domain.application.eventlog
|
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.CoroutineDispatchers
|
||||||
import app.dapk.st.core.withIoContext
|
import app.dapk.st.core.withIoContext
|
||||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||||
|
@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class EventLogPersistence(
|
class EventLogPersistence(
|
||||||
private val database: DapkDb,
|
private val database: StDb,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
@ -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<RoomId, Map<String, MessageService.LocalEcho>>
|
|
||||||
|
|
||||||
class LocalEchoPersistence(
|
|
||||||
private val errorTracker: ErrorTracker,
|
|
||||||
private val database: DapkDb,
|
|
||||||
) : LocalEchoStore {
|
|
||||||
|
|
||||||
private val inMemoryEchos = MutableStateFlow<LocalEchoCache>(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<RoomId, Map<String, MessageService.LocalEcho>> {
|
|
||||||
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<String, MessageService.LocalEcho>.update(localEcho: MessageService.LocalEcho) {
|
|
||||||
this[localEcho.localId] = localEcho
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
)
|
|
|
@ -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<Set<RoomId>>(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<Set<RoomId>> = database.mutedRoomQueries.select()
|
|
||||||
.asFlow()
|
|
||||||
.mapToList()
|
|
||||||
.map { it.map { RoomId(it) }.toSet() }
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<OverviewState> {
|
|
||||||
return database.overviewStateQueries.selectAll()
|
|
||||||
.asFlow()
|
|
||||||
.mapToList()
|
|
||||||
.map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun removeRooms(roomsToRemove: List<RoomId>) {
|
|
||||||
dispatchers.withIoContext {
|
|
||||||
database.transaction {
|
|
||||||
roomsToRemove.forEach {
|
|
||||||
database.inviteStateQueries.remove(it.value)
|
|
||||||
database.overviewStateQueries.remove(it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun persistInvites(invites: List<RoomInvite>) {
|
|
||||||
dispatchers.withIoContext {
|
|
||||||
database.inviteStateQueries.transaction {
|
|
||||||
invites.forEach {
|
|
||||||
database.inviteStateQueries.insert(it.roomId.value, json.encodeToString(RoomInvite.serializer(), it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestInvites(): Flow<List<RoomInvite>> {
|
|
||||||
return database.inviteStateQueries.selectAll()
|
|
||||||
.asFlow()
|
|
||||||
.mapToList()
|
|
||||||
.map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun removeInvites(invites: List<RoomId>) {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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<RoomEvent>) {
|
|
||||||
coroutineDispatchers.withIoContext {
|
|
||||||
database.transaction {
|
|
||||||
events.forEach {
|
|
||||||
database.roomEventQueries.insertRoomEvent(roomId, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun remove(rooms: List<RoomId>) {
|
|
||||||
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<RoomState> {
|
|
||||||
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<EventId>) {
|
|
||||||
coroutineDispatchers.withIoContext {
|
|
||||||
database.transaction {
|
|
||||||
eventIds.forEach { eventId ->
|
|
||||||
database.unreadEventQueries.insertUnread(
|
|
||||||
event_id = eventId.value,
|
|
||||||
room_id = roomId.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> {
|
|
||||||
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<Map<RoomId, Int>> {
|
|
||||||
return database.roomEventQueries.selectAllUnread()
|
|
||||||
.asFlow()
|
|
||||||
.mapToList()
|
|
||||||
.map {
|
|
||||||
it.groupBy { RoomId(it.room_id) }
|
|
||||||
.mapValues { it.value.size }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observeNotMutedUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> {
|
|
||||||
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 <T : Any> Query<T>.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<EventId> {
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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 ?;
|
|
|
@ -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 (?, ?);
|
|
|
@ -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 = ?;
|
|
|
@ -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 = ?;
|
|
|
@ -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;
|
|
|
@ -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 = ?;
|
|
|
@ -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 = ?;
|
|
|
@ -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 (?, ?, ?);
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue