aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-10-13 22:10:38 +0200
committerLinnea Gräf <nea@nea.moe>2025-10-13 22:10:38 +0200
commit733f01be8c2ca986e594816e73cb89ee1c8d105d (patch)
tree7709f194f714b0bcfdbab0c65ec5aa7b3fe49c14
parent05160314e6899ece75779dbd2e5b691ed581c2b9 (diff)
downloadFirmament-733f01be8c2ca986e594816e73cb89ee1c8d105d.tar.gz
Firmament-733f01be8c2ca986e594816e73cb89ee1c8d105d.tar.bz2
Firmament-733f01be8c2ca986e594816e73cb89ee1c8d105d.zip
feat: remove ktor (for a smaller binary)
-rw-r--r--build.gradle.kts8
-rw-r--r--gradle/libs.versions.toml3
-rw-r--r--src/main/kotlin/Firmament.kt29
-rw-r--r--src/main/kotlin/apis/Routes.kt121
-rw-r--r--src/main/kotlin/apis/UrsaManager.kt124
-rw-r--r--src/main/kotlin/commands/rome.kt4
-rw-r--r--src/main/kotlin/features/chat/ChatLinks.kt28
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt8
-rw-r--r--src/main/kotlin/repo/HypixelStaticData.kt26
-rw-r--r--src/main/kotlin/repo/RepoDownloadManager.kt21
-rw-r--r--src/main/kotlin/util/Base64Util.kt20
-rw-r--r--src/main/kotlin/util/net/HttpUtil.kt85
12 files changed, 246 insertions, 231 deletions
diff --git a/build.gradle.kts b/build.gradle.kts
index 222972a..8e44eea 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -269,14 +269,6 @@ dependencies {
(reiSourceSet.modRuntimeOnlyConfigurationName)(reiDeps.fabric)
nonModImplentation(libs.repoparser)
shadowMe(libs.repoparser)
- fun ktor(mod: String) = "io.ktor:ktor-$mod-jvm:${libs.versions.ktor.get()}"
- // TODO: get rid of ktor. lowkey ballooning file size and like not neccessary at all for what i am doing.0
- transInclude(nonModImplentation(ktor("client-core"))!!)
- transInclude(nonModImplentation(ktor("client-java"))!!)
- transInclude(nonModImplentation(ktor("serialization-kotlinx-json"))!!)
- transInclude(nonModImplentation(ktor("client-content-negotiation"))!!)
- transInclude(nonModImplentation(ktor("client-encoding"))!!)
- transInclude(nonModImplentation(ktor("client-logging"))!!)
// Dev environment preinstalled mods
modLocalRuntime(libs.bundles.runtime.required)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a37c54c..2465f3e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -56,9 +56,6 @@ jade = "19.0.4+fabric"
devauth = "1.2.1"
-# Update from https://ktor.io/docs/
-ktor = "3.2.2"
-
# Update from https://repo.nea.moe/#/releases/moe/nea/neurepoparser
neurepoparser = "1.8.0"
diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt
index 8e31848..e707667 100644
--- a/src/main/kotlin/Firmament.kt
+++ b/src/main/kotlin/Firmament.kt
@@ -2,14 +2,6 @@ package moe.nea.firmament
import com.google.gson.Gson
import com.mojang.brigadier.CommandDispatcher
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.UserAgent
-import io.ktor.client.plugins.cache.HttpCache
-import io.ktor.client.plugins.compression.ContentEncoding
-import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
-import io.ktor.client.plugins.logging.LogLevel
-import io.ktor.client.plugins.logging.Logging
-import io.ktor.serialization.kotlinx.json.json
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
@@ -94,27 +86,6 @@ object Firmament {
explicitNulls = false
}
-
- val httpClient by lazy {
- HttpClient {
- install(ContentNegotiation) {
- json(json)
- }
- install(ContentEncoding) {
- gzip()
- deflate()
- }
- install(UserAgent) {
- agent = "Firmament/$version"
- }
- if (DEBUG)
- install(Logging) {
- level = LogLevel.INFO
- }
- install(HttpCache)
- }
- }
-
val globalJob = Job()
val coroutineScope =
CoroutineScope(EmptyCoroutineContext + CoroutineName("Firmament")) + SupervisorJob(globalJob)
diff --git a/src/main/kotlin/apis/Routes.kt b/src/main/kotlin/apis/Routes.kt
index 737763d..839de22 100644
--- a/src/main/kotlin/apis/Routes.kt
+++ b/src/main/kotlin/apis/Routes.kt
@@ -1,91 +1,54 @@
-
-
package moe.nea.firmament.apis
-import io.ktor.client.call.body
-import io.ktor.client.request.get
-import io.ktor.http.isSuccess
-import io.ktor.util.CaseInsensitiveMap
import java.util.UUID
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
+import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MinecraftDispatcher
+import moe.nea.firmament.util.net.HttpUtil
object Routes {
- private val nameToUUID: MutableMap<String, Deferred<UUID?>> = CaseInsensitiveMap()
- private val profiles: MutableMap<UUID, Deferred<Profiles?>> = mutableMapOf()
- private val accounts: MutableMap<UUID, Deferred<PlayerData?>> = mutableMapOf()
- private val UUIDToName: MutableMap<UUID, Deferred<String?>> = mutableMapOf()
-
- suspend fun getPlayerNameForUUID(uuid: UUID): String? {
- return withContext(MinecraftDispatcher) {
- UUIDToName.computeIfAbsent(uuid) {
- async(Firmament.coroutineScope.coroutineContext) {
- val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$uuid")
- if (!response.status.isSuccess()) return@async null
- val data = response.body<MowojangNameLookup>()
- launch(MinecraftDispatcher) {
- nameToUUID[data.name] = async { data.id }
- }
- data.name
- }
- }
- }.await()
- }
-
- suspend fun getUUIDForPlayerName(name: String): UUID? {
- return withContext(MinecraftDispatcher) {
- nameToUUID.computeIfAbsent(name) {
- async(Firmament.coroutineScope.coroutineContext) {
- val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$name")
- if (!response.status.isSuccess()) return@async null
- val data = response.body<MowojangNameLookup>()
- launch(MinecraftDispatcher) {
- UUIDToName[data.id] = async { data.name }
- }
- data.id
- }
- }
- }.await()
- }
-
- suspend fun getAccountData(uuid: UUID): PlayerData? {
- return withContext(MinecraftDispatcher) {
- accounts.computeIfAbsent(uuid) {
- async(Firmament.coroutineScope.coroutineContext) {
- val response = UrsaManager.request(listOf("v1", "hypixel","player", uuid.toString()))
- if (!response.status.isSuccess()) {
- launch(MinecraftDispatcher) {
- @Suppress("DeferredResultUnused")
- accounts.remove(uuid)
- }
- return@async null
- }
- response.body<PlayerResponse>().player
- }
- }
- }.await()
- }
-
- suspend fun getProfiles(uuid: UUID): Profiles? {
- return withContext(MinecraftDispatcher) {
- profiles.computeIfAbsent(uuid) {
- async(Firmament.coroutineScope.coroutineContext) {
- val response = UrsaManager.request(listOf("v1", "hypixel","profiles", uuid.toString()))
- if (!response.status.isSuccess()) {
- launch(MinecraftDispatcher) {
- @Suppress("DeferredResultUnused")
- profiles.remove(uuid)
- }
- return@async null
- }
- response.body<Profiles>()
- }
- }
- }.await()
- }
-
+ private val nameToUUID: MutableMap<String, Deferred<UUID?>> = mutableMapOf()
+ private val UUIDToName: MutableMap<UUID, Deferred<String?>> = mutableMapOf()
+
+ suspend fun getPlayerNameForUUID(uuid: UUID): String? {
+ return withContext(MinecraftDispatcher) {
+ UUIDToName.computeIfAbsent(uuid) {
+ async(Firmament.coroutineScope.coroutineContext) {
+ val data = ErrorUtil.catch("could not get name for uuid $uuid") {
+ HttpUtil.request("https://mowojang.matdoes.dev/$uuid")
+ .forJson<MowojangNameLookup>()
+ .await()
+ }.orNull() ?: return@async null
+ launch(MinecraftDispatcher) {
+ nameToUUID[data.name] = async { data.id }
+ }
+ data.name
+ }
+ }
+ }.await()
+ }
+
+ suspend fun getUUIDForPlayerName(name: String): UUID? {
+ return withContext(MinecraftDispatcher) {
+ nameToUUID.computeIfAbsent(name) {
+ async(Firmament.coroutineScope.coroutineContext) {
+ val data =
+ ErrorUtil.catch("could not get uuid for name $name") {
+ HttpUtil.request("https://mowojang.matdoes.dev/$name")
+ .forJson<MowojangNameLookup>()
+ .await()
+ }.orNull() ?: return@async null
+ launch(MinecraftDispatcher) {
+ UUIDToName[data.id] = async { data.name }
+ }
+ data.id
+ }
+ }
+ }.await()
+ }
}
diff --git a/src/main/kotlin/apis/UrsaManager.kt b/src/main/kotlin/apis/UrsaManager.kt
index 19e030c..cee6904 100644
--- a/src/main/kotlin/apis/UrsaManager.kt
+++ b/src/main/kotlin/apis/UrsaManager.kt
@@ -1,74 +1,80 @@
-
-
package moe.nea.firmament.apis
-import io.ktor.client.request.get
-import io.ktor.client.request.header
-import io.ktor.client.statement.HttpResponse
-import io.ktor.client.statement.bodyAsText
-import io.ktor.http.appendPathSegments
+import java.net.URI
+import java.net.http.HttpResponse
import java.time.Duration
import java.time.Instant
+import java.util.OptionalLong
import java.util.UUID
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.future.await
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
+import kotlinx.serialization.DeserializationStrategy
+import kotlin.jvm.optionals.getOrNull
import net.minecraft.client.MinecraftClient
import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.net.HttpUtil
object UrsaManager {
- private data class Token(
- val validUntil: Instant,
- val token: String,
- val obtainedFrom: String,
- ) {
- fun isValid(host: String) = Instant.now().plusSeconds(60) < validUntil && obtainedFrom == host
- }
+ private data class Token(
+ val validUntil: Instant,
+ val token: String,
+ val obtainedFrom: String,
+ ) {
+ fun isValid(host: String) = Instant.now().plusSeconds(60) < validUntil && obtainedFrom == host
+ }
- private var currentToken: Token? = null
- private val lock = Mutex()
- private fun getToken(host: String) = currentToken?.takeIf { it.isValid(host) }
+ private var currentToken: Token? = null
+ private val lock = Mutex()
+ private fun getToken(host: String) = currentToken?.takeIf { it.isValid(host) }
+
+ suspend fun <T> request(path: List<String>, bodyHandler: HttpResponse.BodyHandler<T>): T {
+ var didLock = false
+ try {
+ val host = "ursa.notenoughupdates.org"
+ var token = getToken(host)
+ if (token == null) {
+ lock.lock()
+ didLock = true
+ token = getToken(host)
+ }
+ var url = URI.create("https://$host")
+ for (segment in path) {
+ url = url.resolve(segment)
+ }
+ val request = HttpUtil.request(url)
+ if (token == null) {
+ withContext(Dispatchers.IO) {
+ val mc = MinecraftClient.getInstance()
+ val serverId = UUID.randomUUID().toString()
+ mc.sessionService.joinServer(mc.session.uuidOrNull, mc.session.accessToken, serverId)
+ request.header("x-ursa-username", mc.session.username)
+ request.header("x-ursa-serverid", serverId)
+ }
+ } else {
+ request.header("x-ursa-token", token.token)
+ }
+ val response = request.execute(bodyHandler)
+ .await()
+ val savedToken = response.headers().firstValue("x-ursa-token").getOrNull()
+ if (savedToken != null) {
+ val validUntil = response.headers().firstValueAsLong("x-ursa-expires").orNull()?.let { Instant.ofEpochMilli(it) }
+ ?: (Instant.now() + Duration.ofMinutes(55))
+ currentToken = Token(validUntil, savedToken, host)
+ }
+ if (response.statusCode() != 200) {
+ Firmament.logger.error("Failed to contact ursa minor: ${response.statusCode()}")
+ }
+ return response.body()
+ } finally {
+ if (didLock)
+ lock.unlock()
+ }
+ }
+}
- suspend fun request(path: List<String>): HttpResponse {
- var didLock = false
- try {
- val host = "ursa.notenoughupdates.org"
- var token = getToken(host)
- if (token == null) {
- lock.lock()
- didLock = true
- token = getToken(host)
- }
- val response = Firmament.httpClient.get {
- url {
- this.host = host
- appendPathSegments(path, encodeSlash = true)
- }
- if (token == null) {
- withContext(Dispatchers.IO) {
- val mc = MinecraftClient.getInstance()
- val serverId = UUID.randomUUID().toString()
- mc.sessionService.joinServer(mc.session.uuidOrNull, mc.session.accessToken, serverId)
- header("x-ursa-username", mc.session.username)
- header("x-ursa-serverid", serverId)
- }
- } else {
- header("x-ursa-token", token.token)
- }
- }
- val savedToken = response.headers["x-ursa-token"]
- if (savedToken != null) {
- val validUntil = response.headers["x-ursa-expires"]?.toLongOrNull()?.let { Instant.ofEpochMilli(it) }
- ?: (Instant.now() + Duration.ofMinutes(55))
- currentToken = Token(validUntil, savedToken, host)
- }
- if (response.status.value != 200) {
- Firmament.logger.error("Failed to contact ursa minor: ${response.bodyAsText()}")
- }
- return response
- } finally {
- if (didLock)
- lock.unlock()
- }
- }
+private fun OptionalLong.orNull(): Long? {
+ if (this.isPresent)return null
+ return this.asLong
}
diff --git a/src/main/kotlin/commands/rome.kt b/src/main/kotlin/commands/rome.kt
index 97acf73..e22ca21 100644
--- a/src/main/kotlin/commands/rome.kt
+++ b/src/main/kotlin/commands/rome.kt
@@ -3,7 +3,7 @@ package moe.nea.firmament.commands
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType.string
-import io.ktor.client.statement.bodyAsText
+import java.net.http.HttpResponse
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import kotlinx.coroutines.launch
import net.minecraft.command.CommandRegistryAccess
@@ -302,7 +302,7 @@ fun firmamentCommand(ctx: CommandRegistryAccess) = literal("firmament") {
thenExecute {
Firmament.coroutineScope.launch {
source.sendFeedback(Text.translatable("firmament.ursa.debugrequest.start"))
- val text = UrsaManager.request(get(path).split("/")).bodyAsText()
+ val text = UrsaManager.request(get(path).split("/"), HttpResponse.BodyHandlers.ofString())
source.sendFeedback(Text.stringifiedTranslatable("firmament.ursa.debugrequest.result", text))
}
}
diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt
index b05a3a0..76eb48d 100644
--- a/src/main/kotlin/features/chat/ChatLinks.kt
+++ b/src/main/kotlin/features/chat/ChatLinks.kt
@@ -1,16 +1,13 @@
package moe.nea.firmament.features.chat
-import io.ktor.client.request.get
-import io.ktor.client.statement.bodyAsChannel
-import io.ktor.utils.io.jvm.javaio.toInputStream
import java.net.URI
-import java.net.URL
import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger
import org.joml.Vector2i
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
+import kotlinx.coroutines.future.await
import kotlin.math.min
import net.minecraft.client.gui.screen.ChatScreen
import net.minecraft.client.texture.NativeImage
@@ -29,6 +26,7 @@ import moe.nea.firmament.jarvis.JarvisIntegration
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.data.Config
import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.net.HttpUtil
import moe.nea.firmament.util.render.drawTexture
import moe.nea.firmament.util.transformEachRecursively
import moe.nea.firmament.util.unformattedString
@@ -73,18 +71,16 @@ object ChatLinks {
}
imageCache[url] = Firmament.coroutineScope.async {
try {
- val response = Firmament.httpClient.get(URI.create(url).toURL())
- if (response.status.value == 200) {
- val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob)
- val image = NativeImage.read(inputStream)
- val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}")
- MC.textureManager.registerTexture(
- texId,
- NativeImageBackedTexture({ texId.path }, image)
- )
- Image(texId, image.width, image.height)
- } else
- null
+ val inputStream = HttpUtil.request(url)
+ .forInputStream()
+ .await()
+ val image = NativeImage.read(inputStream)
+ val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}")
+ MC.textureManager.registerTexture(
+ texId,
+ NativeImageBackedTexture({ texId.path }, image)
+ )
+ Image(texId, image.width, image.height)
} catch (exc: Exception) {
exc.printStackTrace()
null
diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
index 4e40cf1..fddc189 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
@@ -1,9 +1,8 @@
package moe.nea.firmament.features.inventory.storageoverlay
-import io.ktor.util.decodeBase64Bytes
-import io.ktor.util.encodeBase64
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
+import java.util.Base64
import java.util.concurrent.CompletableFuture
import kotlinx.coroutines.async
import kotlinx.serialization.KSerializer
@@ -22,6 +21,7 @@ import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtSizeTracker
import moe.nea.firmament.Firmament
import moe.nea.firmament.features.inventory.storageoverlay.VirtualInventory.Serializer.writeToByteArray
+import moe.nea.firmament.util.Base64Util
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.mc.TolerantRegistriesOps
@@ -68,7 +68,7 @@ data class VirtualInventory(
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
- val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
+ val n = NbtIo.readCompressed(ByteArrayInputStream(Base64Util.decodeBytes(s)), NbtSizeTracker.of(100_000_000))
val items = n.getList(INVENTORY).getOrNull()
val ops = getOps()
return VirtualInventory(items?.map {
@@ -83,7 +83,7 @@ data class VirtualInventory(
fun getOps() = MC.currentOrDefaultRegistryNbtOps
override fun serialize(encoder: Encoder, value: VirtualInventory) {
- encoder.encodeString(value.serializationCache.get().encodeBase64())
+ encoder.encodeString(Base64Util.encodeToString(value.serializationCache.get()))
}
}
}
diff --git a/src/main/kotlin/repo/HypixelStaticData.kt b/src/main/kotlin/repo/HypixelStaticData.kt
index b0ada77..ed37054 100644
--- a/src/main/kotlin/repo/HypixelStaticData.kt
+++ b/src/main/kotlin/repo/HypixelStaticData.kt
@@ -1,11 +1,10 @@
package moe.nea.firmament.repo
-import io.ktor.client.call.body
-import io.ktor.client.request.get
import org.apache.logging.log4j.LogManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
+import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -14,6 +13,7 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.apis.CollectionResponse
import moe.nea.firmament.apis.CollectionSkillData
import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.net.HttpUtil
object HypixelStaticData {
private val logger = LogManager.getLogger("Firmament.HypixelStaticData")
@@ -91,18 +91,19 @@ object HypixelStaticData {
}
private suspend fun fetchPricesFromMoulberry() {
- lowestBin = Firmament.httpClient.get("$moulberryBaseUrl/lowestbin.json")
- .body<Map<SkyblockId, Double>>()
- avg1dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/1day.json")
- .body<Map<SkyblockId, Double>>()
- avg3dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/3day.json")
- .body<Map<SkyblockId, Double>>()
- avg7dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/7day.json")
- .body<Map<SkyblockId, Double>>()
+ lowestBin = HttpUtil.request("$moulberryBaseUrl/lowestbin.json")
+ .forJson<Map<SkyblockId, Double>>().await()
+ avg1dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/1day.json")
+ .forJson<Map<SkyblockId, Double>>().await()
+ avg3dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/3day.json")
+ .forJson<Map<SkyblockId, Double>>().await()
+ avg7dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/7day.json")
+ .forJson<Map<SkyblockId, Double>>().await()
}
private suspend fun fetchBazaarPrices() {
- val response = Firmament.httpClient.get("$hypixelApiBaseUrl/skyblock/bazaar").body<BazaarResponse>()
+ val response = HttpUtil.request("$hypixelApiBaseUrl/skyblock/bazaar").forJson<BazaarResponse>()
+ .await()
if (!response.success) {
logger.warn("Retrieved unsuccessful bazaar data")
}
@@ -111,7 +112,8 @@ object HypixelStaticData {
private suspend fun updateCollectionData() {
val response =
- Firmament.httpClient.get("$hypixelApiBaseUrl/resources/skyblock/collections").body<CollectionResponse>()
+ HttpUtil.request("$hypixelApiBaseUrl/resources/skyblock/collections").forJson<CollectionResponse>()
+ .await()
if (!response.success) {
logger.warn("Retrieved unsuccessful collection data")
}
diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt
index 150a9ca..36ded5c 100644
--- a/src/main/kotlin/repo/RepoDownloadManager.kt
+++ b/src/main/kotlin/repo/RepoDownloadManager.kt
@@ -1,9 +1,5 @@
package moe.nea.firmament.repo
-import io.ktor.client.call.body
-import io.ktor.client.request.get
-import io.ktor.client.statement.bodyAsChannel
-import io.ktor.utils.io.copyTo
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
@@ -11,6 +7,7 @@ import java.nio.file.StandardOpenOption
import java.util.zip.ZipInputStream
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlin.io.path.createDirectories
@@ -23,6 +20,7 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.Firmament.logger
import moe.nea.firmament.repo.RepoDownloadManager.latestSavedVersionHash
import moe.nea.firmament.util.iterate
+import moe.nea.firmament.util.net.HttpUtil
object RepoDownloadManager {
@@ -59,18 +57,17 @@ object RepoDownloadManager {
RepoManager.TConfig.branch = "master"
}
val response =
- Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.TConfig.username}/${RepoManager.TConfig.reponame}/commits/${branchOverride ?: RepoManager.TConfig.branch}")
- if (response.status.value != 200) {
- return null
- }
- return response.body<GithubCommitsResponse>().sha
+ HttpUtil.request("https://api.github.com/repos/${RepoManager.TConfig.username}/${RepoManager.TConfig.reponame}/commits/${branchOverride ?: RepoManager.TConfig.branch}")
+ .forJson<GithubCommitsResponse>()
+ .await()
+ return response.sha
}
private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) {
- val response = Firmament.httpClient.get(url)
+ val response = HttpUtil.request(url)
val targetFile = Files.createTempFile("firmament-repo", ".zip")
- val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
- response.bodyAsChannel().copyTo(outputChannel)
+ val outputChannel = Files.newOutputStream(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
+ response.forInputStream().await().copyTo(outputChannel)
targetFile
}
diff --git a/src/main/kotlin/util/Base64Util.kt b/src/main/kotlin/util/Base64Util.kt
index c39c601..0b7b3ea 100644
--- a/src/main/kotlin/util/Base64Util.kt
+++ b/src/main/kotlin/util/Base64Util.kt
@@ -1,17 +1,23 @@
-
package moe.nea.firmament.util
import java.util.Base64
object Base64Util {
fun decodeString(str: String): String {
+ return decodeBytes(str).decodeToString()
+ }
+
+ fun decodeBytes(str: String): ByteArray {
return Base64.getDecoder().decode(str.padToValidBase64())
- .decodeToString()
}
- fun String.padToValidBase64(): String {
- val align = this.length % 4
- if (align == 0) return this
- return this + "=".repeat(4 - align)
- }
+ fun String.padToValidBase64(): String {
+ val align = this.length % 4
+ if (align == 0) return this
+ return this + "=".repeat(4 - align)
+ }
+
+ fun encodeToString(bytes: ByteArray): String {
+ return Base64.getEncoder().encodeToString(bytes)
+ }
}
diff --git a/src/main/kotlin/util/net/HttpUtil.kt b/src/main/kotlin/util/net/HttpUtil.kt
new file mode 100644
index 0000000..50e0644
--- /dev/null
+++ b/src/main/kotlin/util/net/HttpUtil.kt
@@ -0,0 +1,85 @@
+package moe.nea.firmament.util.net
+
+import java.io.InputStream
+import java.net.URI
+import java.net.URL
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+import java.nio.ByteBuffer
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionStage
+import java.util.concurrent.Flow
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.json.decodeFromStream
+import kotlinx.serialization.serializer
+import moe.nea.firmament.Firmament
+
+object HttpUtil {
+ val httpClient = HttpClient.newBuilder()
+ .build()
+
+ data class Request(val request: HttpRequest.Builder) {
+ fun <T> execute(bodyHandler: HttpResponse.BodyHandler<T>): CompletableFuture<HttpResponse<T>> {
+ return httpClient.sendAsync(request.build(), bodyHandler)
+ }
+
+ fun <T> forBody(bodyHandler: HttpResponse.BodyHandler<T>): CompletableFuture<T> {
+ return execute(bodyHandler).thenApply { it.body() }
+ }
+
+ fun forInputStream(): CompletableFuture<InputStream> {
+ return forBody(HttpResponse.BodyHandlers.ofInputStream())
+ }
+
+ inline fun <reified T> forJson(): CompletableFuture<T> {
+ return forJson(serializer())
+ }
+
+ fun <T> forJson(serializer: DeserializationStrategy<T>): CompletableFuture<T> {
+ return forBody(jsonBodyHandler(serializer))
+ }
+
+ fun header(key: String, value: String) {
+ request.header(key, value)
+ }
+ }
+
+ fun <T> jsonBodyHandler(serializer: DeserializationStrategy<T>): HttpResponse.BodyHandler<T> {
+ val inp = HttpResponse.BodyHandlers.ofInputStream()
+ return HttpResponse.BodyHandler {
+ val subscriber = inp.apply(it)
+ object : HttpResponse.BodySubscriber<T> {
+ override fun getBody(): CompletionStage<T> {
+ return subscriber.body.thenApply { Firmament.json.decodeFromStream(serializer, it) }
+ }
+
+ override fun onSubscribe(subscription: Flow.Subscription?) {
+ subscriber.onSubscribe(subscription)
+ }
+
+ override fun onNext(item: List<ByteBuffer?>?) {
+ subscriber.onNext(item)
+ }
+
+ override fun onError(throwable: Throwable?) {
+ subscriber.onError(throwable)
+ }
+
+ override fun onComplete() {
+ subscriber.onComplete()
+ }
+ }
+ }
+ }
+
+ fun request(url: String): Request = request(URI.create(url))
+ fun request(url: URL): Request = request(url.toURI())
+ fun request(url: URI): Request {
+ return Request(
+ HttpRequest.newBuilder(url)
+ .GET()
+ .header("user-agent", "Firmament/${Firmament.version}")
+ )
+ }
+}