Merge pull request #273 from ouchadam/tech/engine-submodule

Tech/engine submodule
This commit is contained in:
Adam Brown 2022-12-10 10:42:53 +00:00 committed by GitHub
commit 05cbb52aca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
336 changed files with 185 additions and 16370 deletions

View File

@ -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

3
.gitmodules vendored
View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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))!!
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -1,4 +1,4 @@
package app.dapk.st
package app.dapk.st.impl
import android.content.Context
import app.dapk.st.core.CoroutineDispatchers

View File

@ -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)"
}
}

View File

@ -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

1
chat-engine Submodule

@ -0,0 +1 @@
Subproject commit 2ac5fc22a562362acc8b4b4d527580221085971d

View File

@ -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")))
}

View File

@ -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
)

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)

View File

@ -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)
}

View File

@ -1,6 +0,0 @@
package app.dapk.st.core
interface Base64 {
fun encode(input: ByteArray): String
fun decode(input: String): ByteArray
}

View File

@ -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"))
}

View File

@ -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?)
}

View File

@ -2,5 +2,5 @@ applyAndroidLibraryModule(project)
dependencies {
implementation project(':core')
implementation project(':matrix:common')
implementation "chat-engine:chat-engine"
}

View File

@ -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'
}

View File

@ -1,7 +0,0 @@
plugins {
id 'kotlin'
}
dependencies {
compileOnly 'org.json:json:20220924'
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}
}

View File

@ -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();
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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")
}

View File

@ -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()
}
}

View File

@ -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))
)
)
)
}
}

View File

@ -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()
}

View File

@ -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
}
}
}

View File

@ -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?
}

View File

@ -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
}

View File

@ -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"

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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) }
}
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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,
) {

View File

@ -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
}

View File

@ -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,
)

View File

@ -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() }
}

View File

@ -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
)
}

View File

@ -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),
)
)
}

View File

@ -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 ?;

View File

@ -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 (?, ?);

View File

@ -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 = ?;

View File

@ -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 = ?;

View File

@ -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;

View File

@ -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 = ?;

View File

@ -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 = ?;

View File

@ -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 (?, ?, ?);

View File

@ -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 = ?;

BIN
external/jolm.jar vendored

Binary file not shown.

View File

@ -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"))
}

View File

@ -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()

View File

@ -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")

View File

@ -16,7 +16,6 @@ class HomeModule(
internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profile: ProfileState): HomeViewModel {
return HomeViewModel(
chatEngine,
storeModule.credentialsStore(),
directory,
login,
profile,

View File

@ -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 {

View File

@ -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")

View File

@ -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"))
}

View File

@ -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"
}

View File

@ -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"))
}

View File

@ -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"))
}

View File

@ -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"))
}

View File

@ -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")

View File

@ -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")

View File

@ -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)
}
}

View File

@ -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)
// }
}

View File

@ -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

View File

@ -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")))
}

View File

@ -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
}
}
}

View File

@ -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),
)
}
}
}
}
}

View File

@ -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 }
}

View File

@ -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,
)
}
)
}

View File

@ -1,7 +0,0 @@
package app.dapk.st.engine
import java.util.*
internal class LocalIdFactory {
fun create() = "local.${UUID.randomUUID()}"
}

View File

@ -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
}
)
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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"
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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) } }
}

View File

@ -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>

View File

@ -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
}

View File

@ -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,
)
}
)
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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