feat: a real-time websocket connection
- more schemas - schemas for all sendable and receivable socket frames(packets) - multi-threading woohoo! - handling for ready event - sending out the ping event in 30-second intervals - connect-disconnect-reconnect logic (implemented, but to be used) - caches for all the things!
This commit is contained in:
parent
851bbebd43
commit
589c00d3ee
|
|
@ -1,20 +1,27 @@
|
|||
package chat.revolt.api
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import chat.revolt.api.realtime.RealtimeSocket
|
||||
import chat.revolt.api.routes.user.fetchSelf
|
||||
import chat.revolt.api.routes.user.fetchSelfWithNewToken
|
||||
import chat.revolt.api.schemas.CompleteUser
|
||||
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
|
||||
|
||||
|
|
@ -26,6 +33,8 @@ val RevoltHttp = HttpClient(OkHttp) {
|
|||
json(RevoltJson)
|
||||
}
|
||||
|
||||
install(WebSockets)
|
||||
|
||||
if (BACKEND_IS_STABLE) {
|
||||
install(HttpRequestRetry) {
|
||||
retryOnServerErrors(maxRetries = 5)
|
||||
|
|
@ -46,23 +55,70 @@ val RevoltHttp = HttpClient(OkHttp) {
|
|||
}
|
||||
}
|
||||
|
||||
val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
object RevoltAPI {
|
||||
const val TOKEN_HEADER_NAME = "x-session-token"
|
||||
|
||||
// discount caching solution(/-s)! LRU would be better but this is fine for now, until it's not...
|
||||
val userCache =
|
||||
mutableMapOf<String, CompleteUser>()
|
||||
// FIXME discount caching solutions! LRU would be better but this is fine for now
|
||||
val userCache = mutableMapOf<String, User>()
|
||||
val serverCache = mutableMapOf<String, Server>()
|
||||
val channelCache = mutableMapOf<String, Channel>()
|
||||
val emojiCache = mutableMapOf<String, Emoji>()
|
||||
val messageCache = mutableMapOf<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()
|
||||
|
|
@ -85,6 +141,11 @@ object RevoltAPI {
|
|||
sessionToken = ""
|
||||
|
||||
userCache.clear()
|
||||
serverCache.clear()
|
||||
channelCache.clear()
|
||||
emojiCache.clear()
|
||||
|
||||
socketThread?.interrupt()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,7 +153,8 @@ object RevoltAPI {
|
|||
*/
|
||||
suspend fun checkSessionToken(token: String): Boolean {
|
||||
return try {
|
||||
fetchSelfWithNewToken(token)
|
||||
setSessionHeader(token)
|
||||
fetchSelf()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
|
|
@ -100,5 +162,5 @@ object RevoltAPI {
|
|||
}
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class RevoltError(val type: String)
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package chat.revolt.api.realtime
|
||||
|
||||
import android.util.Log
|
||||
import chat.revolt.api.REVOLT_WEBSOCKET
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import chat.revolt.api.realtime.frames.receivable.*
|
||||
import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
|
||||
import chat.revolt.api.realtime.frames.sendable.PingFrame
|
||||
import io.ktor.client.plugins.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import java.util.Calendar
|
||||
|
||||
object RealtimeSocket {
|
||||
var socket: WebSocketSession? = null
|
||||
var open: Boolean = false
|
||||
|
||||
suspend fun connect(token: String) {
|
||||
RevoltHttp.ws(REVOLT_WEBSOCKET) {
|
||||
socket = this
|
||||
|
||||
Log.d("RealtimeSocket", "Connected to websocket.")
|
||||
open = true
|
||||
|
||||
// Send authorization frame
|
||||
val authFrame = AuthorizationFrame("Authenticate", token)
|
||||
val authFrameString =
|
||||
RevoltJson.encodeToString(AuthorizationFrame.serializer(), authFrame)
|
||||
|
||||
Log.d("RealtimeSocket", "Sending authorization frame: $authFrameString")
|
||||
send(RevoltJson.encodeToString(AuthorizationFrame.serializer(), authFrame))
|
||||
|
||||
incoming.consumeEach { frame ->
|
||||
if (frame is Frame.Text) {
|
||||
val frameString = frame.readText()
|
||||
val frameType =
|
||||
RevoltJson.decodeFromString(AnyFrame.serializer(), frameString).type
|
||||
|
||||
handleFrame(frameType, frameString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendPing() {
|
||||
if (!open) return
|
||||
|
||||
val pingPacket = PingFrame("Ping", Calendar.getInstance().timeInMillis.toInt())
|
||||
socket?.send(RevoltJson.encodeToString(PingFrame.serializer(), pingPacket))
|
||||
Log.d("RealtimeSocket", "Sent ping frame with ${pingPacket.data}")
|
||||
}
|
||||
|
||||
private fun handleFrame(type: String, rawFrame: String) {
|
||||
when (type) {
|
||||
"Pong" -> {
|
||||
val pongFrame = RevoltJson.decodeFromString(PongFrame.serializer(), rawFrame)
|
||||
Log.d("RealtimeSocket", "Received pong frame for ${pongFrame.data}")
|
||||
}
|
||||
"Bulk" -> {
|
||||
val bulkFrame = RevoltJson.decodeFromString(BulkFrame.serializer(), rawFrame)
|
||||
Log.d("RealtimeSocket", "Received bulk frame with ${bulkFrame.v.size} sub-frames.")
|
||||
bulkFrame.v.forEach { subFrame ->
|
||||
val subFrameType =
|
||||
RevoltJson.decodeFromString(AnyFrame.serializer(), subFrame.toString()).type
|
||||
handleFrame(subFrameType, subFrame.toString())
|
||||
}
|
||||
}
|
||||
"Ready" -> {
|
||||
val readyFrame = RevoltJson.decodeFromString(ReadyFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received ready frame with ${readyFrame.users.size} users, ${readyFrame.servers.size} servers, ${readyFrame.channels.size} channels, and ${readyFrame.emojis.size} emojis."
|
||||
)
|
||||
|
||||
Log.d("RealtimeSocket", "Adding users to cache.")
|
||||
readyFrame.users.forEach { user ->
|
||||
RevoltAPI.userCache[user.id!!] = user
|
||||
}
|
||||
|
||||
Log.d("RealtimeSocket", "Adding servers to cache.")
|
||||
readyFrame.servers.forEach { server ->
|
||||
RevoltAPI.serverCache[server.id!!] = server
|
||||
}
|
||||
|
||||
Log.d("RealtimeSocket", "Adding channels to cache.")
|
||||
readyFrame.channels.forEach { channel ->
|
||||
RevoltAPI.channelCache[channel.id!!] = channel
|
||||
}
|
||||
|
||||
Log.d("RealtimeSocket", "Adding emojis to cache.")
|
||||
readyFrame.emojis.forEach { emoji ->
|
||||
RevoltAPI.emojiCache[emoji.id!!] = emoji
|
||||
}
|
||||
}
|
||||
"UserUpdate" -> {
|
||||
val userUpdateFrame =
|
||||
RevoltJson.decodeFromString(UserUpdateFrame.serializer(), rawFrame)
|
||||
// We will genuinely just ignore this frame for now, but it gets really spammy in the logs
|
||||
// FIXME handle this frame
|
||||
}
|
||||
else -> {
|
||||
Log.i("RealtimeSocket", "Unknown frame: $rawFrame")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
package chat.revolt.api.realtime.frames.receivable
|
||||
|
||||
import chat.revolt.api.schemas.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
@Serializable
|
||||
data class AnyFrame(
|
||||
val type: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ErrorFrame(
|
||||
val type: String = "Error",
|
||||
val error: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BulkFrame(
|
||||
val type: String = "Bulk",
|
||||
val v: List<JsonObject>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PongFrame(
|
||||
val type: String = "Pong",
|
||||
val data: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadyFrame(
|
||||
val type: String = "Ready",
|
||||
val users: List<User>,
|
||||
val servers: List<Server>,
|
||||
val channels: List<Channel>,
|
||||
val emojis: List<Emoji>
|
||||
)
|
||||
|
||||
typealias MessageFrame = Message
|
||||
|
||||
@Serializable
|
||||
data class MessageUpdateFrame(
|
||||
val type: String = "MessageUpdate",
|
||||
val id: String,
|
||||
val channel: String,
|
||||
val data: JsonObject
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Appendable(
|
||||
val embeds: List<Embed>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessageAppendFrame(
|
||||
val type: String = "MessageAppend",
|
||||
val id: String,
|
||||
val channel: String,
|
||||
val append: Appendable
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessageDeleteFrame(
|
||||
val type: String = "MessageDelete",
|
||||
val id: String,
|
||||
val channel: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessageReactFrame(
|
||||
val type: String = "MessageReact",
|
||||
val id: String,
|
||||
val channel_id: String,
|
||||
val user_id: String,
|
||||
val emoji_id: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessageUnreactFrame(
|
||||
val type: String = "MessageUnreact",
|
||||
val id: String,
|
||||
val channel_id: String,
|
||||
val user_id: String,
|
||||
val emoji_id: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessageRemoveReactionFrame(
|
||||
val type: String = "MessageRemoveReaction",
|
||||
val id: String,
|
||||
val channel_id: String,
|
||||
val emoji_id: String,
|
||||
)
|
||||
|
||||
/* ChannelCreate: we already have a "type" property in channel so we just alias the type */
|
||||
typealias ChannelCreateFrame = Channel
|
||||
|
||||
@Serializable
|
||||
data class ChannelUpdateFrame(
|
||||
val type: String = "ChannelUpdate",
|
||||
val id: String,
|
||||
val data: Channel,
|
||||
val clear: List<String>? = null // "Icon" or "Description"
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChannelDeleteFrame(
|
||||
val type: String = "ChannelDelete",
|
||||
val id: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChannelGroupJoinFrame(
|
||||
val type: String = "ChannelGroupJoin",
|
||||
val id: String,
|
||||
val user: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChannelGroupLeaveFrame(
|
||||
val type: String = "ChannelGroupLeave",
|
||||
val id: String,
|
||||
val user: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChannelStartTypingFrame(
|
||||
val type: String = "ChannelStartTyping",
|
||||
val id: String,
|
||||
val user: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChannelStopTypingFrame(
|
||||
val type: String = "ChannelStopTyping",
|
||||
val id: String,
|
||||
val user: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChannelAckFrame(
|
||||
val type: String = "ChannelAck",
|
||||
val id: String,
|
||||
val user: String,
|
||||
@SerialName("message_id")
|
||||
val messageId: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerCreateFrame(
|
||||
val type: String = "ServerCreate",
|
||||
val id: String,
|
||||
val server: Server
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerUpdateFrame(
|
||||
val type: String = "ServerUpdate",
|
||||
val id: String,
|
||||
val data: Server,
|
||||
val clear: List<String>? = null // "Icon", "Banner" or "Description"
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerDeleteFrame(
|
||||
val type: String = "ServerDelete",
|
||||
val id: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerUserChoice(
|
||||
val server: String,
|
||||
val user: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerMemberUpdateFrame(
|
||||
val type: String = "ServerMemberUpdate",
|
||||
val id: ServerUserChoice,
|
||||
val data: Member,
|
||||
val clear: List<String>? = null // "Nickname" or "Avatar"
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerMemberJoinFrame(
|
||||
val type: String = "ServerMemberJoin",
|
||||
val id: String,
|
||||
val user: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerMemberLeaveFrame(
|
||||
val type: String = "ServerMemberLeave",
|
||||
val id: String,
|
||||
val user: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerRoleUpdateFrame(
|
||||
val type: String = "ServerRoleUpdate",
|
||||
val id: String,
|
||||
@SerialName("role_id")
|
||||
val roleId: String,
|
||||
val data: Role,
|
||||
val clear: List<String>? = null // "Colour"
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerRoleDeleteFrame(
|
||||
val type: String = "ServerRoleDelete",
|
||||
val id: String,
|
||||
@SerialName("role_id")
|
||||
val roleId: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserUpdateFrame(
|
||||
val type: String = "UserUpdate",
|
||||
val id: String,
|
||||
val data: User,
|
||||
val clear: List<String>? = null // "ProfileContent", "ProfileBackground", "StatusText" or "Avatar"
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserRelationshipFrame(
|
||||
val type: String = "UserRelationship",
|
||||
val id: String,
|
||||
val user: User,
|
||||
val status: String,
|
||||
)
|
||||
|
||||
typealias EmojiCreateFrame = Emoji
|
||||
|
||||
@Serializable
|
||||
data class EmojiDeleteFrame(
|
||||
val type: String = "EmojiDelete",
|
||||
val id: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package chat.revolt.api.realtime.frames.sendable
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AuthorizationFrame(
|
||||
val type: String,
|
||||
val token: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PingFrame(
|
||||
val type: String,
|
||||
val data: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BeginTypingFrame(
|
||||
val type: String,
|
||||
val channel: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EndTypingFrame(
|
||||
val type: String,
|
||||
val channel: String
|
||||
)
|
||||
|
|
@ -4,12 +4,12 @@ import chat.revolt.api.RevoltAPI
|
|||
import chat.revolt.api.RevoltError
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import chat.revolt.api.schemas.CompleteUser
|
||||
import chat.revolt.api.schemas.User
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.serialization.SerializationException
|
||||
|
||||
suspend fun fetchSelf(): CompleteUser {
|
||||
suspend fun fetchSelf(): User {
|
||||
val response = RevoltHttp.get("/users/@me") {
|
||||
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||
}
|
||||
|
|
@ -22,15 +22,10 @@ suspend fun fetchSelf(): CompleteUser {
|
|||
// Not an error
|
||||
}
|
||||
|
||||
val user = RevoltJson.decodeFromString(CompleteUser.serializer(), response)
|
||||
val user = RevoltJson.decodeFromString(User.serializer(), response)
|
||||
|
||||
RevoltAPI.userCache[user.id!!] = user
|
||||
RevoltAPI.selfId = user.id
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
suspend fun fetchSelfWithNewToken(token: String): CompleteUser {
|
||||
RevoltAPI.setSessionHeader(token)
|
||||
return fetchSelf()
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import kotlinx.serialization.encoding.*
|
|||
@Serializable
|
||||
data class MessagesInChannel(
|
||||
val messages: List<Message>? = null,
|
||||
val users: List<CompleteUser>? = null,
|
||||
val users: List<User>? = null,
|
||||
val members: List<Member>? = null
|
||||
)
|
||||
|
||||
|
|
@ -20,7 +20,61 @@ data class Member(
|
|||
@SerialName("joined_at")
|
||||
val joinedAt: String? = null,
|
||||
|
||||
val avatar: Avatar? = null,
|
||||
val avatar: AutumnResource? = null,
|
||||
val roles: List<String>? = null,
|
||||
val nickname: String? = null
|
||||
)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Channel(
|
||||
val channelType: ChannelType? = null,
|
||||
@SerialName("_id")
|
||||
val id: String? = null,
|
||||
val user: String? = null,
|
||||
val name: String? = null,
|
||||
val owner: String? = null,
|
||||
val description: String? = null,
|
||||
val recipients: List<String>? = null,
|
||||
val icon: AutumnResource? = null,
|
||||
val lastMessageID: String? = null,
|
||||
val active: Boolean? = null,
|
||||
val permissions: Long? = null,
|
||||
val server: String? = null,
|
||||
val rolePermissions: Map<String, DefaultPermissions>? = null,
|
||||
val defaultPermissions: DefaultPermissions? = null,
|
||||
val nsfw: Boolean? = null,
|
||||
val type: String? = null, // this is _only_ used for websocket events!
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class ChannelType(val value: String) {
|
||||
DirectMessage("DirectMessage"),
|
||||
Group("Group"),
|
||||
SavedMessages("SavedMessages"),
|
||||
TextChannel("TextChannel"),
|
||||
VoiceChannel("VoiceChannel");
|
||||
|
||||
companion object : KSerializer<ChannelType> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() {
|
||||
return PrimitiveSerialDescriptor(
|
||||
"chat.revolt.api.schemas.ChannelType",
|
||||
PrimitiveKind.STRING
|
||||
)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): ChannelType =
|
||||
when (val value = decoder.decodeString()) {
|
||||
"DirectMessage" -> DirectMessage
|
||||
"Group" -> Group
|
||||
"SavedMessages" -> SavedMessages
|
||||
"TextChannel" -> TextChannel
|
||||
"VoiceChannel" -> VoiceChannel
|
||||
else -> throw IllegalArgumentException("ChannelType could not parse: $value")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ChannelType) {
|
||||
return encoder.encodeString(value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package chat.revolt.api.schemas
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AutumnResource(
|
||||
@SerialName("_id")
|
||||
val id: String? = null,
|
||||
|
||||
val tag: String? = null,
|
||||
val filename: String? = null,
|
||||
val metadata: Metadata? = null,
|
||||
|
||||
@SerialName("content_type")
|
||||
val contentType: String? = null,
|
||||
|
||||
val size: Long? = null,
|
||||
val deleted: Boolean? = null,
|
||||
val reported: Boolean? = null,
|
||||
|
||||
@SerialName("message_id")
|
||||
val messageID: String? = null,
|
||||
|
||||
@SerialName("user_id")
|
||||
val userID: String? = null,
|
||||
|
||||
@SerialName("server_id")
|
||||
val serverID: String? = null,
|
||||
|
||||
@SerialName("object_id")
|
||||
val objectID: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
val type: String? = null,
|
||||
val width: Long? = null,
|
||||
val height: Long? = null
|
||||
)
|
||||
|
|
@ -7,17 +7,17 @@ import kotlinx.serialization.Serializable
|
|||
data class Message(
|
||||
@SerialName("_id")
|
||||
val id: String? = null,
|
||||
|
||||
val nonce: String? = null,
|
||||
val channel: String? = null,
|
||||
val author: String? = null,
|
||||
val content: String? = null,
|
||||
val reactions: Map<String, List<String>>? = null,
|
||||
val replies: List<String>? = null,
|
||||
val attachments: List<Avatar>? = null,
|
||||
val attachments: List<AutumnResource>? = null,
|
||||
val edited: String? = null,
|
||||
val embeds: List<Embed>? = null,
|
||||
val mentions: List<String>? = null
|
||||
val mentions: List<String>? = null,
|
||||
val type: String? = null, // this is _only_ used for websocket events!
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package chat.revolt.api.schemas
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Server(
|
||||
@SerialName("_id")
|
||||
val id: String? = null,
|
||||
val owner: String? = null,
|
||||
val name: String? = null,
|
||||
val description: String? = null,
|
||||
val channels: List<String>? = null,
|
||||
val categories: List<Category>? = null,
|
||||
val systemMessages: SystemMessages? = null,
|
||||
val roles: Map<String, Role>? = null,
|
||||
val defaultPermissions: Long? = null,
|
||||
val icon: AutumnResource? = null,
|
||||
val banner: AutumnResource? = null,
|
||||
val flags: Long? = null,
|
||||
val analytics: Boolean? = null,
|
||||
val discoverable: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Category(
|
||||
val id: String? = null,
|
||||
val title: String? = null,
|
||||
val channels: List<String>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SystemMessages(
|
||||
val userJoined: String? = null,
|
||||
val userLeft: String? = null,
|
||||
val userKicked: String? = null,
|
||||
val userBanned: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Role(
|
||||
val name: String? = null,
|
||||
val permissions: DefaultPermissions? = null,
|
||||
val colour: String? = null,
|
||||
val hoist: Boolean? = null,
|
||||
val rank: Double? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DefaultPermissions(
|
||||
val a: Long? = null,
|
||||
val d: Long? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Emoji(
|
||||
@SerialName("_id")
|
||||
val id: String? = null,
|
||||
val parent: EmojiParent? = null,
|
||||
val creatorID: String? = null,
|
||||
val name: String? = null,
|
||||
val animated: Boolean? = null,
|
||||
val type: String? = null, // this is _only_ used for websocket events!
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EmojiParent(
|
||||
val type: String? = null,
|
||||
val id: String? = null
|
||||
)
|
||||
|
|
@ -6,12 +6,11 @@ import kotlinx.serialization.descriptors.*
|
|||
import kotlinx.serialization.encoding.*
|
||||
|
||||
@Serializable
|
||||
data class CompleteUser(
|
||||
data class User(
|
||||
@SerialName("_id")
|
||||
val id: String? = null,
|
||||
|
||||
val username: String? = null,
|
||||
val avatar: Avatar? = null,
|
||||
val avatar: AutumnResource? = null,
|
||||
val relations: List<Relation>? = null,
|
||||
val badges: Long? = null,
|
||||
val status: Status? = null,
|
||||
|
|
@ -23,42 +22,6 @@ data class CompleteUser(
|
|||
val online: Boolean? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Avatar(
|
||||
@SerialName("_id")
|
||||
val id: String? = null,
|
||||
|
||||
val tag: String? = null,
|
||||
val filename: String? = null,
|
||||
val metadata: Metadata? = null,
|
||||
|
||||
@SerialName("content_type")
|
||||
val contentType: String? = null,
|
||||
|
||||
val size: Long? = null,
|
||||
val deleted: Boolean? = null,
|
||||
val reported: Boolean? = null,
|
||||
|
||||
@SerialName("message_id")
|
||||
val messageID: String? = null,
|
||||
|
||||
@SerialName("user_id")
|
||||
val userID: String? = null,
|
||||
|
||||
@SerialName("server_id")
|
||||
val serverID: String? = null,
|
||||
|
||||
@SerialName("object_id")
|
||||
val objectID: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
val type: String? = null,
|
||||
val width: Long? = null,
|
||||
val height: Long? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Bot(
|
||||
val owner: String? = null
|
||||
|
|
@ -67,7 +30,7 @@ data class Bot(
|
|||
@Serializable
|
||||
data class Profile(
|
||||
val content: String? = null,
|
||||
val background: Avatar? = null
|
||||
val background: AutumnResource? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package chat.revolt.screens.chat
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material3.Button
|
||||
|
|
@ -55,7 +57,7 @@ class HomeScreenViewModel @Inject constructor(
|
|||
fun sendMessage() {
|
||||
viewModelScope.launch {
|
||||
chat.revolt.api.routes.channel.sendMessage(
|
||||
"01F7ZSBSFHCAAJQ92ZGTY67HMN",
|
||||
"01F7ZSBSFHCAAJQ92ZGTY67HMN", // revolt lounge #general (temporarily hardcoded) FIXME
|
||||
messageContent
|
||||
)
|
||||
}
|
||||
|
|
@ -103,6 +105,28 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
|
|||
}
|
||||
}
|
||||
|
||||
// a scrollable list of all users in user cache
|
||||
Text(
|
||||
text = "User cache",
|
||||
style = MaterialTheme.typography.displaySmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Left,
|
||||
fontSize = 24.sp
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 15.dp, vertical = 15.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.height(200.dp)
|
||||
) {
|
||||
RevoltAPI.userCache.forEach { (_, user) ->
|
||||
Text(text = user.username ?: user.id ?: "null")
|
||||
}
|
||||
}
|
||||
|
||||
Column() {
|
||||
FormTextField(
|
||||
value = viewModel.messageContent,
|
||||
|
|
|
|||
|
|
@ -62,9 +62,8 @@ class GreeterViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
RevoltAPI.initialize()
|
||||
|
||||
if (RevoltAPI.isLoggedIn()) {
|
||||
RevoltAPI.loginAs(token ?: "")
|
||||
_skipLogin = true
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +100,7 @@ fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = hi
|
|||
.height(60.dp)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.navigation.NavController
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.REVOLT_SUPPORT
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.routes.account.EmailPasswordAssessment
|
||||
import chat.revolt.api.routes.account.negotiateAuthentication
|
||||
import chat.revolt.api.routes.user.fetchSelfWithNewToken
|
||||
import chat.revolt.components.generic.AnyLink
|
||||
import chat.revolt.components.generic.FormTextField
|
||||
import chat.revolt.components.generic.Weblink
|
||||
|
|
@ -76,7 +76,7 @@ class LoginViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
try {
|
||||
fetchSelfWithNewToken(response.firstUserHints.token)
|
||||
RevoltAPI.loginAs(response.firstUserHints.token)
|
||||
kvStorage.set("sessionToken", response.firstUserHints.token)
|
||||
|
||||
_navigateTo = "home"
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavController
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.routes.account.MfaResponseRecoveryCode
|
||||
import chat.revolt.api.routes.account.MfaResponseTotpCode
|
||||
import chat.revolt.api.routes.account.authenticateWithMfaRecoveryCode
|
||||
import chat.revolt.api.routes.account.authenticateWithMfaTotpCode
|
||||
import chat.revolt.api.routes.user.fetchSelfWithNewToken
|
||||
import chat.revolt.components.generic.CollapsibleCard
|
||||
import chat.revolt.components.generic.FormTextField
|
||||
import chat.revolt.persistence.KVStorage
|
||||
|
|
@ -80,7 +80,7 @@ class MfaScreenViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
try {
|
||||
fetchSelfWithNewToken(response.firstUserHints.token)
|
||||
RevoltAPI.loginAs(response.firstUserHints.token)
|
||||
kvStorage.set("sessionToken", response.firstUserHints.token)
|
||||
|
||||
_navigateToHome = true
|
||||
|
|
@ -105,7 +105,7 @@ class MfaScreenViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
try {
|
||||
fetchSelfWithNewToken(response.firstUserHints.token)
|
||||
RevoltAPI.loginAs(response.firstUserHints.token)
|
||||
kvStorage.set("sessionToken", response.firstUserHints.token)
|
||||
|
||||
_navigateToHome = true
|
||||
|
|
|
|||
Loading…
Reference in New Issue