/* * 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.plugins.SWPermissionCheck import de.steamwar.sql.* import de.steamwar.sql.EventGroup.EventGroupType import de.steamwar.sql.EventRelation.FromType 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.lang.StringBuilder import java.sql.Timestamp import java.time.Instant import java.util.* @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 ResponseGroups( val id: Int, val name: String, val pointsPerWin: Int, val pointsPerLoss: Int, val pointsPerDraw: Int, val type: EventGroupType, val points: Map ) { constructor(group: EventGroup, short: Boolean = false) : this( group.getId(), group.name, group.pointsPerWin, group.pointsPerLoss, group.pointsPerDraw, group.type, if (short) mapOf() else group.calculatePoints().mapKeys { it.key.teamId }) } @Serializable data class ResponseRelation( val id: Int, val fight: Int, val team: EventRelation.FightTeam, val type: FromType, val fromFight: ResponseEventFight? = null, val fromGroup: ResponseGroups? = null, val fromPlace: Int ) { constructor(relation: EventRelation) : this( relation.getId(), relation.fightId, relation.fightTeam, relation.fromType, relation.fromFight?.let { ResponseEventFight(it) }, relation.fromGroup?.let { ResponseGroups(it) }, relation.fromPlace ) } @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, ) { constructor(event: Event) : this( event.eventID, event.eventName, event.deadline.time, event.start.time, event.end.time, event.maximumTeamMembers, event.schematicType?.toDB(), event.publicSchemsOnly(), ) } @Serializable data class ExtendedResponseEvent( val event: ResponseEvent, val teams: List, val groups: List, val fights: List, val referees: List, val relations: List ) { constructor(event: Event) : this( ResponseEvent(event), TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) }, EventGroup.get(event).map { ResponseGroups(it) }, EventFight.getEvent(event.eventID).map { ResponseEventFight(it) }, Referee.get(event.eventID).map { ResponseUser(SteamwarUser.byId(it)!!) }, EventRelation.get(event).map { ResponseRelation(it) } ) } @Serializable data class CreateEvent(val name: String, val start: Long, val end: Long) @Serializable data class UpdateEvent( val name: String? = null, val deadline: Long? = null, val start: Long? = null, val end: Long? = null, val maxTeamMembers: Int? = null, val schemType: String? = null, val publicSchemsOnly: Boolean? = null, val addReferee: Set? = null, val removeReferee: Set? = null, ) fun Route.configureEventsRoute() { route("/events") { install(SWPermissionCheck) { allowMethod(HttpMethod.Get) permission = UserPerm.MODERATION } get { call.respond(Event.getAll().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 event = call.receiveEvent() ?: return@get call.respond( ExtendedResponseEvent(event) ) } get("/csv") { val event = call.receiveEvent() ?: return@get val fights = EventFight.getEvent(event.eventID) val csv = StringBuilder(); csv.append(arrayOf("Start", "BlueTeam", "RedTeam", "WinnerTeam", "Group").joinToString(",")) fights.forEach { csv.appendLine() val blue = Team.byId(it.teamBlue) val red = Team.byId(it.teamRed) val winner = when (it.ergebnis) { 1 -> blue.teamName 2 -> red.teamName 3 -> "Tie" else -> "Unknown" } csv.append( arrayOf( it.startTime.toString(), Team.byId(it.teamBlue).teamName, Team.byId(it.teamRed).teamName, winner, it.group.map { it.name }.orElse("Ungrouped") ).joinToString(",") ) } call.response.header("Content-Disposition", "attachment; filename=\"${event.eventName}.csv\"") call.response.header("Content-Type", "text/csv") call.response.header("Content-Transfer-Encoding", "binary") call.response.header("Pragma", "no-cache") call.respondText(csv.toString()) } put { val event = call.receiveEvent() ?: return@put val updateEvent = call.receiveNullable() if (updateEvent == null) { call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) return@put } val eventName = updateEvent.name ?: event.eventName val deadline = updateEvent.deadline?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.deadline val start = updateEvent.start?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.start val end = updateEvent.end?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.end val maxTeamMembers = updateEvent.maxTeamMembers ?: event.maximumTeamMembers val schemType = if (updateEvent.schemType == "null") null else updateEvent.schemType?.let { SchematicType.fromDB(it) } ?: event.schematicType val publicSchemsOnly = updateEvent.publicSchemsOnly ?: event.publicSchemsOnly() if (updateEvent.addReferee != null) { updateEvent.addReferee.forEach { Referee.add(event.eventID, SteamwarUser.get(UUID.fromString(it))!!.getId()) } } if (updateEvent.removeReferee != null) { updateEvent.removeReferee.forEach { Referee.remove(event.eventID, SteamwarUser.get(UUID.fromString(it))!!.getId()) } } event.update(eventName, deadline, start, end, schemType, maxTeamMembers, publicSchemsOnly) call.respond(ResponseEvent(Event.byId(event.eventID)!!)) } delete { val id = call.parameters["id"]?.toIntOrNull() if (id == null) { call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) return@delete } val event = Event.byId(id) if (event == null) { call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) return@delete } event.delete() call.respond(HttpStatusCode.NoContent) } configureEventFightRoutes() configureEventTeams() configureEventGroups() configureEventRelations() configureEventRefereesRouting() } } } suspend fun ApplicationCall.receiveEvent(fieldName: String = "id"): Event? { val eventId = parameters[fieldName]?.toIntOrNull() if (eventId == null) { respond(HttpStatusCode.BadRequest, ResponseError("Invalid event ID")) return null } val event = Event.byId(eventId) if (event == null) { respond(HttpStatusCode.NotFound, ResponseError("Event not found")) return null } return event }