wip: Migrate to Trixnity

This commit is contained in:
2026-02-21 16:48:31 +03:00
parent 8d6a76ccb5
commit c7b5f20c06
19 changed files with 217 additions and 1007 deletions
@@ -1,19 +1,11 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 20:21
* Copyright (c) 2026. All rights reserved.
*/
package ru.risdeveau.pixeldragon.api
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.statement.bodyAsText
import org.json.JSONObject
import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.token
import java.time.Instant
class Event (
@@ -1,97 +1,4 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Copyright (c) 2026. All rights reserved.
*/
package ru.risdeveau.pixeldragon.api
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.json.JSONObject
import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.repo.Room
import ru.risdeveau.pixeldragon.repo.User
import ru.risdeveau.pixeldragon.token
//fun getRooms(): List<Room> {
// return db.roomDoa().getAllJoined()
//}
//
//fun updateRooms(): List<Room> {
//
//}
suspend fun getJoinedRooms(): List<Room> {
val r = client.get("$baseUrl/joined_rooms")
{ bearerAuth(token) }
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
return List(
rooms.length()
) { i -> getRoom(rooms.getString(i), true) }
}
suspend fun isJoined(id: String): Boolean {
val r = client.get("$baseUrl/joined_rooms")
{ bearerAuth(token) }
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
return List<String>(
rooms.length()
) { i -> rooms.getString(i) }.contains(id)
}
suspend fun getRoom(rid: String, joined: Boolean? = null): Room {
val direct = getAccountData(getMe()!!.userId, "m.direct")
var directWith = ""
direct?.let {
for (user in direct.keys()) {
val roomsWithUser = direct.getJSONArray(user)
for (i in 0 until roomsWithUser.length()) {
if (rid == roomsWithUser.getString(i)) {
directWith = user
break
}
}
if (directWith.isNotEmpty()) break
}
}
return coroutineScope {
val name = async { getState(rid, "m.room.name", "name") }
val type = async { getState(rid, "m.room.create", "type") ?: "m.room" }
val creator = async { getState(rid, "m.room.create", "creator") }
val avatar = async { getState(rid, "m.room.avatar", "url") }
val joined = async { joined ?: isJoined(rid) }
val direct = async { if (directWith.isNotEmpty()) User.getById(directWith) else null }
Room(
rid,
name.await(),
type.await(),
creator.await(),
null,
avatar.await(),
null,
joined.await(),
direct.await()
)
}
}
private suspend fun getState(rid: String, state: String, key: String): String? {
val r = client.get("$baseUrl/rooms/$rid/state/$state") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
val json = JSONObject(r.bodyAsText())
if (!json.has(key)) return null
return json.getString(key)
}
suspend fun getAccountData(user: String, room: String, state: String): JSONObject? {
val r = client.get("$baseUrl/user/$user/rooms/$room/account_data/$state") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
return JSONObject(r.bodyAsText())
}
@@ -1,6 +1,6 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Copyright (c) 2026. All rights reserved.
*/
package ru.risdeveau.pixeldragon.api
@@ -9,11 +9,11 @@ import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import org.json.JSONException
import org.json.JSONObject
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.homeserver
import ru.risdeveau.pixeldragon.webClient
//import ru.risdeveau.pixeldragon.homeserver
suspend fun getHomeserver(url: String): String? {
val r = try { client.get("https://$url/.well-known/matrix/client") }
val r = try { webClient.get("https://$url/.well-known/matrix/client") }
catch (_: Exception) { return null }
val json = try { JSONObject(r.bodyAsText()) }
@@ -27,11 +27,11 @@ suspend fun getHomeserver(url: String): String? {
return homeserver
}
fun mxcToUrl(mxc: String): String? {
val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([^#?]+)")
val match = pattern.find(mxc)
if ((match?.groupValues[1] == null) or (match?.groupValues[2] == null)) return null
return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}"
}
//fun mxcToUrl(mxc: String): String? {
// val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([^#?]+)")
// val match = pattern.find(mxc)
//
// if ((match?.groupValues[1] == null) or (match?.groupValues[2] == null)) return null
//
// return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}"
//}
@@ -1,100 +1,73 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Copyright (c) 2026. 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.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import org.json.JSONObject
import ru.risdeveau.pixeldragon.AccountData
import ru.risdeveau.pixeldragon.baseUrl
import io.ktor.http.Url
import net.folivo.trixnity.client.MatrixClient
import net.folivo.trixnity.client.login
import net.folivo.trixnity.clientserverapi.model.authentication.IdentifierType
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.initCheck
import ru.risdeveau.pixeldragon.token
import ru.risdeveau.pixeldragon.util.getMediaStore
import ru.risdeveau.pixeldragon.util.getRoomStore
import splitties.experimental.ExperimentalSplittiesApi
import splitties.init.appCtx
import splitties.preferences.edit
data class Me (val userId: String, val deviceId: String)
data class UserProfile (val displayName: String, val avatarUrl: String, val other: JSONObject)
/**
* This func is to validate the token
*/
suspend fun getMe(): Me? {
val r = client.get("$baseUrl/account/whoami") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) {
Log.e("getMe", r.bodyAsText())
return null
}
val json = JSONObject(r.bodyAsText())
return Me(json.getString("user_id"), json.getString("device_id"))
}
//data class Me (val userId: String, val deviceId: String)
//data class UserProfile (val displayName: String, val avatarUrl: String, val other: JSONObject)
//
///**
// * This func is to validate the token
// */
//suspend fun getMe(): Me? {
// val r = webClient.get("$baseUrl/account/whoami") { bearerAuth(token) }
// if (r.status != HttpStatusCode.OK) {
// Log.e("getMe", r.bodyAsText())
// return null
// }
//
// val json = JSONObject(r.bodyAsText())
// return Me(json.getString("user_id"), json.getString("device_id"))
//}
@OptIn(ExperimentalSplittiesApi::class)
suspend fun login(server: String, login: String, pass: String): Boolean {
val hs = getHomeserver(server)!!
val hs = Url(getHomeserver(server)!!)
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
val pattern = """
{
"type": "m.login.password",
"identifier": {
"type": "m.id.user"
},
"initial_device_display_name": "PixelDragon Android v${pinfo.versionName}"
}
""".trimIndent()
val json = JSONObject(pattern)
json.getJSONObject("identifier").put("user", login)
json.put("password", pass)
try {
client = MatrixClient.login(
baseUrl = hs,
identifier = IdentifierType.User(login),
password = pass,
initialDeviceDisplayName = "PixelDragon Android v${pinfo.versionName}",
repositoriesModule = getRoomStore(appCtx),
mediaStore = getMediaStore()
).getOrThrow()
val r = try {
client.post("$hs/_matrix/client/v3/login") {
setBody(json.toString())
contentType(ContentType.Application.Json)
}
return true
} catch (e: Exception) {
Log.e("login", e.toString())
return false
Log.i("Login", "Failed to login", e)
}
if (r.status != HttpStatusCode.OK) {
Log.e("login", r.bodyAsText())
return false // TODO: Inform a user of error code
}
val res = JSONObject(r.bodyAsText())
AccountData.edit {
token = res.getString("access_token")
homeserver = hs
}
return initCheck()
return false
}
suspend fun getAccountData(user: String, state: String): JSONObject? {
val r = client.get("$baseUrl/user/$user/account_data/$state") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
return JSONObject(r.bodyAsText())
}
suspend fun getUserProfile(userId: String): UserProfile? {
val r = client.get("$baseUrl/profile/$userId") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
val json = JSONObject(r.bodyAsText())
val name = json.optString("displayname", ""); json.remove("displayname")
val avatar = json.optString("avatar_url", ""); json.remove("avatar_url")
return UserProfile(name, avatar, json)
}
//suspend fun getAccountData(user: String, state: String): JSONObject? {
// val r = webClient.get("$baseUrl/user/$user/account_data/$state") { bearerAuth(token) }
// if (r.status != HttpStatusCode.OK) return null
// return JSONObject(r.bodyAsText())
//}
//
//suspend fun getUserProfile(userId: String): UserProfile? {
// val r = webClient.get("$baseUrl/profile/$userId") { bearerAuth(token) }
// if (r.status != HttpStatusCode.OK) return null
// val json = JSONObject(r.bodyAsText())
// val name = json.optString("displayname", ""); json.remove("displayname")
// val avatar = json.optString("avatar_url", ""); json.remove("avatar_url")
// return UserProfile(name, avatar, json)
//}
@@ -1,242 +1,4 @@
/*
* 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
import io.ktor.http.contentLength
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
import ru.risdeveau.pixeldragon.AccountData
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() {
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")
// if (!isInitialized)
// throw IllegalStateException("Sync service not initialized")
if (syncJob?.isActive == true) return
syncJob = CoroutineScope(Dispatchers.IO).launch {
if (syncLastBatch == null) {
Log.i("MatrixSyncService", "Init syncing")
_syncState.value = SyncState.Syncing
try {
initialSync()
_syncState.value = SyncState.Idle
} catch (e: Exception) {
Log.e("MatrixSyncService", "Initial sync error", e)
_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
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)
}
}
}
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 {
fetchRoomMeta()
}
/**
* Fetch rooms metadata during initial sync
*/
suspend fun fetchRoomMeta() {
val filterId = getFilterId("""
{
"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
},
"presence": {
"types": []
}
}
""".trimIndent())
// val filterId = "vmNk"
val r = client.get("$baseUrl/sync") {
bearerAuth(token)
url {
parameter("filter", filterId)
}
}
if (r.status != HttpStatusCode.OK)
throw IllegalStateException("Failed to sync")
Log.v("initialSync", "Response size: ${r.contentLength()}")
r.contentLength()?.let {
if (it >= 50*1024*1024)
Log.w("initialSync", "Response size is too large")
}
val json = JSONObject(r.bodyAsText())
syncLastBatch = json.getString("next_batch")
}
/**
* 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)
}
return JSONObject(r.bodyAsText()).getString("filter_id")
}
* Copyright (c) 2026. All rights reserved.
*/