diff --git a/app/src/main/java/chat/revolt/api/internals/ULID.kt b/app/src/main/java/chat/revolt/api/internals/ULID.kt new file mode 100644 index 00000000..a29e5f28 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/internals/ULID.kt @@ -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()) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/routes/channel/Channel.kt b/app/src/main/java/chat/revolt/api/routes/channel/Channel.kt new file mode 100644 index 00000000..549c1170 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/routes/channel/Channel.kt @@ -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? = 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 +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Channel.kt b/app/src/main/java/chat/revolt/api/schemas/Channel.kt new file mode 100644 index 00000000..8ff04a90 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/schemas/Channel.kt @@ -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? = null, + val users: List? = null, + val members: List? = 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? = null, + val nickname: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Messages.kt b/app/src/main/java/chat/revolt/api/schemas/Messages.kt new file mode 100644 index 00000000..7b2ac396 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/schemas/Messages.kt @@ -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>? = null, + val replies: List? = null, + val attachments: List? = null, + val edited: String? = null, + val embeds: List? = null, + val mentions: List? = 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 +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/User.kt b/app/src/main/java/chat/revolt/api/schemas/User.kt index a42cdeb7..4b2a492a 100644 --- a/app/src/main/java/chat/revolt/api/schemas/User.kt +++ b/app/src/main/java/chat/revolt/api/schemas/User.kt @@ -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 diff --git a/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt b/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt index 8dbed927..fe7915e1 100644 --- a/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt @@ -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 = {