new file: .gitignore
new file: README.md new file: database/schema.sql new file: paper-plugin/pom.xml new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/PaperLoggerPlugin.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/database/DatabaseManager.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/BlockListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/EntityListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/InventoryListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.java new file: paper-plugin/src/main/java/de/simolzimol/mclogger/paper/listeners/WorldListener.java new file: paper-plugin/src/main/resources/config.yml new file: paper-plugin/src/main/resources/plugin.yml new file: paper-plugin/target/classes/config.yml new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/PaperLoggerPlugin.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand$RsConsumer.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager$ThrowingRunnable.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/BlockListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/EntityListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.class new file: paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/WorldListener.class new file: paper-plugin/target/classes/plugin.yml new file: paper-plugin/target/maven-archiver/pom.properties new file: paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file: paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file: paper-plugin/target/mclogger-paper-1.0.0.jar new file: paper-plugin/target/original-mclogger-paper-1.0.0.jar new file: velocity-plugin/pom.xml new file: velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/VelocityLoggerPlugin.java new file: velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager.java new file: velocity-plugin/src/main/java/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.java new file: velocity-plugin/src/main/resources/velocity-config.yml new file: velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/VelocityLoggerPlugin.class new file: velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager$ThrowingRunnable.class new file: velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/database/VelocityDatabaseManager.class new file: velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.class new file: velocity-plugin/target/classes/velocity-config.yml new file: velocity-plugin/target/classes/velocity-plugin.json new file: velocity-plugin/target/maven-archiver/pom.properties new file: velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file: velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file: velocity-plugin/target/mclogger-velocity-1.0.0.jar new file: velocity-plugin/target/original-mclogger-velocity-1.0.0.jar new file: web/Dockerfile new file: web/app.py new file: web/blueprints/__init__.py new file: web/blueprints/auth.py new file: web/blueprints/group_admin.py new file: web/blueprints/panel.py new file: web/blueprints/site_admin.py new file: web/config.py new file: web/crypto.py new file: web/docker-compose.yml new file: web/panel_db.py new file: web/requirements.txt new file: web/static/css/style.css new file: web/static/js/main.js new file: web/templates/_pagination.html new file: web/templates/admin/base.html new file: web/templates/admin/dashboard.html new file: web/templates/admin/group_edit.html new file: web/templates/admin/group_members.html new file: web/templates/admin/groups.html new file: web/templates/admin/user_edit.html new file: web/templates/admin/users.html new file: web/templates/auth/admin_login.html new file: web/templates/auth/login.html new file: web/templates/base.html new file: web/templates/blocks.html new file: web/templates/chat.html new file: web/templates/commands.html new file: web/templates/dashboard.html new file: web/templates/deaths.html new file: web/templates/group_admin/base.html new file: web/templates/group_admin/dashboard.html new file: web/templates/group_admin/database.html new file: web/templates/group_admin/member_edit.html new file: web/templates/group_admin/members.html new file: web/templates/login.html new file: web/templates/panel/blocks.html new file: web/templates/panel/chat.html new file: web/templates/panel/commands.html new file: web/templates/panel/dashboard.html new file: web/templates/panel/deaths.html new file: web/templates/panel/no_db.html new file: web/templates/panel/perms.html new file: web/templates/panel/player_detail.html new file: web/templates/panel/players.html new file: web/templates/panel/proxy.html new file: web/templates/panel/server_events.html new file: web/templates/panel/sessions.html new file: web/templates/perms.html new file: web/templates/player_detail.html new file: web/templates/players.html new file: web/templates/proxy.html new file: web/templates/server_events.html new file: web/templates/sessions.html
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package de.simolzimol.mclogger.velocity;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||
import de.simolzimol.mclogger.velocity.database.VelocityDatabaseManager;
|
||||
import de.simolzimol.mclogger.velocity.listeners.VelocityEventListener;
|
||||
import org.spongepowered.configurate.ConfigurationNode;
|
||||
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCLogger – Velocity Proxy Plugin entry point
|
||||
*
|
||||
* @author SimolZimol
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Plugin(
|
||||
id = "mclogger-velocity",
|
||||
name = "MCLogger-Velocity",
|
||||
version = "1.0.0",
|
||||
description = "Comprehensive proxy event logger with MariaDB storage",
|
||||
authors = {"SimolZimol"},
|
||||
url = "https://github.com/SimolZimol/MCLogger"
|
||||
)
|
||||
public class VelocityLoggerPlugin {
|
||||
|
||||
private final ProxyServer server;
|
||||
private final Logger logger;
|
||||
private final Path dataDirectory;
|
||||
|
||||
private VelocityDatabaseManager db;
|
||||
|
||||
@Inject
|
||||
public VelocityLoggerPlugin(ProxyServer server, Logger logger,
|
||||
@DataDirectory Path dataDirectory) {
|
||||
this.server = server;
|
||||
this.logger = logger;
|
||||
this.dataDirectory = dataDirectory;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onInitialize(ProxyInitializeEvent event) {
|
||||
// Load configuration
|
||||
ConfigurationNode cfg = loadConfig();
|
||||
if (cfg == null) {
|
||||
logger.error("[MCLogger] Failed to load configuration!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Database connection
|
||||
db = new VelocityDatabaseManager(this);
|
||||
if (!db.connect(cfg)) {
|
||||
logger.error("[MCLogger] No database connection – plugin inactive.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Register plugin messaging channel incoming from Paper backends
|
||||
server.getChannelRegistrar().register(MinecraftChannelIdentifier.from("mclogger:logged"));
|
||||
|
||||
// Register listeners
|
||||
server.getEventManager().register(this, new VelocityEventListener(this));
|
||||
|
||||
// Log proxy start
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("velocity_version", server.getVersion().getVersion());
|
||||
details.put("max_players", server.getConfiguration().getShowMaxPlayers());
|
||||
db.insertProxyEvent("proxy_start", null, null, null, null, null, details);
|
||||
|
||||
logger.info("[MCLogger] Velocity plugin started! Proxy: " + db.getProxyName());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onShutdown(ProxyShutdownEvent event) {
|
||||
if (db != null && db.isConnected()) {
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("online_players", server.getPlayerCount());
|
||||
db.insertProxyEvent("proxy_stop", null, null, null, null, null, details);
|
||||
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
|
||||
db.disconnect();
|
||||
}
|
||||
logger.info("[MCLogger] Velocity plugin shut down.");
|
||||
}
|
||||
|
||||
private ConfigurationNode loadConfig() {
|
||||
try {
|
||||
if (!Files.exists(dataDirectory)) {
|
||||
Files.createDirectories(dataDirectory);
|
||||
}
|
||||
Path configFile = dataDirectory.resolve("config.yml");
|
||||
if (!Files.exists(configFile)) {
|
||||
try (InputStream in = getClass().getResourceAsStream("/velocity-config.yml")) {
|
||||
if (in != null) Files.copy(in, configFile);
|
||||
}
|
||||
}
|
||||
YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
|
||||
.path(configFile)
|
||||
.build();
|
||||
return loader.load();
|
||||
} catch (IOException e) {
|
||||
logger.error("[MCLogger] Error loading configuration: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ProxyServer getServer() { return server; }
|
||||
public Logger getLogger() { return logger; }
|
||||
public VelocityDatabaseManager getDb() { return db; }
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package de.simolzimol.mclogger.velocity.database;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import de.simolzimol.mclogger.velocity.VelocityLoggerPlugin;
|
||||
import org.spongepowered.configurate.ConfigurationNode;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* MariaDB manager for the Velocity plugin.
|
||||
* Writes proxy events asynchronously to the database.
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class VelocityDatabaseManager {
|
||||
|
||||
private final VelocityLoggerPlugin plugin;
|
||||
private HikariDataSource dataSource;
|
||||
private String proxyName;
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
public VelocityDatabaseManager(VelocityLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Connection
|
||||
// --------------------------------------------------------
|
||||
|
||||
public boolean connect(ConfigurationNode cfg) {
|
||||
proxyName = cfg.node("proxy", "name").getString("proxy-01");
|
||||
|
||||
HikariConfig hk = new HikariConfig();
|
||||
hk.setDriverClassName("de.simolzimol.mclogger.velocity.lib.mariadb.jdbc.Driver");
|
||||
hk.setJdbcUrl(String.format("jdbc:mariadb://%s:%d/%s?useSSL=%b&autoReconnect=true&characterEncoding=UTF-8",
|
||||
cfg.node("database", "host").getString("localhost"),
|
||||
cfg.node("database", "port").getInt(3306),
|
||||
cfg.node("database", "database").getString("mclogger"),
|
||||
cfg.node("database", "ssl").getBoolean(false)));
|
||||
hk.setUsername(cfg.node("database", "username").getString("root"));
|
||||
hk.setPassword(cfg.node("database", "password").getString(""));
|
||||
hk.setMaximumPoolSize(cfg.node("database", "pool-size").getInt(5));
|
||||
hk.setMinimumIdle(1);
|
||||
hk.setConnectionTimeout(30_000);
|
||||
hk.setIdleTimeout(600_000);
|
||||
hk.setPoolName("MCLogger-Velocity");
|
||||
|
||||
try {
|
||||
dataSource = new HikariDataSource(hk);
|
||||
registerProxy(cfg);
|
||||
plugin.getLogger().info("MCLogger-Velocity: Database connected - Proxy: " + proxyName);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().error("MCLogger-Velocity: Database connection failed!", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
if (dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void registerProxy(ConfigurationNode cfg) throws SQLException {
|
||||
try (Connection con = dataSource.getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO servers (server_name, server_type) VALUES (?,?) " +
|
||||
"ON DUPLICATE KEY UPDATE last_seen = CURRENT_TIMESTAMP(3)")) {
|
||||
ps.setString(1, proxyName);
|
||||
ps.setString(2, "velocity");
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Async helper
|
||||
// --------------------------------------------------------
|
||||
|
||||
private void asyncExec(ThrowingRunnable r) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
r.run();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warn("MCLogger-Velocity: DB error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ThrowingRunnable {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Players
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void upsertPlayer(String uuid, String username, String ip) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO players (uuid, username, ip_address) VALUES (?,?,?) " +
|
||||
"ON DUPLICATE KEY UPDATE username=VALUES(username), ip_address=VALUES(ip_address), last_seen=CURRENT_TIMESTAMP(3)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, username);
|
||||
ps.setString(3, ip);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Proxy Events (central table)
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertProxyEvent(String type, String playerUuid, String playerName,
|
||||
String fromServer, String toServer,
|
||||
String ip, Map<String, Object> details) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO proxy_events (event_type, player_uuid, player_name, proxy_name, from_server, to_server, ip_address, details) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?)")) {
|
||||
ps.setString(1, type);
|
||||
ps.setString(2, playerUuid);
|
||||
ps.setString(3, playerName);
|
||||
ps.setString(4, proxyName);
|
||||
ps.setString(5, fromServer);
|
||||
ps.setString(6, toServer);
|
||||
ps.setString(7, ip);
|
||||
ps.setString(8, details != null ? GSON.toJson(details) : null);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Chat & Commands (shared tables)
|
||||
// --------------------------------------------------------
|
||||
|
||||
public void insertChat(String uuid, String name, String message) {
|
||||
insertChatWithServer(uuid, name, proxyName, message);
|
||||
}
|
||||
|
||||
public void insertChatWithServer(String uuid, String name, String serverName, String message) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_chat (player_uuid, player_name, server_name, message, channel) VALUES (?,?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, serverName);
|
||||
ps.setString(4, message);
|
||||
ps.setString(5, "global");
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void insertCommand(String uuid, String name, String command) {
|
||||
insertCommandWithServer(uuid, name, proxyName, command);
|
||||
}
|
||||
|
||||
public void insertCommandWithServer(String uuid, String name, String serverName, String command) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_commands (player_uuid, player_name, server_name, command) VALUES (?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, serverName);
|
||||
ps.setString(4, command);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Sessions
|
||||
// --------------------------------------------------------
|
||||
|
||||
/** Opens a new session row. server_name is updated on disconnect to the last known backend. */
|
||||
public void insertSessionLogin(String uuid, String name, String ip, String clientBrand) {
|
||||
asyncExec(() -> {
|
||||
String country = lookupCountry(ip);
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO player_sessions (player_uuid, player_name, server_name, ip_address, country, client_version) VALUES (?,?,?,?,?,?)")) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, proxyName);
|
||||
ps.setString(4, ip);
|
||||
ps.setString(5, country);
|
||||
ps.setString(6, clientBrand);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Closes the open session, sets the actual backend server name and duration. */
|
||||
public void closeSession(String uuid, String lastServer, long durationSec) {
|
||||
asyncExec(() -> {
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"UPDATE player_sessions SET logout_time=CURRENT_TIMESTAMP(3), duration_sec=?, server_name=? " +
|
||||
"WHERE player_uuid=? AND logout_time IS NULL ORDER BY login_time DESC LIMIT 1")) {
|
||||
ps.setLong(1, durationSec);
|
||||
ps.setString(2, lastServer);
|
||||
ps.setString(3, uuid);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
try (Connection con = getConnection();
|
||||
PreparedStatement ps = con.prepareStatement(
|
||||
"UPDATE players SET total_playtime_sec = total_playtime_sec + ? WHERE uuid = ?")) {
|
||||
ps.setLong(1, durationSec);
|
||||
ps.setString(2, uuid);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String lookupCountry(String ip) {
|
||||
if (ip == null || ip.equals("unknown") || ip.equals("::1")
|
||||
|| ip.startsWith("127.") || ip.startsWith("10.")
|
||||
|| ip.startsWith("192.168.") || ip.startsWith("172.")) {
|
||||
return "Local";
|
||||
}
|
||||
try {
|
||||
java.net.URL url = new java.net.URL("http://ip-api.com/json/" + ip + "?fields=country");
|
||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(3000);
|
||||
conn.setReadTimeout(3000);
|
||||
conn.setRequestMethod("GET");
|
||||
try (java.io.BufferedReader reader = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(conn.getInputStream()))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) sb.append(line);
|
||||
com.google.gson.JsonObject obj = new com.google.gson.Gson().fromJson(sb.toString(), com.google.gson.JsonObject.class);
|
||||
if (obj != null && obj.has("country")) return obj.get("country").getAsString();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Getter
|
||||
// --------------------------------------------------------
|
||||
|
||||
public String getProxyName() { return proxyName; }
|
||||
public boolean isConnected() { return dataSource != null && !dataSource.isClosed(); }
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package de.simolzimol.mclogger.velocity.listeners;
|
||||
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
||||
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import de.simolzimol.mclogger.velocity.VelocityLoggerPlugin;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* All Velocity event listeners:
|
||||
* - Login / Disconnect
|
||||
* - Server switch
|
||||
* - Chat
|
||||
* - Commands
|
||||
*
|
||||
* @author SimolZimol
|
||||
*/
|
||||
public class VelocityEventListener {
|
||||
|
||||
private final VelocityLoggerPlugin plugin;
|
||||
/** Login timestamps for session duration calculation */
|
||||
private final Map<UUID, Long> loginTimes = new ConcurrentHashMap<>();
|
||||
/** Commands already logged by a Paper backend – Velocity skips these */
|
||||
private final Set<String> paperLoggedCommands = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public VelocityEventListener(VelocityLoggerPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Login
|
||||
// --------------------------------------------------------
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void onLogin(PostLoginEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
loginTimes.put(p.getUniqueId(), System.currentTimeMillis());
|
||||
|
||||
String ip = p.getRemoteAddress() != null
|
||||
? p.getRemoteAddress().getAddress().getHostAddress() : "unknown";
|
||||
|
||||
// Upsert player in DB
|
||||
plugin.getDb().upsertPlayer(p.getUniqueId().toString(), p.getUsername(), ip);
|
||||
|
||||
// Open session row (server_name is filled in on disconnect with last known backend)
|
||||
String clientBrand = p.getClientBrand() != null ? p.getClientBrand() : "unknown";
|
||||
plugin.getDb().insertSessionLogin(p.getUniqueId().toString(), p.getUsername(), ip, clientBrand);
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("ip", ip);
|
||||
details.put("client_brand", p.getClientBrand() != null ? p.getClientBrand() : "unknown");
|
||||
details.put("protocol", p.getProtocolVersion().getProtocol());
|
||||
details.put("ping", p.getPing());
|
||||
details.put("online_mode", !p.isOnlineMode() ? "offline/bedrock" : "java");
|
||||
|
||||
plugin.getDb().insertProxyEvent(
|
||||
"login",
|
||||
p.getUniqueId().toString(),
|
||||
p.getUsername(),
|
||||
null, null, ip, details
|
||||
);
|
||||
|
||||
plugin.getLogger().info("[MCLogger] LOGIN: " + p.getUsername() + " (" + ip + ")");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Disconnect
|
||||
// --------------------------------------------------------
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void onDisconnect(DisconnectEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
UUID uuid = p.getUniqueId();
|
||||
|
||||
Long loginTimeMs = loginTimes.remove(uuid);
|
||||
long durationSec = loginTimeMs != null ? (System.currentTimeMillis() - loginTimeMs) / 1000 : 0;
|
||||
|
||||
String lastServer = p.getCurrentServer()
|
||||
.map(s -> s.getServerInfo().getName())
|
||||
.orElse(plugin.getDb().getProxyName());
|
||||
|
||||
// Close the session opened in onLogin
|
||||
plugin.getDb().closeSession(uuid.toString(), lastServer, durationSec);
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("reason", event.getLoginStatus().name());
|
||||
details.put("session_duration_sec", durationSec);
|
||||
details.put("current_server", lastServer);
|
||||
|
||||
plugin.getDb().insertProxyEvent(
|
||||
"disconnect",
|
||||
uuid.toString(),
|
||||
p.getUsername(),
|
||||
p.getCurrentServer().map(s -> s.getServerInfo().getName()).orElse(null),
|
||||
null, null, details
|
||||
);
|
||||
|
||||
plugin.getLogger().info("[MCLogger] DISCONNECT: " + p.getUsername() +
|
||||
" (duration: " + durationSec + "s, reason: " + event.getLoginStatus().name() + ")");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Server switch (after switching)
|
||||
// --------------------------------------------------------
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void onServerConnected(ServerConnectedEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
String from = event.getPreviousServer().map(s -> s.getServerInfo().getName()).orElse(null);
|
||||
String to = event.getServer().getServerInfo().getName();
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("ping", p.getPing());
|
||||
|
||||
plugin.getDb().insertProxyEvent(
|
||||
"server_switch",
|
||||
p.getUniqueId().toString(),
|
||||
p.getUsername(),
|
||||
from, to, null, details
|
||||
);
|
||||
|
||||
plugin.getLogger().info("[MCLogger] SERVER-SWITCH: " + p.getUsername() +
|
||||
" | " + (from != null ? from : "joining") + " → " + to);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Server connect attempt (before switching)
|
||||
// --------------------------------------------------------
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void onServerPreConnect(ServerPreConnectEvent event) {
|
||||
if (event.getResult().getServer().isEmpty()) return;
|
||||
Player p = event.getPlayer();
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("target_server", event.getResult().getServer().get().getServerInfo().getName());
|
||||
|
||||
plugin.getDb().insertProxyEvent(
|
||||
"server_switch",
|
||||
p.getUniqueId().toString(),
|
||||
p.getUsername(),
|
||||
p.getCurrentServer().map(s -> s.getServerInfo().getName()).orElse(null),
|
||||
event.getResult().getServer().get().getServerInfo().getName(),
|
||||
null, details
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Commands – only proxy-native commands (e.g. /server, /glist)
|
||||
// Commands – Paper logs with position and notifies proxy; Velocity is fallback.
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Receives plugin messages from Paper backends.
|
||||
* When Paper has logged a command, it sends the key here so Velocity skips it.
|
||||
*/
|
||||
@Subscribe
|
||||
public void onPluginMessage(PluginMessageEvent event) {
|
||||
if (!event.getIdentifier().getId().equals("mclogger:logged")) return;
|
||||
// Prevent Velocity from forwarding this internal message to the client
|
||||
event.setResult(PluginMessageEvent.ForwardResult.handled());
|
||||
String key = new String(event.getData(), StandardCharsets.UTF_8);
|
||||
paperLoggedCommands.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules command logging with a 300 ms delay.
|
||||
* If a Paper backend signals it already logged the command, the task is cancelled.
|
||||
* On servers without the Paper plugin, Velocity logs it as fallback.
|
||||
*/
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void onCommand(CommandExecuteEvent event) {
|
||||
if (!(event.getCommandSource() instanceof Player)) return;
|
||||
Player p = (Player) event.getCommandSource();
|
||||
|
||||
String rawCommand = event.getCommand();
|
||||
// Key format matches what Paper sends: uuid|/command args
|
||||
String key = p.getUniqueId() + "|/" + rawCommand;
|
||||
String serverName = p.getCurrentServer()
|
||||
.map(s -> s.getServerInfo().getName())
|
||||
.orElse(plugin.getDb().getProxyName());
|
||||
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> {
|
||||
// If Paper already sent us a confirmation, skip – avoid duplicate
|
||||
if (paperLoggedCommands.remove(key)) return;
|
||||
plugin.getDb().insertCommandWithServer(
|
||||
p.getUniqueId().toString(),
|
||||
p.getUsername(),
|
||||
serverName,
|
||||
"/" + rawCommand
|
||||
);
|
||||
})
|
||||
.delay(Duration.ofMillis(300))
|
||||
.schedule();
|
||||
}
|
||||
|
||||
// Note: Chat is logged at proxy level (see onChat below).
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Chat – logged at proxy level with correct backend server name
|
||||
// --------------------------------------------------------
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void onChat(PlayerChatEvent event) {
|
||||
if (!event.getResult().isAllowed()) return;
|
||||
Player p = event.getPlayer();
|
||||
String serverName = p.getCurrentServer()
|
||||
.map(s -> s.getServerInfo().getName())
|
||||
.orElse(plugin.getDb().getProxyName());
|
||||
plugin.getDb().insertChatWithServer(
|
||||
p.getUniqueId().toString(),
|
||||
p.getUsername(),
|
||||
serverName,
|
||||
event.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
17
velocity-plugin/src/main/resources/velocity-config.yml
Normal file
17
velocity-plugin/src/main/resources/velocity-config.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
# ============================================================
|
||||
# MCLogger – Velocity Konfiguration
|
||||
# Author: SimolZimol
|
||||
# ============================================================
|
||||
|
||||
proxy:
|
||||
# Eindeutiger Name dieses Proxies (wird in der DB gespeichert)
|
||||
name: "proxy-01"
|
||||
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
database: "mclogger"
|
||||
username: "mclogger"
|
||||
password: "change_me_please"
|
||||
ssl: false
|
||||
pool-size: 5
|
||||
Reference in New Issue
Block a user