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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
|
|
|
@ -13,8 +13,11 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
|
|
|
@ -28,14 +28,14 @@ jobs:
|
|||
id: size
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ steps.size.outputs.PR_NUMBER }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: APK Size
|
||||
- name: Publish size to PR
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ steps.size.outputs.PR_NUMBER }}
|
||||
|
|
|
@ -15,8 +15,11 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
|
|
|
@ -15,7 +15,9 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
|
|
@ -16,32 +16,19 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
- uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Create pip requirements
|
||||
run: |
|
||||
echo "matrix-synapse" > requirements.txt
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: 'pip'
|
||||
|
||||
- name: Start synapse server
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
curl -sL https://gist.githubusercontent.com/ouchadam/e3ad09ec382bd91a66d88ab575ea7c31/raw/run.sh \
|
||||
| bash -s -- --no-rate-limit
|
||||
|
||||
- name: Run all unit tests
|
||||
run: ./gradlew clean allCodeCoverageReport --no-daemon
|
||||
run: ./gradlew allCodeCoverageReport
|
||||
|
||||
- uses: codecov/codecov-action@v2
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
||||
verbose: true
|
||||
files: ./build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
|
@ -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
|
||||
- [UnifiedPush](https://unifiedpush.org/)
|
||||
- FOSS variant
|
||||
- Minimal HTML formatting
|
||||
- Invitations
|
||||
- Image attachments
|
||||
|
||||
### Planned
|
||||
|
||||
- Device verification (technically supported but has no UI)
|
||||
- Invitations (technically supported but has no UI)
|
||||
- Room history
|
||||
- Message media
|
||||
- Cross signing
|
||||
- Google drive backups
|
||||
- Markdown subset (bold, italic, blocks)
|
||||
- Changing user name/avatar
|
||||
- Room settings and information
|
||||
- Exporting E2E room keys
|
||||
|
@ -118,4 +118,25 @@
|
|||
|
||||
---
|
||||
|
||||
|
||||
#### Repositories
|
||||
|
||||
`SmallTalk` is split into multiple repositories through git submodules.
|
||||
|
||||
##### [small-talk](https://github.com/ouchadam/small-talk)
|
||||
- The main repository, responsibile for rendering data into _screens_ and generating the android application artifact.
|
||||
|
||||
##### [chat-engine](https://github.com/ouchadam/chat-engine)
|
||||
- All things chat and matrix, where the vast majority of business logic sits.
|
||||
- Pure kotlin with no UI.
|
||||
|
||||
##### [conventions](https://github.com/ouchadam/conventions)
|
||||
- Reusable gradle plugins for modular android projects
|
||||
|
||||
##### [screen-state](https://github.com/ouchadam/screen-state)
|
||||
- Reusable state management and generic screen flow components.
|
||||
- Wrapper around android's `ViewModel` focused on testability.
|
||||
|
||||
---
|
||||
|
||||
#### Join the conversation @ https://matrix.to/#/#small-talk:iswell.cool
|
||||
|
|
100
app/build.gradle
100
app/build.gradle
|
@ -1,76 +1,27 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id "st-application-conventions"
|
||||
alias libs.plugins.firebase.crashlytics apply false
|
||||
}
|
||||
|
||||
applyCommonAndroidParameters(project)
|
||||
applyCrashlyticsIfRelease(project)
|
||||
applyCrashlyticsIfRelease()
|
||||
|
||||
android {
|
||||
ndkVersion "25.0.8141415"
|
||||
namespace "app.dapk.st"
|
||||
defaultConfig {
|
||||
applicationId "app.dapk.st"
|
||||
def versionJson = new groovy.json.JsonSlurper().parseText(rootProject.file('version.json').text)
|
||||
versionCode versionJson.code
|
||||
versionName versionJson.name
|
||||
|
||||
if (isDebugBuild) {
|
||||
resConfigs "en", "xxhdpi"
|
||||
} else {
|
||||
resConfigs "en"
|
||||
}
|
||||
|
||||
if (isFoss()) {
|
||||
archivesBaseName = "$archivesBaseName-foss"
|
||||
}
|
||||
}
|
||||
|
||||
bundle {
|
||||
abi.enableSplit true
|
||||
density.enableSplit true
|
||||
language.enableSplit true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
versionNameSuffix = " [debug]"
|
||||
matchingFallbacks = ['release']
|
||||
signingConfig.storeFile rootProject.file("tools/debug.keystore")
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
'proguard/app.pro',
|
||||
"proguard/serializationx.pro",
|
||||
"proguard/olm.pro"
|
||||
|
||||
if (project.hasProperty("unsigned")) {
|
||||
// releases are signed externally
|
||||
} else {
|
||||
signingConfig = buildTypes.debug.signingConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
}
|
||||
}
|
||||
|
||||
if (isDebugBuild) {
|
||||
androidComponents {
|
||||
def release = selector().withBuildType("release")
|
||||
beforeVariants(release) { it.enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring Dependencies.google.jdkLibs
|
||||
coreLibraryDesugaring libs.android.desugar
|
||||
|
||||
implementation project(":features:home")
|
||||
implementation project(":features:directory")
|
||||
|
@ -82,38 +33,37 @@ dependencies {
|
|||
implementation project(":features:navigator")
|
||||
implementation project(":features:share-entry")
|
||||
|
||||
implementation project(':domains:store')
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:core")
|
||||
implementation project(":domains:android:tracking")
|
||||
implementation project(":domains:android:push")
|
||||
implementation project(":domains:android:work")
|
||||
implementation project(":domains:android:imageloader")
|
||||
implementation project(":domains:olm")
|
||||
implementation project(":domains:store")
|
||||
|
||||
firebase(it, "messaging")
|
||||
|
||||
implementation project(":matrix:matrix")
|
||||
implementation project(":matrix:matrix-http-ktor")
|
||||
implementation project(":matrix:services:auth")
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:room")
|
||||
implementation project(":matrix:services:push")
|
||||
implementation project(":matrix:services:message")
|
||||
implementation project(":matrix:services:device")
|
||||
implementation project(":matrix:services:crypto")
|
||||
implementation project(":matrix:services:profile")
|
||||
|
||||
implementation project(":core")
|
||||
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":matrix-chat-engine")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation "chat-engine:matrix-chat-engine"
|
||||
implementation "chat-engine.matrix:store"
|
||||
|
||||
implementation Dependencies.google.androidxComposeUi
|
||||
implementation Dependencies.mavenCentral.ktorAndroid
|
||||
implementation Dependencies.mavenCentral.sqldelightAndroid
|
||||
implementation Dependencies.mavenCentral.matrixOlm
|
||||
implementation libs.ktor.android
|
||||
implementation libs.sqldelight.android
|
||||
implementation libs.matrix.olm
|
||||
|
||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
debugImplementation Dependencies.mavenCentral.leakCanary
|
||||
implementation libs.kotlin.serialization
|
||||
debugImplementation libs.leakcanary
|
||||
}
|
||||
|
||||
def applyCrashlyticsIfRelease() {
|
||||
if (isReleaseBuild && !isFoss()) {
|
||||
project.apply plugin: libs.plugins.firebase.crashlytics.get().pluginId
|
||||
project.afterEvaluate {
|
||||
project.tasks.withType(com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask).configureEach {
|
||||
it.googleServicesResourceRoot.set(project.file("src/release/res/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.dapk.st">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.dapk.st">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<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.Scope
|
||||
import app.dapk.st.directory.DirectoryModule
|
||||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.firebase.messaging.MessagingModule
|
||||
import app.dapk.st.graph.AppModule
|
||||
import app.dapk.st.home.HomeModule
|
||||
|
@ -55,23 +54,20 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
|||
attachAppLogger(logger)
|
||||
_appLogger = logger
|
||||
|
||||
onApplicationLaunch(notificationsModule, storeModule)
|
||||
onApplicationLaunch(notificationsModule)
|
||||
}
|
||||
|
||||
private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
|
||||
private fun onApplicationLaunch(notificationsModule: NotificationsModule) {
|
||||
applicationScope.launch {
|
||||
featureModules.homeModule.betaVersionUpgradeUseCase.waitUnitReady()
|
||||
|
||||
storeModule.credentialsStore().credentials()?.let {
|
||||
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
||||
}
|
||||
runCatching { storeModule.localEchoStore.preload() }
|
||||
featureModules.chatEngineModule.engine.preload()
|
||||
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
||||
val notificationsUseCase = notificationsModule.notificationsUseCase()
|
||||
notificationsUseCase.listenForNotificationChanges(this)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ProvidableModule> provide(klass: KClass<T>): T {
|
||||
return when (klass) {
|
||||
DirectoryModule::class -> featureModules.directoryModule
|
||||
|
@ -99,7 +95,6 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
|||
lazyFeatureModules.reset()
|
||||
|
||||
val notificationsModule = featureModules.notificationsModule
|
||||
val storeModule = appModule.storeModule.value
|
||||
onApplicationLaunch(notificationsModule, storeModule)
|
||||
onApplicationLaunch(notificationsModule)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,42 +2,38 @@ package app.dapk.st.graph
|
|||
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.ExifInterface
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.OpenableColumns
|
||||
import app.dapk.db.DapkDb
|
||||
import app.dapk.db.app.StDb
|
||||
import app.dapk.engine.core.Base64
|
||||
import app.dapk.st.BuildConfig
|
||||
import app.dapk.st.SharedPreferencesDelegate
|
||||
import app.dapk.st.core.*
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.core.extensions.unsafeLazy
|
||||
import app.dapk.st.directory.DirectoryModule
|
||||
import app.dapk.st.domain.MatrixStoreModule
|
||||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.engine.ImageContentReader
|
||||
import app.dapk.st.engine.MatrixEngine
|
||||
import app.dapk.st.firebase.messaging.MessagingModule
|
||||
import app.dapk.st.home.BetaVersionUpgradeUseCase
|
||||
import app.dapk.st.home.HomeModule
|
||||
import app.dapk.st.home.MainActivity
|
||||
import app.dapk.st.imageloader.ImageLoaderModule
|
||||
import app.dapk.st.impl.*
|
||||
import app.dapk.st.login.LoginModule
|
||||
import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.JsonString
|
||||
import app.dapk.st.matrix.common.MatrixLogger
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import app.dapk.st.messenger.MessengerActivity
|
||||
import app.dapk.st.messenger.MessengerModule
|
||||
import app.dapk.st.messenger.gallery.ImageGalleryModule
|
||||
import app.dapk.st.navigator.IntentFactory
|
||||
import app.dapk.st.navigator.MessageAttachment
|
||||
import app.dapk.st.notifications.NotificationsModule
|
||||
import app.dapk.st.olm.OlmPersistenceWrapper
|
||||
import app.dapk.st.profile.ProfileModule
|
||||
import app.dapk.st.push.PushHandler
|
||||
import app.dapk.st.push.PushModule
|
||||
|
@ -51,7 +47,6 @@ import app.dapk.st.work.WorkModule
|
|||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.InputStream
|
||||
|
||||
internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||
|
||||
|
@ -64,26 +59,48 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||
}
|
||||
|
||||
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
|
||||
private val database = DapkDb(driver)
|
||||
private val stDriver = AndroidSqliteDriver(DapkDb.Schema, context, "stdb.db")
|
||||
private val engineDatabase = DapkDb(driver)
|
||||
private val stDatabase = StDb(stDriver)
|
||||
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
||||
private val base64 = AndroidBase64()
|
||||
|
||||
val storeModule = unsafeLazy {
|
||||
StoreModule(
|
||||
database = database,
|
||||
database = stDatabase,
|
||||
preferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", coroutineDispatchers),
|
||||
errorTracker = trackingModule.errorTracker,
|
||||
credentialPreferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-credentials-preferences", coroutineDispatchers),
|
||||
databaseDropper = DefaultDatabaseDropper(coroutineDispatchers, driver),
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private val workModule = WorkModule(context)
|
||||
private val imageLoaderModule = ImageLoaderModule(context)
|
||||
|
||||
private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) }
|
||||
private val chatEngineModule =
|
||||
ChatEngineModule(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta)
|
||||
private val chatEngineModule = ChatEngineModule(
|
||||
unsafeLazy { matrixStoreModule() },
|
||||
trackingModule,
|
||||
workModule,
|
||||
logger,
|
||||
coroutineDispatchers,
|
||||
imageContentReader,
|
||||
base64,
|
||||
buildMeta
|
||||
)
|
||||
|
||||
private fun matrixStoreModule(): MatrixStoreModule {
|
||||
val value = storeModule.value
|
||||
return MatrixStoreModule(
|
||||
engineDatabase,
|
||||
value.preferences.engine(),
|
||||
value.credentialPreferences.engine(),
|
||||
trackingModule.errorTracker.engine(),
|
||||
coroutineDispatchers.engine(),
|
||||
)
|
||||
}
|
||||
|
||||
val domainModules = DomainModules(chatEngineModule, trackingModule.errorTracker, context, coroutineDispatchers)
|
||||
|
||||
|
@ -133,7 +150,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||
|
||||
internal class FeatureModules internal constructor(
|
||||
private val storeModule: Lazy<StoreModule>,
|
||||
private val chatEngineModule: ChatEngineModule,
|
||||
val chatEngineModule: ChatEngineModule,
|
||||
private val domainModules: DomainModules,
|
||||
private val trackingModule: TrackingModule,
|
||||
private val coreAndroidModule: CoreAndroidModule,
|
||||
|
@ -173,6 +190,9 @@ internal class FeatureModules internal constructor(
|
|||
storeModule.value.applicationStore(),
|
||||
buildMeta,
|
||||
),
|
||||
profileModule,
|
||||
loginModule,
|
||||
directoryModule
|
||||
)
|
||||
}
|
||||
val settingsModule by unsafeLazy {
|
||||
|
@ -220,7 +240,7 @@ internal class FeatureModules internal constructor(
|
|||
}
|
||||
|
||||
internal class ChatEngineModule(
|
||||
private val storeModule: Lazy<StoreModule>,
|
||||
private val matrixStoreModule: Lazy<MatrixStoreModule>,
|
||||
private val trackingModule: TrackingModule,
|
||||
private val workModule: WorkModule,
|
||||
private val logger: MatrixLogger,
|
||||
|
@ -231,26 +251,22 @@ internal class ChatEngineModule(
|
|||
) {
|
||||
|
||||
val engine by unsafeLazy {
|
||||
val store = storeModule.value
|
||||
val matrixCoroutineDispatchers = app.dapk.engine.core.CoroutineDispatchers(
|
||||
coroutineDispatchers.io,
|
||||
coroutineDispatchers.main,
|
||||
coroutineDispatchers.global
|
||||
)
|
||||
val matrixStore = matrixStoreModule.value
|
||||
MatrixEngine.Factory().create(
|
||||
base64,
|
||||
buildMeta,
|
||||
logger,
|
||||
SmallTalkDeviceNameGenerator(),
|
||||
coroutineDispatchers,
|
||||
trackingModule.errorTracker,
|
||||
matrixCoroutineDispatchers,
|
||||
trackingModule.errorTracker.engine(),
|
||||
imageContentReader,
|
||||
BackgroundWorkAdapter(workModule.workScheduler()),
|
||||
store.memberStore(),
|
||||
store.roomStore(),
|
||||
store.profileStore(),
|
||||
store.syncStore(),
|
||||
store.overviewStore(),
|
||||
store.filterStore(),
|
||||
store.localEchoStore,
|
||||
store.credentialsStore(),
|
||||
store.knownDevicesStore(),
|
||||
OlmPersistenceWrapper(store.olmStore(), base64),
|
||||
matrixStore,
|
||||
includeLogging = buildMeta.isDebug,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -289,43 +305,23 @@ internal class DomainModules(
|
|||
val taskRunnerModule by unsafeLazy {
|
||||
TaskRunnerModule(TaskRunnerAdapter(chatEngineModule.engine, AppTaskRunner(chatEngineModule.engine)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {
|
||||
override fun meta(uri: String): ImageContentReader.ImageContent {
|
||||
val androidUri = Uri.parse(uri)
|
||||
val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")
|
||||
private fun CoroutineDispatchers.engine() = app.dapk.engine.core.CoroutineDispatchers(this.io, this.main, this.global)
|
||||
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeStream(fileStream, null, options)
|
||||
|
||||
val fileSize = contentResolver.query(androidUri, null, null, null, null)?.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
cursor.getLong(columnIndex)
|
||||
} ?: throw IllegalArgumentException("Could not process $uri")
|
||||
|
||||
val shouldSwapSizes = ExifInterface(contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")).let {
|
||||
val orientation = it.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
|
||||
orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270
|
||||
}
|
||||
|
||||
return ImageContentReader.ImageContent(
|
||||
height = if (shouldSwapSizes) options.outWidth else options.outHeight,
|
||||
width = if (shouldSwapSizes) options.outHeight else options.outWidth,
|
||||
size = fileSize,
|
||||
mimeType = options.outMimeType,
|
||||
fileName = androidUri.lastPathSegment ?: "file",
|
||||
)
|
||||
private fun ErrorTracker.engine(): app.dapk.engine.core.extensions.ErrorTracker {
|
||||
val tracker = this
|
||||
return object : app.dapk.engine.core.extensions.ErrorTracker {
|
||||
override fun track(throwable: Throwable, extra: String) = tracker.track(throwable, extra)
|
||||
}
|
||||
|
||||
override fun inputStream(uri: String): InputStream = contentResolver.openInputStream(Uri.parse(uri))!!
|
||||
}
|
||||
|
||||
internal class SmallTalkDeviceNameGenerator : DeviceDisplayNameGenerator {
|
||||
override fun generate(): String {
|
||||
val randomIdentifier = (('A'..'Z') + ('a'..'z') + ('0'..'9')).shuffled().take(4).joinToString("")
|
||||
return "SmallTalk Android ($randomIdentifier)"
|
||||
private fun Preferences.engine(): app.dapk.engine.core.Preferences {
|
||||
val prefs = this
|
||||
return object : app.dapk.engine.core.Preferences {
|
||||
override suspend fun store(key: String, value: String) = prefs.store(key, value)
|
||||
override suspend fun readString(key: String) = prefs.readString(key)
|
||||
override suspend fun clear() = prefs.clear()
|
||||
override suspend fun remove(key: String) = prefs.remove(key)
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package app.dapk.st.graph
|
||||
package app.dapk.st.impl
|
||||
|
||||
import app.dapk.st.core.Base64
|
||||
import app.dapk.engine.core.Base64
|
||||
|
||||
class AndroidBase64 : Base64 {
|
||||
internal class AndroidBase64 : Base64 {
|
||||
override fun encode(input: ByteArray): String {
|
||||
return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT)
|
||||
}
|
|
@ -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.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
|
||||
|
||||
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.withIoContext
|
|
@ -1,4 +1,4 @@
|
|||
package app.dapk.st
|
||||
package app.dapk.st.impl
|
||||
|
||||
import android.content.Context
|
||||
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.ChatEngineTask
|
127
build.gradle
127
build.gradle
|
@ -1,16 +1,5 @@
|
|||
buildscript {
|
||||
apply from: "dependencies.gradle"
|
||||
|
||||
repositories {
|
||||
Dependencies._repositories.call(it)
|
||||
}
|
||||
dependencies {
|
||||
classpath Dependencies.google.androidGradlePlugin
|
||||
classpath Dependencies.mavenCentral.kotlinGradlePlugin
|
||||
classpath Dependencies.mavenCentral.sqldelightGradlePlugin
|
||||
classpath Dependencies.mavenCentral.kotlinSerializationGradlePlugin
|
||||
classpath Dependencies.google.firebaseCrashlyticsPlugin
|
||||
}
|
||||
plugins {
|
||||
id "st-base-conventions" apply false
|
||||
}
|
||||
|
||||
def launchTask = getGradle()
|
||||
|
@ -18,7 +7,7 @@ def launchTask = getGradle()
|
|||
.getTaskRequests()
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
def isReleaseBuild = launchTask.contains("release")
|
||||
ext.isReleaseBuild = launchTask.contains("bundlerelease") || launchTask.contains("assemblerelease")
|
||||
ext.isDebugBuild = !isReleaseBuild
|
||||
|
||||
subprojects {
|
||||
|
@ -37,112 +26,17 @@ task clean(type: Delete) {
|
|||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
ext.applyMatrixServiceModule = { project ->
|
||||
project.apply plugin: 'kotlin'
|
||||
project.apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
|
||||
|
||||
def dependencies = project.dependencies
|
||||
|
||||
dependencies.api project.project(":matrix:matrix")
|
||||
dependencies.api project.project(":matrix:common")
|
||||
dependencies.implementation project.project(":matrix:matrix-http")
|
||||
dependencies.implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
}
|
||||
|
||||
ext.applyLibraryPlugins = { project ->
|
||||
project.apply plugin: 'com.android.library'
|
||||
project.apply plugin: 'kotlin-android'
|
||||
}
|
||||
|
||||
ext.androidSdkVersion = 33
|
||||
|
||||
ext.applyCommonAndroidParameters = { project ->
|
||||
def android = project.android
|
||||
android.compileSdk androidSdkVersion
|
||||
android.compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
incremental = true
|
||||
}
|
||||
android.defaultConfig {
|
||||
minSdkVersion 24
|
||||
targetSdkVersion androidSdkVersion
|
||||
}
|
||||
}
|
||||
|
||||
ext.applyLibraryModuleOptimisations = { project ->
|
||||
project.android {
|
||||
variantFilter { variant ->
|
||||
if (variant.name == "debug") {
|
||||
variant.ignore = true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = false
|
||||
dataBinding = false
|
||||
aidl = false
|
||||
renderScript = false
|
||||
resValues = false
|
||||
shaders = false
|
||||
viewBinding = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ext.applyCompose = { project ->
|
||||
def dependencies = project.dependencies
|
||||
|
||||
dependencies.implementation Dependencies.google.androidxComposeUi
|
||||
dependencies.implementation Dependencies.google.androidxComposeFoundation
|
||||
dependencies.implementation Dependencies.google.androidxComposeMaterial
|
||||
dependencies.implementation Dependencies.google.androidxComposeIconsExtended
|
||||
dependencies.implementation Dependencies.google.androidxActivityCompose
|
||||
|
||||
def android = project.android
|
||||
android.buildFeatures.compose = true
|
||||
android.composeOptions {
|
||||
kotlinCompilerExtensionVersion = Dependencies.google.kotlinCompilerExtensionVersion
|
||||
}
|
||||
}
|
||||
|
||||
ext.applyAndroidComposeLibraryModule = { project ->
|
||||
applyAndroidLibraryModule(project)
|
||||
applyCompose(project)
|
||||
}
|
||||
|
||||
ext.applyAndroidLibraryModule = { project ->
|
||||
applyLibraryPlugins(project)
|
||||
applyCommonAndroidParameters(project)
|
||||
applyLibraryModuleOptimisations(project)
|
||||
}
|
||||
|
||||
ext.applyCrashlyticsIfRelease = { project ->
|
||||
if (isReleaseBuild && !isFoss()) {
|
||||
project.apply plugin: 'com.google.firebase.crashlytics'
|
||||
project.afterEvaluate {
|
||||
project.tasks.withType(com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask).configureEach {
|
||||
it.googleServicesResourceRoot.set(project.file("src/release/res/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ext.kotlinTest = { dependencies ->
|
||||
dependencies.testImplementation Dependencies.mavenCentral.kluent
|
||||
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
|
||||
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.7.20"
|
||||
dependencies.testImplementation Dependencies.mavenCentral.mockk
|
||||
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||
|
||||
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
|
||||
dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
|
||||
dependencies.testImplementation libs.kluent
|
||||
dependencies.testImplementation libs.kotlin.test
|
||||
dependencies.testImplementation libs.mockk
|
||||
dependencies.testImplementation libs.kotlin.coroutines.test
|
||||
}
|
||||
|
||||
ext.kotlinFixtures = { dependencies ->
|
||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.mockk
|
||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
|
||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
dependencies.testFixturesImplementation libs.mockk
|
||||
dependencies.testFixturesImplementation libs.kluent
|
||||
dependencies.testFixturesImplementation libs.kotlin.coroutines
|
||||
}
|
||||
|
||||
ext.androidImportFixturesWorkaround = { project, fixtures ->
|
||||
|
@ -163,7 +57,6 @@ ext.firebase = { dependencies, name ->
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if (launchTask.contains("codeCoverageReport".toLowerCase())) {
|
||||
apply from: 'tools/coverage.gradle'
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
api Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
testFixturesImplementation Dependencies.mavenCentral.kluent
|
||||
testFixturesImplementation Dependencies.mavenCentral.mockk
|
||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
|
||||
api libs.kotlin.coroutines
|
||||
testFixturesImplementation libs.kotlin.coroutines
|
||||
testFixturesImplementation libs.kluent
|
||||
testFixturesImplementation libs.mockk
|
||||
testFixturesImplementation libs.kotlin.coroutines.test
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
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 {
|
||||
implementation project(":core")
|
||||
implementation Dependencies.mavenCentral.coil
|
||||
implementation Dependencies.mavenCentral.accompanistSystemuicontroller
|
||||
}
|
||||
implementation libs.compose.coil
|
||||
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 {
|
||||
implementation project(":core")
|
||||
implementation project(":features:navigator")
|
||||
implementation project(":design-library")
|
||||
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 })
|
||||
}
|
||||
|
||||
|
||||
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.LifecycleEventObserver
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -27,6 +28,10 @@ class StartScope(private val scope: CoroutineScope) {
|
|||
fun <T> SharedFlow<T>.launch(onEach: suspend (T) -> Unit) {
|
||||
this.onEach(onEach).launchIn(scope)
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.launch(onEach: suspend (T) -> Unit) {
|
||||
this.onEach(onEach).launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
compileOnly project(":domains:android:stub")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
plugins {
|
||||
id "st-android-library-conventions"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":core")
|
||||
implementation Dependencies.mavenCentral.coil
|
||||
implementation libs.compose.coil
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
apply plugin: "org.jetbrains.kotlin.plugin.serialization"
|
||||
plugins {
|
||||
id "st-android-library-conventions"
|
||||
alias libs.plugins.kotlin.serialization
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(':core')
|
||||
implementation project(':domains:android:core')
|
||||
implementation project(':domains:store')
|
||||
implementation project(':domains:android:core')
|
||||
|
||||
firebase(it, "messaging")
|
||||
|
||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
implementation Dependencies.jitPack.unifiedPush
|
||||
implementation libs.kotlin.serialization
|
||||
implementation libs.unifiedpush
|
||||
|
||||
kotlinTest(it)
|
||||
testImplementation 'chat-engine:chat-engine-test'
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@ import app.dapk.st.matrix.common.RoomId
|
|||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
interface PushHandler {
|
||||
fun onNewToken(payload: PushTokenPayload)
|
||||
fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PushTokenPayload(
|
||||
@SerialName("token") val token: String,
|
||||
@SerialName("gateway_url") val gatewayUrl: String,
|
||||
)
|
||||
)
|
||||
|
||||
interface PushHandler {
|
||||
fun onNewToken(payload: PushTokenPayload)
|
||||
fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
plugins {
|
||||
id 'kotlin'
|
||||
id "kotlin"
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ if (localProperties.exists()) {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
def androidVer = androidSdkVersion
|
||||
def androidVer = 33
|
||||
api files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar")
|
||||
|
||||
kotlinFixtures(it)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
plugins {
|
||||
id "st-android-library-conventions"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
|
|
|
@ -10,6 +10,19 @@ class CrashTrackerLogger : ErrorTracker {
|
|||
override fun track(throwable: Throwable, extra: String) {
|
||||
Log.e("ST", throwable.message, throwable)
|
||||
log(AppLogTag.ERROR_NON_FATAL, "${throwable.message ?: "N/A"} extra=$extra")
|
||||
|
||||
throwable.findCauseMessage()?.let {
|
||||
if (throwable.message != it) {
|
||||
log(AppLogTag.ERROR_NON_FATAL, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.findCauseMessage(): String? {
|
||||
return when (val inner = this.cause) {
|
||||
null -> this.message ?: ""
|
||||
else -> inner.findCauseMessage()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
plugins { id 'kotlin' }
|
||||
plugins {
|
||||
id "kotlin"
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
plugins {
|
||||
id 'kotlin'
|
||||
id "kotlin"
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly project(":domains:android:viewmodel-stub")
|
||||
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
implementation libs.kotlin.coroutines
|
||||
|
||||
kotlinFixtures(it)
|
||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
|
||||
testFixturesImplementation libs.kotlin.coroutines
|
||||
testFixturesImplementation libs.kotlin.coroutines.test
|
||||
testFixturesImplementation testFixtures(project(":core"))
|
||||
testFixturesCompileOnly project(":domains:android:viewmodel-stub")
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
plugins {
|
||||
id "st-android-library-conventions"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
plugins { id 'kotlin' }
|
||||
plugins {
|
||||
id "kotlin"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
plugins {
|
||||
id "st-android-library-conventions"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
implementation platform(Dependencies.google.firebaseBom)
|
||||
implementation platform(libs.firebase.bom)
|
||||
implementation 'com.google.firebase:firebase-crashlytics'
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
plugins {
|
||||
id "st-android-library-conventions"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
implementation project(':matrix:common')
|
||||
implementation "chat-engine:chat-engine"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
plugins {
|
||||
id "st-android-library-conventions"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
implementation project(':domains:android:core')
|
||||
implementation project(':matrix:common')
|
||||
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation platform(libs.firebase.bom)
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
id 'kotlin'
|
||||
id 'com.squareup.sqldelight'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization'
|
||||
alias libs.plugins.kotlin.serialization
|
||||
alias libs.plugins.sqldelight
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
DapkDb {
|
||||
packageName = "app.dapk.db"
|
||||
StDb {
|
||||
packageName = "app.dapk.db.app"
|
||||
}
|
||||
linkSqlite = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(":matrix:common")
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:message")
|
||||
implementation project(":matrix:services:profile")
|
||||
implementation project(":matrix:services:device")
|
||||
implementation project(":matrix:services:room")
|
||||
implementation project(":core")
|
||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
implementation "com.squareup.sqldelight:coroutines-extensions:1.5.4"
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation libs.kotlin.serialization
|
||||
implementation libs.sqldelight.extensions
|
||||
|
||||
kotlinFixtures(it)
|
||||
testImplementation(testFixtures(project(":core")))
|
||||
|
|
|
@ -16,6 +16,5 @@ class ApplicationPreferences(
|
|||
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class ApplicationVersion(val value: Int)
|
||||
data 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
|
||||
|
||||
import app.dapk.db.DapkDb
|
||||
import app.dapk.db.app.StDb
|
||||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.Preferences
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.core.extensions.unsafeLazy
|
||||
import app.dapk.st.domain.application.eventlog.EventLogPersistence
|
||||
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||
import app.dapk.st.domain.localecho.LocalEchoPersistence
|
||||
import app.dapk.st.domain.preference.CachingPreferences
|
||||
import app.dapk.st.domain.preference.PropertyCache
|
||||
import app.dapk.st.domain.profile.ProfilePersistence
|
||||
import app.dapk.st.domain.push.PushTokenRegistrarPreferences
|
||||
import app.dapk.st.domain.room.MutedStorePersistence
|
||||
import app.dapk.st.domain.sync.OverviewPersistence
|
||||
import app.dapk.st.domain.sync.RoomPersistence
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.message.LocalEchoStore
|
||||
import app.dapk.st.matrix.room.MemberStore
|
||||
import app.dapk.st.matrix.room.ProfileStore
|
||||
import app.dapk.st.matrix.sync.FilterStore
|
||||
import app.dapk.st.matrix.sync.OverviewStore
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import app.dapk.st.matrix.sync.SyncStore
|
||||
|
||||
class StoreModule(
|
||||
private val database: DapkDb,
|
||||
private val database: StDb,
|
||||
private val databaseDropper: DatabaseDropper,
|
||||
val preferences: Preferences,
|
||||
private val credentialPreferences: Preferences,
|
||||
private val errorTracker: ErrorTracker,
|
||||
val credentialPreferences: Preferences,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
|
||||
private val muteableStore by unsafeLazy { MutedStorePersistence(database, coroutineDispatchers) }
|
||||
|
||||
fun overviewStore(): OverviewStore = OverviewPersistence(database, coroutineDispatchers)
|
||||
fun roomStore(): RoomStore {
|
||||
return RoomPersistence(
|
||||
database = database,
|
||||
overviewPersistence = OverviewPersistence(database, coroutineDispatchers),
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
muteableStore = muteableStore,
|
||||
)
|
||||
}
|
||||
|
||||
fun credentialsStore(): CredentialsStore = CredentialsPreferences(credentialPreferences)
|
||||
fun syncStore(): SyncStore = SyncTokenPreferences(preferences)
|
||||
fun filterStore(): FilterStore = FilterPreferences(preferences)
|
||||
val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) }
|
||||
|
||||
private val cache = PropertyCache()
|
||||
val cachingPreferences = CachingPreferences(cache, preferences)
|
||||
|
||||
|
@ -58,11 +25,6 @@ class StoreModule(
|
|||
|
||||
fun applicationStore() = ApplicationPreferences(preferences)
|
||||
|
||||
fun olmStore() = OlmPersistence(database, credentialsStore(), coroutineDispatchers)
|
||||
fun knownDevicesStore() = DevicePersistence(database, KnownDevicesCache(), coroutineDispatchers)
|
||||
|
||||
fun profileStore(): ProfileStore = ProfilePersistence(preferences)
|
||||
|
||||
fun cacheCleaner() = StoreCleaner { cleanCredentials ->
|
||||
if (cleanCredentials) {
|
||||
credentialPreferences.clear()
|
||||
|
@ -79,8 +41,4 @@ class StoreModule(
|
|||
|
||||
fun messageStore(): MessageOptionsStore = MessageOptionsStore(cachingPreferences)
|
||||
|
||||
fun memberStore(): MemberStore {
|
||||
return MemberPersistence(database, coroutineDispatchers)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
import app.dapk.db.DapkDb
|
||||
import app.dapk.db.app.StDb
|
||||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.withIoContext
|
||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||
|
@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class EventLogPersistence(
|
||||
private val database: DapkDb,
|
||||
private val database: StDb,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
|
||||
|
|
|
@ -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