feat: rudimentary message fetching+sending functionality
currently only hardcoded IDs
This commit is contained in:
parent
e185350d4c
commit
7c86b07221
|
|
@ -0,0 +1,91 @@
|
|||
package chat.revolt.api.internals
|
||||
|
||||
import kotlin.experimental.and
|
||||
import kotlin.random.Random
|
||||
|
||||
object ULID {
|
||||
private const val entropy = 10
|
||||
private const val len = 26
|
||||
|
||||
private const val maxTimestamp = 281474976710655L
|
||||
private const val minTimestamp = 0L
|
||||
|
||||
private val b32chars = charArrayOf(
|
||||
0x30.toChar(), 0x31.toChar(), 0x32.toChar(), 0x33.toChar(), 0x34.toChar(), 0x35.toChar(),
|
||||
0x36.toChar(), 0x37.toChar(), 0x38.toChar(), 0x39.toChar(), 0x41.toChar(), 0x42.toChar(),
|
||||
0x43.toChar(), 0x44.toChar(), 0x45.toChar(), 0x46.toChar(), 0x47.toChar(), 0x48.toChar(),
|
||||
0x4a.toChar(), 0x4b.toChar(), 0x4d.toChar(), 0x4e.toChar(), 0x50.toChar(), 0x51.toChar(),
|
||||
0x52.toChar(), 0x53.toChar(), 0x54.toChar(), 0x56.toChar(), 0x57.toChar(), 0x58.toChar(),
|
||||
0x59.toChar(), 0x5a.toChar()
|
||||
)
|
||||
|
||||
fun makeSpecial(timestamp: Long, entropy: ByteArray): String {
|
||||
if (timestamp < minTimestamp || timestamp > maxTimestamp) {
|
||||
throw IllegalArgumentException("timestamp out of range: $timestamp")
|
||||
}
|
||||
|
||||
if (entropy.size != this.entropy) {
|
||||
throw IllegalArgumentException("entropy must be exactly ${this.entropy} bytes")
|
||||
}
|
||||
|
||||
val chars = CharArray(len)
|
||||
|
||||
// Time part (10 chars)
|
||||
chars[0] = b32chars[timestamp.ushr(45).toInt() and 0x1f]
|
||||
chars[1] = b32chars[timestamp.ushr(40).toInt() and 0x1f]
|
||||
chars[2] = b32chars[timestamp.ushr(35).toInt() and 0x1f]
|
||||
chars[3] = b32chars[timestamp.ushr(30).toInt() and 0x1f]
|
||||
chars[4] = b32chars[timestamp.ushr(25).toInt() and 0x1f]
|
||||
chars[5] = b32chars[timestamp.ushr(20).toInt() and 0x1f]
|
||||
chars[6] = b32chars[timestamp.ushr(15).toInt() and 0x1f]
|
||||
chars[7] = b32chars[timestamp.ushr(10).toInt() and 0x1f]
|
||||
chars[8] = b32chars[timestamp.ushr(5).toInt() and 0x1f]
|
||||
chars[9] = b32chars[timestamp.toInt() and 0x1f]
|
||||
|
||||
// Entropy part (16 chars)
|
||||
chars[10] = b32chars[(entropy[0].toShort() and 0xff).toInt().ushr(3)]
|
||||
chars[11] =
|
||||
b32chars[(entropy[0].toInt() shl 2 or (entropy[1].toShort() and 0xff).toInt()
|
||||
.ushr(6) and 0x1f)]
|
||||
chars[12] = b32chars[((entropy[1].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
|
||||
chars[13] =
|
||||
b32chars[(entropy[1].toInt() shl 4 or (entropy[2].toShort() and 0xff).toInt()
|
||||
.ushr(4) and 0x1f)]
|
||||
chars[14] =
|
||||
b32chars[(entropy[2].toInt() shl 5 or (entropy[3].toShort() and 0xff).toInt()
|
||||
.ushr(7) and 0x1f)]
|
||||
chars[15] = b32chars[((entropy[3].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
|
||||
chars[16] =
|
||||
b32chars[(entropy[3].toInt() shl 3 or (entropy[4].toShort() and 0xff).toInt()
|
||||
.ushr(5) and 0x1f)]
|
||||
chars[17] = b32chars[(entropy[4].toInt() and 0x1f)]
|
||||
chars[18] = b32chars[(entropy[5].toShort() and 0xff).toInt().ushr(3)]
|
||||
chars[19] =
|
||||
b32chars[(entropy[5].toInt() shl 2 or (entropy[6].toShort() and 0xff).toInt()
|
||||
.ushr(6) and 0x1f)]
|
||||
chars[20] = b32chars[((entropy[6].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
|
||||
chars[21] =
|
||||
b32chars[(entropy[6].toInt() shl 4 or (entropy[7].toShort() and 0xff).toInt()
|
||||
.ushr(4) and 0x1f)]
|
||||
chars[22] =
|
||||
b32chars[(entropy[7].toInt() shl 5 or (entropy[8].toShort() and 0xff).toInt()
|
||||
.ushr(7) and 0x1f)]
|
||||
chars[23] = b32chars[((entropy[8].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
|
||||
chars[24] =
|
||||
b32chars[(entropy[8].toInt() shl 3 or (entropy[9].toShort() and 0xff).toInt()
|
||||
.ushr(5) and 0x1f)]
|
||||
chars[25] = b32chars[(entropy[9].toInt() and 0x1f)]
|
||||
|
||||
return String(chars)
|
||||
}
|
||||
|
||||
private fun fetchEntropy(): ByteArray {
|
||||
val bytes = ByteArray(entropy)
|
||||
Random.nextBytes(bytes)
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun makeNext(): String {
|
||||
return makeSpecial(System.currentTimeMillis(), fetchEntropy())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package chat.revolt.api.routes.channel
|
||||
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import chat.revolt.api.internals.ULID
|
||||
import chat.revolt.api.schemas.Embed
|
||||
import chat.revolt.api.schemas.MessagesInChannel
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
|
||||
suspend fun fetchMessagesFromChannel(
|
||||
channelId: String,
|
||||
limit: Int = 50,
|
||||
include_users: Boolean = false,
|
||||
before: String? = null,
|
||||
after: String? = null,
|
||||
nearby: String? = null,
|
||||
sort: String? = null
|
||||
): MessagesInChannel {
|
||||
val response = RevoltHttp.get("/channels/$channelId/messages") {
|
||||
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||
|
||||
parameter("limit", limit)
|
||||
parameter("include_users", include_users)
|
||||
|
||||
if (before != null) parameter("before", before)
|
||||
if (after != null) parameter("after", after)
|
||||
if (nearby != null) parameter("nearby", nearby)
|
||||
if (sort != null) parameter("sort", sort)
|
||||
}
|
||||
.bodyAsText()
|
||||
|
||||
return RevoltJson.decodeFromString(
|
||||
MessagesInChannel.serializer(),
|
||||
response
|
||||
)
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class SendMessageReply(
|
||||
val id: String,
|
||||
val mention: Boolean
|
||||
)
|
||||
|
||||
suspend fun sendMessage(
|
||||
channelId: String,
|
||||
content: String,
|
||||
nonce: String? = ULID.makeNext(),
|
||||
replies: List<SendMessageReply>? = null,
|
||||
embed: Embed? = null
|
||||
): String {
|
||||
val response = RevoltHttp.post("/channels/$channelId/messages") {
|
||||
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(
|
||||
mapOf(
|
||||
"content" to content,
|
||||
"nonce" to nonce,
|
||||
"replies" to replies,
|
||||
"embed" to embed
|
||||
)
|
||||
)
|
||||
}
|
||||
.bodyAsText()
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package chat.revolt.api.schemas
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
@Serializable
|
||||
data class MessagesInChannel(
|
||||
val messages: List<Message>? = null,
|
||||
val users: List<CompleteUser>? = null,
|
||||
val members: List<Member>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Member(
|
||||
@SerialName("_id")
|
||||
val id: String? = null,
|
||||
|
||||
@SerialName("joined_at")
|
||||
val joinedAt: String? = null,
|
||||
|
||||
val avatar: Avatar? = null,
|
||||
val roles: List<String>? = null,
|
||||
val nickname: String? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package chat.revolt.api.schemas
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@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 edited: String? = null,
|
||||
val embeds: List<Embed>? = null,
|
||||
val mentions: List<String>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Embed(
|
||||
val type: String? = null,
|
||||
val url: String? = null,
|
||||
|
||||
@SerialName("original_url")
|
||||
val originalURL: String? = null,
|
||||
|
||||
val special: Special? = null,
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
val image: Image? = null,
|
||||
|
||||
@SerialName("icon_url")
|
||||
val iconURL: String? = null,
|
||||
|
||||
@SerialName("site_name")
|
||||
val siteName: String? = null,
|
||||
|
||||
val colour: String? = null,
|
||||
val width: Long? = null,
|
||||
val height: Long? = null,
|
||||
val size: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Image(
|
||||
val url: String? = null,
|
||||
val width: Long? = null,
|
||||
val height: Long? = null,
|
||||
val size: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Special(
|
||||
val type: String? = null
|
||||
)
|
||||
|
|
@ -54,7 +54,9 @@ data class Avatar(
|
|||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
val type: String? = null
|
||||
val type: String? = null,
|
||||
val width: Long? = null,
|
||||
val height: Long? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.navigation.NavController
|
||||
import chat.revolt.api.REVOLT_FILES
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.components.generic.CollapsibleCard
|
||||
import chat.revolt.components.generic.RemoteImage
|
||||
import chat.revolt.persistence.KVStorage
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -34,6 +35,15 @@ class HomeScreenViewModel @Inject constructor(
|
|||
RevoltAPI.logout()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage() {
|
||||
runBlocking {
|
||||
chat.revolt.api.routes.channel.sendMessage(
|
||||
"01FD4WDPDDPH5523ASF50ADSSN",
|
||||
"this is technically the first message sent from revolt android"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -76,6 +86,12 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleCard(title = "Send a message") {
|
||||
Button(onClick = { viewModel.sendMessage() }) {
|
||||
Text(text = "Send")
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue