Files
Nekosu/app/src/main/java/ru/risdeveau/pixeldragon/api/sync.kt
T

242 lines
7.0 KiB
Kotlin
Raw Normal View History

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
}