feat: rudimentary message fetching+sending functionality

currently only hardcoded IDs
This commit is contained in:
Infi 2022-12-12 23:49:53 +01:00
parent e185350d4c
commit 7c86b07221
6 changed files with 265 additions and 1 deletions

View File

@ -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())
}
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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

View File

@ -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 = {