diff --git a/.gitignore b/.gitignore index 2477e2d..027fab0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ out/ /config.json /logs/ /data/ +/skins/ diff --git a/src/main/kotlin/de/steamwar/data/SkinCache.kt b/src/main/kotlin/de/steamwar/data/SkinCache.kt new file mode 100644 index 0000000..5d9d7c3 --- /dev/null +++ b/src/main/kotlin/de/steamwar/data/SkinCache.kt @@ -0,0 +1,110 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.data + +import io.ktor.client.* +import io.ktor.client.engine.java.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.utils.io.jvm.javaio.* +import kotlinx.coroutines.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray +import java.io.File +import java.time.Instant +import java.time.temporal.ChronoUnit + +const val kCacheFolder: String = "skins" +val kCacheFolderFile: File = File(kCacheFolder) +const val kCacheConfigName: String = "cache.cbor" +val kCacheConfigFile: File = File(kCacheFolder, kCacheConfigName) + +@Serializable +data class CacheConfig(val lastUpdate: MutableMap) { + @OptIn(ExperimentalSerializationApi::class) + companion object { + private var config: CacheConfig = if (kCacheConfigFile.exists()) { + kCacheConfigFile.inputStream().use { + Cbor.decodeFromByteArray(it.readBytes()) + } + } else { + kCacheConfigFile.createNewFile() + kCacheConfigFile.outputStream().use { + it.write(Cbor.encodeToByteArray(CacheConfig(mutableMapOf()))) + } + + CacheConfig(mutableMapOf()) + } + + private fun save() { + kCacheConfigFile.outputStream().use { + it.write(Cbor.encodeToByteArray(config)) + } + } + + fun update(uuid: String) { + config.lastUpdate[uuid] = Instant.now().toEpochMilli() + save() + } + + fun isOutdated(uuid: String): Boolean { + return config.lastUpdate[uuid]?.let { + it < Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli() + } ?: true + } + } +} + +val client = HttpClient(Java) { + install(ContentNegotiation) { + json() + } + defaultRequest { + header("User-Agent", "SteamWar/1.0") + } +} + +suspend fun getCachedSkin(uuid: String): Pair { + val file = File(kCacheFolderFile, "$uuid.webp") + if (file.exists()) { + if (CacheConfig.isOutdated(uuid)) { + val skin = client.get("https://visage.surgeplay.com/bust/150/$uuid") + skin.bodyAsChannel().copyTo(file.outputStream()) + + CacheConfig.update(uuid) + return file to false + } + return file to true + } + + withContext(Dispatchers.IO) { + file.createNewFile() + } + val skin = client.get("https://visage.surgeplay.com/bust/150/$uuid") + skin.bodyAsChannel().copyTo(file.outputStream()) + CacheConfig.update(uuid) + return file to false +} diff --git a/src/main/kotlin/de/steamwar/routes/Data.kt b/src/main/kotlin/de/steamwar/routes/Data.kt index 9536fd2..a7f1079 100644 --- a/src/main/kotlin/de/steamwar/routes/Data.kt +++ b/src/main/kotlin/de/steamwar/routes/Data.kt @@ -21,6 +21,7 @@ package de.steamwar.routes import de.steamwar.ResponseError import de.steamwar.data.Groups +import de.steamwar.data.getCachedSkin import de.steamwar.plugins.SWAuthPrincipal import de.steamwar.plugins.SWPermissionCheck import de.steamwar.sql.SchematicType @@ -37,6 +38,7 @@ import kotlinx.serialization.Serializable import org.bspfsystems.yamlconfiguration.file.YamlConfiguration import java.io.File import java.net.InetSocketAddress +import java.util.UUID @Serializable data class ResponseSchematicType(val name: String, val db: String) @@ -119,6 +121,17 @@ fun Route.configureDataRoutes() { return@get } } + get("/skin/{uuid}") { + val uuid = call.parameters["uuid"] + if (uuid == null || catchException { UUID.fromString(uuid) } == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid UUID")) + return@get + } + + val skin = getCachedSkin(uuid) + call.response.header("X-Cache", if (skin.second) "HIT" else "MISS") + call.respondFile(skin.first) + } route("/me") { install(SWPermissionCheck) @@ -127,4 +140,12 @@ fun Route.configureDataRoutes() { } } } -} \ No newline at end of file +} + +inline fun catchException(yield: () -> T): T? { + return try { + yield() + } catch (e: Exception) { + null + } +} diff --git a/src/main/kotlin/de/steamwar/routes/Events.kt b/src/main/kotlin/de/steamwar/routes/Events.kt index 6ffc876..b8f84d6 100644 --- a/src/main/kotlin/de/steamwar/routes/Events.kt +++ b/src/main/kotlin/de/steamwar/routes/Events.kt @@ -171,8 +171,12 @@ fun Route.configureEventsRoute() { csv.appendLine() val blue = Team.get(it.teamBlue) val red = Team.get(it.teamRed) - val winner = - if (it.ergebnis == 1) blue.teamName else if (it.ergebnis == 2) red.teamName else if (it.ergebnis == 3) "Tie" else "Unknown" + val winner = when(it.ergebnis) { + 1 -> blue.teamName + 2 -> red.teamName + 3 -> "Tie" + else -> "Unknown" + } csv.append( arrayOf( it.startTime.toString(),