Merge pull request #273 from ouchadam/tech/engine-submodule
Tech/engine submodule
This commit is contained in:
commit
05cbb52aca
|
@ -44,6 +44,7 @@ jobs:
|
|||
- name: Run all unit tests
|
||||
run: ./gradlew clean allCodeCoverageReport --no-daemon
|
||||
|
||||
- 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,./chat-engine/build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
[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
|
||||
|
|
|
@ -82,32 +82,21 @@ dependencies {
|
|||
implementation project(":features:navigator")
|
||||
implementation project(":features:share-entry")
|
||||
|
||||
implementation project(':domains:store')
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:core")
|
||||
implementation project(":domains:android:tracking")
|
||||
implementation project(":domains:android:push")
|
||||
implementation project(":domains:android:work")
|
||||
implementation project(":domains:android:imageloader")
|
||||
implementation project(":domains:olm")
|
||||
implementation project(":domains:store")
|
||||
|
||||
firebase(it, "messaging")
|
||||
|
||||
implementation project(":matrix:matrix")
|
||||
implementation project(":matrix:matrix-http-ktor")
|
||||
implementation project(":matrix:services:auth")
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:room")
|
||||
implementation project(":matrix:services:push")
|
||||
implementation project(":matrix:services:message")
|
||||
implementation project(":matrix:services:device")
|
||||
implementation project(":matrix:services:crypto")
|
||||
implementation project(":matrix:services:profile")
|
||||
|
||||
implementation project(":core")
|
||||
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":matrix-chat-engine")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation "chat-engine:matrix-chat-engine"
|
||||
implementation "chat-engine.matrix:store"
|
||||
|
||||
implementation Dependencies.google.androidxComposeUi
|
||||
implementation Dependencies.mavenCentral.ktorAndroid
|
||||
|
|
|
@ -9,7 +9,6 @@ import app.dapk.st.core.attachAppLogger
|
|||
import app.dapk.st.core.extensions.ResettableUnsafeLazy
|
||||
import app.dapk.st.core.extensions.Scope
|
||||
import app.dapk.st.directory.DirectoryModule
|
||||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.firebase.messaging.MessagingModule
|
||||
import app.dapk.st.graph.AppModule
|
||||
import app.dapk.st.home.HomeModule
|
||||
|
@ -55,17 +54,14 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
|||
attachAppLogger(logger)
|
||||
_appLogger = logger
|
||||
|
||||
onApplicationLaunch(notificationsModule, storeModule)
|
||||
onApplicationLaunch(notificationsModule)
|
||||
}
|
||||
|
||||
private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
|
||||
private fun onApplicationLaunch(notificationsModule: NotificationsModule) {
|
||||
applicationScope.launch {
|
||||
featureModules.homeModule.betaVersionUpgradeUseCase.waitUnitReady()
|
||||
|
||||
storeModule.credentialsStore().credentials()?.let {
|
||||
featureModules.chatEngineModule.engine.preload()
|
||||
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
||||
}
|
||||
runCatching { storeModule.localEchoStore.preload() }
|
||||
val notificationsUseCase = notificationsModule.notificationsUseCase()
|
||||
notificationsUseCase.listenForNotificationChanges(this)
|
||||
}
|
||||
|
@ -99,7 +95,6 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
|||
lazyFeatureModules.reset()
|
||||
|
||||
val notificationsModule = featureModules.notificationsModule
|
||||
val storeModule = appModule.storeModule.value
|
||||
onApplicationLaunch(notificationsModule, storeModule)
|
||||
onApplicationLaunch(notificationsModule)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -220,7 +237,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 +248,22 @@ internal class ChatEngineModule(
|
|||
) {
|
||||
|
||||
val engine by unsafeLazy {
|
||||
val store = storeModule.value
|
||||
val matrixCoroutineDispatchers = app.dapk.engine.core.CoroutineDispatchers(
|
||||
coroutineDispatchers.io,
|
||||
coroutineDispatchers.main,
|
||||
coroutineDispatchers.global
|
||||
)
|
||||
val matrixStore = matrixStoreModule.value
|
||||
MatrixEngine.Factory().create(
|
||||
base64,
|
||||
buildMeta,
|
||||
logger,
|
||||
SmallTalkDeviceNameGenerator(),
|
||||
coroutineDispatchers,
|
||||
trackingModule.errorTracker,
|
||||
matrixCoroutineDispatchers,
|
||||
trackingModule.errorTracker.engine(),
|
||||
imageContentReader,
|
||||
BackgroundWorkAdapter(workModule.workScheduler()),
|
||||
store.memberStore(),
|
||||
store.roomStore(),
|
||||
store.profileStore(),
|
||||
store.syncStore(),
|
||||
store.overviewStore(),
|
||||
store.filterStore(),
|
||||
store.localEchoStore,
|
||||
store.credentialsStore(),
|
||||
store.knownDevicesStore(),
|
||||
OlmPersistenceWrapper(store.olmStore(), base64),
|
||||
matrixStore,
|
||||
includeLogging = buildMeta.isDebug,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -289,43 +302,23 @@ internal class DomainModules(
|
|||
val taskRunnerModule by unsafeLazy {
|
||||
TaskRunnerModule(TaskRunnerAdapter(chatEngineModule.engine, AppTaskRunner(chatEngineModule.engine)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {
|
||||
override fun meta(uri: String): ImageContentReader.ImageContent {
|
||||
val androidUri = Uri.parse(uri)
|
||||
val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")
|
||||
private fun CoroutineDispatchers.engine() = app.dapk.engine.core.CoroutineDispatchers(this.io, this.main, this.global)
|
||||
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeStream(fileStream, null, options)
|
||||
|
||||
val fileSize = contentResolver.query(androidUri, null, null, null, null)?.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
cursor.getLong(columnIndex)
|
||||
} ?: throw IllegalArgumentException("Could not process $uri")
|
||||
|
||||
val shouldSwapSizes = ExifInterface(contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")).let {
|
||||
val orientation = it.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
|
||||
orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270
|
||||
}
|
||||
|
||||
return ImageContentReader.ImageContent(
|
||||
height = if (shouldSwapSizes) options.outWidth else options.outHeight,
|
||||
width = if (shouldSwapSizes) options.outHeight else options.outWidth,
|
||||
size = fileSize,
|
||||
mimeType = options.outMimeType,
|
||||
fileName = androidUri.lastPathSegment ?: "file",
|
||||
)
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 2ac5fc22a562362acc8b4b4d527580221085971d
|
|
@ -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,21 +0,0 @@
|
|||
package fake
|
||||
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import test.delegateEmit
|
||||
import test.delegateReturn
|
||||
import java.io.InputStream
|
||||
|
||||
class FakeChatEngine : ChatEngine by mockk() {
|
||||
|
||||
fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn()
|
||||
fun givenDirectory() = every { directory() }.delegateReturn()
|
||||
fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn()
|
||||
fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit()
|
||||
fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit()
|
||||
fun givenInvites() = every { invites() }.delegateEmit()
|
||||
fun givenMe(forceRefresh: Boolean) = coEvery { me(forceRefresh) }.delegateReturn()
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package app.dapk.st.core
|
||||
|
||||
interface Base64 {
|
||||
fun encode(input: ByteArray): String
|
||||
fun decode(input: String): ByteArray
|
||||
}
|
|
@ -2,9 +2,10 @@ applyAndroidLibraryModule(project)
|
|||
apply plugin: "org.jetbrains.kotlin.plugin.serialization"
|
||||
|
||||
dependencies {
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(':core')
|
||||
implementation project(':domains:android:core')
|
||||
implementation project(':domains:store')
|
||||
implementation project(':domains:android:core')
|
||||
|
||||
firebase(it, "messaging")
|
||||
|
||||
|
@ -12,7 +13,7 @@ dependencies {
|
|||
implementation Dependencies.jitPack.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?)
|
||||
}
|
|
@ -2,5 +2,5 @@ applyAndroidLibraryModule(project)
|
|||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
implementation project(':matrix:common')
|
||||
implementation "chat-engine:chat-engine"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ applyAndroidLibraryModule(project)
|
|||
dependencies {
|
||||
implementation project(':core')
|
||||
implementation project(':domains:android:core')
|
||||
implementation project(':matrix:common')
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -6,20 +6,15 @@ plugins {
|
|||
}
|
||||
|
||||
sqldelight {
|
||||
DapkDb {
|
||||
packageName = "app.dapk.db"
|
||||
StDb {
|
||||
packageName = "app.dapk.db.app"
|
||||
}
|
||||
linkSqlite = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(":matrix:common")
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:message")
|
||||
implementation project(":matrix:services:profile")
|
||||
implementation project(":matrix:services:device")
|
||||
implementation project(":matrix:services:room")
|
||||
implementation project(":core")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
implementation "com.squareup.sqldelight:coroutines-extensions:1.5.4"
|
||||
|
|
|
@ -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 (?, ?, ?);
|
|
@ -1,18 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS dbUnreadEvent (
|
||||
event_id TEXT NOT NULL,
|
||||
room_id TEXT NOT NULL,
|
||||
PRIMARY KEY (event_id)
|
||||
);
|
||||
|
||||
insertUnread:
|
||||
INSERT OR REPLACE INTO dbUnreadEvent(event_id, room_id)
|
||||
VALUES (?, ?);
|
||||
|
||||
removeRead:
|
||||
DELETE FROM dbUnreadEvent
|
||||
WHERE room_id = ?;
|
||||
|
||||
selectUnreadByRoom:
|
||||
SELECT event_id
|
||||
FROM dbUnreadEvent
|
||||
WHERE room_id = ?;
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation 'screen-state:screen-android'
|
||||
implementation project(":features:messenger")
|
||||
implementation project(":core")
|
||||
|
@ -12,9 +12,7 @@ dependencies {
|
|||
kotlinTest(it)
|
||||
|
||||
testImplementation 'screen-state:state-test'
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
testImplementation 'chat-engine:chat-engine-test'
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
}
|
|
@ -8,7 +8,6 @@ import fixture.aRoomOverview
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Test
|
||||
import test.expect
|
||||
import test.testReducer
|
||||
|
||||
private val AN_OVERVIEW = aRoomOverview()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(":features:directory")
|
||||
implementation project(":features:login")
|
||||
implementation project(":features:settings")
|
||||
|
|
|
@ -16,7 +16,6 @@ class HomeModule(
|
|||
internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profile: ProfileState): HomeViewModel {
|
||||
return HomeViewModel(
|
||||
chatEngine,
|
||||
storeModule.credentialsStore(),
|
||||
directory,
|
||||
login,
|
||||
profile,
|
||||
|
|
|
@ -21,7 +21,6 @@ import kotlinx.coroutines.launch
|
|||
|
||||
internal class HomeViewModel(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val credentialsProvider: CredentialsStore,
|
||||
private val directoryState: DirectoryState,
|
||||
private val loginViewModel: LoginViewModel,
|
||||
private val profileState: ProfileState,
|
||||
|
@ -39,7 +38,7 @@ internal class HomeViewModel(
|
|||
|
||||
fun start() {
|
||||
viewModelScope.launch {
|
||||
state = if (credentialsProvider.isSignedIn()) {
|
||||
state = if (chatEngine.isSignedIn()) {
|
||||
_events.emit(HomeEvent.OnShowContent)
|
||||
initialHomeContent()
|
||||
} else {
|
||||
|
@ -48,11 +47,10 @@ internal class HomeViewModel(
|
|||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
if (credentialsProvider.isSignedIn()) {
|
||||
if (chatEngine.isSignedIn()) {
|
||||
listenForInviteChanges()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private suspend fun initialHomeContent(): SignedIn {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:push")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
|
|
|
@ -2,7 +2,7 @@ applyAndroidComposeLibraryModule(project)
|
|||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":domains:store")
|
||||
|
@ -15,10 +15,10 @@ dependencies {
|
|||
kotlinTest(it)
|
||||
|
||||
testImplementation 'screen-state:state-test'
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
testImplementation 'chat-engine:chat-engine-test'
|
||||
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
}
|
|
@ -4,5 +4,5 @@ apply plugin: 'kotlin-parcelize'
|
|||
dependencies {
|
||||
compileOnly project(":domains:android:stub")
|
||||
implementation project(":core")
|
||||
implementation project(":chat-engine")
|
||||
implementation "chat-engine:chat-engine"
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation project(':domains:store')
|
||||
implementation "chat-engine:chat-engine"
|
||||
// implementation project(':domains:store')
|
||||
implementation project(":domains:android:work")
|
||||
implementation project(':domains:android:push')
|
||||
implementation project(":domains:android:core")
|
||||
|
@ -15,9 +15,7 @@ dependencies {
|
|||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
|
||||
kotlinTest(it)
|
||||
|
||||
testImplementation 'chat-engine:chat-engine-test'
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(":features:settings")
|
||||
implementation project(':domains:store')
|
||||
implementation 'screen-state:screen-android'
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":design-library")
|
||||
|
@ -12,9 +11,7 @@ dependencies {
|
|||
kotlinTest(it)
|
||||
|
||||
testImplementation 'screen-state:state-test'
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
testImplementation 'chat-engine:chat-engine-test'
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(":features:navigator")
|
||||
implementation project(':domains:store')
|
||||
implementation project(':domains:android:push')
|
||||
|
@ -14,10 +14,9 @@ dependencies {
|
|||
kotlinTest(it)
|
||||
|
||||
testImplementation 'screen-state:state-test'
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
testImplementation 'chat-engine:chat-engine-test'
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(':domains:store')
|
||||
implementation project(':chat-engine')
|
||||
// implementation project(':domains:store')
|
||||
implementation project(":core")
|
||||
implementation project(":design-library")
|
||||
implementation project(":features:navigator")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":matrix:services:crypto")
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":design-library")
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package app.dapk.st.verification
|
||||
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.matrix.crypto.CryptoService
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
|
||||
class VerificationModule(
|
||||
private val cryptoService: CryptoService
|
||||
private val chatEngine: ChatEngine,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun verificationViewModel(): VerificationViewModel {
|
||||
return VerificationViewModel(cryptoService)
|
||||
return VerificationViewModel(chatEngine)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,20 +1,18 @@
|
|||
package app.dapk.st.verification
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.matrix.crypto.CryptoService
|
||||
import app.dapk.st.matrix.crypto.Verification
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class VerificationViewModel(
|
||||
private val cryptoService: CryptoService,
|
||||
private val chatEngine: ChatEngine,
|
||||
) : DapkViewModel<VerificationScreenState, VerificationEvent>(
|
||||
initialState = VerificationScreenState(foo = "")
|
||||
) {
|
||||
fun inSecureAccept() {
|
||||
viewModelScope.launch {
|
||||
cryptoService.verificationAction(Verification.Action.InsecureAccept)
|
||||
}
|
||||
// TODO verify via chat-engine
|
||||
// viewModelScope.launch {
|
||||
// cryptoService.verificationAction(Verification.Action.InsecureAccept)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ org.gradle.jvmargs=-Xmx6144M -Xms2048M -Dfile.encoding=UTF-8 -XX:+UseParallelGC
|
|||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.unsafe.configuration-cache=true
|
||||
org.gradle.unsafe.configuration-cache-problems=warn
|
||||
org.gradle.caching=true
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.vfs.watch=true
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
plugins {
|
||||
id 'java-test-fixtures'
|
||||
id 'kotlin'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
|
||||
implementation project(":core")
|
||||
implementation project(":chat-engine")
|
||||
|
||||
implementation project(":domains:olm")
|
||||
|
||||
implementation project(":matrix:matrix")
|
||||
implementation project(":matrix:matrix-http-ktor")
|
||||
implementation project(":matrix:services:auth")
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:room")
|
||||
implementation project(":matrix:services:push")
|
||||
implementation project(":matrix:services:message")
|
||||
implementation project(":matrix:services:device")
|
||||
implementation project(":matrix:services:crypto")
|
||||
implementation project(":matrix:services:profile")
|
||||
|
||||
kotlinTest(it)
|
||||
kotlinFixtures(it)
|
||||
|
||||
testImplementation(testFixtures(project(":matrix:services:sync")))
|
||||
testImplementation(testFixtures(project(":matrix:services:message")))
|
||||
testImplementation(testFixtures(project(":matrix:common")))
|
||||
testImplementation(testFixtures(project(":core")))
|
||||
testImplementation(testFixtures(project(":domains:store")))
|
||||
testImplementation(testFixtures(project(":chat-engine")))
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import app.dapk.st.matrix.common.asString
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
|
||||
internal typealias DirectoryMergeWithLocalEchosUseCase = suspend (OverviewState, UserId, Map<RoomId, List<MessageService.LocalEcho>>) -> OverviewState
|
||||
|
||||
internal class DirectoryMergeWithLocalEchosUseCaseImpl(
|
||||
private val roomService: RoomService,
|
||||
) : DirectoryMergeWithLocalEchosUseCase {
|
||||
|
||||
override suspend fun invoke(overview: OverviewState, selfId: UserId, echos: Map<RoomId, List<MessageService.LocalEcho>>): OverviewState {
|
||||
return when {
|
||||
echos.isEmpty() -> overview
|
||||
else -> overview.map {
|
||||
when (val roomEchos = echos[it.roomId]) {
|
||||
null -> it
|
||||
else -> it.mergeWithLocalEchos(
|
||||
member = roomService.findMember(it.roomId, selfId) ?: RoomMember(
|
||||
selfId,
|
||||
null,
|
||||
avatarUrl = null,
|
||||
),
|
||||
echos = roomEchos,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomOverview.mergeWithLocalEchos(member: RoomMember, echos: List<MessageService.LocalEcho>): RoomOverview {
|
||||
val latestEcho = echos.maxByOrNull { it.timestampUtc }
|
||||
return if (latestEcho != null && latestEcho.timestampUtc > (this.lastMessage?.utcTimestamp ?: 0)) {
|
||||
this.copy(
|
||||
lastMessage = RoomOverview.LastMessage(
|
||||
content = when (val message = latestEcho.message) {
|
||||
is MessageService.Message.TextMessage -> message.content.body.asString()
|
||||
is MessageService.Message.ImageMessage -> "\uD83D\uDCF7"
|
||||
},
|
||||
utcTimestamp = latestEcho.timestampUtc,
|
||||
author = member,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.core.extensions.combine
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DirectoryUseCase(
|
||||
private val syncService: SyncService,
|
||||
private val messageService: MessageService,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val roomStore: RoomStore,
|
||||
private val mergeLocalEchosUseCase: DirectoryMergeWithLocalEchosUseCase,
|
||||
) {
|
||||
|
||||
fun state(): Flow<DirectoryState> {
|
||||
return flow { emit(credentialsStore.credentials()!!.userId) }.flatMapConcat { userId ->
|
||||
combine(
|
||||
syncService.startSyncing(),
|
||||
syncService.overview().map { it.map { it.engine() } },
|
||||
messageService.localEchos(),
|
||||
roomStore.observeUnreadCountById(),
|
||||
syncService.events(),
|
||||
roomStore.observeMuted(),
|
||||
) { _, overviewState, localEchos, unread, events, muted ->
|
||||
mergeLocalEchosUseCase.invoke(overviewState, userId, localEchos).map { roomOverview ->
|
||||
DirectoryItem(
|
||||
overview = roomOverview,
|
||||
unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0),
|
||||
typing = events.filterIsInstance<Typing>().firstOrNull { it.roomId == roomOverview.roomId }?.engine(),
|
||||
isMuted = muted.contains(roomOverview.roomId),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class InviteUseCase(
|
||||
private val syncService: SyncService
|
||||
) {
|
||||
|
||||
fun invites() = invitesDatasource()
|
||||
|
||||
private fun invitesDatasource() = combine(
|
||||
syncService.startSyncing(),
|
||||
syncService.invites().map { it.map { it.engine() } }
|
||||
) { _, invites -> invites }
|
||||
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
|
||||
internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
|
||||
|
||||
fun MessageService.LocalEcho.toMessage(member: RoomMember): RoomEvent {
|
||||
return when (val message = this.message) {
|
||||
is MessageService.Message.TextMessage -> {
|
||||
val mappedMessage = RoomEvent.Message(
|
||||
eventId = this.eventId ?: EventId(this.localId),
|
||||
content = message.content.body,
|
||||
author = member,
|
||||
utcTimestamp = message.timestampUtc,
|
||||
meta = metaMapper.toMeta(this)
|
||||
)
|
||||
|
||||
when (val reply = message.reply) {
|
||||
null -> mappedMessage
|
||||
else -> RoomEvent.Reply(
|
||||
mappedMessage, RoomEvent.Message(
|
||||
eventId = reply.eventId,
|
||||
content = reply.originalMessage,
|
||||
author = reply.author,
|
||||
utcTimestamp = reply.timestampUtc,
|
||||
meta = MessageMeta.FromServer
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is MessageService.Message.ImageMessage -> {
|
||||
RoomEvent.Image(
|
||||
eventId = this.eventId ?: EventId(this.localId),
|
||||
author = member,
|
||||
utcTimestamp = message.timestampUtc,
|
||||
meta = metaMapper.toMeta(this),
|
||||
imageMeta = RoomEvent.Image.ImageMeta(message.content.meta.width, message.content.meta.height, message.content.uri, null),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RoomEvent.mergeWith(echo: MessageService.LocalEcho): RoomEvent = when (this) {
|
||||
is RoomEvent.Message -> this.copy(meta = metaMapper.toMeta(echo))
|
||||
is RoomEvent.Reply -> this.copy(message = this.message.mergeWith(echo))
|
||||
is RoomEvent.Image -> this.copy(meta = metaMapper.toMeta(echo))
|
||||
is RoomEvent.Encrypted -> this.copy(meta = metaMapper.toMeta(echo))
|
||||
is RoomEvent.Redacted -> this
|
||||
}
|
||||
}
|
||||
|
||||
internal class MetaMapper {
|
||||
|
||||
fun toMeta(echo: MessageService.LocalEcho) = MessageMeta.LocalEcho(
|
||||
echoId = echo.localId,
|
||||
state = when (val localEchoState = echo.state) {
|
||||
MessageService.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending
|
||||
MessageService.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent
|
||||
is MessageService.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error(
|
||||
localEchoState.message,
|
||||
type = MessageMeta.LocalEcho.State.Error.Type.UNKNOWN,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import java.util.*
|
||||
|
||||
internal class LocalIdFactory {
|
||||
fun create() = "local.${UUID.randomUUID()}"
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.auth.AuthService
|
||||
import app.dapk.st.matrix.sync.InviteMeta
|
||||
import app.dapk.st.matrix.auth.AuthService.LoginRequest as MatrixLoginRequest
|
||||
import app.dapk.st.matrix.auth.AuthService.LoginResult as MatrixLoginResult
|
||||
import app.dapk.st.matrix.crypto.ImportResult as MatrixImportResult
|
||||
import app.dapk.st.matrix.room.ProfileService.Me as MatrixMe
|
||||
import app.dapk.st.matrix.sync.LastMessage as MatrixLastMessage
|
||||
import app.dapk.st.matrix.sync.MessageMeta as MatrixMessageMeta
|
||||
import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite
|
||||
import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview
|
||||
import app.dapk.st.matrix.sync.RoomState as MatrixRoomState
|
||||
import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing as MatrixTyping
|
||||
|
||||
fun MatrixRoomOverview.engine() = RoomOverview(
|
||||
this.roomId,
|
||||
this.roomCreationUtc,
|
||||
this.roomName,
|
||||
this.roomAvatarUrl,
|
||||
this.lastMessage?.engine(),
|
||||
this.isGroup,
|
||||
this.readMarker,
|
||||
this.isEncrypted
|
||||
)
|
||||
|
||||
fun MatrixLastMessage.engine() = RoomOverview.LastMessage(
|
||||
this.content,
|
||||
this.utcTimestamp,
|
||||
this.author,
|
||||
)
|
||||
|
||||
fun MatrixTyping.engine() = Typing(
|
||||
this.roomId,
|
||||
this.members,
|
||||
)
|
||||
|
||||
fun LoginRequest.engine() = MatrixLoginRequest(
|
||||
this.userName,
|
||||
this.password,
|
||||
this.serverUrl
|
||||
)
|
||||
|
||||
fun MatrixLoginResult.engine() = when (this) {
|
||||
is AuthService.LoginResult.Error -> LoginResult.Error(this.cause)
|
||||
AuthService.LoginResult.MissingWellKnown -> LoginResult.MissingWellKnown
|
||||
is AuthService.LoginResult.Success -> LoginResult.Success(this.userCredentials)
|
||||
}
|
||||
|
||||
fun MatrixMe.engine() = Me(
|
||||
this.userId,
|
||||
this.displayName,
|
||||
this.avatarUrl,
|
||||
this.homeServerUrl,
|
||||
)
|
||||
|
||||
fun MatrixRoomInvite.engine() = RoomInvite(
|
||||
this.from,
|
||||
this.roomId,
|
||||
this.inviteMeta.engine(),
|
||||
)
|
||||
|
||||
fun InviteMeta.engine() = when (this) {
|
||||
InviteMeta.DirectMessage -> RoomInvite.InviteMeta.DirectMessage
|
||||
is InviteMeta.Room -> RoomInvite.InviteMeta.Room(this.roomName)
|
||||
}
|
||||
|
||||
fun MatrixImportResult.engine() = when (this) {
|
||||
is MatrixImportResult.Error -> ImportResult.Error(
|
||||
when (val error = this.cause) {
|
||||
MatrixImportResult.Error.Type.InvalidFile -> ImportResult.Error.Type.InvalidFile
|
||||
MatrixImportResult.Error.Type.NoKeysFound -> ImportResult.Error.Type.NoKeysFound
|
||||
MatrixImportResult.Error.Type.UnableToOpenFile -> ImportResult.Error.Type.UnableToOpenFile
|
||||
MatrixImportResult.Error.Type.UnexpectedDecryptionOutput -> ImportResult.Error.Type.UnexpectedDecryptionOutput
|
||||
is MatrixImportResult.Error.Type.Unknown -> ImportResult.Error.Type.Unknown(error.cause)
|
||||
}
|
||||
)
|
||||
|
||||
is MatrixImportResult.Success -> ImportResult.Success(this.roomIds, this.totalImportedKeysCount)
|
||||
is MatrixImportResult.Update -> ImportResult.Update(this.importedKeysCount)
|
||||
}
|
||||
|
||||
fun MatrixRoomState.engine() = RoomState(
|
||||
this.roomOverview.engine(),
|
||||
this.events.map { it.engine() }
|
||||
)
|
||||
|
||||
fun MatrixRoomEvent.engine(): RoomEvent = when (this) {
|
||||
is MatrixRoomEvent.Image -> RoomEvent.Image(this.eventId, this.utcTimestamp, this.imageMeta.engine(), this.author, this.meta.engine(), this.edited)
|
||||
is MatrixRoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, this.content, this.author, this.meta.engine(), this.edited)
|
||||
is MatrixRoomEvent.Reply -> RoomEvent.Reply(this.message.engine(), this.replyingTo.engine())
|
||||
is MatrixRoomEvent.Encrypted -> RoomEvent.Encrypted(this.eventId, this.utcTimestamp, this.author, this.meta.engine())
|
||||
is MatrixRoomEvent.Redacted -> RoomEvent.Redacted(this.eventId, this.utcTimestamp, this.author)
|
||||
}
|
||||
|
||||
fun MatrixRoomEvent.Image.ImageMeta.engine() = RoomEvent.Image.ImageMeta(
|
||||
this.width,
|
||||
this.height,
|
||||
this.url,
|
||||
this.keys?.let { RoomEvent.Image.ImageMeta.Keys(it.k, it.iv, it.v, it.hashes) }
|
||||
)
|
||||
|
||||
fun MatrixMessageMeta.engine() = when (this) {
|
||||
MatrixMessageMeta.FromServer -> MessageMeta.FromServer
|
||||
is MatrixMessageMeta.LocalEcho -> MessageMeta.LocalEcho(
|
||||
this.echoId, when (val echo = this.state) {
|
||||
is MatrixMessageMeta.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error(
|
||||
echo.message, when (echo.type) {
|
||||
MatrixMessageMeta.LocalEcho.State.Error.Type.UNKNOWN -> MessageMeta.LocalEcho.State.Error.Type.UNKNOWN
|
||||
}
|
||||
)
|
||||
|
||||
MatrixMessageMeta.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending
|
||||
MatrixMessageMeta.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,224 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.core.Base64
|
||||
import app.dapk.st.core.BuildMeta
|
||||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.matrix.MatrixClient
|
||||
import app.dapk.st.matrix.MatrixTaskRunner
|
||||
import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator
|
||||
import app.dapk.st.matrix.auth.authService
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.common.MatrixLogger
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.crypto.MatrixMediaDecrypter
|
||||
import app.dapk.st.matrix.crypto.cryptoService
|
||||
import app.dapk.st.matrix.device.KnownDeviceStore
|
||||
import app.dapk.st.matrix.message.BackgroundScheduler
|
||||
import app.dapk.st.matrix.message.LocalEchoStore
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import app.dapk.st.matrix.message.messageService
|
||||
import app.dapk.st.matrix.push.pushService
|
||||
import app.dapk.st.matrix.room.MemberStore
|
||||
import app.dapk.st.matrix.room.ProfileStore
|
||||
import app.dapk.st.matrix.room.profileService
|
||||
import app.dapk.st.matrix.room.roomService
|
||||
import app.dapk.st.matrix.sync.*
|
||||
import app.dapk.st.olm.OlmStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.io.InputStream
|
||||
import java.time.Clock
|
||||
|
||||
class MatrixEngine internal constructor(
|
||||
private val directoryUseCase: Lazy<DirectoryUseCase>,
|
||||
private val matrix: Lazy<MatrixClient>,
|
||||
private val timelineUseCase: Lazy<ReadMarkingTimeline>,
|
||||
private val sendMessageUseCase: Lazy<SendMessageUseCase>,
|
||||
private val matrixMediaDecrypter: Lazy<MatrixMediaDecrypter>,
|
||||
private val matrixPushHandler: Lazy<MatrixPushHandler>,
|
||||
private val inviteUseCase: Lazy<InviteUseCase>,
|
||||
private val notificationMessagesUseCase: Lazy<ObserveUnreadNotificationsUseCase>,
|
||||
private val notificationInvitesUseCase: Lazy<ObserveInviteNotificationsUseCase>,
|
||||
) : ChatEngine {
|
||||
|
||||
override fun directory() = directoryUseCase.value.state()
|
||||
override fun invites() = inviteUseCase.value.invites()
|
||||
|
||||
override fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerPageState> {
|
||||
return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts)
|
||||
}
|
||||
|
||||
override fun notificationsMessages(): Flow<UnreadNotifications> {
|
||||
return notificationMessagesUseCase.value.invoke()
|
||||
}
|
||||
|
||||
override fun notificationsInvites(): Flow<InviteNotification> {
|
||||
return notificationInvitesUseCase.value.invoke()
|
||||
}
|
||||
|
||||
override suspend fun login(request: LoginRequest): LoginResult {
|
||||
return matrix.value.authService().login(request.engine()).engine()
|
||||
}
|
||||
|
||||
override suspend fun me(forceRefresh: Boolean): Me {
|
||||
return matrix.value.profileService().me(forceRefresh).engine()
|
||||
}
|
||||
|
||||
override suspend fun InputStream.importRoomKeys(password: String): Flow<ImportResult> {
|
||||
return with(matrix.value.cryptoService()) {
|
||||
importRoomKeys(password).map { it.engine() }.onEach {
|
||||
when (it) {
|
||||
is ImportResult.Error,
|
||||
is ImportResult.Update -> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
is ImportResult.Success -> matrix.value.syncService().forceManualRefresh(it.roomIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun send(message: SendMessage, room: RoomOverview) {
|
||||
sendMessageUseCase.value.send(message, room)
|
||||
}
|
||||
|
||||
override suspend fun registerPushToken(token: String, gatewayUrl: String) {
|
||||
matrix.value.pushService().registerPush(token, gatewayUrl)
|
||||
}
|
||||
|
||||
override suspend fun joinRoom(roomId: RoomId) {
|
||||
matrix.value.roomService().joinRoom(roomId)
|
||||
}
|
||||
|
||||
override suspend fun rejectJoinRoom(roomId: RoomId) {
|
||||
matrix.value.roomService().rejectJoinRoom(roomId)
|
||||
}
|
||||
|
||||
override suspend fun findMembersSummary(roomId: RoomId) = matrix.value.roomService().findMembersSummary(roomId)
|
||||
|
||||
override fun mediaDecrypter(): MediaDecrypter {
|
||||
val mediaDecrypter = matrixMediaDecrypter.value
|
||||
return object : MediaDecrypter {
|
||||
override fun decrypt(input: InputStream, k: String, iv: String): MediaDecrypter.Collector {
|
||||
return MediaDecrypter.Collector {
|
||||
mediaDecrypter.decrypt(input, k, iv).collect(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pushHandler() = matrixPushHandler.value
|
||||
|
||||
override suspend fun muteRoom(roomId: RoomId) = matrix.value.roomService().muteRoom(roomId)
|
||||
|
||||
override suspend fun unmuteRoom(roomId: RoomId) = matrix.value.roomService().unmuteRoom(roomId)
|
||||
|
||||
override suspend fun runTask(task: ChatEngineTask): TaskRunner.TaskResult {
|
||||
return when (val result = matrix.value.run(MatrixTaskRunner.MatrixTask(task.type, task.jsonPayload))) {
|
||||
is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(result.canRetry)
|
||||
MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
class Factory {
|
||||
|
||||
fun create(
|
||||
base64: Base64,
|
||||
buildMeta: BuildMeta,
|
||||
logger: MatrixLogger,
|
||||
nameGenerator: DeviceDisplayNameGenerator,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
errorTracker: ErrorTracker,
|
||||
imageContentReader: ImageContentReader,
|
||||
backgroundScheduler: BackgroundScheduler,
|
||||
memberStore: MemberStore,
|
||||
roomStore: RoomStore,
|
||||
profileStore: ProfileStore,
|
||||
syncStore: SyncStore,
|
||||
overviewStore: OverviewStore,
|
||||
filterStore: FilterStore,
|
||||
localEchoStore: LocalEchoStore,
|
||||
credentialsStore: CredentialsStore,
|
||||
knownDeviceStore: KnownDeviceStore,
|
||||
olmStore: OlmStore,
|
||||
): ChatEngine {
|
||||
val lazyMatrix = lazy {
|
||||
MatrixFactory.createMatrix(
|
||||
base64,
|
||||
buildMeta,
|
||||
logger,
|
||||
nameGenerator,
|
||||
coroutineDispatchers,
|
||||
errorTracker,
|
||||
imageContentReader,
|
||||
backgroundScheduler,
|
||||
memberStore,
|
||||
roomStore,
|
||||
profileStore,
|
||||
syncStore,
|
||||
overviewStore,
|
||||
filterStore,
|
||||
localEchoStore,
|
||||
credentialsStore,
|
||||
knownDeviceStore,
|
||||
olmStore
|
||||
)
|
||||
}
|
||||
val directoryUseCase = unsafeLazy {
|
||||
val matrix = lazyMatrix.value
|
||||
DirectoryUseCase(
|
||||
matrix.syncService(),
|
||||
matrix.messageService(),
|
||||
credentialsStore,
|
||||
roomStore,
|
||||
DirectoryMergeWithLocalEchosUseCaseImpl(matrix.roomService()),
|
||||
)
|
||||
}
|
||||
val timelineUseCase = unsafeLazy {
|
||||
val matrix = lazyMatrix.value
|
||||
val mergeWithLocalEchosUseCase = TimelineMergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper()))
|
||||
val timeline = TimelineUseCaseImpl(matrix.syncService(), matrix.messageService(), matrix.roomService(), mergeWithLocalEchosUseCase)
|
||||
ReadMarkingTimeline(roomStore, credentialsStore, timeline, matrix.roomService())
|
||||
}
|
||||
|
||||
val sendMessageUseCase = unsafeLazy {
|
||||
val matrix = lazyMatrix.value
|
||||
SendMessageUseCase(matrix.messageService(), LocalIdFactory(), imageContentReader, Clock.systemUTC())
|
||||
}
|
||||
|
||||
val mediaDecrypter = unsafeLazy { MatrixMediaDecrypter(base64) }
|
||||
val pushHandler = unsafeLazy {
|
||||
MatrixPushHandler(
|
||||
backgroundScheduler,
|
||||
credentialsStore,
|
||||
lazyMatrix.value.syncService(),
|
||||
roomStore,
|
||||
coroutineDispatchers,
|
||||
JobBag(),
|
||||
)
|
||||
}
|
||||
|
||||
val invitesUseCase = unsafeLazy { InviteUseCase(lazyMatrix.value.syncService()) }
|
||||
|
||||
return MatrixEngine(
|
||||
directoryUseCase,
|
||||
lazyMatrix,
|
||||
timelineUseCase,
|
||||
sendMessageUseCase,
|
||||
mediaDecrypter,
|
||||
pushHandler,
|
||||
invitesUseCase,
|
||||
unsafeLazy { ObserveUnreadNotificationsUseCaseImpl(roomStore) },
|
||||
unsafeLazy { ObserveInviteNotificationsUseCaseImpl(overviewStore) },
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)
|
|
@ -1,258 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.core.Base64
|
||||
import app.dapk.st.core.BuildMeta
|
||||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.SingletonFlows
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.matrix.MatrixClient
|
||||
import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator
|
||||
import app.dapk.st.matrix.auth.installAuthService
|
||||
import app.dapk.st.matrix.common.*
|
||||
import app.dapk.st.matrix.crypto.RoomMembersProvider
|
||||
import app.dapk.st.matrix.crypto.Verification
|
||||
import app.dapk.st.matrix.crypto.cryptoService
|
||||
import app.dapk.st.matrix.crypto.installCryptoService
|
||||
import app.dapk.st.matrix.device.KnownDeviceStore
|
||||
import app.dapk.st.matrix.device.deviceService
|
||||
import app.dapk.st.matrix.device.installEncryptionService
|
||||
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
||||
import app.dapk.st.matrix.message.*
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import app.dapk.st.matrix.push.installPushService
|
||||
import app.dapk.st.matrix.room.*
|
||||
import app.dapk.st.matrix.room.internal.SingleRoomStore
|
||||
import app.dapk.st.matrix.sync.*
|
||||
import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent
|
||||
import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
|
||||
import app.dapk.st.olm.DeviceKeyFactory
|
||||
import app.dapk.st.olm.OlmStore
|
||||
import app.dapk.st.olm.OlmWrapper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.time.Clock
|
||||
|
||||
internal object MatrixFactory {
|
||||
|
||||
fun createMatrix(
|
||||
base64: Base64,
|
||||
buildMeta: BuildMeta,
|
||||
logger: MatrixLogger,
|
||||
nameGenerator: DeviceDisplayNameGenerator,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
errorTracker: ErrorTracker,
|
||||
imageContentReader: ImageContentReader,
|
||||
backgroundScheduler: BackgroundScheduler,
|
||||
memberStore: MemberStore,
|
||||
roomStore: RoomStore,
|
||||
profileStore: ProfileStore,
|
||||
syncStore: SyncStore,
|
||||
overviewStore: OverviewStore,
|
||||
filterStore: FilterStore,
|
||||
localEchoStore: LocalEchoStore,
|
||||
credentialsStore: CredentialsStore,
|
||||
knownDeviceStore: KnownDeviceStore,
|
||||
olmStore: OlmStore,
|
||||
) = MatrixClient(
|
||||
KtorMatrixHttpClientFactory(
|
||||
credentialsStore,
|
||||
includeLogging = buildMeta.isDebug,
|
||||
),
|
||||
logger
|
||||
).also {
|
||||
it.install {
|
||||
installAuthService(credentialsStore, nameGenerator)
|
||||
installEncryptionService(knownDeviceStore)
|
||||
|
||||
val singletonFlows = SingletonFlows(coroutineDispatchers)
|
||||
val olm = OlmWrapper(
|
||||
olmStore = olmStore,
|
||||
singletonFlows = singletonFlows,
|
||||
jsonCanonicalizer = JsonCanonicalizer(),
|
||||
deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()),
|
||||
errorTracker = errorTracker,
|
||||
logger = logger,
|
||||
clock = Clock.systemUTC(),
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
installCryptoService(
|
||||
credentialsStore,
|
||||
olm,
|
||||
roomMembersProvider = { services ->
|
||||
RoomMembersProvider {
|
||||
services.roomService().joinedMembers(it).map { it.userId }
|
||||
}
|
||||
},
|
||||
base64 = base64,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
installMessageService(
|
||||
localEchoStore,
|
||||
backgroundScheduler,
|
||||
imageContentReader,
|
||||
messageEncrypter = {
|
||||
val cryptoService = it.cryptoService()
|
||||
MessageEncrypter { message ->
|
||||
val result = cryptoService.encrypt(
|
||||
roomId = message.roomId,
|
||||
credentials = credentialsStore.credentials()!!,
|
||||
messageJson = message.contents,
|
||||
)
|
||||
|
||||
MessageEncrypter.EncryptedMessagePayload(
|
||||
result.algorithmName,
|
||||
result.senderKey,
|
||||
result.cipherText,
|
||||
result.sessionId,
|
||||
result.deviceId,
|
||||
)
|
||||
}
|
||||
},
|
||||
mediaEncrypter = {
|
||||
val cryptoService = it.cryptoService()
|
||||
MediaEncrypter { input ->
|
||||
val result = cryptoService.encrypt(input)
|
||||
MediaEncrypter.Result(
|
||||
uri = result.uri,
|
||||
contentLength = result.contentLength,
|
||||
algorithm = result.algorithm,
|
||||
ext = result.ext,
|
||||
keyOperations = result.keyOperations,
|
||||
kty = result.kty,
|
||||
k = result.k,
|
||||
iv = result.iv,
|
||||
hashes = result.hashes,
|
||||
v = result.v,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
installRoomService(
|
||||
memberStore,
|
||||
roomMessenger = {
|
||||
val messageService = it.messageService()
|
||||
object : RoomMessenger {
|
||||
override suspend fun enableEncryption(roomId: RoomId) {
|
||||
messageService.sendEventMessage(
|
||||
roomId, MessageService.EventMessage.Encryption(
|
||||
algorithm = AlgorithmName("m.megolm.v1.aes-sha2")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
roomInviteRemover = {
|
||||
overviewStore.removeInvites(listOf(it))
|
||||
},
|
||||
singleRoomStore = singleRoomStoreAdapter(roomStore)
|
||||
)
|
||||
|
||||
installProfileService(profileStore, singletonFlows, credentialsStore)
|
||||
|
||||
installSyncService(
|
||||
credentialsStore,
|
||||
overviewStore,
|
||||
roomStore,
|
||||
syncStore,
|
||||
filterStore,
|
||||
deviceNotifier = { services ->
|
||||
val encryption = services.deviceService()
|
||||
val crypto = services.cryptoService()
|
||||
DeviceNotifier { userIds, syncToken ->
|
||||
encryption.updateStaleDevices(userIds)
|
||||
crypto.updateOlmSession(userIds, syncToken)
|
||||
}
|
||||
},
|
||||
messageDecrypter = { serviceProvider ->
|
||||
val cryptoService = serviceProvider.cryptoService()
|
||||
MessageDecrypter {
|
||||
cryptoService.decrypt(it)
|
||||
}
|
||||
},
|
||||
keySharer = { serviceProvider ->
|
||||
val cryptoService = serviceProvider.cryptoService()
|
||||
KeySharer { sharedRoomKeys ->
|
||||
cryptoService.importRoomKeys(sharedRoomKeys)
|
||||
}
|
||||
},
|
||||
verificationHandler = { services ->
|
||||
val cryptoService = services.cryptoService()
|
||||
VerificationHandler { apiEvent ->
|
||||
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
|
||||
cryptoService.onVerificationEvent(
|
||||
when (apiEvent) {
|
||||
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.methods,
|
||||
apiEvent.content.timestampPosix,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.methods,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
apiEvent.content.method,
|
||||
apiEvent.content.protocols,
|
||||
apiEvent.content.hashes,
|
||||
apiEvent.content.codes,
|
||||
apiEvent.content.short,
|
||||
apiEvent.content.transactionId,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
||||
is ApiToDeviceEvent.VerificationAccept -> TODO()
|
||||
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.key
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.keys,
|
||||
apiEvent.content.mac,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
oneTimeKeyProducer = { services ->
|
||||
val cryptoService = services.cryptoService()
|
||||
MaybeCreateMoreKeys {
|
||||
cryptoService.maybeCreateMoreKeys(it)
|
||||
}
|
||||
},
|
||||
roomMembersService = { services ->
|
||||
val roomService = services.roomService()
|
||||
object : RoomMembersService {
|
||||
override suspend fun find(roomId: RoomId, userIds: List<UserId>) = roomService.findMembers(roomId, userIds)
|
||||
override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId)
|
||||
override suspend fun insert(roomId: RoomId, members: List<RoomMember>) = roomService.insertMembers(roomId, members)
|
||||
}
|
||||
},
|
||||
errorTracker = errorTracker,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
|
||||
installPushService(credentialsStore)
|
||||
}
|
||||
}
|
||||
|
||||
private fun singleRoomStoreAdapter(roomStore: RoomStore) = object : SingleRoomStore {
|
||||
override suspend fun mute(roomId: RoomId) = roomStore.mute(roomId)
|
||||
override suspend fun unmute(roomId: RoomId) = roomStore.unmute(roomId)
|
||||
override fun isMuted(roomId: RoomId): Flow<Boolean> = roomStore.observeMuted().map { it.contains(roomId) }.distinctUntilChanged()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.core.AppLogTag
|
||||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.core.log
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.JsonString
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.message.BackgroundScheduler
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class MatrixPushHandler(
|
||||
private val backgroundScheduler: BackgroundScheduler,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val syncService: SyncService,
|
||||
private val roomStore: RoomStore,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val jobBag: JobBag,
|
||||
) : PushHandler {
|
||||
|
||||
override fun onNewToken(payload: JsonString) {
|
||||
log(AppLogTag.PUSH, "new push token received")
|
||||
backgroundScheduler.schedule(
|
||||
key = "2",
|
||||
task = BackgroundScheduler.Task(
|
||||
type = "push_token",
|
||||
jsonPayload = payload
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(eventId: EventId?, roomId: RoomId?) {
|
||||
log(AppLogTag.PUSH, "push received")
|
||||
jobBag.replace(MatrixPushHandler::class, dispatchers.global.launch {
|
||||
when (credentialsStore.credentials()) {
|
||||
null -> log(AppLogTag.PUSH, "push ignored due to missing api credentials")
|
||||
else -> doSync(roomId, eventId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private suspend fun doSync(roomId: RoomId?, eventId: EventId?) {
|
||||
when (roomId) {
|
||||
null -> {
|
||||
log(AppLogTag.PUSH, "empty push payload - keeping sync alive until unread changes")
|
||||
waitForUnreadChange(60_000) ?: log(AppLogTag.PUSH, "timed out waiting for sync")
|
||||
}
|
||||
|
||||
else -> {
|
||||
log(AppLogTag.PUSH, "push with eventId payload - keeping sync alive until the event shows up in the sync response")
|
||||
waitForEvent(
|
||||
timeout = 60_000,
|
||||
eventId!!,
|
||||
) ?: log(AppLogTag.PUSH, "timed out waiting for sync")
|
||||
}
|
||||
}
|
||||
log(AppLogTag.PUSH, "push sync finished")
|
||||
}
|
||||
|
||||
private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? {
|
||||
return withTimeoutOrNull(timeout) {
|
||||
combine(syncService.startSyncing(), syncService.observeEvent(eventId)) { _, event -> event }
|
||||
.firstOrNull {
|
||||
it == eventId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForUnreadChange(timeout: Long): String? {
|
||||
return withTimeoutOrNull(timeout) {
|
||||
combine(syncService.startSyncing(), roomStore.observeUnread()) { _, unread -> unread }
|
||||
.first()
|
||||
"ignored"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
|
||||
internal typealias TimelineMergeWithLocalEchosUseCase = (RoomState, RoomMember, List<MessageService.LocalEcho>) -> RoomState
|
||||
|
||||
internal class TimelineMergeWithLocalEchosUseCaseImpl(
|
||||
private val localEventMapper: LocalEchoMapper,
|
||||
) : TimelineMergeWithLocalEchosUseCase {
|
||||
|
||||
override fun invoke(roomState: RoomState, member: RoomMember, echos: List<MessageService.LocalEcho>): RoomState {
|
||||
val echosByEventId = echos.associateBy { it.eventId }
|
||||
val stateByEventId = roomState.events.associateBy { it.eventId }
|
||||
|
||||
val uniqueEchos = uniqueEchos(echos, stateByEventId, member)
|
||||
val existingWithEcho = updateExistingEventsWithLocalEchoMeta(roomState, echosByEventId)
|
||||
|
||||
val sortedEvents = (existingWithEcho + uniqueEchos)
|
||||
.sortedByDescending { it.utcTimestamp }
|
||||
.distinctBy { it.eventId }
|
||||
return roomState.copy(events = sortedEvents)
|
||||
}
|
||||
|
||||
private fun uniqueEchos(echos: List<MessageService.LocalEcho>, stateByEventId: Map<EventId, RoomEvent>, member: RoomMember): List<RoomEvent> {
|
||||
return with(localEventMapper) {
|
||||
echos
|
||||
.filter { echo -> echo.eventId == null || stateByEventId[echo.eventId] == null }
|
||||
.map { localEcho -> localEcho.toMessage(member) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExistingEventsWithLocalEchoMeta(roomState: RoomState, echosByEventId: Map<EventId?, MessageService.LocalEcho>): List<RoomEvent> {
|
||||
return with(localEventMapper) {
|
||||
roomState.events.map { roomEvent ->
|
||||
when (val echo = echosByEventId[roomEvent.eventId]) {
|
||||
null -> roomEvent
|
||||
else -> roomEvent.mergeWith(echo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.sync.InviteMeta
|
||||
import app.dapk.st.matrix.sync.OverviewStore
|
||||
import app.dapk.st.matrix.sync.RoomInvite
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal typealias ObserveInviteNotificationsUseCase = () -> Flow<InviteNotification>
|
||||
|
||||
class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase {
|
||||
|
||||
override fun invoke(): Flow<InviteNotification> {
|
||||
return overviewStore.latestInvites()
|
||||
.diff()
|
||||
.drop(1)
|
||||
.flatten()
|
||||
.map {
|
||||
val text = when (val meta = it.inviteMeta) {
|
||||
InviteMeta.DirectMessage -> "${it.inviterName()} has invited you to chat"
|
||||
is InviteMeta.Room -> "${it.inviterName()} has invited you to ${meta.roomName ?: "unnamed room"}"
|
||||
}
|
||||
InviteNotification(content = text, roomId = it.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<List<RoomInvite>>.diff(): Flow<Set<RoomInvite>> {
|
||||
val previousInvites = mutableSetOf<RoomInvite>()
|
||||
return this.distinctUntilChanged()
|
||||
.map {
|
||||
val diff = it.toSet() - previousInvites
|
||||
previousInvites.clear()
|
||||
previousInvites.addAll(it)
|
||||
diff
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun <T> Flow<Set<T>>.flatten() = this.flatMapConcat { items ->
|
||||
flow { items.forEach { this.emit(it) } }
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.core.AppLogTag
|
||||
import app.dapk.st.core.extensions.clearAndPutAll
|
||||
import app.dapk.st.core.extensions.containsKey
|
||||
import app.dapk.st.core.log
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import kotlinx.coroutines.flow.*
|
||||
import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview
|
||||
|
||||
internal typealias ObserveUnreadNotificationsUseCase = () -> Flow<UnreadNotifications>
|
||||
|
||||
class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase {
|
||||
|
||||
override fun invoke(): Flow<UnreadNotifications> {
|
||||
return roomStore.observeNotMutedUnread()
|
||||
.mapWithDiff()
|
||||
.avoidShowingPreviousNotificationsOnLaunch()
|
||||
.onlyRenderableChanges()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun Flow<Map<MatrixRoomOverview, List<MatrixRoomEvent>>>.mapWithDiff(): Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>> {
|
||||
val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>()
|
||||
return this.map { each ->
|
||||
val allUnreadIds = each.toTimestampedIds()
|
||||
val notificationDiff = calculateDiff(allUnreadIds, previousUnreadEvents)
|
||||
previousUnreadEvents.clearAndPutAll(allUnreadIds)
|
||||
each to notificationDiff
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateDiff(allUnread: Map<RoomId, List<TimestampedEventId>>, previousUnread: Map<RoomId, List<TimestampedEventId>>?): NotificationDiff {
|
||||
val previousLatestEventTimestamps = previousUnread.toLatestTimestamps()
|
||||
val newRooms = allUnread.filter { !previousUnread.containsKey(it.key) }.keys
|
||||
|
||||
val unchanged = previousUnread?.filter {
|
||||
allUnread.containsKey(it.key) && (it.value == allUnread[it.key])
|
||||
} ?: emptyMap()
|
||||
val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) }.mapValues { (key, value) ->
|
||||
val isChangedRoom = !newRooms.contains(key)
|
||||
if (isChangedRoom) {
|
||||
val latest = previousLatestEventTimestamps[key] ?: 0L
|
||||
value.filter {
|
||||
val isExistingEvent = (previousUnread?.get(key)?.contains(it) ?: false)
|
||||
!isExistingEvent && it.second > latest
|
||||
}
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}.filter { it.value.isNotEmpty() }
|
||||
val removed = previousUnread?.filter { !allUnread.containsKey(it.key) } ?: emptyMap()
|
||||
return NotificationDiff(unchanged.toEventIds(), changedOrNew.toEventIds(), removed.toEventIds(), newRooms)
|
||||
}
|
||||
|
||||
private fun Map<RoomId, List<TimestampedEventId>>?.toLatestTimestamps() = this?.mapValues { it.value.maxOf { it.second } } ?: emptyMap()
|
||||
|
||||
private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } }
|
||||
|
||||
private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.toTimestampedIds() = this
|
||||
.mapValues { it.value.toEventIds() }
|
||||
.mapKeys { it.key.roomId }
|
||||
|
||||
private fun List<MatrixRoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp }
|
||||
|
||||
private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1)
|
||||
|
||||
private fun Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>>.onlyRenderableChanges(): Flow<UnreadNotifications> {
|
||||
val inferredCurrentNotifications = mutableMapOf<RoomId, List<MatrixRoomEvent>>()
|
||||
return this
|
||||
.filter { (_, diff) ->
|
||||
when {
|
||||
diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> {
|
||||
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes")
|
||||
false
|
||||
}
|
||||
|
||||
inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> {
|
||||
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read")
|
||||
false
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
.onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) }
|
||||
.map {
|
||||
val engineModels = it.first
|
||||
.mapKeys { it.key.engine() }
|
||||
.mapValues { it.value.map { it.engine() } }
|
||||
engineModels to it.second
|
||||
}
|
||||
}
|
||||
|
||||
typealias TimestampedEventId = Pair<EventId, Long>
|
|
@ -1,55 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class ReadMarkingTimeline(
|
||||
private val roomStore: RoomStore,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val observeTimelineUseCase: ObserveTimelineUseCase,
|
||||
private val roomService: RoomService,
|
||||
) {
|
||||
|
||||
fun fetch(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow<MessengerPageState> {
|
||||
return flow {
|
||||
val credentials = credentialsStore.credentials()!!
|
||||
roomStore.markRead(roomId)
|
||||
emit(credentials)
|
||||
}.flatMapConcat { credentials ->
|
||||
var lastKnownReadEvent: EventId? = null
|
||||
observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state ->
|
||||
state.latestMessageEventFromOthers(self = credentials.userId)?.let {
|
||||
if (lastKnownReadEvent != it) {
|
||||
updateRoomReadStateAsync(latestReadEvent = it, state, isReadReceiptsDisabled)
|
||||
lastKnownReadEvent = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DeferredResultUnused")
|
||||
private suspend fun updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerPageState, isReadReceiptsDisabled: Boolean) {
|
||||
coroutineScope {
|
||||
async {
|
||||
runCatching {
|
||||
roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = isReadReceiptsDisabled)
|
||||
roomStore.markRead(state.roomState.roomOverview.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessengerPageState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
|
||||
.filterIsInstance<RoomEvent.Message>()
|
||||
.filterNot { it.author.id == self }
|
||||
.firstOrNull()
|
||||
?.eventId
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.RichText
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import java.time.Clock
|
||||
|
||||
internal class SendMessageUseCase(
|
||||
private val messageService: MessageService,
|
||||
private val localIdFactory: LocalIdFactory,
|
||||
private val imageContentReader: ImageContentReader,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
suspend fun send(message: SendMessage, room: RoomOverview) {
|
||||
when (message) {
|
||||
is SendMessage.ImageMessage -> createImageMessage(message, room)
|
||||
is SendMessage.TextMessage -> messageService.scheduleMessage(createTextMessage(message, room))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createImageMessage(message: SendMessage.ImageMessage, room: RoomOverview) {
|
||||
val meta = imageContentReader.meta(message.uri)
|
||||
messageService.scheduleMessage(
|
||||
MessageService.Message.ImageMessage(
|
||||
MessageService.Message.Content.ImageContent(
|
||||
uri = message.uri,
|
||||
MessageService.Message.Content.ImageContent.Meta(
|
||||
height = meta.height,
|
||||
width = meta.width,
|
||||
size = meta.size,
|
||||
fileName = meta.fileName,
|
||||
mimeType = meta.mimeType,
|
||||
)
|
||||
),
|
||||
roomId = room.roomId,
|
||||
sendEncrypted = room.isEncrypted,
|
||||
localId = localIdFactory.create(),
|
||||
timestampUtc = clock.millis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTextMessage(message: SendMessage.TextMessage, room: RoomOverview) = MessageService.Message.TextMessage(
|
||||
content = MessageService.Message.Content.TextContent(RichText.of(message.content)),
|
||||
roomId = room.roomId,
|
||||
sendEncrypted = room.isEncrypted,
|
||||
localId = localIdFactory.create(),
|
||||
timestampUtc = clock.millis(),
|
||||
reply = message.reply?.let {
|
||||
MessageService.Message.TextMessage.Reply(
|
||||
author = it.author,
|
||||
originalMessage = RichText.of(it.originalMessage),
|
||||
replyContent = message.content,
|
||||
eventId = it.eventId,
|
||||
timestampUtc = it.timestampUtc,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerPageState>
|
||||
|
||||
internal class TimelineUseCaseImpl(
|
||||
private val syncService: SyncService,
|
||||
private val messageService: MessageService,
|
||||
private val roomService: RoomService,
|
||||
private val timelineMergeWithLocalEchosUseCase: TimelineMergeWithLocalEchosUseCase,
|
||||
) : ObserveTimelineUseCase {
|
||||
|
||||
override fun invoke(roomId: RoomId, userId: UserId): Flow<MessengerPageState> {
|
||||
return combine(
|
||||
roomDatasource(roomId),
|
||||
messageService.localEchos(roomId),
|
||||
syncService.events(roomId),
|
||||
roomService.observeIsMuted(roomId),
|
||||
) { roomState, localEchos, events, isMuted ->
|
||||
MessengerPageState(
|
||||
roomState = when {
|
||||
localEchos.isEmpty() -> roomState
|
||||
else -> {
|
||||
timelineMergeWithLocalEchosUseCase.invoke(
|
||||
roomState,
|
||||
roomService.findMember(roomId, userId) ?: userId.toFallbackMember(),
|
||||
localEchos,
|
||||
)
|
||||
}
|
||||
},
|
||||
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(),
|
||||
self = userId,
|
||||
isMuted = isMuted,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun roomDatasource(roomId: RoomId) = combine(
|
||||
syncService.startSyncing(),
|
||||
syncService.room(roomId).map { it.engine() }
|
||||
) { _, room -> room }
|
||||
}
|
||||
|
||||
private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null)
|
|
@ -1,115 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import fake.FakeCredentialsStore
|
||||
import fake.FakeRoomStore
|
||||
import fake.FakeSyncService
|
||||
import fixture.aMatrixRoomOverview
|
||||
import fixture.aRoomMember
|
||||
import fixture.aTypingEvent
|
||||
import fixture.aUserCredentials
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import test.delegateReturn
|
||||
|
||||
private val A_ROOM_OVERVIEW = aMatrixRoomOverview()
|
||||
private const val AN_UNREAD_COUNT = 10
|
||||
private const val MUTED_ROOM = true
|
||||
private val TYPING_MEMBERS = listOf(aRoomMember())
|
||||
|
||||
class DirectoryUseCaseTest {
|
||||
|
||||
private val fakeSyncService = FakeSyncService()
|
||||
private val fakeMessageService = FakeMessageService()
|
||||
private val fakeCredentialsStore = FakeCredentialsStore()
|
||||
private val fakeRoomStore = FakeRoomStore()
|
||||
private val fakeMergeLocalEchosUseCase = FakeDirectoryMergeWithLocalEchosUseCase()
|
||||
|
||||
private val useCase = DirectoryUseCase(
|
||||
fakeSyncService,
|
||||
fakeMessageService,
|
||||
fakeCredentialsStore,
|
||||
fakeRoomStore,
|
||||
fakeMergeLocalEchosUseCase,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given empty values, then reads default directory state and maps to engine`() = runTest {
|
||||
givenEmitsDirectoryState(
|
||||
A_ROOM_OVERVIEW,
|
||||
unreadCount = null,
|
||||
isMuted = false,
|
||||
)
|
||||
|
||||
val result = useCase.state().first()
|
||||
|
||||
result shouldBeEqualTo listOf(
|
||||
DirectoryItem(
|
||||
A_ROOM_OVERVIEW.engine(),
|
||||
unreadCount = UnreadCount(0),
|
||||
typing = null,
|
||||
isMuted = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given extra state, then reads directory state and maps to engine`() = runTest {
|
||||
givenEmitsDirectoryState(
|
||||
A_ROOM_OVERVIEW,
|
||||
unreadCount = AN_UNREAD_COUNT,
|
||||
isMuted = MUTED_ROOM,
|
||||
typing = TYPING_MEMBERS
|
||||
)
|
||||
|
||||
val result = useCase.state().first()
|
||||
|
||||
result shouldBeEqualTo listOf(
|
||||
DirectoryItem(
|
||||
A_ROOM_OVERVIEW.engine(),
|
||||
unreadCount = UnreadCount(AN_UNREAD_COUNT),
|
||||
typing = aTypingEvent(A_ROOM_OVERVIEW.roomId, TYPING_MEMBERS),
|
||||
isMuted = MUTED_ROOM
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun givenEmitsDirectoryState(
|
||||
roomOverview: RoomOverview,
|
||||
unreadCount: Int? = null,
|
||||
isMuted: Boolean = false,
|
||||
typing: List<RoomMember> = emptyList(),
|
||||
) {
|
||||
val userCredentials = aUserCredentials()
|
||||
fakeCredentialsStore.givenCredentials().returns(userCredentials)
|
||||
|
||||
val matrixOverviewState = listOf(roomOverview)
|
||||
|
||||
fakeSyncService.givenStartsSyncing()
|
||||
fakeSyncService.givenOverview().returns(flowOf(matrixOverviewState))
|
||||
fakeSyncService.givenEvents().returns(flowOf(if (typing.isEmpty()) emptyList() else listOf(aTypingSyncEvent(roomOverview.roomId, typing))))
|
||||
|
||||
fakeMessageService.givenEchos().returns(flowOf(emptyMap()))
|
||||
fakeRoomStore.givenUnreadByCount().returns(flowOf(unreadCount?.let { mapOf(roomOverview.roomId to it) } ?: emptyMap()))
|
||||
fakeRoomStore.givenMuted().returns(flowOf(if (isMuted) setOf(roomOverview.roomId) else emptySet()))
|
||||
|
||||
val mappedOverview = roomOverview.engine()
|
||||
val expectedOverviewState = listOf(mappedOverview)
|
||||
fakeMergeLocalEchosUseCase.givenMergedEchos(expectedOverviewState, userCredentials.userId, emptyMap()).returns(expectedOverviewState)
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDirectoryMergeWithLocalEchosUseCase : DirectoryMergeWithLocalEchosUseCase by mockk() {
|
||||
fun givenMergedEchos(overviewState: OverviewState, selfId: UserId, echos: Map<RoomId, List<MessageService.LocalEcho>>) = coEvery {
|
||||
this@FakeDirectoryMergeWithLocalEchosUseCase.invoke(overviewState, selfId, echos)
|
||||
}.delegateReturn()
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.sync.InviteMeta
|
||||
import fake.FakeSyncService
|
||||
import fixture.aRoomId
|
||||
import fixture.aRoomMember
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite
|
||||
|
||||
class InviteUseCaseTest {
|
||||
|
||||
private val fakeSyncService = FakeSyncService()
|
||||
private val useCase = InviteUseCase(fakeSyncService)
|
||||
|
||||
@Test
|
||||
fun `reads invites from sync service and maps to engine`() = runTest {
|
||||
val aMatrixRoomInvite = aMatrixRoomInvite()
|
||||
fakeSyncService.givenStartsSyncing()
|
||||
fakeSyncService.givenInvites().returns(flowOf(listOf(aMatrixRoomInvite)))
|
||||
|
||||
val result = useCase.invites().first()
|
||||
|
||||
result shouldBeEqualTo listOf(aMatrixRoomInvite.engine())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun aMatrixRoomInvite(
|
||||
from: RoomMember = aRoomMember(),
|
||||
roomId: RoomId = aRoomId(),
|
||||
inviteMeta: InviteMeta = InviteMeta.DirectMessage,
|
||||
) = MatrixRoomInvite(from, roomId, inviteMeta)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue