Compare commits
10 Commits
SettingsCo
...
api-v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
a8204a181b
|
|||
|
2208dcc0fb
|
|||
| b466216b3a | |||
| 5a862b251b | |||
| 60a82a685d | |||
| 573b0c14ae | |||
| 82abe7e20f | |||
| 34da59714e | |||
| 97071165cd | |||
| 634465fbf1 |
@@ -440,6 +440,13 @@ public final class GameModeConfig<M, W> {
|
||||
*/
|
||||
public final boolean DisableSnowMelt;
|
||||
|
||||
/**
|
||||
* Disable ice forming
|
||||
*
|
||||
* @implSpec {@code false} by default
|
||||
*/
|
||||
public final boolean DisableIceForm;
|
||||
|
||||
/**
|
||||
* Allow leaving the arena area as spectator
|
||||
*
|
||||
@@ -470,6 +477,7 @@ public final class GameModeConfig<M, W> {
|
||||
BorderFromSchematic = loader.getInt("BorderFromSchematic", 21);
|
||||
GroundWalkable = loader.getBoolean("GroundWalkable", true);
|
||||
DisableSnowMelt = loader.getBoolean("DisableSnowMelt", false);
|
||||
DisableIceForm = loader.getBoolean("DisableIceForm", false);
|
||||
Leaveable = loader.getBoolean("Leaveable", false);
|
||||
AllowMissiles = loader.getBoolean("AllowMissiles", !EnterStages.isEmpty());
|
||||
NoFloor = loader.getBoolean("NoFloor", false);
|
||||
|
||||
@@ -94,7 +94,7 @@ class NodeMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
|
||||
{ Optional.ofNullable(it?.value) })
|
||||
private set
|
||||
|
||||
fun setParentId(id: Int?) {
|
||||
fun setParentId(id: Int?) = useDb {
|
||||
parent = Optional.ofNullable(id)
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ class SteamwarUser(id: EntityID<Int>): IntEntity(id) {
|
||||
|
||||
var discordId by SteamwarUserTable.discordId
|
||||
|
||||
private val punishments by lazy { Punishment.getPunishmentsOfPlayer(id.value) }
|
||||
val punishments by lazy { Punishment.getPunishmentsOfPlayer(id.value) }
|
||||
private val perms by lazy { UserPerm.getPerms(id.value) }
|
||||
private val prefix by lazy { perms.firstOrNull { UserPerm.prefixes.containsKey(it) }?.let { UserPerm.prefixes[it]} ?: UserPerm.emptyPrefix }
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ class Team(id: EntityID<Int>) : IntEntity(id) {
|
||||
var deleted by TeamTable.deleted
|
||||
private set
|
||||
val members by lazy { useDb { SteamwarUserTable.select(SteamwarUserTable.id).where { SteamwarUserTable.team eq teamId }.map { it[SteamwarUserTable.id].value } } }
|
||||
val membersUser by lazy { useDb { SteamwarUser.find { SteamwarUserTable.team eq teamId }.toList() } }
|
||||
|
||||
fun size() = useDb { SteamwarUser.find { SteamwarUserTable.team eq teamId }.count().toInt() }
|
||||
fun disband(user: SteamwarUser) = useDb {
|
||||
|
||||
@@ -79,7 +79,7 @@ class TeamTeilnahme(id: EntityID<CompositeID>) : CompositeEntity(id) {
|
||||
|
||||
@JvmStatic
|
||||
fun getEvents(teamId: Int) = useDb {
|
||||
find { TeamTeilnahmeTable.teamId eq teamId }.map { Event.byId(it.eventId.value) }.toSet()
|
||||
find { TeamTeilnahmeTable.teamId eq teamId }.mapNotNull { Event.byId(it.eventId.value) }.toSet()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -33,9 +33,6 @@ import de.steamwar.fightsystem.states.FightState;
|
||||
import de.steamwar.fightsystem.states.OneShotStateDependent;
|
||||
import de.steamwar.fightsystem.states.StateDependentListener;
|
||||
import de.steamwar.fightsystem.utils.*;
|
||||
import de.steamwar.fightsystem.winconditions.Wincondition;
|
||||
import de.steamwar.fightsystem.winconditions.WinconditionComparisonTimeout;
|
||||
import de.steamwar.fightsystem.winconditions.Winconditions;
|
||||
import de.steamwar.linkage.AbstractLinker;
|
||||
import de.steamwar.linkage.SpigotLinker;
|
||||
import de.steamwar.message.Message;
|
||||
@@ -43,6 +40,7 @@ import de.steamwar.sql.NodeData;
|
||||
import de.steamwar.sql.SchematicNode;
|
||||
import lombok.Getter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameRule;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public class FightSystem extends JavaPlugin {
|
||||
@@ -100,6 +98,11 @@ public class FightSystem extends JavaPlugin {
|
||||
new StateDependentListener(ArenaMode.All, FightState.All, BountifulWrapper.impl.newDenyArrowPickupListener());
|
||||
new OneShotStateDependent(ArenaMode.All, FightState.PreSchemSetup, () -> Fight.playSound(SWSound.BLOCK_NOTE_PLING.getSound(), 100.0f, 2.0f));
|
||||
new OneShotStateDependent(ArenaMode.Test, FightState.All, WorldEditRendererCUIEditor::new);
|
||||
try {
|
||||
Bukkit.getWorlds().get(0).setGameRule(GameRule.REDUCED_DEBUG_INFO, ArenaMode.AntiTest.contains(Config.mode));
|
||||
} catch (Exception e) {
|
||||
// Ignore if failed!
|
||||
}
|
||||
|
||||
techHider = new TechHiderWrapper();
|
||||
hullHider = new HullHider();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 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.fightsystem.listener;
|
||||
|
||||
import de.steamwar.fightsystem.Config;
|
||||
import de.steamwar.fightsystem.states.FightState;
|
||||
import de.steamwar.fightsystem.states.StateDependentListener;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockFormEvent;
|
||||
|
||||
@Linked
|
||||
public class BlockFormListener implements Listener {
|
||||
|
||||
public BlockFormListener() {
|
||||
new StateDependentListener(Config.GameModeConfig.Arena.DisableIceForm, FightState.All, this);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBlockForm(BlockFormEvent event) {
|
||||
if (Config.ArenaRegion.inRegion(event.getBlock()) && event.getNewState().getType() == Material.ICE) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ package de.steamwar.towerrun.commands;
|
||||
import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.command.TypeValidator;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.linkage.LinkedInstance;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import de.steamwar.towerrun.TowerRun;
|
||||
@@ -30,6 +31,8 @@ import org.bukkit.entity.Player;
|
||||
|
||||
@Linked
|
||||
public class StartCommand extends SWCommand {
|
||||
|
||||
@LinkedInstance
|
||||
private LobbyCountdown countdown;
|
||||
|
||||
public StartCommand() {
|
||||
|
||||
@@ -159,8 +159,8 @@ public class TowerGenerator {
|
||||
noKeyFloors--;
|
||||
if (!chestBlocks.isEmpty() && noKeyFloors < 0 && random.nextDouble() < config.keyChance) {
|
||||
noKeyFloors = random.nextInt(config.maxNoKeyFloors - config.minNoKeyFloors) + config.minNoKeyFloors;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
Container container = chestBlocks.get(random.nextInt(chestBlocks.size()));
|
||||
for (int i = 0; i < 2 && !chestBlocks.isEmpty(); i++) {
|
||||
Container container = chestBlocks.remove(random.nextInt(chestBlocks.size()));
|
||||
keys.add(container.getLocation());
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.bukkit.event.entity.EntityRegainHealthEvent;
|
||||
import org.bukkit.event.entity.ItemSpawnEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.*;
|
||||
@@ -158,10 +159,16 @@ public class IngameListener extends GameStateBukkitListener {
|
||||
public void onKeyUse(PlayerInteractEvent event) {
|
||||
if (!event.hasItem()) return;
|
||||
if (event.getItem().getType() != Material.LEVER) return;
|
||||
event.setCancelled(true);
|
||||
if (!event.hasBlock()) return;
|
||||
if (event.getClickedBlock().getType() != Material.IRON_DOOR) return;
|
||||
event.getPlayer().getInventory().setItemInMainHand(null);
|
||||
if (event.getHand() == null) return;
|
||||
event.setCancelled(true);
|
||||
ItemStack itemStack = event.getItem();
|
||||
itemStack.setAmount(event.getItem().getAmount() - 1);
|
||||
switch (event.getHand()) {
|
||||
case OFF_HAND -> event.getPlayer().getInventory().setItemInOffHand(itemStack);
|
||||
case HAND -> event.getPlayer().getInventory().setItemInMainHand(itemStack);
|
||||
}
|
||||
event.getClickedBlock().breakNaturally();
|
||||
}
|
||||
|
||||
@@ -223,6 +230,8 @@ public class IngameListener extends GameStateBukkitListener {
|
||||
shouldMelt(block.getRelative(0, 0, -1));
|
||||
}
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private void shouldMelt(Block block) {
|
||||
if (block.getType().isBurnable()) return;
|
||||
if (block.getType().isAir()) return;
|
||||
@@ -269,7 +278,9 @@ public class IngameListener extends GameStateBukkitListener {
|
||||
break;
|
||||
}
|
||||
Pos pos = new Pos(block.getLocation().getBlockX(), block.getLocation().getBlockY(), block.getLocation().getBlockZ());
|
||||
blocksToMelt.putIfAbsent(pos, time + meltingTime + 1);
|
||||
int delay = meltingTime + 1 + RANDOM.nextInt(30*20)-30*10;
|
||||
if (delay < 0) delay = meltingTime + 1;
|
||||
blocksToMelt.putIfAbsent(pos, time + delay);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
|
||||
@@ -42,6 +42,10 @@ public abstract class OutsideWincondition extends WinCondition {
|
||||
@EventHandler
|
||||
public void onPlayerMove(PlayerMoveEvent event) {
|
||||
if (event.getTo().getY() > WorldConfig.ESCAPE_HEIGHT) {
|
||||
if (event.getTo().getY() > WorldConfig.ESCAPE_HEIGHT + 10 && Arrays.stream(WorldConfig.REGIONS).noneMatch(region -> region.contains(event.getTo().toVector()))) {
|
||||
TowerRunPlayer tPlayer = TowerRunPlayer.get(event.getPlayer());
|
||||
tPlayer.player().damage(Integer.MAX_VALUE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ public class BanListener extends BasicListener {
|
||||
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
|
||||
String ip = IPSanitizer.getTrueAddress(player).getHostAddress();
|
||||
if (user.isPunished(Punishment.PunishmentType.Ban)) {
|
||||
BannedUserIPs.banIP(user.getId(), ip);
|
||||
if (!player.getUsername().startsWith(".")) {
|
||||
BannedUserIPs.banIP(user.getId(), ip);
|
||||
}
|
||||
Chatter.of(event).system(PunishmentCommand.punishmentMessage(user, Punishment.PunishmentType.Ban));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,61 +47,67 @@ data class UsernamePassword(val name: String, val password: String, val keepLogg
|
||||
|
||||
fun Route.configureAuth() {
|
||||
route("/auth") {
|
||||
val client = HttpClient(Java) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
val request = call.receive<UsernamePassword>()
|
||||
|
||||
SteamwarUser.clear()
|
||||
val user = SteamwarUser.get(request.name)
|
||||
val valid = user?.verifyPassword(request.password) ?: false
|
||||
|
||||
if (!valid) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid"))
|
||||
return@post
|
||||
}
|
||||
|
||||
call.sessions.set(SWUserSession(user.getId()))
|
||||
call.respond(ResponseUser(user))
|
||||
}
|
||||
|
||||
delete {
|
||||
call.sessions.clear<SWUserSession>()
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
|
||||
route("/discord") {
|
||||
post {
|
||||
val token = call.receiveText()
|
||||
configureDiscordAuth()
|
||||
configurePasswordAuth()
|
||||
}
|
||||
}
|
||||
|
||||
val res = client.get("https://discord.com/api/v10/oauth2/@me") {
|
||||
headers {
|
||||
append("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
val resJson = Json.parseToJsonElement(res.bodyAsText()).jsonObject
|
||||
val discordId = resJson["user"]?.jsonObject["id"]?.jsonPrimitive?.content
|
||||
fun Route.configurePasswordAuth() {
|
||||
post {
|
||||
val request = call.receive<UsernamePassword>()
|
||||
|
||||
if (discordId == null) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid Discord token", "invalid"))
|
||||
return@post
|
||||
}
|
||||
SteamwarUser.clear()
|
||||
val user = SteamwarUser.get(request.name)
|
||||
val valid = user?.verifyPassword(request.password) ?: false
|
||||
|
||||
SteamwarUser.clear()
|
||||
val user = SteamwarUser.get(discordId.toLong())
|
||||
if (!valid) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid"))
|
||||
return@post
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Discord account not linked", "not_linked"))
|
||||
return@post
|
||||
}
|
||||
call.sessions.set(SWUserSession(user.getId()))
|
||||
call.respond(ResponseUser(user))
|
||||
}
|
||||
}
|
||||
|
||||
call.sessions.set(SWUserSession(user.getId()))
|
||||
call.respond(ResponseUser(user))
|
||||
}
|
||||
fun Route.configureDiscordAuth() {
|
||||
val client = HttpClient(Java) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post("/discord") {
|
||||
val token = call.receiveText()
|
||||
|
||||
val res = client.get("https://discord.com/api/v10/oauth2/@me") {
|
||||
headers {
|
||||
append("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
val resJson = Json.parseToJsonElement(res.bodyAsText()).jsonObject
|
||||
val discordId = resJson["user"]?.jsonObject["id"]?.jsonPrimitive?.content
|
||||
|
||||
if (discordId == null) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid Discord token", "invalid"))
|
||||
return@post
|
||||
}
|
||||
|
||||
SteamwarUser.clear()
|
||||
val user = SteamwarUser.get(discordId.toLong())
|
||||
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Discord account not linked", "not_linked"))
|
||||
return@post
|
||||
}
|
||||
|
||||
call.sessions.set(SWUserSession(user.getId()))
|
||||
call.respond(ResponseUser(user))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ data class ResponseUser(
|
||||
@Serializable
|
||||
data class ResponseUserList(val entries: List<ResponseUser>, val rows: Long)
|
||||
|
||||
private fun Query.addUserFilter(
|
||||
fun Query.addUserFilter(
|
||||
name: String? = null,
|
||||
uuid: UUID? = null,
|
||||
team: Set<Int>? = null,
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import java.sql.Timestamp
|
||||
import java.time.Instant
|
||||
|
||||
@@ -60,6 +61,8 @@ data class ResponseEventFight(
|
||||
@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)
|
||||
|
||||
constructor(row: ResultRow) : this(row[TeamTable.id].value, row[TeamTable.name], row[TeamTable.kuerzel], row[TeamTable.color])
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.routes.v2.configureAuthV2
|
||||
import de.steamwar.routes.v2.configureGameModeRoutes
|
||||
import de.steamwar.routes.v2.configureSchematicsV2Route
|
||||
import de.steamwar.routes.v2.configureSteamWarRoute
|
||||
import de.steamwar.routes.v2.configureTeamRoutes
|
||||
import de.steamwar.routes.v2.configureUsersRouteV2
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.routing.*
|
||||
@@ -34,6 +40,17 @@ fun Application.configureRoutes() {
|
||||
configureSchematic()
|
||||
configureAuth()
|
||||
configureAuditLog()
|
||||
route("/v2") {
|
||||
configureAuditLog()
|
||||
configurePage()
|
||||
configureEventsRoute()
|
||||
configureAuthV2()
|
||||
configureTeamRoutes()
|
||||
configureSteamWarRoute()
|
||||
configureUsersRouteV2()
|
||||
configureSchematicsV2Route()
|
||||
configureGameModeRoutes()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,31 @@ import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
@Serializable
|
||||
data class ResponseSchematic(val name: String, val id: Int, val type: String?, val owner: Int, val item: String, val lastUpdate: Long, val rank: Int, val replaceColor: Boolean, val allowReplay: Boolean) {
|
||||
constructor(node: SchematicNode) : this(node.name, node.getId(), node.schemtype?.name(), node.owner, node.item, node.lastUpdate.time, node.rank, node.replaceColor(), node.allowReplay())
|
||||
data class ResponseSchematic(
|
||||
val name: String,
|
||||
val id: Int,
|
||||
val type: String?,
|
||||
val owner: Int,
|
||||
val item: String,
|
||||
val lastUpdate: Long,
|
||||
val replaceColor: Boolean,
|
||||
val allowReplay: Boolean,
|
||||
val members: List<ResponseUser>
|
||||
) {
|
||||
constructor(node: SchematicNode) : this(
|
||||
node.name,
|
||||
node.getId(),
|
||||
node.schemtype.name(),
|
||||
node.owner,
|
||||
node.item,
|
||||
node.lastUpdate.time,
|
||||
node.replaceColor(),
|
||||
node.allowReplay(),
|
||||
node.members.map {
|
||||
ResponseUser(
|
||||
SteamwarUser.byId(it.member)!!
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -72,9 +95,12 @@ fun Route.configureSchematic() {
|
||||
val node = call.receiveSchematic() ?: return@get
|
||||
|
||||
val user = call.principal<SWAuthPrincipal>()?.user
|
||||
if(user != null && !node.accessibleByUser(user)) {
|
||||
if (user != null && !node.accessibleByUser(user)) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
SWException.log("User ${user.userName} tried to download schematic ${node.name} without permission", user.id.toString())
|
||||
SWException.log(
|
||||
"User ${user.userName} tried to download schematic ${node.name} without permission",
|
||||
user.id.toString()
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
@@ -83,8 +109,15 @@ fun Route.configureSchematic() {
|
||||
return@get
|
||||
}
|
||||
|
||||
call.response.header("Content-Disposition", "attachment; filename=\"${node.name}${data.nodeFormat.fileEnding}\"")
|
||||
call.respondBytes(data.schemData(false).readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK)
|
||||
call.response.header(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"${node.name}${data.nodeFormat.fileEnding}\""
|
||||
)
|
||||
call.respondBytes(
|
||||
data.schemData(false).readAllBytes(),
|
||||
contentType = ContentType.Application.OctetStream,
|
||||
status = HttpStatusCode.OK
|
||||
)
|
||||
}
|
||||
get("/info") {
|
||||
val node = call.receiveSchematic() ?: return@get
|
||||
@@ -100,18 +133,22 @@ fun Route.configureSchematic() {
|
||||
val schemName = file.name.substringBeforeLast(".")
|
||||
|
||||
if (SchematicNode.invalidSchemName(arrayOf(schemName))) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError(
|
||||
error = "INVALID_NAME"
|
||||
))
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest, ResponseError(
|
||||
error = "INVALID_NAME"
|
||||
)
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val schemType = file.name.substringAfterLast(".")
|
||||
|
||||
if (schemType != "schem" && schemType != "schematic") {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError(
|
||||
error = "INVALID_SUFFIX"
|
||||
))
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest, ResponseError(
|
||||
error = "INVALID_SUFFIX"
|
||||
)
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
@@ -146,22 +183,27 @@ fun Route.configureSchematic() {
|
||||
.value
|
||||
|
||||
if (fawe.equals("2.12.3-SNAPSHOT")) {
|
||||
SWException.log("Schematic with Bugged Version Uploaded", """
|
||||
SWException.log(
|
||||
"Schematic with Bugged Version Uploaded", """
|
||||
Schematic=$schemName
|
||||
User=${user.userName}
|
||||
Id=${user.id}
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
NodeData.saveFromStream(node, content.inputStream(), version)
|
||||
|
||||
call.respond(ResponseSchematic(node))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError(
|
||||
error = e.message ?: "GENERIC", code = "UPLOAD_ERROR"
|
||||
))
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest, ResponseError(
|
||||
error = e.message ?: "GENERIC", code = "UPLOAD_ERROR"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +220,7 @@ suspend fun ApplicationCall.receiveSchematic(fieldName: String = "code", delete:
|
||||
return null
|
||||
}
|
||||
|
||||
if(dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) {
|
||||
if (dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) {
|
||||
respond(HttpStatusCode.Gone)
|
||||
return null
|
||||
}
|
||||
|
||||
46
WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt
Normal file
46
WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 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.v2
|
||||
|
||||
import de.steamwar.plugins.SWUserSession
|
||||
import de.steamwar.routes.configureDiscordAuth
|
||||
import de.steamwar.routes.configurePasswordAuth
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.delete
|
||||
import io.ktor.server.routing.route
|
||||
import io.ktor.server.sessions.clear
|
||||
import io.ktor.server.sessions.sessions
|
||||
|
||||
fun Route.configureAuthV2() {
|
||||
route("/auth") {
|
||||
delete {
|
||||
call.sessions.clear<SWUserSession>()
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
|
||||
configureDiscordAuth()
|
||||
route("/legacy") {
|
||||
configurePasswordAuth()
|
||||
}
|
||||
}
|
||||
}
|
||||
55
WebsiteBackend/src/de/steamwar/routes/v2/GameModes.kt
Normal file
55
WebsiteBackend/src/de/steamwar/routes/v2/GameModes.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 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.v2
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.route
|
||||
import org.bspfsystems.yamlconfiguration.file.YamlConfiguration
|
||||
import java.io.File
|
||||
import kotlin.io.nameWithoutExtension
|
||||
|
||||
fun Route.configureGameModeRoutes() {
|
||||
route("/gamemodes") {
|
||||
get {
|
||||
call.respond(
|
||||
File("/configs/GameModes/").listFiles()!!
|
||||
.filter { it.name.endsWith(".yml") && !it.name.endsWith(".kits.yml") }
|
||||
.map { it.nameWithoutExtension })
|
||||
}
|
||||
get("/{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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
210
WebsiteBackend/src/de/steamwar/routes/v2/Schematics.kt
Normal file
210
WebsiteBackend/src/de/steamwar/routes/v2/Schematics.kt
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 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.v2
|
||||
|
||||
import com.sun.tools.jdeprscan.Main.call
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.routes.ResponseSchematic
|
||||
import de.steamwar.routes.ResponseSchematicType
|
||||
import de.steamwar.routes.UploadSchematic
|
||||
import de.steamwar.routes.nbt
|
||||
import de.steamwar.routes.receiveSchematic
|
||||
import de.steamwar.sql.NodeData
|
||||
import de.steamwar.sql.NodeData.SchematicFormat
|
||||
import de.steamwar.sql.NodeDownload
|
||||
import de.steamwar.sql.SWException
|
||||
import de.steamwar.sql.SchematicNode
|
||||
import de.steamwar.sql.SchematicType
|
||||
import dev.dewy.nbt.tags.collection.CompoundTag
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.auth.authentication
|
||||
import io.ktor.server.auth.principal
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.response.header
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondBytes
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.route
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.DataInputStream
|
||||
import java.util.Base64
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
data class ListedSchematicNode(val name: String, val id: Int, val type: String)
|
||||
|
||||
fun Route.configureSchematicsV2Route() {
|
||||
route("/schematics") {
|
||||
get("/types") {
|
||||
call.respond(SchematicType.values().filter { !it.check() }
|
||||
.map { ResponseSchematicType(it.name(), it.toDB()) })
|
||||
}
|
||||
|
||||
get("/list/{path...}") {
|
||||
val path = call.parameters.getAll("path")?.joinToString("/") ?: "/"
|
||||
|
||||
val user = call.authentication.principal<SWAuthPrincipal>()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.Unauthorized)
|
||||
return@get
|
||||
}
|
||||
|
||||
val node = SchematicNode.getNodeFromPath(user.user, path)
|
||||
call.respond(SchematicNode.list(user.user, node?.id?.value).map { ListedSchematicNode(it.name, it.id.value, it.schemtype.toDB()) })
|
||||
}
|
||||
|
||||
get("/download/{code}") {
|
||||
val node = call.receiveSchematic() ?: return@get
|
||||
|
||||
val user = call.principal<SWAuthPrincipal>()?.user
|
||||
if(user != null && !node.accessibleByUser(user)) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
SWException.log("User ${user.userName} tried to download schematic ${node.name} without permission", user.id.toString())
|
||||
return@get
|
||||
}
|
||||
|
||||
val data = NodeData.getLatest(node) ?: run {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.response.header("Content-Disposition", "attachment; filename=\"${node.name}${data.nodeFormat.fileEnding}\"")
|
||||
call.respondBytes(data.schemData(false).readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
post("/upload") {
|
||||
val user = call.principal<SWAuthPrincipal>()?.user
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.Unauthorized)
|
||||
return@post
|
||||
}
|
||||
|
||||
val file = call.receive<UploadSchematic>()
|
||||
val schemName = file.name.substringBeforeLast(".")
|
||||
|
||||
if (SchematicNode.invalidSchemName(arrayOf(schemName))) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError(
|
||||
error = "INVALID_NAME"
|
||||
))
|
||||
return@post
|
||||
}
|
||||
|
||||
val schemType = file.name.substringAfterLast(".")
|
||||
|
||||
if (schemType != "schem" && schemType != "schematic") {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError(
|
||||
error = "INVALID_SUFFIX"
|
||||
))
|
||||
return@post
|
||||
}
|
||||
|
||||
var node = SchematicNode.getSchematicNode(user.getId(), schemName, null as Int?)
|
||||
if (node == null) {
|
||||
node = SchematicNode.createSchematic(user.getId(), schemName, null)
|
||||
}
|
||||
|
||||
try {
|
||||
val content = Base64.getDecoder().decode(file.content)
|
||||
|
||||
var schem = nbt.fromStream(DataInputStream(BufferedInputStream(GZIPInputStream(content.inputStream()))))
|
||||
|
||||
if (schem.size() == 1) schem = schem.first() as CompoundTag
|
||||
|
||||
val version = schem.let {
|
||||
if (it.contains("Materials"))
|
||||
return@let SchematicFormat.MCEDIT
|
||||
else if (it.contains("Blocks"))
|
||||
return@let SchematicFormat.SPONGE_V3
|
||||
else
|
||||
return@let SchematicFormat.SPONGE_V2
|
||||
}
|
||||
|
||||
if (version == SchematicFormat.SPONGE_V3) {
|
||||
try {
|
||||
val fawe = schem.getCompound("Metadata")
|
||||
.getCompound("WorldEdit")
|
||||
.getString("Version")
|
||||
.value
|
||||
|
||||
if (fawe.equals("2.12.3-SNAPSHOT")) {
|
||||
SWException.log("Schematic with Bugged Version Uploaded", """
|
||||
Schematic=$schemName
|
||||
User=${user.userName}
|
||||
Id=${user.id}
|
||||
""".trimIndent())
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
NodeData.saveFromStream(node, content.inputStream(), version)
|
||||
|
||||
call.respond(ResponseSchematic(node))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError(
|
||||
error = e.message ?: "GENERIC", code = "UPLOAD_ERROR"
|
||||
))
|
||||
}
|
||||
}
|
||||
route("/{id}") {
|
||||
install(SWPermissionCheck) {
|
||||
mustAuth = true
|
||||
}
|
||||
|
||||
get {
|
||||
val node = call.receiveSchem() ?: return@get
|
||||
call.respond(ResponseSchematic(node))
|
||||
}
|
||||
|
||||
post("/download") {
|
||||
val node = call.receiveSchem() ?: return@post
|
||||
|
||||
call.respond(HttpStatusCode.OK, NodeDownload.getLink(node))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun ApplicationCall.receiveSchem(): SchematicNode? {
|
||||
val schemId = parameters["id"]?.toIntOrNull()
|
||||
if (schemId == null) {
|
||||
respond(HttpStatusCode.BadRequest)
|
||||
return null
|
||||
}
|
||||
|
||||
val schem = SchematicNode.getSchematicNode(schemId)
|
||||
if (schem == null) {
|
||||
respond(HttpStatusCode.NotFound)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!(principal<SWAuthPrincipal>()?.user?.let { schem.accessibleByUser(it) } ?: false)) {
|
||||
respond(HttpStatusCode.Forbidden)
|
||||
return null
|
||||
}
|
||||
|
||||
return schem
|
||||
}
|
||||
60
WebsiteBackend/src/de/steamwar/routes/v2/SteamWar.kt
Normal file
60
WebsiteBackend/src/de/steamwar/routes/v2/SteamWar.kt
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 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.v2
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.routes.ResponseUser
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import de.steamwar.sql.UserPerm
|
||||
import de.steamwar.util.fetchData
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.route
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
fun Route.configureSteamWarRoute() {
|
||||
route("/steamwar") {
|
||||
get {
|
||||
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) } })
|
||||
}
|
||||
}
|
||||
}
|
||||
87
WebsiteBackend/src/de/steamwar/routes/v2/Teams.kt
Normal file
87
WebsiteBackend/src/de/steamwar/routes/v2/Teams.kt
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 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.v2
|
||||
|
||||
import de.steamwar.routes.ResponseEvent
|
||||
import de.steamwar.routes.ResponseTeam
|
||||
import de.steamwar.routes.ResponseUser
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import de.steamwar.sql.Team
|
||||
import de.steamwar.sql.TeamTable
|
||||
import de.steamwar.sql.TeamTeilnahme
|
||||
import de.steamwar.sql.internal.useDb
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.route
|
||||
import kotlinx.serialization.Serializable
|
||||
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
|
||||
|
||||
fun Query.addTeamFilter(teamName: String?, teamKuerzel: String?): Query {
|
||||
teamName?.let { andWhere { TeamTable.name like "%$it%" } }
|
||||
teamKuerzel?.let { andWhere { TeamTable.kuerzel like "%$it%" } }
|
||||
return this
|
||||
}
|
||||
|
||||
fun Route.configureTeamRoutes() {
|
||||
route("/teams") {
|
||||
get {
|
||||
val teamName = call.request.queryParameters["name"]
|
||||
val teamKuerzel = call.request.queryParameters["kuerzel"]
|
||||
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
||||
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
|
||||
|
||||
call.respond(useDb {
|
||||
TeamTable.selectAll().addTeamFilter(teamName, teamKuerzel).limit(limit).offset((page * limit).toLong())
|
||||
.map { ResponseTeam(it) }
|
||||
})
|
||||
}
|
||||
|
||||
get("/{team}") {
|
||||
@Serializable
|
||||
data class TeamResponse(
|
||||
val team: ResponseTeam,
|
||||
val members: List<ResponseUser>,
|
||||
val leaders: List<ResponseUser>,
|
||||
val events: List<ResponseEvent>
|
||||
)
|
||||
|
||||
val team = call.parameters["team"]?.toIntOrNull()?.let { Team.byId(it) }
|
||||
if (team == null) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(useDb {
|
||||
TeamResponse(
|
||||
ResponseTeam(team),
|
||||
team.membersUser.map { ResponseUser(it) },
|
||||
team.membersUser.filter { it.leader }.map { ResponseUser(it) },
|
||||
TeamTeilnahme.getEvents(team.teamId).map { ResponseEvent(it) })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
274
WebsiteBackend/src/de/steamwar/routes/v2/Users.kt
Normal file
274
WebsiteBackend/src/de/steamwar/routes/v2/Users.kt
Normal file
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 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.v2
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.data.getCachedSkin
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.routes.ResponseTeam
|
||||
import de.steamwar.routes.ResponseUser
|
||||
import de.steamwar.routes.ResponseUserList
|
||||
import de.steamwar.routes.UserStats
|
||||
import de.steamwar.routes.addUserFilter
|
||||
import de.steamwar.routes.catchException
|
||||
import de.steamwar.sql.Punishment
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import de.steamwar.sql.SteamwarUserTable
|
||||
import de.steamwar.sql.Team
|
||||
import de.steamwar.sql.UserPerm
|
||||
import de.steamwar.sql.internal.useDb
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.auth.principal
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.response.header
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondFile
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.delete
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.put
|
||||
import io.ktor.server.routing.route
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import java.sql.Timestamp
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class ResponsePunishment(
|
||||
val id: Int,
|
||||
val type: Punishment.PunishmentType,
|
||||
val reason: String,
|
||||
val issuer: ResponseUser,
|
||||
val startTime: Long,
|
||||
val endTime: Long,
|
||||
val perma: Boolean,
|
||||
val active: Boolean
|
||||
) {
|
||||
constructor(punishment: Punishment) : this(
|
||||
punishment.id.value,
|
||||
punishment.type,
|
||||
punishment.reason,
|
||||
ResponseUser(SteamwarUser.byId(punishment.punisher)!!),
|
||||
punishment.startTime.toInstant().toEpochMilli(),
|
||||
punishment.endTime.toInstant().toEpochMilli(),
|
||||
punishment.perma,
|
||||
punishment.isCurrent()
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CreatePunishment(
|
||||
val type: Punishment.PunishmentType,
|
||||
val reason: String,
|
||||
val perma: Boolean,
|
||||
val endTime: Long
|
||||
)
|
||||
|
||||
fun Route.configureUsersRouteV2() {
|
||||
get("/users/{user}/skin") {
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val skin = getCachedSkin(user.uuid.toString())
|
||||
call.response.header("X-Cache", if (skin.second) "HIT" else "MISS")
|
||||
call.response.header("Cache-Control", "public, max-age=604800")
|
||||
call.respondFile(skin.first)
|
||||
}
|
||||
get("/users/{user}/stats") {
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val authUser = call.principal<SWAuthPrincipal>()
|
||||
|
||||
if (authUser == null || !(authUser.user.id == user.id || authUser.user.hasPerm(UserPerm.MODERATION))) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(UserStats(user))
|
||||
}
|
||||
route("/users") {
|
||||
install(SWPermissionCheck) {
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
get {
|
||||
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()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
route("/{user}") {
|
||||
get {
|
||||
@Serializable
|
||||
data class UserResponse(val user: ResponseUser, val team: ResponseTeam)
|
||||
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(
|
||||
useDb {
|
||||
UserResponse(ResponseUser(user), ResponseTeam(Team.byId(user.team)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
put("/prefix/{prefix}") {
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
|
||||
return@put
|
||||
}
|
||||
|
||||
val prefix = call.parameters["prefix"]?.let { name -> UserPerm.entries.find { it.name == name } }
|
||||
if (prefix == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid prefix"))
|
||||
return@put
|
||||
}
|
||||
|
||||
user.perms().filter { it.name.startsWith("PREFIX_") }.forEach {
|
||||
UserPerm.removePerm(user, it)
|
||||
}
|
||||
|
||||
if (prefix != UserPerm.PREFIX_NONE) {
|
||||
UserPerm.addPerm(user, prefix)
|
||||
}
|
||||
call.respond(HttpStatusCode.Accepted)
|
||||
}
|
||||
|
||||
route("/perms") {
|
||||
get {
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(user.perms())
|
||||
}
|
||||
route("/{perm}") {
|
||||
put {
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
|
||||
return@put
|
||||
}
|
||||
|
||||
val perm = call.parameters["perm"]?.let { name -> UserPerm.entries.find { it.name == name } }
|
||||
if (perm == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid perm"))
|
||||
return@put
|
||||
}
|
||||
|
||||
UserPerm.addPerm(user, perm)
|
||||
call.respond(HttpStatusCode.Accepted)
|
||||
}
|
||||
delete {
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
|
||||
return@delete
|
||||
}
|
||||
|
||||
val perm = call.parameters["perm"]?.let { name -> UserPerm.entries.find { it.name == name } }
|
||||
if (perm == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid perm"))
|
||||
return@delete
|
||||
}
|
||||
|
||||
UserPerm.removePerm(user, perm)
|
||||
call.respond(HttpStatusCode.Accepted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route("/punishments") {
|
||||
get {
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val punishments = user.punishments.toList()
|
||||
|
||||
call.respond(punishments.map { ResponsePunishment(it.second) })
|
||||
}
|
||||
post {
|
||||
val user = call.receiveUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
|
||||
return@post
|
||||
}
|
||||
|
||||
val punishment = call.receive<CreatePunishment>()
|
||||
val punisher = call.principal<SWAuthPrincipal>()?.user ?: return@post
|
||||
|
||||
user.punish(punishment.type, Timestamp.from(Instant.ofEpochMilli(punishment.endTime)), punishment.reason, punisher.getId(), punishment.perma)
|
||||
|
||||
call.respond(HttpStatusCode.Accepted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ApplicationCall.receiveUser(): SteamwarUser? {
|
||||
val userString = parameters["user"] ?: return null
|
||||
|
||||
if (userString == "me") {
|
||||
return principal<SWAuthPrincipal>()?.user
|
||||
}
|
||||
|
||||
userString.toIntOrNull()?.let { return SteamwarUser.byId(it) }
|
||||
userString.let { catchException { UUID.fromString(it) } }?.let { return SteamwarUser.get(it) }
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user