2026-02-20 09:41:06 +03:00
|
|
|
/*
|
|
|
|
|
* Created by sweetbread
|
|
|
|
|
* Copyright (c) 2025. All rights reserved.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package ru.risdeveau.pixeldragon.api
|
|
|
|
|
|
|
|
|
|
import android.util.Log
|
|
|
|
|
import io.ktor.client.request.bearerAuth
|
|
|
|
|
import io.ktor.client.request.get
|
|
|
|
|
import io.ktor.client.request.header
|
|
|
|
|
import io.ktor.client.request.parameter
|
|
|
|
|
import io.ktor.client.request.post
|
|
|
|
|
import io.ktor.client.request.setBody
|
|
|
|
|
import io.ktor.client.statement.bodyAsText
|
|
|
|
|
import io.ktor.http.ContentType
|
|
|
|
|
import io.ktor.http.HttpStatusCode
|
2026-02-20 17:28:12 +03:00
|
|
|
import io.ktor.http.contentLength
|
2026-02-20 09:41:06 +03:00
|
|
|
import io.ktor.http.contentType
|
|
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
import kotlinx.coroutines.delay
|
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
|
|
|
import kotlinx.coroutines.isActive
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import org.json.JSONObject
|
2026-02-20 17:28:12 +03:00
|
|
|
import ru.risdeveau.pixeldragon.AccountData
|
2026-02-20 09:41:06 +03:00
|
|
|
import ru.risdeveau.pixeldragon.AccountData.syncLastBatch
|
|
|
|
|
import ru.risdeveau.pixeldragon.baseUrl
|
|
|
|
|
import ru.risdeveau.pixeldragon.client
|
|
|
|
|
import ru.risdeveau.pixeldragon.db.isConnected
|
|
|
|
|
import ru.risdeveau.pixeldragon.token
|
|
|
|
|
import ru.risdeveau.pixeldragon.ui.activity.ME
|
|
|
|
|
import splitties.init.appCtx
|
|
|
|
|
import java.io.File
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MatrixSyncService {
|
|
|
|
|
private val _syncState = MutableStateFlow<SyncState>(SyncState.Idle)
|
|
|
|
|
val syncState: StateFlow<SyncState> = _syncState.asStateFlow()
|
|
|
|
|
|
|
|
|
|
private var syncJob: Job? = null
|
|
|
|
|
// private var isInitialized = false
|
|
|
|
|
//
|
|
|
|
|
// fun initialize() {
|
|
|
|
|
// isInitialized = true
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
fun startSync() {
|
2026-02-20 17:28:12 +03:00
|
|
|
if (AccountData.token == null)
|
|
|
|
|
Log.wtf("MatrixSyncService", "Token is null")
|
|
|
|
|
if (AccountData.userId == null)
|
|
|
|
|
Log.wtf("MatrixSyncService", "User ID is null")
|
|
|
|
|
if (AccountData.homeserver == null)
|
|
|
|
|
Log.wtf("MatrixSyncService", "Homeserver is null")
|
|
|
|
|
|
|
|
|
|
Log.i("MatrixSyncService", "Start syncing")
|
2026-02-20 09:41:06 +03:00
|
|
|
// if (!isInitialized)
|
|
|
|
|
// throw IllegalStateException("Sync service not initialized")
|
|
|
|
|
|
|
|
|
|
if (syncJob?.isActive == true) return
|
|
|
|
|
|
|
|
|
|
syncJob = CoroutineScope(Dispatchers.IO).launch {
|
2026-02-20 17:28:12 +03:00
|
|
|
if (syncLastBatch == null) {
|
|
|
|
|
Log.i("MatrixSyncService", "Init syncing")
|
2026-02-20 09:41:06 +03:00
|
|
|
_syncState.value = SyncState.Syncing
|
|
|
|
|
try {
|
2026-02-20 17:28:12 +03:00
|
|
|
initialSync()
|
2026-02-20 09:41:06 +03:00
|
|
|
_syncState.value = SyncState.Idle
|
|
|
|
|
} catch (e: Exception) {
|
2026-02-20 17:28:12 +03:00
|
|
|
Log.e("MatrixSyncService", "Initial sync error", e)
|
2026-02-20 09:41:06 +03:00
|
|
|
_syncState.value = SyncState.Error(e.message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// while (isActive) {
|
|
|
|
|
// try {
|
|
|
|
|
// val response = sync (
|
|
|
|
|
// timeout = 30000,
|
|
|
|
|
// filter = getFilter()
|
|
|
|
|
// )
|
|
|
|
|
//
|
|
|
|
|
// processSyncResponse(response)
|
|
|
|
|
//
|
|
|
|
|
// } catch (e: Exception) {
|
|
|
|
|
// Log.w("sync", e.message.toString())
|
|
|
|
|
// delay(5000) // Wait before retry
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fun stopSync() {
|
|
|
|
|
syncJob?.cancel()
|
|
|
|
|
_syncState.value = SyncState.Idle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun pauseSync() {
|
|
|
|
|
// Called when app goes to background
|
|
|
|
|
syncJob?.cancel()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun resumeSync() {
|
|
|
|
|
// Called when app comes to foreground
|
|
|
|
|
if (!isSyncActive()) {
|
|
|
|
|
startSync()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun isSyncActive(): Boolean = syncJob?.isActive == true
|
|
|
|
|
|
2026-02-20 17:28:12 +03:00
|
|
|
private suspend fun parseSyncResponse(response: JSONObject) {
|
|
|
|
|
Log.v("syncResponse", response.toString(2))
|
|
|
|
|
val newMessages = mutableListOf<Message>()
|
|
|
|
|
val roomUpdates = mutableListOf<RoomUpdate>()
|
|
|
|
|
|
|
|
|
|
response.rooms?.join?.forEach { (roomId, roomData) ->
|
|
|
|
|
// Process timeline events (messages)
|
|
|
|
|
roomData.timeline?.events?.forEach { event ->
|
|
|
|
|
val message = event.toMessage(roomId)
|
|
|
|
|
database.messageDao().insertMessage(message)
|
|
|
|
|
newMessages.add(message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process room state updates
|
|
|
|
|
roomData.state?.events?.forEach { event ->
|
|
|
|
|
when (event.type) {
|
|
|
|
|
"m.room.name", "m.room.avatar" -> {
|
|
|
|
|
roomUpdates.add(RoomUpdate(roomId, event.type, event.content))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process ephemeral events (typing, receipts)
|
|
|
|
|
roomData.ephemeral?.events?.forEach { event ->
|
|
|
|
|
when (event.type) {
|
|
|
|
|
"m.typing" -> handleTypingEvent(roomId, event)
|
|
|
|
|
"m.receipt" -> handleReceiptEvent(roomId, event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Notify the app about new data
|
|
|
|
|
if (newMessages.isNotEmpty()) {
|
|
|
|
|
_newMessages.emit(newMessages)
|
|
|
|
|
}
|
|
|
|
|
if (roomUpdates.isNotEmpty()) {
|
|
|
|
|
_roomUpdates.emit(roomUpdates)
|
|
|
|
|
}
|
2026-02-20 09:41:06 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sealed class SyncState {
|
|
|
|
|
object Idle : SyncState()
|
|
|
|
|
object Syncing : SyncState()
|
|
|
|
|
object Success : SyncState()
|
|
|
|
|
data class Error(val message: String?) : SyncState()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun sync(): JSONObject {
|
|
|
|
|
|
|
|
|
|
return JSONObject()
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun initialSync(): JSONObject {
|
2026-02-20 17:28:12 +03:00
|
|
|
fetchRoomMeta()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch rooms metadata during initial sync
|
|
|
|
|
*/
|
|
|
|
|
suspend fun fetchRoomMeta() {
|
|
|
|
|
val filterId = getFilterId("""
|
2026-02-20 09:41:06 +03:00
|
|
|
{
|
2026-02-20 17:28:12 +03:00
|
|
|
"room": {
|
|
|
|
|
"state": {
|
|
|
|
|
"types": [
|
|
|
|
|
"m.room.name",
|
|
|
|
|
"m.room.avatar",
|
|
|
|
|
"m.room.canonical_alias",
|
|
|
|
|
"m.room.member"
|
|
|
|
|
],
|
|
|
|
|
"lazy_load_members": true
|
|
|
|
|
},
|
|
|
|
|
"timeline": {
|
|
|
|
|
"limit": 0,
|
|
|
|
|
"types": []
|
|
|
|
|
},
|
|
|
|
|
"ephemeral": {
|
|
|
|
|
"types": []
|
|
|
|
|
},
|
|
|
|
|
"include_leave": false
|
2026-02-20 09:41:06 +03:00
|
|
|
},
|
2026-02-20 17:28:12 +03:00
|
|
|
"presence": {
|
|
|
|
|
"types": []
|
|
|
|
|
}
|
2026-02-20 09:41:06 +03:00
|
|
|
}
|
2026-02-20 17:28:12 +03:00
|
|
|
""".trimIndent())
|
2026-02-20 09:41:06 +03:00
|
|
|
// val filterId = "vmNk"
|
|
|
|
|
|
2026-02-20 17:28:12 +03:00
|
|
|
val r = client.get("$baseUrl/sync") {
|
2026-02-20 09:41:06 +03:00
|
|
|
bearerAuth(token)
|
|
|
|
|
url {
|
|
|
|
|
parameter("filter", filterId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (r.status != HttpStatusCode.OK)
|
|
|
|
|
throw IllegalStateException("Failed to sync")
|
2026-02-20 17:28:12 +03:00
|
|
|
Log.v("initialSync", "Response size: ${r.contentLength()}")
|
2026-02-20 09:41:06 +03:00
|
|
|
|
2026-02-20 17:28:12 +03:00
|
|
|
r.contentLength()?.let {
|
|
|
|
|
if (it >= 50*1024*1024)
|
|
|
|
|
Log.w("initialSync", "Response size is too large")
|
|
|
|
|
}
|
2026-02-20 09:41:06 +03:00
|
|
|
val json = JSONObject(r.bodyAsText())
|
|
|
|
|
syncLastBatch = json.getString("next_batch")
|
2026-02-20 17:28:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send a filter to the server and get its id
|
|
|
|
|
* @param filter JSON Filter
|
|
|
|
|
* @return Filter ID
|
|
|
|
|
*/
|
|
|
|
|
suspend fun getFilterId(filter: String): String {
|
|
|
|
|
val userId = AccountData.userId
|
|
|
|
|
if (userId == null)
|
|
|
|
|
Log.wtf("getFilter", "user_id is not defined")
|
|
|
|
|
|
|
|
|
|
val r = client.post("$baseUrl/user/$userId/filter") {
|
|
|
|
|
setBody(filter)
|
|
|
|
|
contentType(ContentType.Application.Json)
|
|
|
|
|
bearerAuth(token)
|
|
|
|
|
}
|
2026-02-20 09:41:06 +03:00
|
|
|
|
2026-02-20 17:28:12 +03:00
|
|
|
return JSONObject(r.bodyAsText()).getString("filter_id")
|
2026-02-20 09:41:06 +03:00
|
|
|
}
|