Merge branch 'main' into TNTLeague/finish

This commit is contained in:
2024-11-24 22:13:01 +01:00
44 changed files with 2414 additions and 55 deletions

View File

@@ -56,7 +56,7 @@ public class BauScoreboard implements Listener {
public void handlePlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
SWScoreboard.createScoreboard(player, new ScoreboardCallback() {
SWScoreboard.impl.createScoreboard(player, new ScoreboardCallback() {
@Override
public HashMap<String, Integer> getData() {
Region region = Region.getRegion(player.getLocation());

View File

@@ -21,6 +21,7 @@ package de.steamwar.sql;
import de.steamwar.sql.internal.Field;
import de.steamwar.sql.internal.SelectStatement;
import de.steamwar.sql.internal.Statement;
import de.steamwar.sql.internal.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -42,6 +43,11 @@ public class Event {
private static final SelectStatement<Event> byId = table.select(Table.PRIMARY);
private static final SelectStatement<Event> byName = table.select("eventName");
private static final SelectStatement<Event> byComing = new SelectStatement<>(table, "SELECT * FROM Event WHERE Start > now()");
private static final SelectStatement<Event> all = new SelectStatement<>(table, "SELECT * FROM Event");
private static final Statement create = table.insertFields(true, "eventName", "deadline", "start", "end", "maximumTeamMembers", "publicSchemsOnly");
private static final Statement update = table.update(Table.PRIMARY, "eventName", "deadline", "start", "end", "schemType", "maximumTeamMembers", "publicSchemsOnly");
private static final Statement delete = table.delete(Table.PRIMARY);
private static Event current = null;
@@ -53,6 +59,14 @@ public class Event {
return current;
}
public static List<Event> getAll(){
return all.listSelect();
}
public static Event create(String eventName, Timestamp start, Timestamp end){
return get(create.insertGetKey(eventName, start, start, end, 5, false, false));
}
public static Event get(int eventID){
return byId.select(eventID);
}
@@ -87,16 +101,10 @@ public class Event {
private final SchematicType schemType;
@Field
private final boolean publicSchemsOnly;
@Deprecated
@Field
private final boolean spectateSystem;
public boolean publicSchemsOnly() {
return publicSchemsOnly;
}
public boolean spectateSystem(){
return spectateSystem;
}
public SchematicType getSchematicType() {
return schemType;
@@ -106,4 +114,12 @@ public class Event {
Instant now = Instant.now();
return now.isAfter(start.toInstant()) && now.isBefore(end.toInstant());
}
public void update(String eventName, Timestamp deadline, Timestamp start, Timestamp end, SchematicType schemType, int maximumTeamMembers, boolean publicSchemsOnly) {
update.update(eventName, deadline, start, end, schemType, maximumTeamMembers, publicSchemsOnly, eventID);
}
public void delete() {
delete.update(eventID);
}
}

View File

@@ -25,6 +25,7 @@ import de.steamwar.sql.internal.Statement;
import de.steamwar.sql.internal.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.sql.Timestamp;
import java.util.*;
@@ -42,6 +43,11 @@ public class EventFight implements Comparable<EventFight> {
private static final Statement setResult = table.update(Table.PRIMARY, "Ergebnis");
private static final Statement setFight = table.update(Table.PRIMARY, "Fight");
private static final Statement create = table.insertAll(true);
private static final Statement update = table.update(Table.PRIMARY, "startTime", "spielModus", "map", "teamBlue", "teamRed", "spectatePort");
private static final Statement delete = table.delete(Table.PRIMARY);
@Getter
private static final Queue<EventFight> fights = new PriorityQueue<>();
public static EventFight get(int fightID) {
@@ -57,8 +63,8 @@ public class EventFight implements Comparable<EventFight> {
return event.listSelect(eventID);
}
public static Queue<EventFight> getFights() {
return fights;
public static EventFight create(int event, Timestamp from, String spielmodus, String map, int blueTeam, int redTeam, Integer spectatePort) {
return get(create.insertGetKey(event, from, spielmodus, map, blueTeam, redTeam, spectatePort));
}
@Getter
@@ -68,27 +74,29 @@ public class EventFight implements Comparable<EventFight> {
@Field(keys = {Table.PRIMARY}, autoincrement = true)
private final int fightID;
@Getter
@Setter
@Field
private Timestamp startTime;
@Getter
@Setter
@Field
private final String spielmodus;
private String spielmodus;
@Getter
@Setter
@Field
private final String map;
private String map;
@Getter
@Setter
@Field
private final int teamBlue;
private int teamBlue;
@Getter
@Setter
@Field
private final int teamRed;
private int teamRed;
@Getter
@Field
@Deprecated
private final int kampfleiter;
@Getter
@Field
private final int spectatePort;
@Setter
@Field(nullable = true)
private Integer spectatePort;
@Getter
@Field(def = "0")
private int ergebnis;
@@ -133,4 +141,18 @@ public class EventFight implements Comparable<EventFight> {
public int compareTo(EventFight o) {
return startTime.compareTo(o.startTime);
}
public void update(Timestamp startTime, String spielmodus, String map, int teamBlue, int teamRed, Integer spectatePort) {
update.update(startTime, spielmodus, map, teamBlue, teamRed, spectatePort, fightID);
this.startTime = startTime;
this.spielmodus = spielmodus;
this.map = map;
this.teamBlue = teamBlue;
this.teamRed = teamRed;
this.spectatePort = spectatePort;
}
public void delete() {
delete.update(fightID);
}
}

View File

@@ -20,9 +20,11 @@
package de.steamwar.sql;
import de.steamwar.sql.internal.Field;
import de.steamwar.sql.internal.SelectStatement;
import de.steamwar.sql.internal.Statement;
import de.steamwar.sql.internal.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -38,11 +40,21 @@ public class NodeDownload {
private static final Table<NodeDownload> table = new Table<>(NodeDownload.class);
private static final Statement insert = table.insertFields("NodeId", "Link");
private static final SelectStatement<NodeDownload> select = table.selectFields("link");
private static final Statement delete = table.delete(Table.PRIMARY);
public static NodeDownload get(String link) {
return select.select(link);
}
@Getter
@Field(keys = {Table.PRIMARY})
private final int nodeId;
@Field
private final String link;
@Field(def = "CURRENT_TIMESTAMP")
@Getter
private final Timestamp timestamp;
public static String getLink(SchematicNode schem){
@@ -60,10 +72,15 @@ public class NodeDownload {
insert.update(schem.getId(), hash);
return LINK_BASE + hash;
}
public static String base16encode(byte[] byteArray) {
StringBuilder hexBuffer = new StringBuilder(byteArray.length * 2);
for (byte b : byteArray)
hexBuffer.append(HEX[(b >>> 4) & 0xF]).append(HEX[b & 0xF]);
return hexBuffer.toString();
}
public void delete() {
delete.update(nodeId);
}
}

View File

@@ -72,6 +72,10 @@ public class NodeMember {
return new NodeMember(node, member, null);
}
public static NodeMember getNodeMember(int node, SteamwarUser member) {
return getNodeMember(node, member.getId());
}
public static NodeMember getNodeMember(int node, int member) {
return getNodeMember.select(node, member);
}

View File

@@ -21,6 +21,7 @@ package de.steamwar.sql;
import de.steamwar.sql.internal.Field;
import de.steamwar.sql.internal.SelectStatement;
import de.steamwar.sql.internal.Statement;
import de.steamwar.sql.internal.Table;
import lombok.AllArgsConstructor;
@@ -33,6 +34,17 @@ public class Referee {
private static final Table<Referee> table = new Table<>(Referee.class);
private static final SelectStatement<Referee> byEvent = table.selectFields("eventID");
private static final Statement insert = table.insertAll();
private static final Statement delete = table.delete("eventReferee");
public static void add(int eventID, int userID) {
insert.update(eventID, userID);
}
public static void remove(int eventID, int userID) {
delete.update(eventID, userID);
}
public static Set<Integer> get(int eventID) {
return byEvent.listSelect(eventID).stream().map(referee -> referee.userID).collect(Collectors.toSet());
}

View File

@@ -443,7 +443,7 @@ public class SchematicNode {
return SchemElo.getElo(this, season);
}
public boolean accessibleByUser(int user) {
public boolean accessibleByUser(SteamwarUser user) {
return NodeMember.getNodeMember(nodeId, user) != null;
}
@@ -506,6 +506,19 @@ public class SchematicNode {
return builder.toString();
}
public List<Map.Entry<String, Integer>> generateBreadcrumbsMap(SteamwarUser user) {
List<Map.Entry<String, Integer>> map = new ArrayList<>();
Optional<SchematicNode> currentNode = Optional.of(this);
if(currentNode.map(SchematicNode::isDir).orElse(false)) {
map.add(new AbstractMap.SimpleEntry<>(getName(), getId()));
}
while (currentNode.isPresent()) {
currentNode = currentNode.flatMap(schematicNode -> Optional.ofNullable(NodeMember.getNodeMember(schematicNode.getId(), effectiveOwner)).map(NodeMember::getParent).orElse(schematicNode.getOptionalParent())).map(SchematicNode::getSchematicNode);
currentNode.ifPresent(node -> map.add(0, new AbstractMap.SimpleEntry<>(node.getName(), node.getId())));
}
return map;
}
private static final List<String> FORBIDDEN_NAMES = Collections.unmodifiableList(Arrays.asList("public"));
public static boolean invalidSchemName(String[] layers) {
for (String layer : layers) {

View File

@@ -108,7 +108,7 @@ public class SchematicType {
return name.toLowerCase();
}
public static SchematicType fromDB(String input){
public static SchematicType fromDB(String input) {
return fromDB.get(input.toLowerCase());
}

View File

@@ -62,6 +62,7 @@ public class SteamwarUser {
private static final SelectStatement<SteamwarUser> byDiscord = table.selectFields("DiscordId");
private static final SelectStatement<SteamwarUser> byTeam = table.selectFields("Team");
private static final SelectStatement<SteamwarUser> getUsersWithPerm = new SelectStatement<>(table, "SELECT S.* FROM UserData S JOIN UserPerm P ON S.id = P.User WHERE P.Perm = ?");
private static final SelectStatement<SteamwarUser> getAll = new SelectStatement<SteamwarUser>(table, "SELECT * FROM UserData");
private static final Statement updateName = table.update(Table.PRIMARY, "UserName");
private static final Statement updatePassword = table.update(Table.PRIMARY, "Password");
@@ -370,4 +371,8 @@ public class SteamwarUser {
permissions = UserPerm.getPerms(id);
prefix = permissions.stream().filter(UserPerm.prefixes::containsKey).findAny().map(UserPerm.prefixes::get).orElse(UserPerm.emptyPrefix);
}
public static List<SteamwarUser> getAll() {
return getAll.listSelect();
}
}

View File

@@ -19,10 +19,7 @@
package de.steamwar.sql;
import de.steamwar.sql.internal.Field;
import de.steamwar.sql.internal.SelectStatement;
import de.steamwar.sql.internal.SqlTypeMapper;
import de.steamwar.sql.internal.Table;
import de.steamwar.sql.internal.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -68,11 +65,21 @@ public enum UserPerm {
private static final Table<UserPermTable> table = new Table<>(UserPermTable.class, "UserPerm");
private static final SelectStatement<UserPermTable> getPerms = table.selectFields("user");
private static final Statement addPerm = table.insertAll();
private static final Statement removePerm = table.delete(Table.PRIMARY);
public static Set<UserPerm> getPerms(int user) {
return getPerms.listSelect(user).stream().map(up -> up.perm).collect(Collectors.toSet());
}
public static void addPerm(SteamwarUser user, UserPerm perm) {
addPerm.update(user, perm);
}
public static void removePerm(SteamwarUser user, UserPerm perm) {
removePerm.update(user, perm);
}
@Getter
@AllArgsConstructor
public static class Prefix {

View File

@@ -83,7 +83,11 @@ public class Table<T> {
}
public Statement insertAll() {
return insertFields(false, Arrays.stream(fields).map(f -> f.identifier).toArray(String[]::new));
return insertAll(false);
}
public Statement insertAll(boolean returnGeneratedKeys) {
return insertFields(returnGeneratedKeys, Arrays.stream(fields).map(f -> f.identifier).toArray(String[]::new));
}
public Statement insertFields(String... fields) {

View File

@@ -60,17 +60,17 @@ public class FightScoreboard implements Listener, ScoreboardCallback {
private FightScoreboard(){
new StateDependentListener(ArenaMode.Replay, FightState.All, this);
Bukkit.getOnlinePlayers().forEach(player -> SWScoreboard.createScoreboard(player, this));
Bukkit.getOnlinePlayers().forEach(player -> SWScoreboard.impl.createScoreboard(player, this));
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
SWScoreboard.createScoreboard(event.getPlayer(), this);
SWScoreboard.impl.createScoreboard(event.getPlayer(), this);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
SWScoreboard.removeScoreboard(event.getPlayer());
SWScoreboard.impl.removeScoreboard(event.getPlayer());
}
public void setTitle(String t) {

View File

@@ -53,7 +53,7 @@ class FightScoreboard implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
SWScoreboard.createScoreboard(event.getPlayer(), new ScoreboardCallback() {
SWScoreboard.impl.createScoreboard(event.getPlayer(), new ScoreboardCallback() {
@Override
public String getTitle() {
return "§eMissileWars";
@@ -81,7 +81,7 @@ class FightScoreboard implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
SWScoreboard.removeScoreboard(event.getPlayer());
SWScoreboard.impl.removeScoreboard(event.getPlayer());
}
static Scoreboard getScoreboard() {

View File

@@ -30,7 +30,7 @@ import org.bukkit.scoreboard.Scoreboard;
import java.util.HashMap;
import java.util.Map;
public class SWScoreboard21 implements SWScoreboard.ISWScoreboard {
public class SWScoreboard21 implements SWScoreboard {
private static final HashMap<Player, ScoreboardCallback> playerBoards = new HashMap<>();
private static final String SIDEBAR = "sw-sidebar";
@@ -40,7 +40,7 @@ public class SWScoreboard21 implements SWScoreboard.ISWScoreboard {
for(Map.Entry<Player, ScoreboardCallback> scoreboard : playerBoards.entrySet()) {
render(scoreboard.getKey(), scoreboard.getValue());
}
}, 5, 10);
}, 10, 5);
}
private static void render(Player player, ScoreboardCallback callback) {

View File

@@ -29,7 +29,7 @@ import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
public class SWScoreboard8 implements SWScoreboard.ISWScoreboard {
public class SWScoreboard8 implements SWScoreboard {
private static final Reflection.FieldAccessor<String> scoreboardName = Reflection.getField(FlatteningWrapper.scoreboardObjective, String.class, 0);
private static final Reflection.FieldAccessor<Integer> scoreboardAction = Reflection.getField(FlatteningWrapper.scoreboardObjective, int.class, Core.getVersion() > 15 ? 3 : 0);
private static final Class<?> scoreboardDisplayEnum = Reflection.getClass("{nms.world.scores.criteria}.IScoreboardCriteria$EnumScoreboardHealthDisplay");

View File

@@ -23,21 +23,9 @@ import de.steamwar.core.Core;
import de.steamwar.core.VersionDependent;
import org.bukkit.entity.Player;
public class SWScoreboard {
private SWScoreboard() {}
public interface SWScoreboard {
public static final SWScoreboard impl = VersionDependent.getVersionImpl(Core.getInstance());
private static final ISWScoreboard impl = VersionDependent.getVersionImpl(Core.getInstance());
public static boolean createScoreboard(Player player, ScoreboardCallback callback) {
return impl.createScoreboard(player, callback);
}
public static void removeScoreboard(Player player) {
impl.removeScoreboard(player);
}
public interface ISWScoreboard {
boolean createScoreboard(Player player, ScoreboardCallback callback);
void removeScoreboard(Player player);
}
boolean createScoreboard(Player player, ScoreboardCallback callback);
void removeScoreboard(Player player);
}

View File

@@ -61,7 +61,7 @@ object IngameListener: Listener {
@EventHandler
fun onJoin(e: PlayerJoinEvent) {
SWScoreboard.createScoreboard(e.player, TNTLeagueScoreboard(e.player))
SWScoreboard.impl.createScoreboard(e.player, TNTLeagueScoreboard(e.player))
}
@EventHandler

View File

@@ -76,7 +76,7 @@ object TNTLeagueGame {
state = GameState.RUNNING
plugin.server.onlinePlayers.forEach { SWScoreboard.createScoreboard(it, TNTLeagueScoreboard(it)) }
plugin.server.onlinePlayers.forEach { SWScoreboard.impl.createScoreboard(it, TNTLeagueScoreboard(it)) }
blueTeam.start()
redTeam.start()
@@ -120,7 +120,7 @@ object TNTLeagueGame {
plugin.server.onlinePlayers.forEach {
it.gameMode = GameMode.SPECTATOR
SWScoreboard.removeScoreboard(it)
SWScoreboard.impl.removeScoreboard(it)
it.playSound(Sound.sound(org.bukkit.Sound.ENTITY_ENDER_DRAGON_DEATH.key, Sound.Source.MASTER, 1f, 1f))
}

View File

@@ -62,7 +62,7 @@ public class EventStarter {
//Don't start EventServer if not the event bungee
String command;
if(VelocityCore.get().getConfig().isEventmode() || next.getSpectatePort() == 0) {
if(VelocityCore.get().getConfig().isEventmode() || next.getSpectatePort() == null) {
ServerStarter starter = new ServerStarter().event(next);
starter.callback(subserver -> {

View File

@@ -20,6 +20,7 @@
package de.steamwar.velocitycore.mods;
import com.velocitypowered.api.proxy.Player;
import de.steamwar.sql.Punishment;
import de.steamwar.velocitycore.VelocityCore;
import de.steamwar.velocitycore.commands.PunishmentCommand;
import de.steamwar.messages.Chatter;
@@ -82,6 +83,7 @@ public class ModUtils {
}
if(max == ModType.RED) {
user.punish(Punishment.PunishmentType.Ban, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), message, -1, false);
PunishmentCommand.ban(user, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), message, SteamwarUser.get(-1), false);
VelocityCore.getLogger().log(Level.SEVERE, "%s %s wurde automatisch wegen der Mods %s gebannt.".formatted(user.getUserName(), user.getId(), modList));
}

View File

@@ -0,0 +1,56 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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/>.
*/
plugins {
steamwar.kotlin
id("io.ktor.plugin") version "2.3.12"
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.10"
application
}
application {
mainClass.set("de.steamwar.ApplicationKt")
}
tasks.build {
finalizedBy(tasks.buildFatJar)
}
dependencies {
implementation(libs.logback)
implementation(libs.ktor)
implementation(libs.ktorContentNegotiation)
implementation(libs.ktorCors)
implementation(libs.ktorSerialization)
implementation(libs.ktorNetty)
implementation(libs.ktorHost)
implementation(libs.ktorRequestValidation)
implementation(libs.ktorAuth)
implementation(libs.ktorAuthJvm)
implementation(libs.ktorAuthLdap)
implementation(libs.ktorClientCore)
implementation(libs.ktorClientJava)
implementation(libs.ktorClientContentNegotiation)
implementation(libs.ktorClientAuth)
implementation(libs.mysql)
implementation(project(":CommonCore"))
implementation(libs.yamlconfig)
implementation(libs.kotlinxSerializationCbor)
implementation(libs.ktorRateLimit)
}

View File

@@ -0,0 +1,60 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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
import de.steamwar.plugins.configurePlugins
import de.steamwar.routes.ResponseUser
import de.steamwar.routes.SchematicCode
import io.ktor.server.application.*
import io.ktor.server.engine.*
import de.steamwar.routes.configureRoutes
import de.steamwar.sql.SchematicType
import de.steamwar.sql.SteamwarUser
import io.ktor.server.netty.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.File
@Serializable
data class ResponseError(val error: String, val code: String = error)
@Serializable
data class Config(val giteaToken: String)
@OptIn(ExperimentalSerializationApi::class)
val config = Json.decodeFromStream<Config>(File("config.json").inputStream())
fun main() {
Thread {
while (true) {
Thread.sleep(1000 * 10)
ResponseUser.clearCache()
}
}.start()
embeddedServer(Netty, port = 1337, host = "127.0.0.1", module = Application::module)
.start(wait = true)
}
fun Application.module() {
configurePlugins()
configureRoutes()
}

View File

@@ -0,0 +1,86 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.data
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
@Serializable
data class GroupsData(val groups: MutableList<GroupData>)
@Serializable
data class GroupData(val name: String, val fights: MutableList<Int>)
@OptIn(ExperimentalSerializationApi::class)
class Groups {
companion object {
private var groups: GroupsData = if (kGroupsFile.exists()) {
Cbor.decodeFromByteArray(kGroupsFile.readBytes())
} else {
kGroupsFile.createNewFile()
kGroupsFile.writeBytes(Cbor.encodeToByteArray(GroupsData(mutableListOf())))
GroupsData(mutableListOf())
}
fun getGroup(name: String): GroupData? {
return groups.groups.find { it.name == name }
}
fun getGroup(fight: Int): GroupData? {
return groups.groups.find { it.fights.contains(fight) }
}
fun getOrCreateGroup(name: String): GroupData {
val group = getGroup(name)
if (group != null) {
return group
}
val newGroup = GroupData(name, mutableListOf())
groups.groups.add(newGroup)
return newGroup
}
fun resetGroup(fight: Int, save: Boolean = false) {
val oldGroup = getGroup(fight)
oldGroup?.fights?.remove(fight)
if(oldGroup?.fights?.isEmpty() == true) {
groups.groups.remove(oldGroup)
}
if(save) {
kGroupsFile.writeBytes(Cbor.encodeToByteArray(groups))
}
}
fun setGroup(fight: Int, group: String) {
resetGroup(fight)
val newGroup = getOrCreateGroup(group)
newGroup.fights.add(fight)
kGroupsFile.writeBytes(Cbor.encodeToByteArray(groups))
}
fun getAllGroups(): List<String> {
return groups.groups.map { it.name }
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.data
import java.io.File
const val kDataFolder: String = "data"
const val kGroupsName: String = "groups.cbor"
val kGroupsFile: File = File(kDataFolder, kGroupsName)
const val kRelationsName = "relations.cbor"
val kRelationsFile: File = File(kDataFolder, kRelationsName)

View File

@@ -0,0 +1,110 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.data
import io.ktor.client.*
import io.ktor.client.engine.java.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.utils.io.jvm.javaio.*
import kotlinx.coroutines.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import java.io.File
import java.time.Instant
import java.time.temporal.ChronoUnit
const val kCacheFolder: String = "skins"
val kCacheFolderFile: File = File(kCacheFolder)
const val kCacheConfigName: String = "cache.cbor"
val kCacheConfigFile: File = File(kCacheFolder, kCacheConfigName)
@Serializable
data class CacheConfig(val lastUpdate: MutableMap<String, Long>) {
@OptIn(ExperimentalSerializationApi::class)
companion object {
private var config: CacheConfig = if (kCacheConfigFile.exists()) {
kCacheConfigFile.inputStream().use {
Cbor.decodeFromByteArray(it.readBytes())
}
} else {
kCacheConfigFile.createNewFile()
kCacheConfigFile.outputStream().use {
it.write(Cbor.encodeToByteArray(CacheConfig(mutableMapOf())))
}
CacheConfig(mutableMapOf())
}
private fun save() {
kCacheConfigFile.outputStream().use {
it.write(Cbor.encodeToByteArray(config))
}
}
fun update(uuid: String) {
config.lastUpdate[uuid] = Instant.now().toEpochMilli()
save()
}
fun isOutdated(uuid: String): Boolean {
return config.lastUpdate[uuid]?.let {
it < Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()
} ?: true
}
}
}
val client = HttpClient(Java) {
install(ContentNegotiation) {
json()
}
defaultRequest {
header("User-Agent", "SteamWar/1.0")
}
}
suspend fun getCachedSkin(uuid: String): Pair<File, Boolean> {
val file = File(kCacheFolderFile, "$uuid.webp")
if (file.exists()) {
if (CacheConfig.isOutdated(uuid)) {
val skin = client.get("https://vzge.me/bust/150/$uuid")
skin.bodyAsChannel().copyTo(file.outputStream())
CacheConfig.update(uuid)
return file to false
}
return file to true
}
withContext(Dispatchers.IO) {
file.createNewFile()
}
val skin = client.get("https://vzge.me/bust/150/$uuid")
skin.bodyAsChannel().copyTo(file.outputStream())
CacheConfig.update(uuid)
return file to false
}

View File

@@ -0,0 +1,128 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.plugins
import de.steamwar.sql.SWException
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.Token
import de.steamwar.sql.UserPerm
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.application.hooks.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
data class SWAuthPrincipal(val token: Token, val user: SteamwarUser) : Principal
class SWAuthConfig {
var permission: UserPerm? = null
var allowedMethods = mutableListOf<HttpMethod>()
var userCheck: SWAuthPrincipal.(ApplicationRequest) -> Boolean = { true }
var mustAuth: Boolean = false
fun allowMethod(method: HttpMethod) {
allowedMethods.add(method)
}
fun allowMethods(methods: List<HttpMethod>) {
allowedMethods.addAll(methods)
}
fun userCheck(check: SWAuthPrincipal.(ApplicationRequest) -> Boolean) {
userCheck = check
}
}
val SWPermissionCheck = createRouteScopedPlugin("SWAuth", ::SWAuthConfig) {
pluginConfig.apply {
on(AuthenticationChecked) { call ->
if (call.request.httpMethod in allowedMethods) {
if(mustAuth) {
val token = call.principal<SWAuthPrincipal>()
if (token == null) {
call.respond(HttpStatusCode.Unauthorized)
}
}
return@on
}
val token = call.principal<SWAuthPrincipal>()
if (token == null) {
call.respond(HttpStatusCode.Unauthorized)
return@on
}
if (permission != null && !token.user.hasPerm(permission)) {
call.respond(HttpStatusCode.Forbidden)
return@on
}
if (!token.userCheck(call.request)) {
call.respond(HttpStatusCode.Forbidden)
return@on
}
}
}
}
val ErrorLogger = createApplicationPlugin("SWLogger") {
on(CallFailed) { call, cause ->
val msg = """
{
URI: ${call.request.uri}
Method: ${call.request.httpMethod.value}
Headers: ${call.request.headers.entries().joinToString("\n ") { "${it.key}: ${it.value}" }}
Message: ${cause.message}
}
"""
SWException.log(msg, cause.stackTraceToString())
call.response.headers.append("X-Caught", "1")
}
onCallRespond { call ->
if (call.response.status()?.isSuccess() == true) {
return@onCallRespond
}
val msg = """
URI: ${call.request.uri}
Method: ${call.request.httpMethod.value}
Response: ${call.response.status()?.value}
IP: ${call.request.headers["X-Forwarded-For"] ?: call.request.local.remoteHost}
UserAgent: ${call.request.headers["User-Agent"]}
""".trimIndent()
val stack = """
Headers:
${call.request.headers.entries().joinToString("\n ") { "${it.key}: ${it.value}" }}
Body:
${call.request.receiveChannel()}
""".trimIndent()
SWException.log(msg, stack)
}
}

View File

@@ -0,0 +1,78 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.plugins
import de.steamwar.sql.Token
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.ratelimit.*
import kotlinx.serialization.json.Json
import kotlin.time.Duration.Companion.seconds
fun Application.configurePlugins() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.AccessControlAllowOrigin)
allowHeader(HttpHeaders.ContentType)
anyHost()
allowXHttpMethodOverride()
}
install(RateLimit) {
global {
rateLimiter(limit = 60, refillPeriod = 60.seconds)
requestKey {
it.request.headers["X-Forwarded-For"] ?: it.request.local.remoteHost
}
requestWeight { applicationCall, _ ->
if(applicationCall.request.headers["X-Forwarded-For"] != null) {
0
} else {
1
}
}
}
}
authentication {
bearer("sw-auth") {
realm = "SteamWar API"
authenticate { call ->
val token = Token.getTokenByCode(call.token)
if (token == null) {
null
} else {
SWAuthPrincipal(token, token.owner)
}
}
}
}
install(ContentNegotiation) {
json(Json)
}
install(ErrorLogger)
}

View File

@@ -0,0 +1,29 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.plugins
import de.steamwar.routes.catchException
import de.steamwar.sql.SteamwarUser
import io.ktor.server.request.*
import java.util.UUID
fun ApplicationRequest.getUser(key: String = "id"): SteamwarUser? {
return SteamwarUser.get(call.parameters[key]?.let { catchException { UUID.fromString(it) } } ?: return null)
}

View File

@@ -0,0 +1,94 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.Token
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import java.time.format.DateTimeFormatter
import java.time.LocalDateTime
@Serializable
data class AuthLoginRequest(val username: String, val password: String)
@Serializable
data class AuthTokenResponse(val token: String)
@Serializable
data class ResponseToken(val id: Int, val name: String, val created: String) {
constructor(token: Token) : this(token.id, token.name, token.created.toLocalDateTime().toString())
}
@Serializable
data class CreateTokenRequest(val name: String, val password: String)
fun Route.configureAuthRoutes() {
route("/auth") {
post("/login") {
if (call.principal<SWAuthPrincipal>() != null) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Already logged in", "already_logged_in"))
return@post
}
val request = call.receive<AuthLoginRequest>()
val user = SteamwarUser.get(request.username)
if (user == null) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid"))
return@post
}
if (!user.verifyPassword(request.password)) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid"))
return@post
}
val code = Token.createToken("Website: ${DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now())}", user)
call.respond(AuthTokenResponse(code))
}
route("/tokens") {
install(SWPermissionCheck) {
mustAuth = true
}
post("/logout") {
val auth = call.principal<SWAuthPrincipal>()
if(auth == null) {
call.respond(HttpStatusCode.InternalServerError)
return@post
}
auth.token.delete()
call.respond(HttpStatusCode.OK)
}
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.data.Groups
import de.steamwar.data.getCachedSkin
import de.steamwar.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.SchematicType
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.UserPerm
import de.steamwar.sql.loadSchematicTypes
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 java.io.File
import java.net.InetSocketAddress
import java.util.UUID
@Serializable
data class ResponseSchematicType(val name: String, val db: String)
@Serializable
data class ResponseUser(val name: String, val uuid: String, val prefix: String, val perms: List<String>) {
constructor(user: SteamwarUser) : this(user.userName, user.uuid.toString(), user.prefix().chatPrefix, user.perms().map { it.name }) {
synchronized(cache) {
cache[user.id] = this
}
}
companion object {
private val cache = mutableMapOf<Int, ResponseUser>()
fun get(id: Int): ResponseUser {
synchronized(cache) {
return cache[id] ?: ResponseUser(SteamwarUser.get(id)).also { cache[id] = it }
}
}
fun clearCache() {
synchronized(cache) {
cache.clear()
}
}
}
}
fun Route.configureDataRoutes() {
route("/data") {
route("/admin") {
install(SWPermissionCheck) {
mustAuth = true
permission = UserPerm.PREFIX_MODERATOR
}
get("/users") {
call.respond(SteamwarUser.getAll().map { ResponseUser(it) })
}
get("/schematicTypes") {
val types = mutableListOf<SchematicType>()
loadSchematicTypes(types, mutableMapOf())
call.respond(types.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("/groups") {
call.respond(Groups.getAllGroups())
}
}
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<SWAuthPrincipal>()!!.user))
}
}
}
}
inline fun <T> catchException(yield: () -> T): T? = try {
yield()
} catch (e: Exception) {
null
}

View File

@@ -0,0 +1,168 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.data.Groups
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.EventFight
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.Team
import de.steamwar.sql.UserPerm
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 start: Long,
val ergebnis: Int,
val spectatePort: Int?,
val group: String?
) {
constructor(eventFight: EventFight) : this(
eventFight.fightID,
eventFight.spielmodus,
eventFight.map,
ResponseTeam(Team.get(eventFight.teamBlue)),
ResponseTeam(Team.get(eventFight.teamRed)),
eventFight.startTime.time,
eventFight.ergebnis,
eventFight.spectatePort,
Groups.getGroup(eventFight.fightID)?.name
)
}
@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 start: Long? = null,
val spielmodus: String? = null,
val map: String? = null,
val group: String? = null,
val spectatePort: Int? = null
)
@Serializable
data class CreateEventFight(
val event: Int,
val spielmodus: String,
val map: String,
val blueTeam: Int,
val redTeam: Int,
val start: Long,
val spectatePort: Int? = null,
val group: String? = null
)
fun Route.configureEventFightRoutes() {
route("/fights") {
install(SWPermissionCheck) {
allowMethod(HttpMethod.Get)
permission = UserPerm.MODERATION
}
post {
val fight = call.receiveNullable<CreateEventFight>()
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,
fight.spectatePort
)
if (fight.group != null) {
if (fight.group != "null") {
Groups.setGroup(eventFight.fightID, fight.group)
}
}
call.respond(HttpStatusCode.Created, ResponseEventFight(eventFight))
}
route("/{fight}") {
put {
val fight = call.receiveFight() ?: return@put
val updateFight = call.receiveNullable<UpdateEventFight>()
if (updateFight == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body"))
return@put
}
val teamBlue = updateFight.blueTeam ?: fight.teamBlue
val teamRed = updateFight.redTeam ?: fight.teamRed
val start = updateFight.start?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: fight.startTime
val spielmodus = updateFight.spielmodus ?: fight.spielmodus
val map = updateFight.map ?: fight.map
val spectatePort = updateFight.spectatePort ?: fight.spectatePort
if (updateFight.group != null) {
if (updateFight.group == "null") {
Groups.resetGroup(fight.fightID, true)
} else {
Groups.setGroup(fight.fightID, updateFight.group)
}
}
fight.update(start, spielmodus, map, teamBlue, teamRed, spectatePort)
call.respond(HttpStatusCode.OK, ResponseEventFight(fight))
}
delete {
val fight = call.receiveFight() ?: return@delete
fight.delete()
call.respond(HttpStatusCode.OK)
}
}
}
}
suspend fun ApplicationCall.receiveFight(fieldName: String = "fight"): EventFight? {
val fightId = parameters[fieldName]?.toIntOrNull()
if (fightId == null) {
respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID"))
return null
}
val fight = EventFight.get(fightId)
if (fight == null) {
respond(HttpStatusCode.NotFound, ResponseError("Fight not found"))
return null
}
return fight
}

View File

@@ -0,0 +1,251 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.data.Groups
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.*
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
@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 referees: List<ResponseUser>,
) {
constructor(event: Event) : this(
event.eventID,
event.eventName,
event.deadline.time,
event.start.time,
event.end.time,
event.maximumTeamMembers,
event.schematicType?.toDB(),
event.publicSchemsOnly(),
Referee.get(event.eventID).map { ResponseUser(SteamwarUser.get(it)) }
)
}
@Serializable
data class ExtendedResponseEvent(
val event: ResponseEvent,
val teams: List<ResponseTeam>,
val fights: List<ResponseEventFight>
)
@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<Int>? = null,
val removeReferee: Set<Int>? = 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 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.getEvent(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.getEvent(event.eventID).map { ResponseEventFight(it) })
}
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,
Groups.getGroup(it.fightID)?.name ?: "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, it)
}
}
if (updateEvent.removeReferee != null) {
updateEvent.removeReferee.forEach {
Referee.remove(event.eventID, it)
}
}
event.update(eventName, deadline, start, end, schemType, maxTeamMembers, publicSchemsOnly)
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()
call.respond(HttpStatusCode.NoContent)
}
}
}
}
suspend fun ApplicationCall.receiveEvent(fieldName: String = "event"): 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
}

View File

@@ -0,0 +1,251 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.config
import de.steamwar.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.UserPerm
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.java.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.reflect.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
import java.util.Base64
val pathPageIdMap = mutableMapOf<String, Int>()
var pageId = 1
@Serializable
data class Identity(val name: String, val email: String)
@Serializable
data class PageResponseList(
val path: String,
val name: String,
val sha: String,
val downloadUrl: String,
val id: Int
) {
constructor(res: JsonObject, id: Int) : this(
res["path"]?.jsonPrimitive?.content!!,
res["name"]?.jsonPrimitive?.content!!,
res["sha"]?.jsonPrimitive?.content!!,
res["download_url"]?.jsonPrimitive?.content!!,
id
)
}
@Serializable
data class PageResponse(
val path: String,
val name: String,
val sha: String,
val downloadUrl: String,
val content: String,
val size: Int,
val id: Int,
) {
constructor(res: JsonObject, id: Int) : this(
res["path"]?.jsonPrimitive?.content!!,
res["name"]?.jsonPrimitive?.content!!,
res["sha"]?.jsonPrimitive?.content!!,
res["download_url"]?.jsonPrimitive?.content!!,
res["content"]?.jsonPrimitive?.content!!,
res["size"]?.jsonPrimitive?.int!!,
id
)
}
@Serializable
data class CreatePageRequest(val path: String, val slug: String?, val title: String?)
@Serializable
data class CreateBranchRequest(val branch: String)
@Serializable
data class UpdatePageRequest(val content: String, val sha: String, val message: String)
@Serializable
data class MergeBranchRequest(val branch: String, val message: String)
@Serializable
data class DeletePageRequest(val sha: String, val message: String)
fun Route.configurePage() {
val client = HttpClient(Java) {
install(ContentNegotiation) {
json()
}
defaultRequest {
url("https://steamwar.de/devlabs/api/v1/")
header("Authorization", "token " + config.giteaToken)
}
}
route("page") {
install(SWPermissionCheck) {
permission = UserPerm.MODERATION
}
get {
val branch = call.request.queryParameters["branch"] ?: "master"
val filesToCheck = mutableListOf("src/content")
val files = mutableListOf<PageResponseList>()
while (filesToCheck.isNotEmpty()) {
val path = filesToCheck.removeAt(0)
val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch")
val fileJson = Json.parseToJsonElement(res.bodyAsText())
if (fileJson is JsonArray) {
fileJson.forEach {
val obj = it.jsonObject
if (obj["type"]?.jsonPrimitive?.content == "dir") {
filesToCheck.add(obj["path"]?.jsonPrimitive?.content!!)
} else if (obj["type"]?.jsonPrimitive?.content == "file" && (obj["name"]?.jsonPrimitive?.content?.endsWith(".md") == true || obj["name"]?.jsonPrimitive?.content?.endsWith(".json") == true)) {
files.add(PageResponseList(obj, pathPageIdMap.computeIfAbsent(obj["path"]?.jsonPrimitive?.content!!) { pageId++ }))
}
}
} else {
files.add(PageResponseList(fileJson.jsonObject, pathPageIdMap.computeIfAbsent(fileJson.jsonObject["path"]?.jsonPrimitive?.content!!) { pageId++ }))
}
}
call.respond(files)
}
get("branch") {
val res = client.get("repos/SteamWar/Website/branches")
call.respond(res.status, Json.parseToJsonElement(res.bodyAsText()).jsonArray.map { it.jsonObject["name"]?.jsonPrimitive?.content!! })
}
post("branch") {
@Serializable
data class CreateGiteaBranchRequest(val new_branch_name: String, val old_branch_name: String)
val branch = call.receive<CreateBranchRequest>().branch
val res = client.post("repos/SteamWar/Website/branches") {
contentType(ContentType.Application.Json)
setBody(CreateGiteaBranchRequest(branch, "master"))
}
@Serializable
data class CreateGiteaMergeRequest(val base: String, val head: String, val title: String)
client.post("repos/SteamWar/Website/pulls") {
contentType(ContentType.Application.Json)
setBody(CreateGiteaMergeRequest("master", branch, "Merge branch $branch"))
}
call.respond(res.status)
}
delete("branch") {
val branch = call.receive<CreateBranchRequest>().branch
val res = client.delete("repos/SteamWar/Website/branches/$branch")
call.respond(res.status)
}
post {
@Serializable
data class CreateGiteaPageRequest(val message: String, val content: String, val branch: String, val author: Identity)
val req = call.receive<CreatePageRequest>()
if(req.path.startsWith("src/content/")) {
call.respond(HttpStatusCode.BadRequest, "Invalid path")
return@post
}
val res = client.post("repos/SteamWar/Website/contents/src/content/${req.path}") {
contentType(ContentType.Application.Json)
setBody(CreateGiteaPageRequest(
"Create page ${req.path}",
Base64.getEncoder().encodeToString("""
---
title: ${req.title ?: "[Enter Title]"}
description: [Enter Description]
slug: ${req.slug ?: "[Enter Slug]"}
---
# ${req.path}
""".trimIndent().toByteArray()),
call.request.queryParameters["branch"] ?: "master",
Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de"
)))
}
call.respond(res.status)
}
get("{id}") {
val id = call.parameters["id"]?.toIntOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
val path = pathPageIdMap.entries.find { it.value == id }?.key ?: return@get call.respond(HttpStatusCode.NotFound, "Page not found")
val branch = call.request.queryParameters["branch"] ?: "master"
val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch")
val fileJson = Json.parseToJsonElement(res.bodyAsText())
if (fileJson is JsonArray) {
return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
}
val file = PageResponse(fileJson.jsonObject, id)
call.respond(file)
}
delete("{id}") {
val data = call.receive<DeletePageRequest>()
val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@delete call.respond(HttpStatusCode.NotFound, "Page not found")
val branch = call.request.queryParameters["branch"] ?: "master"
@Serializable
data class DeleteGiteaPageRequest(val sha: String, val message: String, val branch: String, val author: Identity)
val res = client.delete("repos/SteamWar/Website/contents/$path") {
contentType(ContentType.Application.Json)
setBody(DeleteGiteaPageRequest(data.sha, data.message, branch, Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de")))
}
call.respond(res.status)
}
put("{id}") {
@Serializable
data class UpdateGiteaPageRequest(val content: String, val sha: String, val message: String, val branch: String, val author: Identity)
val data = call.receive<UpdatePageRequest>()
val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@put call.respond(HttpStatusCode.NotFound, "Page not found")
val res = client.put("repos/SteamWar/Website/contents/$path") {
contentType(ContentType.Application.Json)
setBody(UpdateGiteaPageRequest(data.content, data.sha, data.message, (call.request.queryParameters["branch"] ?: "master"), Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de")))
}
call.respond(res.status)
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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 io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.routing.*
fun Application.configureRoutes() {
routing {
authenticate("sw-auth", optional = true) {
configureEventsRoute()
configureDataRoutes()
configureEventFightRoutes()
configureUserPerms()
configureStats()
configurePage()
configureSchematic()
configureAuthRoutes()
}
}
}

View File

@@ -0,0 +1,155 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.NodeData
import de.steamwar.sql.NodeDownload
import de.steamwar.sql.NodeMember
import de.steamwar.sql.SWException
import de.steamwar.sql.SchematicNode
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import java.security.MessageDigest
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
@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.id, node.schemtype?.name(), node.owner, node.item, node.lastUpdate.time, node.rank, node.replaceColor(), node.allowReplay())
}
@Serializable
data class ResponseSchematicLong(val members: List<ResponseUser>, val path: String, val schem: ResponseSchematic) {
constructor(node: SchematicNode, path: String): this(NodeMember.getNodeMembers(node.id).map { ResponseUser.get(it.member) }, path, ResponseSchematic(node))
}
@Serializable
data class ResponseSchematicList(val breadcrumbs: List<ResponseBreadcrumb>, val schematics: List<ResponseSchematic>, val players: Map<String, ResponseUser>) {
constructor(schematics: List<ResponseSchematic>, breadcrumbs: List<ResponseBreadcrumb>) : this(breadcrumbs, schematics, schematics.map { it.owner }.distinct().map { ResponseUser.get(it) }.associateBy { it.uuid })
}
@Serializable
data class ResponseBreadcrumb(val name: String, val id: Int)
fun generateCode(): String {
val md = MessageDigest.getInstance("SHA-256")
val random = ByteArray(64).map { (0..255).random().toByte() }.toByteArray()
val code = md.digest(random)
return code.joinToString("") { "%02x".format(it) }
}
@Serializable
data class SchematicCode(val id: Int, val code: String, val expires: Long)
@Serializable
data class UploadSchematic(val name: String, val content: String)
fun Route.configureSchematic() {
route("/download/{code}") {
get {
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.get(node) ?: run {
call.respond(HttpStatusCode.InternalServerError)
return@get
}
call.response.header("Content-Disposition", "attachment; filename=\"${node.name}.${if (data.nodeFormat) "schem" else "schematic"}\"")
call.respondBytes(data.schemData().readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK)
}
get("/info") {
val node = call.receiveSchematic() ?: return@get
call.respond(ResponseSchematic(node))
}
}
route("/schem") {
install(SWPermissionCheck)
post {
val file = call.receive<UploadSchematic>()
val schemName = file.name.substringBeforeLast(".")
val schemType = file.name.substringAfterLast(".")
if (schemType != "schem" && schemType != "schematic") {
call.respond(HttpStatusCode.BadRequest)
return@post
}
val user = call.principal<SWAuthPrincipal>()!!.user
val content = Base64.getDecoder().decode(file.content)
var node = SchematicNode.getSchematicNode(user.id, schemName, 0)
if (node == null) {
node = SchematicNode.createSchematic(user.id, schemName, 0)
}
val data = NodeData(node.id, false)
data.saveFromStream(content.inputStream(), schemType == "schem")
call.respond(ResponseSchematic(node))
}
}
}
suspend fun ApplicationCall.receiveSchematic(fieldName: String = "code", delete: Boolean = false): SchematicNode? {
val code = parameters[fieldName] ?: run {
respond(HttpStatusCode.BadRequest)
return null
}
val dl = NodeDownload.get(code) ?: run {
respond(HttpStatusCode.NotFound)
return null
}
if(dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) {
respond(HttpStatusCode.Gone)
return null
}
if (delete) {
dl.delete()
}
val node = SchematicNode.getSchematicNode(dl.nodeId) ?: run {
respond(HttpStatusCode.NotFound)
return null
}
return node
}

View File

@@ -0,0 +1,76 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.plugins.getUser
import de.steamwar.sql.*
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
@Serializable
data class UserStats(val eventFightParticipation: Int, val eventParticipation: Int, val acceptedSchematics: Int, val fights: Int, val playtime: Double) {
constructor(user: SteamwarUser): this(
getEventFightParticipation(user) ?: 0,
getEventParticipation(user) ?: 0,
getAcceptedSchematics(user) ?: 0,
getFightCount(user) ?: 0,
user.onlinetime / 3600.0
)
}
fun Route.configureStats() {
route("/stats") {
get("/ranked/{gamemode}") {
val gamemode = call.parameters["gamemode"] ?: return@get call.respond(HttpStatusCode.NotFound)
@Serializable
data class RankedUser(val name: String, val elo: Int)
call.respond(getRankedList(gamemode).map { RankedUser(it.first, it.second) })
}
get("/fights") {
val list = getFightList()
@Serializable
data class Fight(val date: String, val gamemode: String, val count: Int)
call.respond(list.map { Fight(it.first, it.second, it.third) })
}
route("/user") {
install(SWPermissionCheck)
get {
val user = call.authentication.principal<SWAuthPrincipal>()
if (user == null) {
call.respond(HttpStatusCode.NotFound, "User not found")
return@get
}
call.respond(UserStats(user.user))
}
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.plugins.SWPermissionCheck
import de.steamwar.plugins.getUser
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.UserPerm
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
@Serializable
data class RespondPrefix(val name: String, val colorCode: String, val chatPrefix: String)
@Serializable
data class RespondUserPerms(val prefixes: Map<String, RespondPrefix>, val perms: List<String>)
@Serializable
data class RespondUserPermsPrefix(val prefix: RespondPrefix, val perms: List<String>)
fun Route.configureUserPerms() {
route("/perms") {
install(SWPermissionCheck) {
permission = UserPerm.MODERATION
}
get {
val perms = mutableListOf<String>()
val prefixes = mutableMapOf<String, RespondPrefix>()
UserPerm.entries.forEach {
if (it.name.startsWith("PREFIX_")) {
val prefix = UserPerm.prefixes[it]!!
prefixes[it.name] = RespondPrefix(it.name, prefix.colorCode, prefix.chatPrefix)
} else {
perms.add(it.name)
}
}
call.respond(RespondUserPerms(prefixes, perms))
}
route("/user/{id}") {
get {
val user = call.request.getUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest)
return@get
}
val perms = mutableListOf<String>()
var prefix = UserPerm.PREFIX_NONE
user.perms().forEach {
if (it.name.startsWith("PREFIX_")) {
prefix = it
} else {
perms.add(it.name)
}
}
val prefixs = UserPerm.prefixes[prefix]!!
call.respond(RespondUserPermsPrefix(RespondPrefix(prefix.name, prefixs.colorCode, prefixs.chatPrefix), perms))
}
put("/prefix/{prefix}") {
val (user, prefix) = call.receivePermission("prefix") ?: return@put
user.perms().filter { it.name.startsWith("PREFIX_") }.forEach {
UserPerm.removePerm(user, it)
}
UserPerm.addPerm(user, UserPerm.entries.find { it == prefix }!!)
call.respond(HttpStatusCode.Accepted)
}
put("/{perm}") {
val (user, permission) = call.receivePermission() ?: return@put
if (!user.hasPerm(permission)) {
UserPerm.addPerm(user, permission)
call.respond(HttpStatusCode.Accepted)
return@put
}
call.respond(HttpStatusCode.NoContent)
}
delete("/{perm}") {
val (user, permission) = call.receivePermission() ?: return@delete
if (user.hasPerm(permission)) {
UserPerm.removePerm(user, permission)
call.respond(HttpStatusCode.Accepted)
return@delete
}
call.respond(HttpStatusCode.NoContent)
}
}
}
}
suspend fun ApplicationCall.receivePermission(fieldName: String = "perm", isPrefix: Boolean = false): Pair<SteamwarUser, UserPerm>? {
val user = request.getUser()
if (user == null) {
respond(HttpStatusCode.BadRequest)
return null
}
val perm = parameters[fieldName]
val permission = UserPerm.entries.find { it.name == perm }
if (perm == null || perm.startsWith("PREFIX_") == isPrefix || permission == null) {
respond(HttpStatusCode.BadRequest)
return null
}
return user to permission
}

View File

@@ -0,0 +1,29 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.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
}

View File

@@ -0,0 +1,68 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.sql
import org.bspfsystems.yamlconfiguration.file.YamlConfiguration
import java.io.File
import java.util.*
import java.util.stream.Collectors
fun loadSchematicTypes(tmpTypes: MutableList<SchematicType>?, tmpFromDB: MutableMap<String, SchematicType>?) {
val folder = File("/configs/GameModes")
if (folder.exists()) {
for (configFile in Arrays.stream(folder.listFiles { _, 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 = config.getString("Schematic.Shortcut")
if (shortcut == null) {
println("No shortcut for $type")
continue
}
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, false)
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,
false
)
tmpTypes!!.add(current)
tmpFromDB[type.lowercase(Locale.getDefault())] = current
}
}
}
class SQLWrapperImpl: SQLWrapper {
override fun loadSchemTypes(tmpTypes: MutableList<SchematicType>?, tmpFromDB: MutableMap<String, SchematicType>?) = loadSchematicTypes(tmpTypes, tmpFromDB)
override fun additionalExceptionMetadata(builder: StringBuilder) {
builder.append("\n\nWebsiteApi")
}
}

View File

@@ -0,0 +1,67 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.sql
import de.steamwar.sql.internal.Statement
import de.steamwar.sql.internal.Statement.ResultSetUser
private val getNum: ResultSetUser<Int?> = ResultSetUser { res ->
if (res.next()) {
res.getInt("num")
} else {
null
}
}
private val eventFightParticipation = Statement("SELECT FightPlayer.UserID, COUNT(UserID) as num FROM FightPlayer INNER JOIN Fight on FightPlayer.FightID = Fight.FightID WHERE Fight.Server LIKE '%vs%' AND FightPlayer.UserID = ? GROUP BY FightPlayer.UserID")
fun getEventFightParticipation(user: SteamwarUser): Int? = eventFightParticipation.select(getNum, user.id)
private val eventParticipation = Statement("SELECT FightPlayer.UserID, COUNT(DISTINCT EventID) as num FROM FightPlayer INNER JOIN core.Fight F on FightPlayer.FightID = F.FightID INNER JOIN core.EventFight EF on F.FightID = EF.Fight WHERE F.FightID = FightPlayer.FightID AND FightPlayer.FightID = EF.Fight AND F.Server LIKE '%vs%' AND FightPlayer.UserID = ? GROUP BY FightPlayer.UserID")
fun getEventParticipation(user: SteamwarUser): Int? = eventParticipation.select(getNum, user.id)
private val acceptedSchematics = Statement("SELECT NodeOwner, COUNT(DISTINCT NodeId) AS num FROM SchematicNode WHERE NodeType != 'normal' AND NodeType IS NOT NULL AND NodeType NOT LIKE 'c%' AND NodeOwner = ?")
fun getAcceptedSchematics(user: SteamwarUser): Int? = acceptedSchematics.select(getNum, user.id)
private val fightCount = Statement("SELECT COUNT(*) AS num FROM FightPlayer WHERE UserID = ?")
fun getFightCount(user: SteamwarUser): Int? = fightCount.select(getNum, user.id)
private val rankedList = Statement("SELECT UserName, Elo FROM UserData, UserElo WHERE UserID = id AND GameMode = ? AND Season = ? ORDER BY Elo DESC")
fun getRankedList(gamemode: String): List<Pair<String, Int>> = rankedList.select({ res ->
val list = mutableListOf<Pair<String, Int>>()
while (res.next()) {
list.add(res.getString("UserName") to res.getInt("Elo"))
}
list
}, gamemode, Season.getSeason())
private val fightList = Statement("SELECT DATE(StartTime) AS Datum, GameMode AS Modus, COUNT(*) AS Anzahl FROM Fight WHERE DATE(StartTime) >= DATE(NOW()) - INTERVAL 1 WEEK GROUP BY Datum, GameMode ORDER BY Datum ASC")
fun getFightList(): List<Triple<String, String, Int>> = fightList.select({ res ->
val list = mutableListOf<Triple<String, String, Int>>()
while (res.next()) {
list.add(Triple(res.getString("Datum"), res.getString("Modus"), res.getInt("Anzahl")))
}
list
})

View File

@@ -0,0 +1,137 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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.util
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import java.io.*
import java.net.InetSocketAddress
import java.net.Socket
/**
*
* @author zh32 <zh32 at zh32.de>
</zh32> */
private fun readVarInt(`in`: DataInputStream): Int {
var i = 0
var j = 0
while (true) {
val k = `in`.readByte().toInt()
i = i or (k and 0x7F shl j++ * 7)
if (j > 5) throw RuntimeException("VarInt too big")
if (k and 0x80 != 128) break
}
return i
}
private fun writeVarInt(out: DataOutputStream, paramInt: Int) {
var paramInts = paramInt
while (true) {
if (paramInts and -0x80 == 0) {
out.writeByte(paramInts)
return
}
out.writeByte(paramInts and 0x7F or 0x80)
paramInts = paramInts ushr 7
}
}
private val JSON = Json {
ignoreUnknownKeys = true
}
fun fetchData(address: InetSocketAddress, timeout: Int = 7000): StatusResponse {
val socket = Socket()
socket.setSoTimeout(timeout)
socket.connect(address, timeout)
val outputStream = socket.getOutputStream()
val dataOutputStream = DataOutputStream(outputStream)
val inputStream = socket.getInputStream()
val inputStreamReader = InputStreamReader(inputStream)
val b = ByteArrayOutputStream()
val handshake = DataOutputStream(b)
handshake.writeByte(0x00) //packet id for handshake
writeVarInt(handshake, 4) //protocol version
writeVarInt(handshake, address.hostString.length) //host length
handshake.writeBytes(address.hostString) //host string
handshake.writeShort(address.port) //port
writeVarInt(handshake, 1) //state (1 for handshake)
writeVarInt(dataOutputStream, b.size()) //prepend size
dataOutputStream.write(b.toByteArray()) //write handshake packet
dataOutputStream.writeByte(0x01) //size is only 1
dataOutputStream.writeByte(0x00) //packet id for ping
val dataInputStream = DataInputStream(inputStream)
readVarInt(dataInputStream) //size of packet
var id = readVarInt(dataInputStream) //packet id
if (id == -1) {
throw IOException("Premature end of stream.")
}
if (id != 0x00) { //we want a status response
throw IOException("Invalid packetID")
}
val length = readVarInt(dataInputStream) //length of json string
if (length == -1) {
throw IOException("Premature end of stream.")
}
if (length == 0) {
throw IOException("Invalid string length.")
}
val `in` = ByteArray(length)
dataInputStream.readFully(`in`) //read json string
val json = String(`in`)
val now = System.currentTimeMillis()
dataOutputStream.writeByte(0x09) //size of packet
dataOutputStream.writeByte(0x01) //0x01 for ping
dataOutputStream.writeLong(now) //time!?
readVarInt(dataInputStream)
id = readVarInt(dataInputStream)
if (id == -1) {
throw IOException("Premature end of stream.")
}
if (id != 0x01) {
throw IOException("Invalid packetID")
}
val pingtime = dataInputStream.readLong() //read response
val response: StatusResponse = JSON.decodeFromString(json)
response.time = (now - pingtime).toInt()
dataOutputStream.close()
outputStream.close()
inputStreamReader.close()
inputStream.close()
socket.close()
return response
}
@Serializable
data class StatusResponse(val description: JsonElement, val players: Players, val version: Version, val favicon: String) {
@Transient
var time = 0
}
@Serializable
data class Players(val max: Int, val online: Int, val sample: List<Player> = emptyList())
@Serializable
data class Player(val name: String, val id: String)
@Serializable
data class Version(val name: String, val protocol: Int)

View File

@@ -0,0 +1,48 @@
<!--
~ This file is a part of the SteamWar software.
~
~ Copyright (C) 2024 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/>.
-->
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<property name="LOG_FILE" value="api" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${LOG_FILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>logs/${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
<!-- keep 30 days' worth of history capped at 3GB total size -->
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
<logger name="org.eclipse.jetty" level="info"/>
</configuration>

View File

@@ -141,6 +141,29 @@ dependencyResolutionManagement {
library("velocityapi", "com.velocitypowered:velocity-api:3.3.0-SNAPSHOT")
library("apolloapi", "com.lunarclient:apollo-api:1.1.0")
library("apollocommon", "com.lunarclient:apollo-common:1.1.0")
library("logback", "ch.qos.logback:logback-classic:1.5.6")
val ktorVersion = "2.3.12"
library("ktor", "io.ktor:ktor-server-core-jvm:$ktorVersion")
library("ktorContentNegotiation", "io.ktor:ktor-server-content-negotiation-jvm:$ktorVersion")
library("ktorCors", "io.ktor:ktor-server-cors-jvm:$ktorVersion")
library("ktorSerialization", "io.ktor:ktor-serialization-kotlinx-json-jvm:$ktorVersion")
library("ktorNetty", "io.ktor:ktor-server-netty-jvm:$ktorVersion")
library("ktorHost", "io.ktor:ktor-server-host-common-jvm:$ktorVersion")
library("ktorRequestValidation", "io.ktor:ktor-server-request-validation:$ktorVersion")
library("ktorAuth", "io.ktor:ktor-server-auth:$ktorVersion")
library("ktorAuthJvm", "io.ktor:ktor-server-auth-jvm:$ktorVersion")
library("ktorAuthLdap", "io.ktor:ktor-server-auth-ldap-jvm:$ktorVersion")
library("ktorClientCore", "io.ktor:ktor-client-core-jvm:$ktorVersion")
library("ktorClientJava", "io.ktor:ktor-client-java:$ktorVersion")
library("ktorClientContentNegotiation", "io.ktor:ktor-client-content-negotiation:$ktorVersion")
library("ktorClientAuth", "io.ktor:ktor-client-auth:$ktorVersion")
library("yamlconfig", "org.bspfsystems:yamlconfiguration:1.3.0")
library("kotlinxSerializationCbor", "org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.4.1")
library("ktorRateLimit", "io.ktor:ktor-server-rate-limit:$ktorVersion")
}
}
}
@@ -220,3 +243,4 @@ include(
)
include("TNTLeague")
include("WebsiteBackend")

View File

@@ -27,4 +27,6 @@ artifacts:
"/binarys/PersistentVelocityCore.jar": "VelocityCore/Persistent/build/libs/Persistent.jar"
"/binarys/VelocityCore.jar": "VelocityCore/build/libs/VelocityCore-all.jar"
"/binarys/deployarena.py": "VelocityCore/deployarena.py"
"/binarys/deployarena.py": "VelocityCore/deployarena.py"
"/binarys/website-api.jar": "WebsiteBackend/build/libs/WebsiteBackend-all.jar"