commit 0567276086bc508bd0c9d2b9e26612b2ecebc4a1 Author: Chaoscaot Date: Fri Dec 23 23:14:36 2022 +0100 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c426c32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ffabdd8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "CommonCore"] + path = CommonCore + url = https://steamwar.de/devlabs/SteamWar/CommonCore.git diff --git a/CommonCore b/CommonCore new file mode 160000 index 0000000..50f4fd4 --- /dev/null +++ b/CommonCore @@ -0,0 +1 @@ +Subproject commit 50f4fd423d854dd15f90d0c28b17dcf92bf74f91 diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c3ff7e8 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,44 @@ +val ktor_version: String by project +val kotlin_version: String by project +val logback_version: String by project + +plugins { + kotlin("jvm") version "1.7.22" + id("io.ktor.plugin") version "2.2.1" + id("org.jetbrains.kotlin.plugin.serialization") version "1.7.22" + application +} + +group = "de.steamwar" +version = "0.0.1" +application { + mainClass.set("de.steamwar.ApplicationKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.ktor:ktor-server-auto-head-response-jvm:$ktor_version") + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") + implementation("io.ktor:ktor-server-caching-headers-jvm:$ktor_version") + implementation("io.ktor:ktor-server-compression-jvm:$ktor_version") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-server-cors-jvm:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") + implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version") + implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") + implementation("ch.qos.logback:logback-classic:$logback_version") + implementation("io.ktor:ktor-server-host-common-jvm:2.2.1") + implementation("io.ktor:ktor-server-status-pages-jvm:2.2.1") + testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") + implementation("io.ktor:ktor-server-request-validation:$ktor_version") + implementation("com.mysql:mysql-connector-j:8.0.31") + implementation(project(":CommonCore")) + implementation("org.bspfsystems:yamlconfiguration:1.3.0") +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..347f151 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +ktor_version=2.2.1 +kotlin_version=1.7.22 +logback_version=1.2.11 +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..4e62873 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "sw_event_api" + +include("CommonCore") \ No newline at end of file diff --git a/src/main/kotlin/de/steamwar/Application.kt b/src/main/kotlin/de/steamwar/Application.kt new file mode 100644 index 0000000..b6286ab --- /dev/null +++ b/src/main/kotlin/de/steamwar/Application.kt @@ -0,0 +1,23 @@ +package de.steamwar + +import de.steamwar.plugins.configurePlugins +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import de.steamwar.routes.configureRoutes +import de.steamwar.sql.SchematicType +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseError(val error: String) + +fun main() { + SchematicType.Normal.name().length + embeddedServer(Netty, port = 8000, host = "0.0.0.0", module = Application::module) + .start(wait = true) +} + +fun Application.module() { + configurePlugins() + configureRoutes() +} diff --git a/src/main/kotlin/de/steamwar/plugins/Auth.kt b/src/main/kotlin/de/steamwar/plugins/Auth.kt new file mode 100644 index 0000000..1d0b926 --- /dev/null +++ b/src/main/kotlin/de/steamwar/plugins/Auth.kt @@ -0,0 +1,40 @@ +package de.steamwar.plugins + +import de.steamwar.ResponseError +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import java.io.File +import java.security.MessageDigest +import java.util.Properties + +val validCodes by lazy { + val file = File(System.getProperty("user.home"), "api-codes.txt") + if(!file.exists()) { + file.createNewFile() + } + Properties().apply { + load(file.inputStream()) + }.map { it.key.toString() to it.value.toString() }.toMap() +} + +fun isValidCode(code: String): Boolean { + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(code.toByteArray()) + return validCodes.contains(hash.joinToString("") { "%02x".format(it) }) +} + +val SWAuth = createApplicationPlugin("SWAuth") { + onCall { call -> + if(call.request.httpMethod == HttpMethod.Options) { + return@onCall + } + val auth = call.request.headers["X-SW-Auth"] + if (auth == null) { + call.respond(HttpStatusCode.Unauthorized, ResponseError("Missing auth header")) + } else if (!isValidCode(auth)) { + call.respond(HttpStatusCode.Unauthorized, ResponseError("Invalid auth code")) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/steamwar/plugins/Plugins.kt b/src/main/kotlin/de/steamwar/plugins/Plugins.kt new file mode 100644 index 0000000..7285880 --- /dev/null +++ b/src/main/kotlin/de/steamwar/plugins/Plugins.kt @@ -0,0 +1,70 @@ +package de.steamwar.plugins + +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.serialization.kotlinx.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.autohead.* +import io.ktor.server.plugins.cachingheaders.* +import io.ktor.server.plugins.compression.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.response.* +import io.ktor.server.websocket.* +import kotlinx.serialization.json.Json +import java.time.Duration + +data class Session(val name: String) + +fun Application.configurePlugins() { + install(WebSockets) { + pingPeriod = Duration.ofSeconds(15) + timeout = Duration.ofSeconds(15) + maxFrameSize = Long.MAX_VALUE + masking = false + contentConverter = KotlinxWebsocketSerializationConverter(Json) + } + install(AutoHeadResponse) + + install(CachingHeaders) { + options { call, outgoingContent -> + when (outgoingContent.contentType?.withoutParameters()) { + ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60)) + else -> null + } + } + } + + install(Compression) { + gzip { + priority = 1.0 + } + deflate { + priority = 10.0 + minimumSize(1024) // condition + } + } + install(StatusPages) { + exception { call, cause -> + call.respondText(text = "500: $cause\n${cause.stackTraceToString()}", status = HttpStatusCode.InternalServerError) + } + } + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowHeader("x-sw-auth") + allowHeader(HttpHeaders.AccessControlAllowOrigin) + allowHeader(HttpHeaders.ContentType) + anyHost() + allowXHttpMethodOverride() + } + install(SWAuth) + install(ContentNegotiation) { + json(Json) + } +} diff --git a/src/main/kotlin/de/steamwar/routes/Data.kt b/src/main/kotlin/de/steamwar/routes/Data.kt new file mode 100644 index 0000000..a489c18 --- /dev/null +++ b/src/main/kotlin/de/steamwar/routes/Data.kt @@ -0,0 +1,48 @@ +package de.steamwar.routes + +import de.steamwar.ResponseError +import de.steamwar.sql.SchematicType +import de.steamwar.sql.SteamwarUser +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable +import org.bspfsystems.yamlconfiguration.file.YamlConfiguration +import java.io.File + +@Serializable +data class ResponseSchematicType(val name: String, val db: String) + +@Serializable +data class ResponseUser(val id: Int, val name: String, val uuid: String) + +fun Routing.configureDataRoutes() { + route("/data") { + get("/schematicTypes") { + call.respond(SchematicType.values().filter { !it.check() }.map { ResponseSchematicType(it.name(), it.toDB()) }) + } + get("/gamemodes") { + call.respond(File("/configs/GameModes/").listFiles().filter { it.name.endsWith(".yml") && !it.name.endsWith(".kits.yml") }.map { it.nameWithoutExtension }) + } + get("/gamemodes/{gamemode}/maps") { + val gamemode = call.parameters["gamemode"] + if (gamemode == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid gamemode")) + return@get + } + val file = File("/configs/GameModes/$gamemode.yml") + if (!file.exists()) { + call.respond(HttpStatusCode.NotFound, ResponseError("Gamemode not found")) + return@get + } + call.respond(YamlConfiguration.loadConfiguration(file).getStringList("Server.Maps")) + } + get("/users") { + call.respond(SteamwarUser.getAll().map { ResponseUser(it.id, it.userName, it.uuid.toString()) }) + } + get { + call.respondText("Hello World!") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/steamwar/routes/EventFights.kt b/src/main/kotlin/de/steamwar/routes/EventFights.kt new file mode 100644 index 0000000..f60af91 --- /dev/null +++ b/src/main/kotlin/de/steamwar/routes/EventFights.kt @@ -0,0 +1,83 @@ +package de.steamwar.routes + +import de.steamwar.ResponseError +import de.steamwar.sql.EventFight +import de.steamwar.sql.SteamwarUser +import de.steamwar.sql.Team +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable +import java.sql.Timestamp +import java.time.Instant + + +@Serializable +data class ResponseEventFight(val id: Int, val spielmodus: String, val map: String, val blueTeam: ResponseTeam, val redTeam: ResponseTeam, val kampfleiter: Int, val start: Long, val ergebnis: Int) { + constructor(eventFight: EventFight): this(eventFight.fightID, eventFight.spielModus, eventFight.map, ResponseTeam( + Team.get(eventFight.teamBlue)), ResponseTeam(Team.get(eventFight.teamRed)), eventFight.kampfleiter, eventFight.startTime.time, eventFight.ergebnis) +} + +@Serializable +data class ResponseTeam(val id: Int, val name: String, val kuerzel: String, val color: String) { + constructor(team: Team): this(team.teamId, team.teamName, team.teamKuerzel, team.teamColor) +} + +@Serializable +data class UpdateEventFight(val blueTeam: Int? = null, val redTeam: Int? = null, val kampfleiter: Int? = null, val start: Long? = null, val spielmodus: String? = null, val map: String? = null) + +@Serializable +data class CreateEventFight(val event: Int, val spielmodus: String, val map: String, val blueTeam: Int, val redTeam: Int, val start: Long) + +fun Routing.configureEventFightRoutes() { + route("/fights") { + post { + val fight = call.receiveNullable() + if (fight == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) + return@post + } + val eventFight = EventFight.create(fight.event, Timestamp.from(Instant.ofEpochMilli(fight.start)), fight.spielmodus, fight.map, fight.blueTeam, fight.redTeam) + call.respond(HttpStatusCode.Created, ResponseEventFight(eventFight)) + } + put("/{fight}") { + val fightId = call.parameters["fight"]?.toIntOrNull() + if (fightId == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID")) + return@put + } + val fight = EventFight.get(fightId) + if (fight == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Fight not found")) + return@put + } + val updateFight = call.receiveNullable() + if (updateFight == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) + return@put + } + if(updateFight.blueTeam != null && Team.get(updateFight.blueTeam) != null) { + fight.teamBlue = updateFight.blueTeam + } + if(updateFight.redTeam != null && Team.get(updateFight.redTeam) != null) { + fight.teamRed = updateFight.redTeam + } + if(updateFight.kampfleiter != null && SteamwarUser.get(updateFight.kampfleiter) != null) { + fight.kampfleiter = updateFight.kampfleiter + } + if(updateFight.start != null) { + fight.startTime = Timestamp.from(Instant.ofEpochMilli(updateFight.start)) + } + if(updateFight.map != null) { + fight.map = updateFight.map + } + if(updateFight.spielmodus != null) { + fight.spielModus = updateFight.spielmodus + } + fight.update() + call.respond(HttpStatusCode.OK, ResponseEventFight(fight)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/steamwar/routes/Events.kt b/src/main/kotlin/de/steamwar/routes/Events.kt new file mode 100644 index 0000000..ed43bba --- /dev/null +++ b/src/main/kotlin/de/steamwar/routes/Events.kt @@ -0,0 +1,155 @@ +package de.steamwar.routes + +import de.steamwar.ResponseError +import de.steamwar.sql.Event +import de.steamwar.sql.EventFight +import de.steamwar.sql.SchematicType +import de.steamwar.sql.TeamTeilnahme +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable +import java.sql.Timestamp +import java.time.Instant + +@Serializable +data class ShortEvent(val id: Int, val name: String, val start: Long, val end: Long) { + constructor(event: Event): this(event.eventID, event.eventName, event.start.time, event.end.time) +} + +@Serializable +data class ResponseEvent(val id: Int, val name: String, val deadline: Long, val start: Long, val end: Long, val maxTeamMembers: Int, val schemType: String?, val publicSchemsOnly: Boolean, val spectateSystem: Boolean) { + constructor(event: Event): this(event.eventID, event.eventName, event.deadline.time, event.start.time, event.end.time, event.maximumTeamMembers, event.schematicType?.toDB(), event.publicSchemsOnly(), event.spectateSystem()) +} + +@Serializable +data class ExtendedResponseEvent(val event: ResponseEvent, val teams: List, val fights: List) + +@Serializable +data class CreateEvent(val name: String, val start: Long, val end: Long) + +@Serializable +data class UpdateEvent(val name: String?, val deadline: Long?, val start: Long?, val end: Long?, val maxTeamMembers: Int?, val schemType: String?, val publicSchemsOnly: Boolean?, val spectateSystem: Boolean?) + +fun Routing.configureEventsRoute() { + route("/events") { + get { + call.respond(Event.getAllShort().map { ShortEvent(it) }) + } + post { + val createEvent = call.receiveNullable() + if (createEvent == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) + return@post + } + val event = Event.create(createEvent.name, Timestamp.from(Instant.ofEpochMilli(createEvent.start)), Timestamp.from(Instant.ofEpochMilli(createEvent.end))) + call.respond(HttpStatusCode.Created, ResponseEvent(event)) + } + route("/{id}") { + get { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@get + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@get + } + call.respond(ExtendedResponseEvent(ResponseEvent(event), TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) }, EventFight.getFromEvent(event.eventID).map { ResponseEventFight(it) })) + } + get("/teams") { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@get + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@get + } + call.respond(TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) }) + } + get("/fights") { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@get + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@get + } + call.respond(EventFight.getFromEvent(event.eventID).map { ResponseEventFight(it) }) + } + put { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@put + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@put + } + val updateEvent = call.receiveNullable() + if (updateEvent == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) + return@put + } + if (updateEvent.name != null) { + event.eventName = updateEvent.name + } + if (updateEvent.deadline != null) { + event.deadline = Timestamp.from(Instant.ofEpochMilli(updateEvent.deadline)) + } + if (updateEvent.start != null) { + event.start = Timestamp.from(Instant.ofEpochMilli(updateEvent.start)) + } + if (updateEvent.end != null) { + event.end = Timestamp.from(Instant.ofEpochMilli(updateEvent.end)) + } + if (updateEvent.maxTeamMembers != null) { + event.maximumTeamMembers = updateEvent.maxTeamMembers + } + if (updateEvent.schemType != null) { + if(updateEvent.schemType == "null") { + event.setSchemType(null) + } else { + if(SchematicType.fromDB(updateEvent.schemType) != null) { + event.setSchemType(SchematicType.fromDB(updateEvent.schemType)) + } + } + } + if (updateEvent.publicSchemsOnly != null) { + event.setPublicSchemsOnly(updateEvent.publicSchemsOnly) + } + if (updateEvent.spectateSystem != null) { + event.setSpectateSystem(updateEvent.spectateSystem) + } + event.update() + call.respond(ResponseEvent(event)) + } + delete { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@delete + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@delete + } + Event.delete(event.eventID) + call.respond(HttpStatusCode.NoContent) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/steamwar/routes/Routes.kt b/src/main/kotlin/de/steamwar/routes/Routes.kt new file mode 100644 index 0000000..3f3bbe0 --- /dev/null +++ b/src/main/kotlin/de/steamwar/routes/Routes.kt @@ -0,0 +1,13 @@ +package de.steamwar.routes + +import io.ktor.server.application.* +import io.ktor.server.routing.* + +fun Application.configureRoutes() { + routing { + configureEventsRoute() + configureDevServerRoutes() + configureDataRoutes() + configureEventFightRoutes() + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/steamwar/routes/Server.kt b/src/main/kotlin/de/steamwar/routes/Server.kt new file mode 100644 index 0000000..23dd8a5 --- /dev/null +++ b/src/main/kotlin/de/steamwar/routes/Server.kt @@ -0,0 +1,69 @@ +package de.steamwar.routes + +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import io.ktor.utils.io.streams.* +import io.ktor.websocket.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.SendChannel +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import java.io.File +import java.io.OutputStream +import kotlin.concurrent.thread + +@Serializable +data class StartServerPayload(val name: String, val port: Int? = null, val world: String? = null, val plugins: String? = null) { + val arguments + get() = run { + val args = mutableListOf() + if (port != null) { + args.add("-p") + args.add(port.toString()) + } + if (world != null) { + args.add("-w") + args.add(world) + } + if (plugins != null) { + args.add("-pl") + args.add(plugins) + } + args + } +} + +class OutputSender(val outgoing: SendChannel): OutputStream() { + val buffer = StringBuilder() + + override fun write(b: Int) = runBlocking { + buffer.append(b.toChar()) + if (b.toChar() == '\n') { + outgoing.send(Frame.Text(buffer.toString())) + buffer.clear() + } + } +} + +fun Routing.configureDevServerRoutes() { + route("/servers") { + get { + call.respondText(JsonArray(File("/servers/").list()?.map { JsonPrimitive(it) } ?: listOf()).toString()) + } + webSocket("/start") { + val startPayload = receiveDeserialized() + val proc = ProcessBuilder("python3", "/binarys/dev.py", startPayload.name, *startPayload.arguments.toTypedArray()).start() + thread { + proc.inputStream.copyTo(OutputSender(outgoing)) + } + var received: Frame? = null + while (incoming.tryReceive().also { received = it.getOrNull() }.isSuccess) { + proc.outputStream.write(received!!.data + "\n".toByteArray()) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/steamwar/routes/Teams.kt b/src/main/kotlin/de/steamwar/routes/Teams.kt new file mode 100644 index 0000000..88c9afc --- /dev/null +++ b/src/main/kotlin/de/steamwar/routes/Teams.kt @@ -0,0 +1,14 @@ +package de.steamwar.routes + +import de.steamwar.sql.Team +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Routing.configureTeamRoutes() { + route("/team") { + get { + call.respond(Team.getAll().map { ResponseTeam(it) }) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/steamwar/sql/SQLConfigImpl.kt b/src/main/kotlin/de/steamwar/sql/SQLConfigImpl.kt new file mode 100644 index 0000000..da42cda --- /dev/null +++ b/src/main/kotlin/de/steamwar/sql/SQLConfigImpl.kt @@ -0,0 +1,10 @@ +package de.steamwar.sql + +import de.steamwar.sql.internal.SQLConfig +import java.util.logging.Logger + +class SQLConfigImpl: SQLConfig { + override fun getLogger(): Logger = Logger.getGlobal() + + override fun maxConnections(): Int = 1 +} diff --git a/src/main/kotlin/de/steamwar/sql/SQLWrapperImpl.kt b/src/main/kotlin/de/steamwar/sql/SQLWrapperImpl.kt new file mode 100644 index 0000000..f1f5bca --- /dev/null +++ b/src/main/kotlin/de/steamwar/sql/SQLWrapperImpl.kt @@ -0,0 +1,43 @@ +package de.steamwar.sql + +import org.bspfsystems.yamlconfiguration.file.YamlConfiguration +import java.io.File +import java.util.* +import java.util.stream.Collectors + +class SQLWrapperImpl: SQLWrapper { + override fun loadSchemTypes(tmpTypes: MutableList?, tmpFromDB: MutableMap?) { + val folder = File("/configs/GameModes") + if (folder.exists()) { + for (configFile in Arrays.stream(folder.listFiles { file, name -> name.endsWith(".yml") && !name.endsWith(".kits.yml") }) + .sorted().collect(Collectors.toList())) { + val config: YamlConfiguration = YamlConfiguration.loadConfiguration(configFile) + if (!config.isConfigurationSection("Schematic")) continue + val type: String = config.getString("Schematic.Type")!! + val shortcut: String = config.getString("Schematic.Shortcut")!! + if (tmpFromDB!!.containsKey(type!!.lowercase(Locale.getDefault()))) continue + var checktype: SchematicType? = null + val material: String = config.getString("Schematic.Material", "STONE_BUTTON")!! + if (!config.getStringList("CheckQuestions").isEmpty()) { + checktype = SchematicType("C$type", "C$shortcut", SchematicType.Type.CHECK_TYPE, null, material) + tmpTypes!!.add(checktype) + tmpFromDB[checktype.toDB()] = checktype + } + val current = SchematicType( + type, + shortcut, + if (config.isConfigurationSection("Server")) SchematicType.Type.FIGHT_TYPE else SchematicType.Type.NORMAL, + checktype, + material + ) + tmpTypes!!.add(current) + tmpFromDB[type.lowercase(Locale.getDefault())] = current + } + } + + } + + override fun additionalExceptionMetadata(builder: StringBuilder) { + builder.append("EventAPI") + } +} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..bdbb64e --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + +