Files
SteamWar/WebsiteBackend/src/de/steamwar/routes/Events.kt
T
2025-10-27 18:34:31 +01:00

273 lines
9.7 KiB
Kotlin

/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<Int, Int>
) {
constructor(group: EventGroup, short: Boolean = false) : this(
group.id,
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.id,
relation.fightId,
relation.fightTeam,
relation.fromType,
relation.fromFight.map { ResponseEventFight(it) }.orElse(null),
relation.fromGroup.map { ResponseGroups(it) }.orElse(null),
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<ResponseTeam>,
val groups: List<ResponseGroups>,
val fights: List<ResponseEventFight>,
val referees: List<ResponseUser>,
val relations: List<ResponseRelation>
) {
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<String>? = null,
val removeReferee: Set<String>? = 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<CreateEvent>()
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.get(it.teamBlue)
val red = Team.get(it.teamRed)
val winner = when (it.ergebnis) {
1 -> blue.teamName
2 -> red.teamName
3 -> "Tie"
else -> "Unknown"
}
csv.append(
arrayOf(
it.startTime.toString(),
Team.get(it.teamBlue).teamName,
Team.get(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<UpdateEvent>()
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.get(event.eventID)))
}
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()
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.get(eventId)
if (event == null) {
respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
return null
}
return event
}