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(),