From dc5a46e0cdcc0a578cfe6dbf2d297caa200515d4 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 2 Nov 2022 11:48:59 +0000 Subject: [PATCH] lifts room muting to the chat engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 2 - .../kotlin/app/dapk/st/engine/ChatEngine.kt | 2 + .../main/kotlin/app/dapk/st/engine/Models.kt | 3 +- .../kotlin/app/dapk/st/domain/StoreModule.kt | 9 +- .../dapk/st/domain/room/MutedRoomsStore.kt | 14 +- .../dapk/st/domain/sync/RoomPersistence.kt | 9 +- .../app/dapk/st/messenger/MessengerModule.kt | 3 - .../app/dapk/st/messenger/MessengerScreen.kt | 19 +- .../st/messenger/state/MessengerReducer.kt | 14 +- .../dapk/st/messenger/state/MessengerState.kt | 1 - .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 250 ++--------------- .../app/dapk/st/engine/MatrixFactory.kt | 253 ++++++++++++++++++ .../app/dapk/st/engine/TimelineUseCase.kt | 1 + .../app/dapk/st/matrix/room/RoomService.kt | 13 +- .../room/internal/DefaultRoomService.kt | 18 ++ .../kotlin/app/dapk/st/matrix/sync/Store.kt | 10 +- 16 files changed, 342 insertions(+), 279 deletions(-) create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixFactory.kt diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index c26b826..940c2f1 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -18,7 +18,6 @@ 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.StoreModule -import app.dapk.st.domain.room.MutedRoomsStorePersistence import app.dapk.st.engine.MatrixEngine import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.home.BetaVersionUpgradeUseCase @@ -164,7 +163,6 @@ internal class FeatureModules internal constructor( context, storeModule.value.messageStore(), deviceMeta, - MutedRoomsStorePersistence(storeModule.value.cachingPreferences) ) } val homeModule by unsafeLazy { diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 5d75fbb..564560d 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -36,6 +36,8 @@ interface ChatEngine : TaskRunner { fun pushHandler(): PushHandler + suspend fun muteRoom(roomId: RoomId) + suspend fun unmuteRoom(roomId: RoomId) } interface TaskRunner { diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt index 167591e..066ab75 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt @@ -87,7 +87,8 @@ sealed interface ImportResult { data class MessengerPageState( val self: UserId, val roomState: RoomState, - val typing: Typing? + val typing: Typing?, + val isMuted: Boolean, ) data class RoomState( diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt index aa8b1ef..2b4d071 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt @@ -13,6 +13,7 @@ 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 @@ -34,7 +35,13 @@ class StoreModule( ) { fun overviewStore(): OverviewStore = OverviewPersistence(database, coroutineDispatchers) - fun roomStore(): RoomStore = RoomPersistence(database, OverviewPersistence(database, coroutineDispatchers), coroutineDispatchers) + fun roomStore(): RoomStore = RoomPersistence( + database = database, + overviewPersistence = OverviewPersistence(database, coroutineDispatchers), + coroutineDispatchers = coroutineDispatchers, + muteableStore = MutedStorePersistence(preferences), + ) + fun credentialsStore(): CredentialsStore = CredentialsPreferences(credentialPreferences) fun syncStore(): SyncStore = SyncTokenPreferences(preferences) fun filterStore(): FilterStore = FilterPreferences(preferences) diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/room/MutedRoomsStore.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/room/MutedRoomsStore.kt index 81cabcd..0b8eb14 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/room/MutedRoomsStore.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/room/MutedRoomsStore.kt @@ -4,19 +4,13 @@ import app.dapk.st.core.Preferences import app.dapk.st.core.append import app.dapk.st.core.removeFromSet import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.sync.MuteableStore private const val KEY_MUTE = "mute" -interface MutedRoomsStore { - suspend fun mute(roomId: RoomId) - suspend fun unmute(roomId: RoomId) - suspend fun isMuted(roomId: RoomId): Boolean - suspend fun allMuted(): Set -} - -class MutedRoomsStorePersistence( +internal class MutedStorePersistence( private val preferences: Preferences -) : MutedRoomsStore { +) : MuteableStore { override suspend fun mute(roomId: RoomId) { preferences.append(KEY_MUTE, roomId.value) @@ -28,8 +22,6 @@ class MutedRoomsStorePersistence( override suspend fun isMuted(roomId: RoomId): Boolean { val allMuted = allMuted() - println("??? isMuted - $roomId") - println("??? all - $allMuted") return allMuted.contains(roomId) } diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt index 7864bb7..f7a2571 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt @@ -4,12 +4,10 @@ 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.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview -import app.dapk.st.matrix.sync.RoomState -import app.dapk.st.matrix.sync.RoomStore +import app.dapk.st.matrix.sync.* import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToOneNotNull @@ -25,7 +23,8 @@ internal class RoomPersistence( private val database: DapkDb, private val overviewPersistence: OverviewPersistence, private val coroutineDispatchers: CoroutineDispatchers, -) : RoomStore { + private val muteableStore: MutedStorePersistence, +) : RoomStore, MuteableStore by muteableStore { override suspend fun persist(roomId: RoomId, events: List) { coroutineDispatchers.withIoContext { diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt index aaa0b04..50f12ab 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt @@ -10,7 +10,6 @@ import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.engine.ChatEngine import app.dapk.st.matrix.common.RoomId import app.dapk.st.messenger.state.MessengerState -import app.dapk.st.domain.room.MutedRoomsStore import app.dapk.st.messenger.state.messengerReducer class MessengerModule( @@ -18,7 +17,6 @@ class MessengerModule( private val context: Context, private val messageOptionsStore: MessageOptionsStore, private val deviceMeta: DeviceMeta, - private val mutedRoomsStore: MutedRoomsStore, ) : ProvidableModule { internal fun messengerState(launchPayload: MessagerActivityPayload): MessengerState { @@ -29,7 +27,6 @@ class MessengerModule( CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager), deviceMeta, messageOptionsStore, - mutedRoomsStore, RoomId(launchPayload.roomId), launchPayload.attachments, it diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 8258de0..e1c4c32 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -87,16 +87,19 @@ internal fun MessengerScreen( Column { Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = { - OverflowMenu { - when (state.isMuted) { - true -> DropdownMenuItem(text = { Text("Unmute notifications", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = { - viewModel.dispatch(ScreenAction.Notifications.Unmute) - }) + state.roomState.takeIfContent()?.let { + OverflowMenu { + when (it.isMuted) { + true -> DropdownMenuItem(text = { Text("Unmute notifications", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = { + viewModel.dispatch(ScreenAction.Notifications.Unmute) + }) - false -> DropdownMenuItem(text = { Text("Mute notifications", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = { - viewModel.dispatch(ScreenAction.Notifications.Mute) - }) + false -> DropdownMenuItem(text = { Text("Mute notifications", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = { + viewModel.dispatch(ScreenAction.Notifications.Mute) + }) + } } + } }) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt index 0555a4d..0d66d8b 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerReducer.kt @@ -8,7 +8,6 @@ import app.dapk.st.core.asString import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.design.components.BubbleModel import app.dapk.st.domain.application.message.MessageOptionsStore -import app.dapk.st.domain.room.MutedRoomsStore import app.dapk.st.engine.ChatEngine import app.dapk.st.engine.MessengerPageState import app.dapk.st.engine.RoomEvent @@ -27,7 +26,6 @@ internal fun messengerReducer( copyToClipboard: CopyToClipboard, deviceMeta: DeviceMeta, messageOptionsStore: MessageOptionsStore, - mutedRoomsStore: MutedRoomsStore, roomId: RoomId, initialAttachments: List?, eventEmitter: suspend (MessengerEvent) -> Unit, @@ -38,7 +36,6 @@ internal fun messengerReducer( roomState = Lce.Loading(), composerState = initialComposerState(initialAttachments), viewerState = null, - isMuted = false, ), async(ComponentLifecycle::class) { action -> @@ -50,7 +47,6 @@ internal fun messengerReducer( .onEach { dispatch(MessagesStateChange.Content(it)) } .launchIn(coroutineScope) ) - dispatch(MessagesStateChange.MuteContent(mutedRoomsStore.isMuted(roomId))) } ComponentLifecycle.Gone -> jobBag.cancel("messages") @@ -141,15 +137,17 @@ internal fun messengerReducer( }, change(MessagesStateChange.MuteContent::class) { action, state -> - state.copy(isMuted = action.isMuted).also { - println("??? action - $action previous state: ${state.isMuted} next: ${it.isMuted}") + when (val roomState = state.roomState) { + is Lce.Content -> state.copy(roomState = roomState.copy(value = roomState.value.copy(isMuted = action.isMuted))) + is Lce.Error -> state + is Lce.Loading -> state } }, async(ScreenAction.Notifications::class) { action -> when (action) { - ScreenAction.Notifications.Mute -> mutedRoomsStore.mute(roomId) - ScreenAction.Notifications.Unmute -> mutedRoomsStore.unmute(roomId) + ScreenAction.Notifications.Mute -> chatEngine.muteRoom(roomId) + ScreenAction.Notifications.Unmute -> chatEngine.unmuteRoom(roomId) } dispatch( diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerState.kt index b94441d..0fa7a5c 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/state/MessengerState.kt @@ -15,7 +15,6 @@ data class MessengerScreenState( val roomState: Lce, val composerState: ComposerState, val viewerState: ViewerState?, - val isMuted: Boolean, ) data class ViewerState( diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index fce1c4c..6239ad3 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -3,30 +3,28 @@ 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.MatrixTaskRunner import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.auth.authService -import app.dapk.st.matrix.auth.installAuthService -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.* +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.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.BackgroundScheduler +import app.dapk.st.matrix.message.LocalEchoStore import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.push.installPushService +import app.dapk.st.matrix.message.messageService import app.dapk.st.matrix.push.pushService -import app.dapk.st.matrix.room.* +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.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.map import kotlinx.coroutines.flow.onEach @@ -114,6 +112,10 @@ class MatrixEngine internal constructor( 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) @@ -209,222 +211,4 @@ class MatrixEngine internal constructor( } - -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)) - } - ) - - installProfileService(profileStore, singletonFlows, credentialsStore) - - installSyncService( - credentialsStore, - overviewStore, - roomStore, - syncStore, - filterStore, - deviceNotifier = { services -> - val encryption = services.deviceService() - val crypto = services.cryptoService() - DeviceNotifier { userIds, syncToken -> - encryption.updateStaleDevices(userIds) - crypto.updateOlmSession(userIds, syncToken) - } - }, - messageDecrypter = { serviceProvider -> - val cryptoService = serviceProvider.cryptoService() - MessageDecrypter { - cryptoService.decrypt(it) - } - }, - keySharer = { serviceProvider -> - val cryptoService = serviceProvider.cryptoService() - KeySharer { sharedRoomKeys -> - cryptoService.importRoomKeys(sharedRoomKeys) - } - }, - verificationHandler = { services -> - val cryptoService = services.cryptoService() - VerificationHandler { apiEvent -> - logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it") - cryptoService.onVerificationEvent( - when (apiEvent) { - is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.transactionId, - apiEvent.content.methods, - apiEvent.content.timestampPosix, - ) - - is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.transactionId, - apiEvent.content.methods, - ) - - is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.method, - apiEvent.content.protocols, - apiEvent.content.hashes, - apiEvent.content.codes, - apiEvent.content.short, - apiEvent.content.transactionId, - ) - - is ApiToDeviceEvent.VerificationCancel -> TODO() - is ApiToDeviceEvent.VerificationAccept -> TODO() - is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key( - apiEvent.sender, - apiEvent.content.transactionId, - apiEvent.content.key - ) - - is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac( - apiEvent.sender, - apiEvent.content.transactionId, - apiEvent.content.keys, - apiEvent.content.mac, - ) - } - ) - } - }, - oneTimeKeyProducer = { services -> - val cryptoService = services.cryptoService() - MaybeCreateMoreKeys { - cryptoService.maybeCreateMoreKeys(it) - } - }, - roomMembersService = { services -> - val roomService = services.roomService() - object : RoomMembersService { - override suspend fun find(roomId: RoomId, userIds: List) = roomService.findMembers(roomId, userIds) - override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId) - override suspend fun insert(roomId: RoomId, members: List) = roomService.insertMembers(roomId, members) - } - }, - errorTracker = errorTracker, - coroutineDispatchers = coroutineDispatchers, - ) - - installPushService(credentialsStore) - } - } - -} - -fun unsafeLazy(initializer: () -> T): Lazy = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer) +private fun unsafeLazy(initializer: () -> T): Lazy = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer) diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixFactory.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixFactory.kt new file mode 100644 index 0000000..c7cd770 --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixFactory.kt @@ -0,0 +1,253 @@ +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 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 = object : SingleRoomStore { + override suspend fun mute(roomId: RoomId) = roomStore.mute(roomId) + override suspend fun unmute(roomId: RoomId) = roomStore.unmute(roomId) + override suspend fun isMuted(roomId: RoomId) = roomStore.isMuted(roomId) + } + ) + + installProfileService(profileStore, singletonFlows, credentialsStore) + + installSyncService( + credentialsStore, + overviewStore, + roomStore, + syncStore, + filterStore, + deviceNotifier = { services -> + val encryption = services.deviceService() + val crypto = services.cryptoService() + DeviceNotifier { userIds, syncToken -> + encryption.updateStaleDevices(userIds) + crypto.updateOlmSession(userIds, syncToken) + } + }, + messageDecrypter = { serviceProvider -> + val cryptoService = serviceProvider.cryptoService() + MessageDecrypter { + cryptoService.decrypt(it) + } + }, + keySharer = { serviceProvider -> + val cryptoService = serviceProvider.cryptoService() + KeySharer { sharedRoomKeys -> + cryptoService.importRoomKeys(sharedRoomKeys) + } + }, + verificationHandler = { services -> + val cryptoService = services.cryptoService() + VerificationHandler { apiEvent -> + logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it") + cryptoService.onVerificationEvent( + when (apiEvent) { + is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested( + apiEvent.sender, + apiEvent.content.fromDevice, + apiEvent.content.transactionId, + apiEvent.content.methods, + apiEvent.content.timestampPosix, + ) + + is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready( + apiEvent.sender, + apiEvent.content.fromDevice, + apiEvent.content.transactionId, + apiEvent.content.methods, + ) + + is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started( + apiEvent.sender, + apiEvent.content.fromDevice, + apiEvent.content.method, + apiEvent.content.protocols, + apiEvent.content.hashes, + apiEvent.content.codes, + apiEvent.content.short, + apiEvent.content.transactionId, + ) + + is ApiToDeviceEvent.VerificationCancel -> TODO() + is ApiToDeviceEvent.VerificationAccept -> TODO() + is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key( + apiEvent.sender, + apiEvent.content.transactionId, + apiEvent.content.key + ) + + is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac( + apiEvent.sender, + apiEvent.content.transactionId, + apiEvent.content.keys, + apiEvent.content.mac, + ) + } + ) + } + }, + oneTimeKeyProducer = { services -> + val cryptoService = services.cryptoService() + MaybeCreateMoreKeys { + cryptoService.maybeCreateMoreKeys(it) + } + }, + roomMembersService = { services -> + val roomService = services.roomService() + object : RoomMembersService { + override suspend fun find(roomId: RoomId, userIds: List) = roomService.findMembers(roomId, userIds) + override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId) + override suspend fun insert(roomId: RoomId, members: List) = roomService.insertMembers(roomId, members) + } + }, + errorTracker = errorTracker, + coroutineDispatchers = coroutineDispatchers, + ) + + installPushService(credentialsStore) + } + } + +} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt index 2b79e0d..f0e6e72 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt @@ -38,6 +38,7 @@ internal class TimelineUseCaseImpl( }, typing = events.filterIsInstance().firstOrNull { it.roomId == roomId }?.engine(), self = userId, + isMuted = roomService.isMuted(roomId) ) } } diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt index a51817b..f7b7e49 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt @@ -5,10 +5,7 @@ import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.room.internal.DefaultRoomService -import app.dapk.st.matrix.room.internal.RoomInviteRemover -import app.dapk.st.matrix.room.internal.RoomMembers -import app.dapk.st.matrix.room.internal.RoomMembersCache +import app.dapk.st.matrix.room.internal.* private val SERVICE_KEY = RoomService::class @@ -27,6 +24,10 @@ interface RoomService : MatrixService { suspend fun joinRoom(roomId: RoomId) suspend fun rejectJoinRoom(roomId: RoomId) + suspend fun muteRoom(roomId: RoomId) + suspend fun unmuteRoom(roomId: RoomId) + suspend fun isMuted(roomId: RoomId): Boolean + data class JoinedMember( val userId: UserId, val displayName: String?, @@ -39,6 +40,7 @@ fun MatrixServiceInstaller.installRoomService( memberStore: MemberStore, roomMessenger: ServiceDepFactory, roomInviteRemover: RoomInviteRemover, + singleRoomStore: SingleRoomStore, ): InstallExtender { return this.install { (httpClient, _, services, logger) -> SERVICE_KEY to DefaultRoomService( @@ -46,7 +48,8 @@ fun MatrixServiceInstaller.installRoomService( logger, RoomMembers(memberStore, RoomMembersCache()), roomMessenger.create(services), - roomInviteRemover + roomInviteRemover, + singleRoomStore, ) } } diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt index 22e1551..7a824a3 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt @@ -19,6 +19,7 @@ class DefaultRoomService( private val roomMembers: RoomMembers, private val roomMessenger: RoomMessenger, private val roomInviteRemover: RoomInviteRemover, + private val singleRoomStore: SingleRoomStore, ) : RoomService { override suspend fun joinedMembers(roomId: RoomId): List { @@ -82,6 +83,7 @@ class DefaultRoomService( } else { throw it } + } else -> throw it @@ -90,6 +92,22 @@ class DefaultRoomService( ) roomInviteRemover.remove(roomId) } + + override suspend fun muteRoom(roomId: RoomId) { + singleRoomStore.mute(roomId) + } + + override suspend fun unmuteRoom(roomId: RoomId) { + singleRoomStore.unmute(roomId) + } + + override suspend fun isMuted(roomId: RoomId) = singleRoomStore.isMuted(roomId) +} + +interface SingleRoomStore { + suspend fun mute(roomId: RoomId) + suspend fun unmute(roomId: RoomId) + suspend fun isMuted(roomId: RoomId): Boolean } internal fun joinedMembersRequest(roomId: RoomId) = httpRequest( diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt index 2d43ad7..90c7df0 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt @@ -5,7 +5,7 @@ import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.SyncToken import kotlinx.coroutines.flow.Flow -interface RoomStore { +interface RoomStore : MuteableStore { suspend fun persist(roomId: RoomId, events: List) suspend fun remove(rooms: List) @@ -21,6 +21,14 @@ interface RoomStore { } +interface MuteableStore { + suspend fun mute(roomId: RoomId) + suspend fun unmute(roomId: RoomId) + suspend fun isMuted(roomId: RoomId): Boolean + suspend fun allMuted(): Set +} + + interface FilterStore { suspend fun store(key: String, filterId: String)