for-android/app/src/main/java/chat/revolt/api/RevoltAPI.kt

168 lines
4.5 KiB
Kotlin

package chat.revolt.api
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.routes.user.fetchSelf
import chat.revolt.api.schemas.*
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.plugins.websocket.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
const val REVOLT_BASE = "https://api.revolt.chat"
const val REVOLT_SUPPORT = "https://support.revolt.chat"
const val REVOLT_MARKETING = "https://revolt.chat"
const val REVOLT_FILES = "https://autumn.revolt.chat"
const val REVOLT_WEBSOCKET = "wss://ws.revolt.chat"
private const val BACKEND_IS_STABLE = false
val RevoltJson = Json { ignoreUnknownKeys = true }
val RevoltHttp = HttpClient(OkHttp) {
install(DefaultRequest)
install(ContentNegotiation) {
json(RevoltJson)
}
install(WebSockets)
if (BACKEND_IS_STABLE) {
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 5)
retryOnException(maxRetries = 5)
modifyRequest { request ->
request.headers.append("x-retry-count", retryCount.toString())
}
exponentialDelay()
}
}
install(Logging) { level = LogLevel.INFO }
defaultRequest {
url(REVOLT_BASE)
}
}
val mainHandler = Handler(Looper.getMainLooper())
object RevoltAPI {
const val TOKEN_HEADER_NAME = "x-session-token"
// FIXME discount caching solutions! LRU would be better but this is fine for now
val userCache = mutableStateMapOf<String, User>()
val serverCache = mutableStateMapOf<String, Server>()
val channelCache = mutableStateMapOf<String, Channel>()
val emojiCache = mutableStateMapOf<String, Emoji>()
val messageCache = mutableStateMapOf<String, Message>()
var selfId: String? = null
var sessionToken: String = ""
private set
private var socketThread: Thread? = null
fun setSessionHeader(token: String) {
sessionToken = token
}
suspend fun loginAs(token: String) {
setSessionHeader(token)
fetchSelf()
startSocketOps()
}
suspend fun connectWS() {
socketThread = Thread {
try {
runBlocking {
RealtimeSocket.connect(sessionToken)
}
} catch (e: Exception) {
if (e is InterruptedException) {
Log.d("RevoltAPI", "Socket interrupted")
} else {
Log.e("RevoltAPI", "WebSocket error", e)
}
RealtimeSocket.open = false
}
}
socketThread!!.start()
}
private suspend fun startSocketOps() {
connectWS()
// Send a ping every roughly 30 seconds else the socket dies
// Same interval as the web clients (/revolt.js)
// Note: This will run even if the socket is closed (sendPing will just exit early)
mainHandler.post(object : Runnable {
override fun run() {
runBlocking {
RealtimeSocket.sendPing()
}
mainHandler.postDelayed(this, 30 * 1000)
}
})
}
suspend fun initialize() {
if (sessionToken != "") {
fetchSelf()
}
}
/**
* Returns true if the user is logged in and the current user has been fetched at least once.
* Call [initialize] to fetch the current user first, else this will return false.
*/
fun isLoggedIn(): Boolean {
return selfId != null
}
/**
* Clears the API client's state completely.
*/
fun logout() {
selfId = null
sessionToken = ""
userCache.clear()
serverCache.clear()
channelCache.clear()
emojiCache.clear()
messageCache.clear()
socketThread?.interrupt()
}
/**
* Checks if a session token is valid.
*/
suspend fun checkSessionToken(token: String): Boolean {
return try {
setSessionHeader(token)
fetchSelf()
true
} catch (e: Exception) {
false
}
}
}
@Serializable
data class RevoltError(val type: String)