Compare commits

..

2 Commits

Author SHA1 Message Date
a9ea232e13 Refactoring of fields and more race management. 2023-11-19 06:59:26 +01:00
be03f5fb96 Added Discord settings. 2023-11-11 03:27:33 +01:00
10 changed files with 262 additions and 56 deletions

1
.gitignore vendored
View File

@ -183,3 +183,4 @@ gradle-app.setting
# End of https://www.toptal.com/developers/gitignore/api/intellij,java,gradle,kotlin # End of https://www.toptal.com/developers/gitignore/api/intellij,java,gradle,kotlin
data.json data.json
token

22
src/main/java/Discord.kt Normal file
View File

@ -0,0 +1,22 @@
import dev.kord.core.Kord
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object Discord {
private lateinit var kord: Kord
suspend fun init(token: String) {
kord = Kord(token)
CoroutineScope(Dispatchers.IO).launch {
kord.login()
}
}
suspend fun sendMessage(message: String) {
kord.rest.channel.createMessage(Settings.instance.discordChannelId) {
this.content = "<@&${Settings.instance.discordGroupId}>$message"
}
}
}

View File

@ -1,15 +1,18 @@
import dev.kord.common.entity.Snowflake
import dev.kord.core.Kord
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.cio.* import io.ktor.server.cio.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.nio.file.Files import java.nio.file.Files
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.io.path.Path import kotlin.io.path.Path
suspend fun main() { suspend fun main() {
Settings.load()
val path = Path("data.json") val path = Path("data.json")
if (Files.isRegularFile(path)) { if (Files.isRegularFile(path)) {
@ -19,25 +22,32 @@ suspend fun main() {
RaceHolder.save(path) RaceHolder.save(path)
}) })
val kord = Kord("MTE3MTIwODc1MDE5MTg5MDQ4Mw.GOUedL.i3zD6IG5B6fFRvaSOotWwJ5KBRK2whC9xr0vL8") Discord.init(withContext(Dispatchers.IO) {
Files.readString(Path("token"))
})
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate({
runBlocking {
RaceHolder.races.forEach {
it.rescanAll()
}
}
}, 0, 5, TimeUnit.MINUTES)
val server = embeddedServer(CIO, port = 8080){ val server = embeddedServer(CIO, port = 8080){
routing { routing {
get("/"){ get("/"){
call.respondRedirect("/search") call.respondRedirect("/search")
} }
settingsPage()
searchPage() searchPage()
trackPage() trackPage()
racePage() racePage()
get("/hi") { get("/hi") {
kord.rest.channel.createMessage(Snowflake("1040400994355392522")) { Discord.sendMessage("Herro!")
this.content = "<@&978289601506586624> Herro!"
}
} }
} }
} }
println("connection to localhost:8080 now possible.") println("connection to localhost:8080 now possible.")
server.start(false) server.start(true)
kord.login()
} }

View File

