diff options
| author | Linnea Gräf <nea@nea.moe> | 2025-10-13 22:10:38 +0200 |
|---|---|---|
| committer | Linnea Gräf <nea@nea.moe> | 2025-10-13 22:10:38 +0200 |
| commit | 733f01be8c2ca986e594816e73cb89ee1c8d105d (patch) | |
| tree | 7709f194f714b0bcfdbab0c65ec5aa7b3fe49c14 | |
| parent | 05160314e6899ece75779dbd2e5b691ed581c2b9 (diff) | |
| download | Firmament-733f01be8c2ca986e594816e73cb89ee1c8d105d.tar.gz Firmament-733f01be8c2ca986e594816e73cb89ee1c8d105d.tar.bz2 Firmament-733f01be8c2ca986e594816e73cb89ee1c8d105d.zip | |
feat: remove ktor (for a smaller binary)
| -rw-r--r-- | build.gradle.kts | 8 | ||||
| -rw-r--r-- | gradle/libs.versions.toml | 3 | ||||
| -rw-r--r-- | src/main/kotlin/Firmament.kt | 29 | ||||
| -rw-r--r-- | src/main/kotlin/apis/Routes.kt | 121 | ||||
| -rw-r--r-- | src/main/kotlin/apis/UrsaManager.kt | 124 | ||||
| -rw-r--r-- | src/main/kotlin/commands/rome.kt | 4 | ||||
| -rw-r--r-- | src/main/kotlin/features/chat/ChatLinks.kt | 28 | ||||
| -rw-r--r-- | src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt | 8 | ||||
| -rw-r--r-- | src/main/kotlin/repo/HypixelStaticData.kt | 26 | ||||
| -rw-r--r-- | src/main/kotlin/repo/RepoDownloadManager.kt | 21 | ||||
| -rw-r--r-- | src/main/kotlin/util/Base64Util.kt | 20 | ||||
| -rw-r--r-- | src/main/kotlin/util/net/HttpUtil.kt | 85 |
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}") + ) + } +} |
