lifts room muting to the chat engine

This commit is contained in:
Adam Brown 2022-11-02 11:48:59 +00:00
parent 7e184cf200
commit dc5a46e0cd
16 changed files with 342 additions and 279 deletions

View File

@ -18,7 +18,6 @@ import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.directory.DirectoryModule import app.dapk.st.directory.DirectoryModule
import app.dapk.st.domain.StoreModule import app.dapk.st.domain.StoreModule
import app.dapk.st.domain.room.MutedRoomsStorePersistence
import app.dapk.st.engine.MatrixEngine import app.dapk.st.engine.MatrixEngine
import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.firebase.messaging.MessagingModule
import app.dapk.st.home.BetaVersionUpgradeUseCase import app.dapk.st.home.BetaVersionUpgradeUseCase
@ -164,7 +163,6 @@ internal class FeatureModules internal constructor(
context, context,
storeModule.value.messageStore(), storeModule.value.messageStore(),
deviceMeta, deviceMeta,
MutedRoomsStorePersistence(storeModule.value.cachingPreferences)
) )
} }
val homeModule by unsafeLazy { val homeModule by unsafeLazy {

View File

@ -36,6 +36,8 @@ interface ChatEngine : TaskRunner {
fun pushHandler(): PushHandler fun pushHandler(): PushHandler
suspend fun muteRoom(roomId: RoomId)
suspend fun unmuteRoom(roomId: RoomId)
} }
interface TaskRunner { interface TaskRunner {

View File

@ -87,7 +87,8 @@ sealed interface ImportResult {
data class MessengerPageState( data class MessengerPageState(
val self: UserId, val self: UserId,
val roomState: RoomState, val roomState: RoomState,
val typing: Typing? val typing: Typing?,
val isMuted: Boolean,
) )
data class RoomState( data class RoomState(

View File

@ -13,6 +13,7 @@ import app.dapk.st.domain.preference.CachingPreferences
import app.dapk.st.domain.preference.PropertyCache import app.dapk.st.domain.preference.PropertyCache
import app.dapk.st.domain.profile.ProfilePersistence import app.dapk.st.domain.profile.ProfilePersistence
import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.domain.push.PushTokenRegistrarPreferences
import app.dapk.st.domain.room.MutedStorePersistence
import app.dapk.st.domain.sync.OverviewPersistence import app.dapk.st.domain.sync.OverviewPersistence
import app.dapk.st.domain.sync.RoomPersistence import app.dapk.st.domain.sync.RoomPersistence
import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.CredentialsStore
@ -34,7 +35,13 @@ class StoreModule(
) { ) {
fun overviewStore(): OverviewStore = OverviewPersistence(database, coroutineDispatchers) 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 credentialsStore(): CredentialsStore = CredentialsPreferences(credentialPreferences)
fun syncStore(): SyncStore = SyncTokenPreferences(preferences) fun syncStore(): SyncStore = SyncTokenPreferences(preferences)
fun filterStore(): FilterStore = FilterPreferences(preferences) fun filterStore(): FilterStore = FilterPreferences(preferences)

View File

@ -4,19 +4,13 @@ import app.dapk.st.core.Preferences
import app.dapk.st.core.append import app.dapk.st.core.append
import app.dapk.st.core.removeFromSet import app.dapk.st.core.removeFromSet
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.MuteableStore
private const val KEY_MUTE = "mute" private const val KEY_MUTE = "mute"
interface MutedRoomsStore { internal class MutedStorePersistence(
suspend fun mute(roomId: RoomId)
suspend fun unmute(roomId: RoomId)
suspend fun isMuted(roomId: RoomId): Boolean
suspend fun allMuted(): Set<RoomId>
}
class MutedRoomsStorePersistence(
private val preferences: Preferences private val preferences: Preferences
) : MutedRoomsStore { ) : MuteableStore {
override suspend fun mute(roomId: RoomId) { override suspend fun mute(roomId: RoomId) {
preferences.append(KEY_MUTE, roomId.value) preferences.append(KEY_MUTE, roomId.value)
@ -28,8 +22,6 @@ class MutedRoomsStorePersistence(
override suspend fun isMuted(roomId: RoomId): Boolean { override suspend fun isMuted(roomId: RoomId): Boolean {
val allMuted = allMuted() val allMuted = allMuted()
println("??? isMuted - $roomId")
println("??? all - $allMuted")
return allMuted.contains(roomId) return allMuted.contains(roomId)
} }

View File

@ -4,12 +4,10 @@ import app.dapk.db.DapkDb
import app.dapk.db.model.RoomEventQueries import app.dapk.db.model.RoomEventQueries
import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.withIoContext import app.dapk.st.core.withIoContext
import app.dapk.st.domain.room.MutedStorePersistence
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.*
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.RoomStore
import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToList
import com.squareup.sqldelight.runtime.coroutines.mapToOneNotNull import com.squareup.sqldelight.runtime.coroutines.mapToOneNotNull
@ -25,7 +23,8 @@ internal class RoomPersistence(
private val database: DapkDb, private val database: DapkDb,
private val overviewPersistence: OverviewPersistence, private val overviewPersistence: OverviewPersistence,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) : RoomStore { private val muteableStore: MutedStorePersistence,
) : RoomStore, MuteableStore by muteableStore {
override suspend fun persist(roomId: RoomId, events: List<RoomEvent>) { override suspend fun persist(roomId: RoomId, events: List<RoomEvent>) {
coroutineDispatchers.withIoContext { coroutineDispatchers.withIoContext {

View File

@ -10,7 +10,6 @@ import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.engine.ChatEngine import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.messenger.state.MessengerState import app.dapk.st.messenger.state.MessengerState
import app.dapk.st.domain.room.MutedRoomsStore
import app.dapk.st.messenger.state.messengerReducer import app.dapk.st.messenger.state.messengerReducer
class MessengerModule( class MessengerModule(
@ -18,7 +17,6 @@ class MessengerModule(
private val context: Context, private val context: Context,
private val messageOptionsStore: MessageOptionsStore, private val messageOptionsStore: MessageOptionsStore,
private val deviceMeta: DeviceMeta, private val deviceMeta: DeviceMeta,
private val mutedRoomsStore: MutedRoomsStore,
) : ProvidableModule { ) : ProvidableModule {
internal fun messengerState(launchPayload: MessagerActivityPayload): MessengerState { internal fun messengerState(launchPayload: MessagerActivityPayload): MessengerState {
@ -29,7 +27,6 @@ class MessengerModule(
CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager), CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager),
deviceMeta, deviceMeta,
messageOptionsStore, messageOptionsStore,
mutedRoomsStore,
RoomId(launchPayload.roomId), RoomId(launchPayload.roomId),
launchPayload.attachments, launchPayload.attachments,
it it

View File

@ -87,16 +87,19 @@ internal fun MessengerScreen(
Column { Column {
Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = { Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = {
OverflowMenu { state.roomState.takeIfContent()?.let {
when (state.isMuted) { OverflowMenu {
true -> DropdownMenuItem(text = { Text("Unmute notifications", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = { when (it.isMuted) {
viewModel.dispatch(ScreenAction.Notifications.Unmute) 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 = { false -> DropdownMenuItem(text = { Text("Mute notifications", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {
viewModel.dispatch(ScreenAction.Notifications.Mute) viewModel.dispatch(ScreenAction.Notifications.Mute)
}) })
}
} }
} }
}) })

View File

@ -8,7 +8,6 @@ import app.dapk.st.core.asString
import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.design.components.BubbleModel import app.dapk.st.design.components.BubbleModel
import app.dapk.st.domain.application.message.MessageOptionsStore 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.ChatEngine
import app.dapk.st.engine.MessengerPageState import app.dapk.st.engine.MessengerPageState
import app.dapk.st.engine.RoomEvent import app.dapk.st.engine.RoomEvent
@ -27,7 +26,6 @@ internal fun messengerReducer(
copyToClipboard: CopyToClipboard, copyToClipboard: CopyToClipboard,
deviceMeta: DeviceMeta, deviceMeta: DeviceMeta,
messageOptionsStore: MessageOptionsStore, messageOptionsStore: MessageOptionsStore,
mutedRoomsStore: MutedRoomsStore,
roomId: RoomId, roomId: RoomId,
initialAttachments: List<MessageAttachment>?, initialAttachments: List<MessageAttachment>?,
eventEmitter: suspend (MessengerEvent) -> Unit, eventEmitter: suspend (MessengerEvent) -> Unit,
@ -38,7 +36,6 @@ internal fun messengerReducer(
roomState = Lce.Loading(), roomState = Lce.Loading(),
composerState = initialComposerState(initialAttachments), composerState = initialComposerState(initialAttachments),
viewerState = null, viewerState = null,
isMuted = false,
), ),
async(ComponentLifecycle::class) { action -> async(ComponentLifecycle::class) { action ->
@ -50,7 +47,6 @@ internal fun messengerReducer(
.onEach { dispatch(MessagesStateChange.Content(it)) } .onEach { dispatch(MessagesStateChange.Content(it)) }
.launchIn(coroutineScope) .launchIn(coroutineScope)
) )
dispatch(MessagesStateChange.MuteContent(mutedRoomsStore.isMuted(roomId)))
} }
ComponentLifecycle.Gone -> jobBag.cancel("messages") ComponentLifecycle.Gone -> jobBag.cancel("messages")
@ -141,15 +137,17 @@ internal fun messengerReducer(
}, },
change(MessagesStateChange.MuteContent::class) { action, state -> change(MessagesStateChange.MuteContent::class) { action, state ->
state.copy(isMuted = action.isMuted).also { when (val roomState = state.roomState) {
println("??? action - $action previous state: ${state.isMuted} next: ${it.isMuted}") 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 -> async(ScreenAction.Notifications::class) { action ->
when (action) { when (action) {
ScreenAction.Notifications.Mute -> mutedRoomsStore.mute(roomId) ScreenAction.Notifications.Mute -> chatEngine.muteRoom(roomId)
ScreenAction.Notifications.Unmute -> mutedRoomsStore.unmute(roomId) ScreenAction.Notifications.Unmute -> chatEngine.unmuteRoom(roomId)
} }
dispatch( dispatch(

View File

@ -15,7 +15,6 @@ data class MessengerScreenState(
val roomState: Lce<MessengerPageState>, val roomState: Lce<MessengerPageState>,
val composerState: ComposerState, val composerState: ComposerState,
val viewerState: ViewerState?, val viewerState: ViewerState?,
val isMuted: Boolean,
) )
data class ViewerState( data class ViewerState(

View File

@ -3,30 +3,28 @@ package app.dapk.st.engine
import app.dapk.st.core.Base64 import app.dapk.st.core.Base64
import app.dapk.st.core.BuildMeta import app.dapk.st.core.BuildMeta
import app.dapk.st.core.CoroutineDispatchers 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.ErrorTracker
import app.dapk.st.matrix.MatrixClient import app.dapk.st.matrix.MatrixClient
import app.dapk.st.matrix.MatrixTaskRunner import app.dapk.st.matrix.MatrixTaskRunner
import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator
import app.dapk.st.matrix.auth.authService import app.dapk.st.matrix.auth.authService
import app.dapk.st.matrix.auth.installAuthService import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.* import app.dapk.st.matrix.common.MatrixLogger
import app.dapk.st.matrix.crypto.* 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.KnownDeviceStore
import app.dapk.st.matrix.device.deviceService import app.dapk.st.matrix.message.BackgroundScheduler
import app.dapk.st.matrix.device.installEncryptionService import app.dapk.st.matrix.message.LocalEchoStore
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.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.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.*
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.OlmStore
import app.dapk.st.olm.OlmWrapper
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -114,6 +112,10 @@ class MatrixEngine internal constructor(
override fun pushHandler() = matrixPushHandler.value 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 { override suspend fun runTask(task: ChatEngineTask): TaskRunner.TaskResult {
return when (val result = matrix.value.run(MatrixTaskRunner.MatrixTask(task.type, task.jsonPayload))) { return when (val result = matrix.value.run(MatrixTaskRunner.MatrixTask(task.type, task.jsonPayload))) {
is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(result.canRetry) is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(result.canRetry)
@ -209,222 +211,4 @@ class MatrixEngine internal constructor(
} }
private fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)
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<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)
}
}
}
fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)

View File

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

View File

@ -38,6 +38,7 @@ internal class TimelineUseCaseImpl(
}, },
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(), typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(),
self = userId, self = userId,
isMuted = roomService.isMuted(roomId)
) )
} }
} }

View File

@ -5,10 +5,7 @@ import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.room.internal.DefaultRoomService import app.dapk.st.matrix.room.internal.*
import app.dapk.st.matrix.room.internal.RoomInviteRemover
import app.dapk.st.matrix.room.internal.RoomMembers
import app.dapk.st.matrix.room.internal.RoomMembersCache
private val SERVICE_KEY = RoomService::class private val SERVICE_KEY = RoomService::class
@ -27,6 +24,10 @@ interface RoomService : MatrixService {
suspend fun joinRoom(roomId: RoomId) suspend fun joinRoom(roomId: RoomId)
suspend fun rejectJoinRoom(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( data class JoinedMember(
val userId: UserId, val userId: UserId,
val displayName: String?, val displayName: String?,
@ -39,6 +40,7 @@ fun MatrixServiceInstaller.installRoomService(
memberStore: MemberStore, memberStore: MemberStore,
roomMessenger: ServiceDepFactory<RoomMessenger>, roomMessenger: ServiceDepFactory<RoomMessenger>,
roomInviteRemover: RoomInviteRemover, roomInviteRemover: RoomInviteRemover,
singleRoomStore: SingleRoomStore,
): InstallExtender<RoomService> { ): InstallExtender<RoomService> {
return this.install { (httpClient, _, services, logger) -> return this.install { (httpClient, _, services, logger) ->
SERVICE_KEY to DefaultRoomService( SERVICE_KEY to DefaultRoomService(
@ -46,7 +48,8 @@ fun MatrixServiceInstaller.installRoomService(
logger, logger,
RoomMembers(memberStore, RoomMembersCache()), RoomMembers(memberStore, RoomMembersCache()),
roomMessenger.create(services), roomMessenger.create(services),
roomInviteRemover roomInviteRemover,
singleRoomStore,
) )
} }
} }

View File

@ -19,6 +19,7 @@ class DefaultRoomService(
private val roomMembers: RoomMembers, private val roomMembers: RoomMembers,
private val roomMessenger: RoomMessenger, private val roomMessenger: RoomMessenger,
private val roomInviteRemover: RoomInviteRemover, private val roomInviteRemover: RoomInviteRemover,
private val singleRoomStore: SingleRoomStore,
) : RoomService { ) : RoomService {
override suspend fun joinedMembers(roomId: RoomId): List<RoomService.JoinedMember> { override suspend fun joinedMembers(roomId: RoomId): List<RoomService.JoinedMember> {
@ -82,6 +83,7 @@ class DefaultRoomService(
} else { } else {
throw it throw it
} }
} }
else -> throw it else -> throw it
@ -90,6 +92,22 @@ class DefaultRoomService(
) )
roomInviteRemover.remove(roomId) 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<JoinedMembersResponse>( internal fun joinedMembersRequest(roomId: RoomId) = httpRequest<JoinedMembersResponse>(

View File

@ -5,7 +5,7 @@ import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.SyncToken import app.dapk.st.matrix.common.SyncToken
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface RoomStore { interface RoomStore : MuteableStore {
suspend fun persist(roomId: RoomId, events: List<RoomEvent>) suspend fun persist(roomId: RoomId, events: List<RoomEvent>)
suspend fun remove(rooms: List<RoomId>) suspend fun remove(rooms: List<RoomId>)
@ -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<RoomId>
}
interface FilterStore { interface FilterStore {
suspend fun store(key: String, filterId: String) suspend fun store(key: String, filterId: String)