/* * This file is a part of the SteamWar software. * * Copyright (C) 2025 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.routes import de.steamwar.ResponseError import de.steamwar.data.getCachedSkin import de.steamwar.plugins.SWAuthPrincipal import de.steamwar.plugins.SWPermissionCheck import de.steamwar.sql.* import de.steamwar.sql.internal.useDb import de.steamwar.util.fetchData import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.serialization.Serializable import org.bspfsystems.yamlconfiguration.file.YamlConfiguration import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.inList import org.jetbrains.exposed.v1.core.like import org.jetbrains.exposed.v1.jdbc.Query import org.jetbrains.exposed.v1.jdbc.andWhere import org.jetbrains.exposed.v1.jdbc.selectAll import java.io.File import java.net.InetSocketAddress import java.util.* @Serializable data class ResponseSchematicType(val name: String, val db: String) @Serializable data class ResponseUser( val name: String, val uuid: String, val id: Int?, val perms: List?, val prefix: String? ) { constructor(user: SteamwarUser, includeId: Boolean = false, includePerms: Boolean = false) : this( user.userName, user.uuid.toString(), if (includeId) user.getId() else null, if (includePerms) user.perms().map { it.name } else null, if (includePerms) user.prefix().chatPrefix else null, ) constructor(row: ResultRow, includeId: Boolean = false, includePerms: Boolean = false, prefixes: MutableSet = mutableSetOf()) : this( row[SteamwarUserTable.username], row[SteamwarUserTable.uuid], if (includeId) row[SteamwarUserTable.id].value else null, if (includePerms) UserPerm.getPerms(row[SteamwarUserTable.id].value).also { prefixes.addAll(it) }.map { it.name } else null, if (includePerms) prefixes.firstOrNull { UserPerm.prefixes.containsKey(it) }?.let { UserPerm.prefixes[it]!!.chatPrefix } else null ) } @Serializable data class ResponseUserList(val entries: List, val rows: Long) private fun Query.addUserFilter( name: String? = null, uuid: UUID? = null, team: Set? = null, ): Query { name?.let { andWhere { (SteamwarUserTable.username like "%$it%") } } uuid?.let { andWhere { (SteamwarUserTable.uuid eq it.toString()) } } team?.let { andWhere { (SteamwarUserTable.team inList team) } } return this } fun Route.configureDataRoutes() { route("/data") { route("/admin") { install(SWPermissionCheck) { mustAuth = true permission = UserPerm.MODERATION } get("/users") { val name = call.request.queryParameters["name"] val uuid = call.request.queryParameters["uuid"]?.let { catchException { UUID.fromString(it) } } val team = call.request.queryParameters.getAll("team")?.map { it.toInt() }?.toSet() val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100 val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0 val includePerms = call.request.queryParameters["includePerms"]?.toBoolean() ?: false val includeId = call.request.queryParameters["includeId"]?.toBoolean() ?: false call.respond( useDb { ResponseUserList( SteamwarUserTable.selectAll().addUserFilter(name, uuid, team).limit(limit) .offset((page * limit).toLong()) .map { ResponseUser(it, includeId, includePerms) }, SteamwarUserTable.selectAll().addUserFilter(name, uuid, team).count() ) } ) } get("/teams") { call.respond(Team.getAll().map { ResponseTeam(it) }) } 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("/server") { try { val server = fetchData(InetSocketAddress("steamwar.de", 25565), 100) call.respond(server) } catch (e: Exception) { e.printStackTrace() call.respond( HttpStatusCode.InternalServerError, ResponseError( e.message ?: "Unknown error" ) ) return@get } } get("/team") { call.respond( listOf( UserPerm.PREFIX_ADMIN, UserPerm.PREFIX_DEVELOPER, UserPerm.PREFIX_MODERATOR, UserPerm.PREFIX_SUPPORTER, UserPerm.PREFIX_BUILDER ).associateWith { SteamwarUser.getUsersWithPerm(it) }.mapKeys { UserPerm.prefixes[it.key]!!.chatPrefix } .mapValues { it.value.map { ResponseUser(it) } }) } 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.response.header("Cache-Control", "public, max-age=604800") call.respondFile(skin.first) } route("/me") { install(SWPermissionCheck) get { call.respond(ResponseUser(call.principal()!!.user, includePerms = true)) } } } } inline fun catchException(yield: () -> T): T? = try { yield() } catch (e: Exception) { e.printStackTrace() null }