From 3421cbee1cab7cac463d48795e6737810c60a514 Mon Sep 17 00:00:00 2001 From: Bernardo Antunes Date: Mon, 31 Jul 2023 00:44:04 +0100 Subject: [PATCH] Implement straightforward cf parsing. --- .../fetching/CurseforgeDataFetcher.kt | 79 +++++++++++++++++++ .../fetching/ModrinthDataFetcher.kt | 4 +- .../parsing/CurseforgeDataParser.kt | 27 +++++-- .../kotlin/dev/bernasss12/plugins/Routing.kt | 13 +++ src/main/kotlin/dev/bernasss12/util/Common.kt | 10 +++ 5 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/dev/bernasss12/fetching/CurseforgeDataFetcher.kt diff --git a/src/main/kotlin/dev/bernasss12/fetching/CurseforgeDataFetcher.kt b/src/main/kotlin/dev/bernasss12/fetching/CurseforgeDataFetcher.kt new file mode 100644 index 0000000..db7b010 --- /dev/null +++ b/src/main/kotlin/dev/bernasss12/fetching/CurseforgeDataFetcher.kt @@ -0,0 +1,79 @@ +package dev.bernasss12.fetching + +import dev.bernasss12.util.Common +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.java.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.server.plugins.* +import io.ktor.util.logging.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlin.time.Duration.Companion.minutes + +object CurseforgeDataFetcher : DataFetcher { + + private const val CURSEFORGE_URL = "https://api.curseforge.com/v1/mods/" + private val logger = KtorSimpleLogger(javaClass.canonicalName) + private val mutex = Mutex() + private var _key = Common.getCurseforgeSecret(logger) + private val apiKey: String? + get() { + if (_key == null) { + // Tries to get the token everytime if it was not previously gotten. + _key = Common.getCurseforgeSecret(logger) + } + return _key + } + private val httpClient = HttpClient(Java) { + install(UserAgent) { + agent = Common.getUserAgent(logger) + } + } + + private val cache: MutableMap> = hashMapOf() + private val cacheTimeout = 5.minutes + + override suspend fun requestModInfo(modid: String): JsonObject { + val key = apiKey + if (key == null) { + logger.error("No curseforge api key found, this won't do.") + error("No curseforge api key found, this won't do.") + } else { + mutex.withLock { + val id: UInt = modid.toUIntOrNull() ?: throw BadRequestException("modid parameter does not match regular expression for mod ids") + + val cacheEntry = cache[id] + if (cacheEntry != null && System.currentTimeMillis() <= cacheEntry.first + cacheTimeout.inWholeMilliseconds) { + return cacheEntry.second + } + + val requestUrl = "$CURSEFORGE_URL$id" + val response = httpClient.get(requestUrl) { + headers { + append(HttpHeaders.Accept, ContentType.Application.Json) + append("x-api-key", key) + } + } + if (response.status == HttpStatusCode.OK) { + val currentTime = System.currentTimeMillis() + return Json.decodeFromString(response.body() as String).also { jsonObject -> + cache[id] = currentTime to jsonObject + } + } else { + val body: String = try { + response.body() + } catch (err: RuntimeException) { + "unable to get response body: ${err.javaClass.name}" + } + logger.error("Requesting $requestUrl returned bad status ${response.status}:\n$body") + throw RuntimeException("Curseforge API request did not succeed") + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/bernasss12/fetching/ModrinthDataFetcher.kt b/src/main/kotlin/dev/bernasss12/fetching/ModrinthDataFetcher.kt index 093994f..ce02c00 100644 --- a/src/main/kotlin/dev/bernasss12/fetching/ModrinthDataFetcher.kt +++ b/src/main/kotlin/dev/bernasss12/fetching/ModrinthDataFetcher.kt @@ -22,7 +22,7 @@ object ModrinthDataFetcher : DataFetcher { const val MOD_ID_REGEX = "[\\w!@\$()`.+,\"\\-']{3,64}" - private const val modrinthUrl = "https://api.modrinth.com/v2/project/" + private const val MODRINTH_URL = "https://api.modrinth.com/v2/project/" private val modIdRegex = MOD_ID_REGEX.toRegex() private val logger = KtorSimpleLogger(javaClass.canonicalName) private val mutex = Mutex() @@ -50,7 +50,7 @@ object ModrinthDataFetcher : DataFetcher { if (remainingRequests == 0) { delay((resetAt - System.currentTimeMillis()).milliseconds) } - val requestUrl = "$modrinthUrl$modid" + val requestUrl = "$MODRINTH_URL$modid" val response = httpClient.get(requestUrl) { headers { append(HttpHeaders.Accept, ContentType.Application.Json) diff --git a/src/main/kotlin/dev/bernasss12/parsing/CurseforgeDataParser.kt b/src/main/kotlin/dev/bernasss12/parsing/CurseforgeDataParser.kt index ff5e32f..8a3b205 100644 --- a/src/main/kotlin/dev/bernasss12/parsing/CurseforgeDataParser.kt +++ b/src/main/kotlin/dev/bernasss12/parsing/CurseforgeDataParser.kt @@ -1,25 +1,40 @@ package dev.bernasss12.parsing +import dev.bernasss12.fetching.CurseforgeDataFetcher +import io.ktor.util.logging.* +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + object CurseforgeDataParser : DataParser { + private val fetcher = CurseforgeDataFetcher + private val logger = KtorSimpleLogger(javaClass.canonicalName) + + private suspend fun getDataObject(id: String): JsonObject { + return fetcher.requestModInfo(id)["data"]?.jsonObject ?: error("missing \"data\" object") + } + override suspend fun getModName(id: String): String { - TODO("Not yet implemented") + return getDataObject(id)["name"]?.jsonPrimitive?.content ?: "Error".also { + logger.warn("Error while trying to parse 'name' form object...") + } } override suspend fun getSupportedGameVersions(id: String): List { - TODO("Not yet implemented") + TODO("traverse latest files, grab versions and then sort them.") } - override suspend fun getDownloadCount(id: String): UInt { - TODO("Not yet implemented") + override suspend fun getDownloadCount(id: String): UInt? { + return getDataObject(id)["downloadCount"]?.jsonPrimitive?.content?.toUInt() } override suspend fun getSupportedModLoaders(id: String): List { - TODO("Not yet implemented") + TODO("traverse latest files and grab modloader names from \"gameVersions\".") } override suspend fun getLicence(id: String): String { - TODO("Not yet implemented") + TODO("not availible on the given data, don't know how to go about it...") } } diff --git a/src/main/kotlin/dev/bernasss12/plugins/Routing.kt b/src/main/kotlin/dev/bernasss12/plugins/Routing.kt index 61a358f..c177324 100644 --- a/src/main/kotlin/dev/bernasss12/plugins/Routing.kt +++ b/src/main/kotlin/dev/bernasss12/plugins/Routing.kt @@ -60,6 +60,19 @@ fun Application.configureRouting() { status = HttpStatusCode.OK ) } + get("/curseforge/{modid}/name") { + val modId = getModID(call) ?: return@get + call.respondText( + text = SvgGenerator.generate( + Template.CURSEFORGE_ICON_TEXT, + modId, + CurseforgeDataParser, + SvgGenerator.DataTypes.NAME.toString() + ), + contentType = ContentType.Image.SVG, + status = HttpStatusCode.OK + ) + } get("/modrinth/{modid}/versions") { val modId = getModID(call) ?: return@get call.respondText( diff --git a/src/main/kotlin/dev/bernasss12/util/Common.kt b/src/main/kotlin/dev/bernasss12/util/Common.kt index 6477c55..7da5f66 100644 --- a/src/main/kotlin/dev/bernasss12/util/Common.kt +++ b/src/main/kotlin/dev/bernasss12/util/Common.kt @@ -19,6 +19,16 @@ object Common { ?: "Bernasss12/BadgeServer" } + /** + * Gets the curseforge api token from curseforge-api-token. + */ + fun getCurseforgeSecret(logger: Logger): String? { + return getPrivateString("curseforge-api-token", logger).also { + println(it) + println(it?.chars()) + } + } + /** * Tries to get the user-agent to be sent in the GET requests from the following places, in order: * ./ Current directory