wip: Migrate to Trixnity
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
Reference in New Issue
Block a user