diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 23c90d3..bcdca45 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,10 +5,7 @@ * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle * User Manual available at https://docs.gradle.org/7.1/userguide/building_java_projects.html */ -// compileOptions { -// sourceCompatibility(JavaVersion.VERSION_1_8) -// targetCompatibility(JavaVersion.VERSION_1_8) -// } +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. @@ -60,6 +57,13 @@ dependencies { implementation("com.beust:klaxon:5.5") implementation("com.natpryce:konfig:1.6.10.0") + + implementation("io.ktor:ktor-client-core:1.6.1") + implementation("io.ktor:ktor-client-java:1.6.1") +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" } application { diff --git a/app/src/main/kotlin/xyz/etztech/stonks/Api.kt b/app/src/main/kotlin/xyz/etztech/stonks/Api.kt index 82f6d11..b5abee2 100644 --- a/app/src/main/kotlin/xyz/etztech/stonks/Api.kt +++ b/app/src/main/kotlin/xyz/etztech/stonks/Api.kt @@ -9,6 +9,7 @@ import kotlinx.serialization.json.Json import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.* import xyz.etztech.stonks.dsl.LiveStatistics +import xyz.etztech.stonks.dsl.Players import xyz.etztech.stonks.dsl.Statistics private var statisticsCache = "" @@ -86,10 +87,11 @@ fun initApiServer(apiServerPort: Int, database: Database) { addLogger(StdOutSqlLogger) val players = - LiveStatistics.slice(LiveStatistics.playerId) + LiveStatistics.leftJoin(Players, { playerId }, { id }) + .slice(LiveStatistics.playerId, Players.name) .selectAll() .groupBy(LiveStatistics.playerId) - .map { it[LiveStatistics.playerId] } + .map { Player(it[LiveStatistics.playerId], it[Players.name]) } playersCache = Json.encodeToString(players) } } @@ -159,6 +161,8 @@ fun initApiServer(apiServerPort: Int, database: Database) { @Serializable data class Statistic(val type: String, val name: String) +@Serializable data class Player(val id: String, val name: String?) + @Serializable data class StatisticValue( val playerId: String, diff --git a/app/src/main/kotlin/xyz/etztech/stonks/App.kt b/app/src/main/kotlin/xyz/etztech/stonks/App.kt index b50a317..a297330 100644 --- a/app/src/main/kotlin/xyz/etztech/stonks/App.kt +++ b/app/src/main/kotlin/xyz/etztech/stonks/App.kt @@ -11,6 +11,7 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction import xyz.etztech.stonks.api.initApiServer import xyz.etztech.stonks.dsl.LiveStatistics +import xyz.etztech.stonks.dsl.Players import xyz.etztech.stonks.dsl.Statistics import xyz.etztech.stonks.statisticsimporter.StatisticsImporter @@ -78,6 +79,7 @@ fun initH2Server( addLogger(StdOutSqlLogger) SchemaUtils.create(Statistics) SchemaUtils.create(LiveStatistics) + SchemaUtils.create(Players) // Create indexes with explicit SQL because I can't figure out how to do it with exposed // Wrap it in a try block because it throws an exception if index already exists diff --git a/app/src/main/kotlin/xyz/etztech/stonks/DSL.kt b/app/src/main/kotlin/xyz/etztech/stonks/DSL.kt index 0188e71..e774866 100644 --- a/app/src/main/kotlin/xyz/etztech/stonks/DSL.kt +++ b/app/src/main/kotlin/xyz/etztech/stonks/DSL.kt @@ -24,3 +24,11 @@ object Statistics : Table() { override val primaryKey = PrimaryKey(playerId, type, name, timestamp, name = "PK_playerId_type_name_timestamp") } + +object Players : Table() { + val id: Column = varchar("Id", 150) + val name: Column = varchar("Name", 150) + val timestamp: Column = timestamp("Timestamp") + + override val primaryKey = PrimaryKey(id, name = "PK_id") +} diff --git a/app/src/main/kotlin/xyz/etztech/stonks/StatisticsImporter.kt b/app/src/main/kotlin/xyz/etztech/stonks/StatisticsImporter.kt index 80a949b..8adb484 100644 --- a/app/src/main/kotlin/xyz/etztech/stonks/StatisticsImporter.kt +++ b/app/src/main/kotlin/xyz/etztech/stonks/StatisticsImporter.kt @@ -2,17 +2,27 @@ package xyz.etztech.stonks.statisticsimporter import com.beust.klaxon.* import com.beust.klaxon.Klaxon +import io.ktor.client.* +import io.ktor.client.engine.java.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* import java.io.File +import java.time.Duration import java.time.Instant +import kotlinx.coroutines.* import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.`java-time`.timestamp import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction +import xyz.etztech.stonks.dsl.LiveStatistics +import xyz.etztech.stonks.dsl.Players import xyz.etztech.stonks.dsl.Statistics object StatisticsImporter { init {} + private val httpClient = HttpClient(Java) private val klaxon = Klaxon() fun importStatistics(folder: String, database: Database) { @@ -20,6 +30,8 @@ object StatisticsImporter { File(folder).listFiles().forEach { readFile(it, database) } println("Updating live statistics table...") updateLiveStatistics(database) + println("Refreshing player names...") + refreshPlayerNames(database) } fun readFile(file: File, database: Database) { @@ -27,21 +39,22 @@ object StatisticsImporter { val playerId = file.nameWithoutExtension transaction(database) { - val maxExpr = Statistics.value.max() + addLogger(StdOutSqlLogger) val playerStats = emptyMap>().toMutableMap() - Statistics.slice(Statistics.type, Statistics.name, maxExpr) - .select { Statistics.playerId.eq(playerId) } - .groupBy(Statistics.type, Statistics.name) + LiveStatistics.slice(LiveStatistics.type, LiveStatistics.name, LiveStatistics.value) + .select { LiveStatistics.playerId eq playerId } .forEach { - if (playerStats.containsKey(it[Statistics.type])) { - playerStats[it[Statistics.type]]?.put( - it[Statistics.name], it[maxExpr]!!) + if (playerStats.containsKey(it[LiveStatistics.type])) { + playerStats[it[LiveStatistics.type]]?.put( + it[LiveStatistics.name], it[LiveStatistics.value]) } else { playerStats.put( - it[Statistics.type], - mapOf(it[Statistics.name] to it[maxExpr]!!) + it[LiveStatistics.type], + mapOf( + it[LiveStatistics.name] to + it[LiveStatistics.value]) .toMutableMap()) } } @@ -104,9 +117,85 @@ object StatisticsImporter { } } + fun refreshPlayerNames(database: Database) { + transaction(database) { + addLogger(StdOutSqlLogger) + val savedPlayers = + Players.selectAll().map { + object { + val id = it[Players.id] + val timestamp = it[Players.timestamp] + } + } + + val players = + LiveStatistics.slice(LiveStatistics.playerId) + .selectAll() + .groupBy(LiveStatistics.playerId) + .map { it[LiveStatistics.playerId] } + + savedPlayers + .filter { Duration.between(Instant.now(), it.timestamp) > Duration.ofDays(1) } + .forEach { player -> + runBlocking { + val name = getName(player.id) + if (name != null) { + println("Updating ${player.id} -> $name") + Players.update({ Players.id eq player.id }) { + it[Players.name] = name + it[Players.timestamp] = Instant.now() + } + } else { + println("Error updating ${player.id}") + } + } + } + + players + .filterNot { player -> + savedPlayers.any { savedPlayer -> savedPlayer.id == player } + } + .forEach { playerId -> + runBlocking { + val name = getName(playerId) + if (name != null) { + println("Updating $playerId -> $name") + Players.insert { + it[Players.id] = playerId + it[Players.name] = name + it[Players.timestamp] = Instant.now() + } + } else { + println("Error updating $playerId") + } + } + } + } + } + + suspend fun getName(id: String): String? { + val response: HttpResponse = + httpClient.request( + "https://sessionserver.mojang.com/session/minecraft/profile/${id.replace("-", "")}") { + method = HttpMethod.Get + } + if (response.status.isSuccess()) { + try { + return MojangProfileResponse.fromJson(response.readText())?.name + } catch (e: Exception) {} + } + return null + } + data class StatsFile(val stats: Map>) { companion object { public fun fromJson(json: String) = klaxon.parse(json) } } + + data class MojangProfileResponse(val name: String) { + companion object { + public fun fromJson(json: String) = klaxon.parse(json) + } + } }