@ -1,3 +1,7 @@
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -5,49 +9,57 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
@Serializable
data class Track(
val trackId: Long,
val endTime: LocalDateTime? = null,
var leaderboard: Velocidrone.Leaderboard = Velocidrone.Leaderboard(
true, "", "",
arrayOf()
)
)
@Serializable @Serializable
data class RaceData(var name: String, var description: String) { data class RaceData(var name: String, var description: String) {
private val trackList = arrayListOf<Long>() private val trackList = arrayListOf<Track>()
private val leaderboardMap = hashMapOf<Long, Velocidrone.Leaderboard>()
private var totalScores = arrayListOf<Pair<String, Long>>() private var totalScores = arrayListOf<Pair<String, Long>>()
fun addTrack(trackId: Long) { fun addTrack(track: Track) {
trackList.add(trackId) trackList.add(track)
} }
fun removeTrack(trackId: Long) { fun removeTrack(track: Track) {
trackList.remove(trackId) trackList.remove(track)
leaderboardMap.remove(trackId)
} }
private suspend fun rescanLeaderboard(trackId: Long, newLeaderboardCallback: (Velocidrone.Leaderboard) -> Unit) { private suspend fun rescanLeaderboard(track: Track, newLeaderboardCallback: (Velocidrone.Leaderboard) -> Unit) {
if (trackList.contains(trackId)) { Velocidrone.getLeaderboardForId(track.trackId).getOrNull()?.let { newLeaderboard ->
Velocidrone.getLeaderboardForId(trackId).getOrNull()?.let { newLeaderboard -> if (track.leaderboard != newLeaderboard) {
if (leaderboardMap[trackId]?.equals(newLeaderboard) == false) { track.leaderboard = newLeaderboard
leaderboardMap[trackId] = newLeaderboard
newLeaderboardCallback(newLeaderboard) newLeaderboardCallback(newLeaderboard)
} }
} }
} }
}
suspend fun rescanAll() { suspend fun rescanAll() {
var changes = false var changes = false
trackList.forEach { trackList.forEach {
if ((it.endTime?.toInstant(TimeZone.UTC)?.compareTo(Clock.System.now()) ?: -1) < 0) {
rescanLeaderboard(it) { rescanLeaderboard(it) {
changes = true changes = true
} }
} }
}
if (changes) { if (changes) {
calculateTotalScores() calculateTotalScores()
} }
} }
fun calculateTotalScores() { private fun calculateTotalScores() {
val scoreMap = hashMapOf<String, Long>() val scoreMap = hashMapOf<String, Long>()
leaderboardMap.forEach { leaderboardEntry -> trackList.forEach { track ->
leaderboardEntry.value.tracktimes.sortedBy { it.lap_time }.forEach { track.leaderboard.trackTimes.sortedBy { it.lapTime }.forEachIndexed { index, it ->
scoreMap[it.playername] = scoreMap.getOrDefault(it.playername, 0L) + 1 scoreMap[it.playerName] = scoreMap.getOrDefault(it.playerName, 0L) + 1 + index
} }
} }
synchronized(totalScores) { synchronized(totalScores) {
@ -55,6 +67,10 @@ data class RaceData(var name: String, var description: String) {
scoreMap.asIterable().sortedByDescending { it.value }.map { it.toPair() }.toCollection(totalScores) scoreMap.asIterable().sortedByDescending { it.value }.map { it.toPair() }.toCollection(totalScores)
} }
} }
fun getTracks(): ArrayList<Track> {
return trackList
}
} }
object RaceHolder { object RaceHolder {

View File

@ -9,13 +9,33 @@ fun Routing.racePage() {
get("/race/list") { get("/race/list") {
raceOverviewPage(RaceHolder.races) raceOverviewPage(RaceHolder.races)
} }
get("/races/{id}/show") {
}
get("/races/{raceId}/remove/{trackId}") {
val raceId = call.parameters["raceId"]
val trackId = call.parameters["trackId"]?.toLong()
RaceHolder.races.find { it.name == raceId }?.let { raceData ->
val toBeRemovedTrack = raceData.getTracks().find { it.trackId == trackId }
toBeRemovedTrack?.let {
raceData.removeTrack(it)
}
}
call.respondRedirect("/races/$raceId/show")
}
get("/race/new") { get("/race/new") {
newRacePage() newRacePage()
} }
post("/race/new") { post("/race/new") {
val parameters = call.receiveParameters() val parameters = call.receiveParameters()
val name = parameters["name"] val name = parameters["name"]
val description = parameters["description"] val description = parameters["description"]
if (name == null || description == null) { if (name == null || description == null) {
newRacePage("Not all parameters given.", name ?: "", description ?: "") newRacePage("Not all parameters given.", name ?: "", description ?: "")
return@post return@post
@ -24,6 +44,7 @@ fun Routing.racePage() {
newRacePage("Race with the same name already exists.", name, description) newRacePage("Race with the same name already exists.", name, description)
return@post return@post
} }
RaceHolder.races.add(RaceData(name, description)) RaceHolder.races.add(RaceData(name, description))
call.respondRedirect("/race/list") call.respondRedirect("/race/list")
} }

View File

@ -68,7 +68,7 @@ private suspend fun PipelineContext<Unit,ApplicationCall>.searchResultPage(track
} }
} }
} }
trackResults.user_tracks.forEach { trackResults.userTracks.forEach {
tr { tr {
td { td {
form("/track/" + it.id + "/add") { form("/track/" + it.id + "/add") {
@ -81,10 +81,10 @@ private suspend fun PipelineContext<Unit,ApplicationCall>.searchResultPage(track
} }
} }
td { td {
+it.track_name +it.trackName
} }
td { td {
+it.playername +it.playerName
} }
} }
} }

32
src/main/java/Settings.kt Normal file
View File

@ -0,0 +1,32 @@
import dev.kord.common.entity.Snowflake
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.nio.file.Files
import kotlin.io.path.Path
const val SETTINGS = "settings.json"
@Serializable
class Settings {
companion object {
var instance = Settings()
fun load() {
if (Files.isRegularFile(Path(SETTINGS))) {
val reader = Files.newBufferedReader(Path(SETTINGS))
instance = Json.decodeFromString<Settings>(reader.readText())
reader.close()
}
}
fun save() {
val writer = Files.newBufferedWriter(Path(SETTINGS))
writer.write(Json.encodeToString(instance))
writer.close()
}
}
var discordChannelId = Snowflake(0)
var discordGroupId = Snowflake(0)
}

View File

@ -0,0 +1,41 @@
import dev.kord.common.entity.Snowflake
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.html.*
fun Routing.settingsPage() {
get("/settings") {
respondThemedHtml("Settings") {
form("/settings", encType = FormEncType.applicationXWwwFormUrlEncoded, method = FormMethod.post) {
p {
+"GroupID:"
textInput(name = "groupId") {
value = Settings.instance.discordGroupId.toString()
}
}
p {
+"ChannelID:"
textInput(name = "channelId") {
value = Settings.instance.discordChannelId.toString()
}
}
submitInput { }
}
}
}
post("/settings") {
val params = call.receiveParameters()
params["groupId"]?.let { Settings.instance.discordGroupId = Snowflake(it) }
params["channelId"]?.let { Settings.instance.discordChannelId = Snowflake(it) }
CoroutineScope(Dispatchers.IO).launch {
Settings.save()
}
call.respondRedirect("/settings")
}
}

View File

@ -3,6 +3,7 @@ import io.ktor.server.html.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import io.ktor.util.pipeline.* import io.ktor.util.pipeline.*
import kotlinx.datetime.toLocalDateTime
import kotlinx.html.* import kotlinx.html.*
import java.net.URLDecoder import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
@ -22,11 +23,12 @@ fun Routing.trackPage() {
} }
} }
post("/track/{id}/add") { post("/track/{id}/add") {
val endTime = call.receiveParameters()["endTime"]?.toLocalDateTime()
call.parameters["id"]?.toLong()?.let { trackId -> call.parameters["id"]?.toLong()?.let { trackId ->
val param = call.receiveParameters() val param = call.receiveParameters()
param["race"]?.let { raceString -> param["race"]?.let { raceString ->
val decoded = URLDecoder.decode(raceString, Charsets.UTF_8) val decoded = URLDecoder.decode(raceString, Charsets.UTF_8)
RaceHolder.races.find { it.name == decoded }?.addTrack(trackId) RaceHolder.races.find { it.name == decoded }?.addTrack(Track(trackId, endTime))
} }
} }
} }
@ -47,10 +49,10 @@ private suspend fun PipelineContext<Unit, ApplicationCall>.trackPage(leaderboard
td { +"Name" } td { +"Name" }
} }
} }
leaderboard.tracktimes.sortedBy { it.lap_time }.forEach { leaderboard.trackTimes.sortedBy { it.lapTime }.forEach {
tr { tr {
td { +it.lap_time.toString() } td { +it.lapTime.toString() }
td { +it.playername } td { +it.playerName }
} }
} }
} }
@ -70,6 +72,10 @@ private suspend fun PipelineContext<Unit, ApplicationCall>.addTrackPage(trackId:
} }
} }
} }
p {
+"Optional end time:"
dateTimeLocalInput(name = "endTime")
}
submitInput() submitInput()
} }
} }

