Merge branch 'main' into TNTLeague/finish
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
56
WebsiteBackend/build.gradle.kts
Normal file
56
WebsiteBackend/build.gradle.kts
Normal 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)
|
||||
}
|
||||
60
WebsiteBackend/src/de/steamwar/Application.kt
Normal file
60
WebsiteBackend/src/de/steamwar/Application.kt
Normal 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()
|
||||
}
|
||||
86
WebsiteBackend/src/de/steamwar/data/Groups.kt
Normal file
86
WebsiteBackend/src/de/steamwar/data/Groups.kt
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
30
WebsiteBackend/src/de/steamwar/data/Paths.kt
Normal file
30
WebsiteBackend/src/de/steamwar/data/Paths.kt
Normal 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)
|
||||
110
WebsiteBackend/src/de/steamwar/data/SkinCache.kt
Normal file
110
WebsiteBackend/src/de/steamwar/data/SkinCache.kt
Normal 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
|
||||
}
|
||||
128
WebsiteBackend/src/de/steamwar/plugins/Auth.kt
Normal file
128
WebsiteBackend/src/de/steamwar/plugins/Auth.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
78
WebsiteBackend/src/de/steamwar/plugins/Plugins.kt
Normal file
78
WebsiteBackend/src/de/steamwar/plugins/Plugins.kt
Normal 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)
|
||||
}
|
||||
29
WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt
Normal file
29
WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt
Normal 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)
|
||||
}
|
||||
94
WebsiteBackend/src/de/steamwar/routes/Auth.kt
Normal file
94
WebsiteBackend/src/de/steamwar/routes/Auth.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
WebsiteBackend/src/de/steamwar/routes/Data.kt
Normal file
153
WebsiteBackend/src/de/steamwar/routes/Data.kt
Normal 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
|
||||
}
|
||||
168
WebsiteBackend/src/de/steamwar/routes/EventFights.kt
Normal file
168
WebsiteBackend/src/de/steamwar/routes/EventFights.kt
Normal 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
|
||||
}
|
||||
251
WebsiteBackend/src/de/steamwar/routes/Events.kt
Normal file
251
WebsiteBackend/src/de/steamwar/routes/Events.kt
Normal 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
|
||||
}
|
||||
251
WebsiteBackend/src/de/steamwar/routes/Page.kt
Normal file
251
WebsiteBackend/src/de/steamwar/routes/Page.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
WebsiteBackend/src/de/steamwar/routes/Routes.kt
Normal file
39
WebsiteBackend/src/de/steamwar/routes/Routes.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
155
WebsiteBackend/src/de/steamwar/routes/Schematic.kt
Normal file
155
WebsiteBackend/src/de/steamwar/routes/Schematic.kt
Normal 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
|
||||
}
|
||||
76
WebsiteBackend/src/de/steamwar/routes/Stats.kt
Normal file
76
WebsiteBackend/src/de/steamwar/routes/Stats.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
WebsiteBackend/src/de/steamwar/routes/UserPerms.kt
Normal file
130
WebsiteBackend/src/de/steamwar/routes/UserPerms.kt
Normal 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
|
||||
}
|
||||
29
WebsiteBackend/src/de/steamwar/sql/SQLConfigImpl.kt
Normal file
29
WebsiteBackend/src/de/steamwar/sql/SQLConfigImpl.kt
Normal 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
|
||||
}
|
||||
68
WebsiteBackend/src/de/steamwar/sql/SQLWrapperImpl.kt
Normal file
68
WebsiteBackend/src/de/steamwar/sql/SQLWrapperImpl.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
67
WebsiteBackend/src/de/steamwar/sql/Stats.kt
Normal file
67
WebsiteBackend/src/de/steamwar/sql/Stats.kt
Normal 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
|
||||
})
|
||||
137
WebsiteBackend/src/de/steamwar/util/ServerPing.kt
Normal file
137
WebsiteBackend/src/de/steamwar/util/ServerPing.kt
Normal 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)
|
||||
48
WebsiteBackend/src/logback.xml
Normal file
48
WebsiteBackend/src/logback.xml
Normal 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>
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user