diff --git a/CommonCore/SQL/src/de/steamwar/sql/EventFight.kt b/CommonCore/SQL/src/de/steamwar/sql/EventFight.kt index 9449781c..d658b347 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/EventFight.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/EventFight.kt @@ -53,7 +53,7 @@ object EventFightTable : IntIdTable("EventFight", "FightID") { val spectatePort = integer("SpectatePort").nullable() val bestOf = integer("BestOf") val ergebnis = integer("Ergebnis") - val fight = integer("Fight").entityId().nullable() + val fight = optReference("Fight", FightTable) } class EventFight(id: EntityID) : IntEntity(id), Comparable { diff --git a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java index af6c395d..2e6fe55d 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java +++ b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java @@ -45,7 +45,7 @@ public final class GameModeConfig { } private static String internalName(File f) { - return f.getName().replace(".yml", ""); + return f != null ? f.getName().replace(".yml", "") : constWarGear(null); } private static final Map> byFileName; diff --git a/CommonCore/SQL/src/de/steamwar/sql/Punishment.kt b/CommonCore/SQL/src/de/steamwar/sql/Punishment.kt index f3fcbb4e..0ba89672 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/Punishment.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/Punishment.kt @@ -36,7 +36,7 @@ import java.time.Instant import java.util.Date import java.util.function.Consumer -object PunishmentTable : IntIdTable("Punishment", "PunishmentId") { +object PunishmentTable : IntIdTable("Punishments", "PunishmentId") { val userId = reference("UserId", SteamwarUserTable) val punisher = reference("Punisher", SteamwarUserTable) val type = enumerationByName("Type", 32, Punishment.PunishmentType::class) diff --git a/CommonCore/SQL/src/de/steamwar/sql/Team.kt b/CommonCore/SQL/src/de/steamwar/sql/Team.kt index 87419fad..e02fe6e5 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/Team.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/Team.kt @@ -24,6 +24,7 @@ import org.jetbrains.exposed.v1.core.dao.id.EntityID import org.jetbrains.exposed.v1.core.dao.id.IntIdTable import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.lowerCase +import org.jetbrains.exposed.v1.core.or import org.jetbrains.exposed.v1.dao.IntEntity import org.jetbrains.exposed.v1.dao.IntEntityClass import org.jetbrains.exposed.v1.jdbc.select @@ -48,7 +49,7 @@ class Team(id: EntityID) : IntEntity(id) { fun byId(id: Int) = teamCache.computeIfAbsent(id) { useDb { Team[id] } } @JvmStatic - fun get(name: String) = useDb { find { TeamTable.name.lowerCase() eq name.lowercase() }.firstOrNull() } + fun get(name: String) = useDb { find { TeamTable.name.lowerCase() eq name.lowercase() or (TeamTable.kuerzel.lowerCase() eq name.lowercase()) }.firstOrNull() } @JvmStatic fun getAll() = useDb { all().toList() } @@ -70,7 +71,7 @@ class Team(id: EntityID) : IntEntity(id) { private set private var teamAddress by TeamTable.address private var teamPort by TeamTable.port - val members by lazy { SteamwarUserTable.select(SteamwarUserTable.id).where { SteamwarUserTable.team eq teamId }.map { it[SteamwarUserTable.id].value } } + val members by lazy { useDb { SteamwarUserTable.select(SteamwarUserTable.id).where { SteamwarUserTable.team eq teamId }.map { it[SteamwarUserTable.id].value } } } fun size() = useDb { SteamwarUser.find { SteamwarUserTable.team eq teamId }.count().toInt() } fun disband(user: SteamwarUser) = useDb { diff --git a/CommonCore/SQL/src/de/steamwar/sql/internal/KotlinDatabase.kt b/CommonCore/SQL/src/de/steamwar/sql/internal/KotlinDatabase.kt index 735d4e22..b17c2d1f 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/internal/KotlinDatabase.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/internal/KotlinDatabase.kt @@ -25,7 +25,6 @@ import org.jetbrains.exposed.v1.core.Expression import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.StdOutSqlLogger import org.jetbrains.exposed.v1.core.statements.StatementType -import org.jetbrains.exposed.v1.core.statements.api.RowApi import org.jetbrains.exposed.v1.dao.IntEntity import org.jetbrains.exposed.v1.dao.IntEntityClass import org.jetbrains.exposed.v1.jdbc.Database @@ -34,14 +33,13 @@ import org.jetbrains.exposed.v1.jdbc.statements.jdbc.JdbcResult import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager import org.jetbrains.exposed.v1.jdbc.transactions.transaction import java.io.File -import java.sql.ResultSet import java.util.Properties object KotlinDatabase { - lateinit var db: Database + var db: Database? = null fun ensureConnected() { - if(KotlinDatabase::db.isInitialized) return + if(db != null) return val file = File(System.getProperty("user.home"), "mysql.properties") @@ -61,6 +59,12 @@ object KotlinDatabase { password = password ) } + + fun close() { + db?.connector()?.close() + TransactionManager.defaultDatabase?.let { TransactionManager.closeAndUnregister(it) } + db = null + } } fun useDb(statement: JdbcTransaction.() -> T): T { diff --git a/CommonCore/SQL/src/de/steamwar/sql/internal/Statement.kt b/CommonCore/SQL/src/de/steamwar/sql/internal/Statement.kt index 9ce038bc..aa6a81d4 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/internal/Statement.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/internal/Statement.kt @@ -19,15 +19,39 @@ package de.steamwar.sql.internal +import de.steamwar.sql.SteamwarUser +import org.jetbrains.exposed.v1.core.* import org.jetbrains.exposed.v1.jdbc.name import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager +import java.sql.ResultSet +import java.sql.SQLException -class Statement { +data class Statement(val statement: String) { companion object { @JvmStatic - fun closeAll() = TransactionManager.defaultDatabase?.let { TransactionManager.closeAndUnregister(it) } + fun closeAll() = KotlinDatabase.close() @JvmStatic fun productionDatabase() = TransactionManager.defaultDatabase?.name == "production" } + + fun select(user: ResultSetUser, vararg args: Any): T? = useDb { + exec(statement, args = args.map { getArgType(it) }) { + user.use(it) + } + } + + private fun getArgType(obj: Any) = when(obj) { + is String -> VarCharColumnType() to obj + is Int -> IntegerColumnType() to obj + is Long -> LongColumnType() to obj + is Boolean -> BooleanColumnType() to obj + is SteamwarUser -> IntegerColumnType() to obj.id.value + else -> error("Unknown type: ${obj::class.simpleName}") + } + + interface ResultSetUser { + @Throws(SQLException::class) + fun use(rs: ResultSet): T? + } } \ No newline at end of file diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/sql/PersonalKit.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/sql/PersonalKit.java index ec1a9b91..e8ac661e 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/sql/PersonalKit.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/sql/PersonalKit.java @@ -32,6 +32,14 @@ import java.util.stream.Collectors; public class PersonalKit { private final InternalKit kit; + public String getName() { + return kit.getName(); + } + + public boolean isInUse() { + return kit.getInUse(); + } + public String getRawInventory() { return kit.getRawInventory(); } diff --git a/VelocityCore/src/de/steamwar/velocitycore/ArenaMode.java b/VelocityCore/src/de/steamwar/velocitycore/ArenaMode.java index 48a40870..9657e30c 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/ArenaMode.java +++ b/VelocityCore/src/de/steamwar/velocitycore/ArenaMode.java @@ -50,7 +50,7 @@ public class ArenaMode { if(!folder.exists()) return; - for(File file : Arrays.stream(folder.listFiles((file, name) -> name.endsWith(".yml") && !name.endsWith(".kits.yml"))).sorted().toList()) { + for(File file : Arrays.stream(folder.listFiles((file, name) -> name.endsWith(".yml") && !name.endsWith(".kits.yml") && !name.equals("config.yml"))).sorted().toList()) { GameModeConfig gameModeConfig = new GameModeConfig<>(file, GameModeConfig.ToString, GameModeConfig.ToString, GameModeConfig.ToInternalName, false); if (!gameModeConfig.Server.loaded) continue; @@ -61,6 +61,7 @@ public class ArenaMode { } if (gameModeConfig.Schematic.loaded && gameModeConfig.Schematic.Type != SchematicType.Normal) { bySchemType.put(gameModeConfig.Schematic.Type, gameModeConfig); + System.out.println(gameModeConfig.configFile.getAbsolutePath()); SchematicType checkType = gameModeConfig.Schematic.Type.checkType(); if (checkType != null) bySchemType.put(checkType, gameModeConfig); } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java new file mode 100644 index 00000000..d5756890 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java @@ -0,0 +1,286 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2025 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.velocitycore.commands; + +import de.steamwar.linkage.Linked; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.sql.internal.Statement; + +import java.io.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Linked +public class GDPRQuery extends SWCommand { + + public GDPRQuery() { + super("gdprquery", UserPerm.ADMINISTRATION); + } + + @Register + public void generate(PlayerChatter sender) { + generate(sender, sender.user()); + } + + @Register + public void generate(Chatter sender, SteamwarUser user) { + VelocityCore.schedule(() -> { + try { + createZip(sender, user); + } catch (IOException e) { + throw new SecurityException("Could not create zip", e); + } + }).schedule(); + } + + private void createZip(Chatter sender, SteamwarUser user) throws IOException { + sender.system("GDPR_STATUS_WEBSITE"); + + ZipOutputStream out = new ZipOutputStream(new FileOutputStream(user.getUserName() + ".zip")); + + copy(getClass().getClassLoader().getResourceAsStream("GDPRQueryREADME.md"), out, "README.md"); + copy(getClass().getClassLoader().getResourceAsStream("GDPRQueryREADME.md"), out, "README.txt"); + + sender.system("GDPR_STATUS_WORLD"); + copyBauwelt(user, out, "/home/minecraft/userworlds/" + user.getUUID().toString(), "BuildWorld12"); + copyBauwelt(user, out, "/home/minecraft/userworlds15/" + user.getId(), "BuildWorld15"); + + sender.system("GDPR_STATUS_INVENTORIES"); + copyPlayerdata(user, out, "/home/minecraft/userworlds", "BuildInventories12"); + copyPlayerdata(user, out, "/home/minecraft/userworlds15", "BuildInventories15"); + + sender.system("GDPR_STATUS_DATABASE"); + sqlCSV(user, out, bannedIPs, "BannedIPs.csv"); + sqlCSV(user, out, bauweltMember, "BuildMember.csv"); + sqlCSV(user, out, bauweltMembers, "BuildMembers.csv"); + sqlCSV(user, out, checkedSchems, "SchematicChecksessions.csv"); + sqlCSV(user, out, userElo, "UserElo.csv"); + sqlCSV(user, out, fights, "Fights.csv"); + sqlCSV(user, out, ignoredPlayers, "IgnoredPlayers.csv"); + sqlCSV(user, out, ignoringPlayers, "IgnoringPlayers.csv"); + sqlCSV(user, out, schematicMember, "SchematicMember.csv"); + sqlCSV(user, out, schematicMembers, "SchematicMembers.csv"); + sqlCSV(user, out, pollAnswers, "PollAnswers.csv"); + sqlCSV(user, out, punishments, "Punishments.csv"); + sqlCSV(user, out, sessions, "Sessions.csv"); + sqlCSV(user, out, userData, "UserData.csv"); + sqlCSV(user, out, personalKits, "PersonalKits.csv"); + sqlCSV(user, out, schematics, "Schematics.csv"); + + personalKits(user, out); + schematics(user, out); + userConfig(user, out); + + sender.system("GDPR_STATUS_LOGS"); + copyLogs(user, out, new File("/logs"), "logs"); + + out.close(); + sender.system("GDPR_STATUS_FINISHED"); + } + + private static final Statement bannedIPs = new Statement("SELECT Timestamp, IP FROM BannedUserIPs WHERE UserID = ?"); + private static final Statement bauweltMember = new Statement("SELECT BauweltID AS Bauwelt, WorldEdit, World FROM BauweltMember WHERE MemberID = ?"); + private static final Statement bauweltMembers = new Statement("SELECT u.UserName AS 'User', m.WorldEdit AS WorldEdit, m.World AS World FROM BauweltMember m INNER JOIN UserData u ON m.MemberID = u.id WHERE m.BauweltID = ?"); + private static final Statement checkedSchems = new Statement("SELECT NodeName AS Schematic, StartTime, EndTime, DeclineReason AS Result FROM CheckedSchematic WHERE NodeOwner = ? ORDER BY StartTime ASC"); + private static final Statement userElo = new Statement("SELECT GameMode, Elo, Season FROM Elo WHERE UserID = ?"); + private static final Statement fights = new Statement("SELECT p.Team AS Team, p.Kit AS Kit, p.Kills AS Kills, p.IsOut AS Died, f.GameMode AS GameMode, f.Server AS Server, f.StartTime AS StartTime, f.Duration AS Duration, (f.BlueLeader = p.UserID) AS IsBlueLeader, (f.RedLeader = p.UserID) AS IsRedLeader, f.Win AS Winner, f.WinCondition AS WinCondition FROM Fight f INNER JOIN FightPlayer p ON f.FightID = p.FightID WHERE p.UserID = ? ORDER BY StartTime ASC"); + private static final Statement ignoredPlayers = new Statement("SELECT u.UserName AS IgnoredPlayer FROM IgnoredPlayers i INNER JOIN UserData u ON i.Ignored = u.id WHERE Ignorer = ?"); + private static final Statement ignoringPlayers = new Statement("SELECT Ignorer AS IgnoringPlayers FROM IgnoredPlayers WHERE Ignored = ?"); + private static final Statement schematicMember = new Statement("SELECT s.NodeName AS SchematicName, u.UserName AS SchematicOwner FROM NodeMember m INNER JOIN SchematicNode s ON m.NodeId = s.NodeId INNER JOIN UserData u ON s.NodeOwner = u.id WHERE m.UserId = ?"); + private static final Statement schematicMembers = new Statement("SELECT s.NodeName AS SchematicName, u.UserName AS Member FROM NodeMember m INNER JOIN SchematicNode s ON m.NodeId = s.NodeId INNER JOIN UserData u ON m.UserId = u.id WHERE s.NodeOwner = ?"); + private static final Statement pollAnswers = new Statement("SELECT Question, Answer FROM PollAnswer WHERE UserID = ?"); + private static final Statement punishments = new Statement("SELECT Type, StartTime, EndTime, Perma, Reason FROM Punishments WHERE UserId = ?"); + private static final Statement sessions = new Statement("SELECT StartTime, EndTime FROM Session WHERE UserID = ?"); + private static final Statement userData = new Statement("SELECT * FROM UserData WHERE id = ?"); + private static final Statement personalKits = new Statement("SELECT GameMode, Name, InUse FROM PersonalKit WHERE UserID = ?"); + private static final Statement personalKitData = new Statement("SELECT GameMode, Name, Inventory, Armor FROM PersonalKit WHERE UserID = ?"); + private static final Statement schematics = new Statement("SELECT NodeName AS SchematicName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank FROM SchematicNode WHERE NodeOwner = ?"); + private static final Statement schematicData = new Statement("SELECT NodeName, ParentNode, NodeFormat, NodeData FROM SchematicNode WHERE NodeOwner = ?"); + private static final Statement userConfig = new Statement("SELECT * FROM UserConfig WHERE User = ?"); + + private void sqlCSV(SteamwarUser user, ZipOutputStream out, Statement statement, String path) throws IOException { + write(stream -> statement.select(rs -> { + try { + OutputStreamWriter writer = new OutputStreamWriter(stream); + int columns = rs.getMetaData().getColumnCount(); + + for(int i = 1; i <= columns; i++) { + writer.write(rs.getMetaData().getColumnLabel(i)); + writer.write(";"); + } + writer.write("\n"); + + while(rs.next()) { + for(int i = 1; i <= columns; i++) { + try { + writer.write(rs.getString(i)); + } catch (NullPointerException e) { + // ignored + } + writer.write(";"); + } + writer.write("\n"); + } + writer.flush(); + } catch (IOException e) { + throw new SecurityException("Could not write file", e); + } + return null; + }, user.getId()), out, path); + } + + private void personalKits(SteamwarUser user, ZipOutputStream out) { + personalKitData.select(rs -> { + while(rs.next()) { + try { + String path = "PersonalKit/" + rs.getString("GameMode") + "/" + rs.getString("Name"); + try(InputStream data = rs.getBinaryStream("Inventory")) { + copy(data, out, path + ".Inventory.yml"); + } + try(InputStream data = rs.getBinaryStream("Armor")) { + copy(data, out, path + ".Armor.yml"); + } + } catch (IOException e) { + throw new SecurityException("Could not export PersonalKits", e); + } + } + return null; + }, user.getId()); + } + + private void schematics(SteamwarUser user, ZipOutputStream out) { + schematicData.select(rs -> { + while(rs.next()) { + String name = (rs.getString("ParentNode") != null ? rs.getString("ParentNode") : "") + ":" + rs.getString("NodeName"); + boolean format = rs.getBoolean("NodeFormat"); + try(InputStream data = rs.getBinaryStream("NodeData")) { + copy(data, out, "Schematics/" + name + (format ? ".schem" : ".schematic")); + } catch (IOException e) { + throw new SecurityException("Could not export Schematic", e); + } + } + return null; + }, user.getId()); + } + + private void userConfig(SteamwarUser user, ZipOutputStream out) { + userConfig.select(rs -> { + while(rs.next()) { + String name = rs.getString("Config"); + try(InputStream data = rs.getBinaryStream("Value")) { + copy(data, out, name + ".yapion"); + } catch (IOException e) { + throw new SecurityException("Could not export UserConfig", e); + } + } + return null; + }, user.getId()); + } + + private void copyLogs(SteamwarUser user, ZipOutputStream out, File log, String outFile) throws IOException { + if (log.isDirectory()) { + for(File logfile : log.listFiles()) { + copyLogs(user, out, logfile, outFile + "/" + logfile.getName().replace(".gz", "")); + } + } else { + Process reader = new ProcessBuilder("zgrep", "^.*" + user.getUserName() + "\\( issued server command:\\| moved too quickly!\\| executed command:\\| lost connection:\\||\\|ยป\\|\\[\\|\\]\\).*$", log.getPath()).start(); + copy(reader.getInputStream(), out, outFile); + } + } + + private void copyBauwelt(SteamwarUser user, ZipOutputStream out, String inDir, String outDir) throws IOException { + File world = new File(inDir); + if(!world.exists()) + return; + + copy(new File(world, "level.dat"), out, outDir + "/level.dat"); + + File region = new File(world, "region"); + for(File regionfile : region.listFiles()) { + copy(regionfile, out, outDir + "/region/" + regionfile.getName()); + } + + File poi = new File(world, "poi"); + if(poi.exists()) { + for(File regionfile : poi.listFiles()) { + copy(regionfile, out, outDir + "/poi/" + regionfile.getName()); + } + } + + File playerdata = new File(world, "playerdata/" + user.getUUID().toString() + ".dat"); + if(playerdata.exists()) + copy(playerdata, out, outDir + "/playerdata/" + user.getUUID().toString() + ".dat"); + } + + private void copyPlayerdata(SteamwarUser user, ZipOutputStream out, String inDir, String outDir) throws IOException { + File worlds = new File(inDir); + String path = "playerdata/" + user.getUUID().toString() + ".dat"; + + int i = 0; + for(File world : worlds.listFiles()) { + File playerdata = new File(world, path); + if(!playerdata.exists()) + continue; + + copy(playerdata, out, outDir + "/" + (i++) + "/" + user.getUUID().toString() + ".dat"); + } + } + + private void copy(File file, ZipOutputStream out, String path) throws IOException { + try(FileInputStream in = new FileInputStream(file)) { + copy(in, out, path); + } + } + + private void copy(InputStream in, ZipOutputStream out, String path) throws IOException { + boolean initialized = false; + + int bytes; + for(byte[] buf = new byte[8192]; (bytes = in.read(buf)) > 0; ) { + if(!initialized) { + ZipEntry entry = new ZipEntry(path); + out.putNextEntry(entry); + initialized = true; + } + + out.write(buf, 0, bytes); + } + + if(initialized) { + out.closeEntry(); + } + } + + private void write(Writer writer, ZipOutputStream out, String path) throws IOException { + ZipEntry entry = new ZipEntry(path); + out.putNextEntry(entry); + writer.accept(out); + out.closeEntry(); + } + + private interface Writer { + void accept(OutputStream stream) throws IOException; + } +}