View File

@ -2,6 +2,7 @@ import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.engine.cio.* import io.ktor.client.engine.cio.*
import io.ktor.client.request.* import io.ktor.client.request.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.net.URLEncoder import java.net.URLEncoder
@ -13,38 +14,67 @@ import javax.crypto.spec.SecretKeySpec
object Velocidrone{ object Velocidrone{
private val key = "BatCaveGGevaCtaB".toByteArray(Charset.defaultCharset()) private val key = "BatCaveGGevaCtaB".toByteArray(Charset.defaultCharset())
fun decodeVeloBase64(input: ByteArray): Result<String> { private fun decodeVeloBase64(input: ByteArray): Result<String> {
val decoded = Base64.getDecoder().decode(input) val decoded = Base64.getDecoder().decode(input)
val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES")) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"))
return Result.success(String(cipher.doFinal(decoded))) return Result.success(String(cipher.doFinal(decoded)))
} }
fun encodeVeloBase64(input: String): ByteArray { private fun encodeVeloBase64(input: String): ByteArray {
val encoded: ByteArray = input.toByteArray() val encoded: ByteArray = input.toByteArray()
val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES")) cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"))
return Base64.getEncoder().encode(cipher.doFinal(encoded)) return Base64.getEncoder().encode(cipher.doFinal(encoded))
} }
val client = HttpClient(CIO) private val client = HttpClient(CIO)
@Serializable @Serializable
data class Leaderboard( data class Leaderboard(
val success:Boolean, val success:Boolean,
val message_title:String, @SerialName("message_title")
val messageTitle: String,
val message:String, val message:String,
val tracktimes:Array<TrackTime> @SerialName("tracktimes")
) val trackTimes: Array<TrackTime>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Leaderboard
if (success != other.success) return false
if (messageTitle != other.messageTitle) return false
if (message != other.message) return false
if (!trackTimes.contentEquals(other.trackTimes)) return false
return true
}
override fun hashCode(): Int {
var result = success.hashCode()
result = 31 * result + messageTitle.hashCode()
result = 31 * result + message.hashCode()
result = 31 * result + trackTimes.contentHashCode()
return result
}
}
@Serializable @Serializable
data class TrackTime( data class TrackTime(
val lap_time:Float, @SerialName("lap_time")
val playername: String, val lapTime: Float,
val model_id: Long, @SerialName("playername")
val playerName: String,
@SerialName("model_id")
val modelId: Long,
val country: String, val country: String,
val sim_version: String, @SerialName("sim_version")
val device_type: Long val simVersion: String,
@SerialName("device_type")
val deviceType: Long
) )
suspend fun getLeaderboardForId(id:Long): Result<Leaderboard> { suspend fun getLeaderboardForId(id:Long): Result<Leaderboard> {
@ -66,17 +96,44 @@ object Velocidrone{
data class TrackResults( data class TrackResults(
val success:Boolean, val success:Boolean,
val detail:String, val detail:String,
val user_tracks:Array<Track> @SerialName("user_tracks")
) val userTracks: Array<Track>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TrackResults
if (success != other.success) return false
if (detail != other.detail) return false
if (!userTracks.contentEquals(other.userTracks)) return false
return true
}
override fun hashCode(): Int {
var result = success.hashCode()
result = 31 * result + detail.hashCode()
result = 31 * result + userTracks.contentHashCode()
return result
}
}
@Serializable @Serializable
data class Track( data class Track(
val id:Long, val id:Long,
val scenery_id:Long, @SerialName("scenery_id")
val track_name:String, val sceneryId: Long,
val track_type:String, @SerialName("track_name")
val playername:String, val trackName: String,
@SerialName("track_type")
val trackType: String,
@SerialName("playername")
val playerName: String,
val rating:Float, val rating:Float,
val total_ratings:Long, @SerialName("total_ratings")
val totalRatings: Long,
val date:String val date:String
) )