Compare commits
459 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627559356b | ||
|
|
5012bcd95b | ||
|
|
de0255965a | ||
|
|
88520bd34a | ||
|
|
87274ffc9c | ||
|
|
6694c40842 | ||
| 7a8261a6e5 | |||
| 63a18e224e | |||
|
|
84853c7727 | ||
|
|
70f98452a8 | ||
| 122e7735d5 | |||
|
|
81786dc084 | ||
|
|
a32f087353 | ||
|
|
2c3050c87a | ||
|
|
2c2dd9c248 | ||
|
|
23cbb557e5 | ||
|
|
42e80980a3 | ||
| 2c608469df | |||
|
|
f29bb25435 | ||
|
|
417947c396 | ||
|
|
3b2218e37e | ||
|
|
39e86e4292 | ||
| 8ffb5ca510 | |||
| a7a87fcfcd | |||
| f0df58fb1e | |||
| 216d6aa791 | |||
| b339ccc136 | |||
| 618882032f | |||
| 2bbec8d313 | |||
| 2593df4868 | |||
| 9a139df523 | |||
| bedbf19c6d | |||
| c7265b3d9e | |||
| c707326b3d | |||
| a6a4713dd7 | |||
| 3f282ec83b | |||
| 431f017636 | |||
| 06af578f1b | |||
| a4c5b88841 | |||
| 5492479774 | |||
| 1c3ced16b9 | |||
| 5ad81eae41 | |||
| 2fd226595f | |||
| 5a1ed997d7 | |||
| 5172242544 | |||
| a3b81a23c6 | |||
| f099f5f739 | |||
| b15cb38242 | |||
| c4c8e5f5d7 | |||
| 38c1cd2f4f | |||
| 7292bfbba3 | |||
| e469490626 | |||
| c8b89fc74d | |||
| c38efbe7f9 | |||
| 8a603cd4c8 | |||
| 41af81fb0b | |||
| da51744f6c | |||
| e945c7c214 | |||
| 8b39bd6ed7 | |||
| 120a4e138c | |||
| a9b6a49ff6 | |||
| 1195dfd94e | |||
| 82aa8ded53 | |||
| c9940fb435 | |||
| 229e1ee1e4 | |||
| 6799e4f87d | |||
| 1d12f92bfd | |||
| a3d863ef82 | |||
| ed5758f816 | |||
| f6a1256ebe | |||
| 8f5da4eab1 | |||
| 03033c2ff4 | |||
| 46c5fa9034 | |||
| e66cb264d9 | |||
| 5c4f984215 | |||
| 2f207b0a5f | |||
| 43e4fb440b | |||
| 6f2cc360f5 | |||
| 93f437ac06 | |||
| 244a54620d | |||
| 0cf19987b7 | |||
| abbec8b1c6 | |||
| c0550435bf | |||
| ff1a8b7ad9 | |||
| fcb3fae8f6 | |||
| 03ea5527b1 | |||
| 0c261a2cbf | |||
| 6d64a2234d | |||
| 5eccd10123 | |||
| 8a988a6af0 | |||
| 8a61a54ed8 | |||
| b4ecea72eb | |||
| 6058703c1a | |||
| bf0a723b79 | |||
| bc05772dca | |||
| 1066333de6 | |||
| b138ce421b | |||
| a348c7d371 | |||
| c8d4bec89f | |||
| f702e3e4af | |||
| cf94fe4c21 | |||
| 421c59d274 | |||
| ccff5f1a69 | |||
| 440f384b98 | |||
| bd8fae80ff | |||
| 83f3c9a203 | |||
| 7d74bb1a4f | |||
| 9b0904a3c8 | |||
| e021a832fc | |||
| b8644552ed | |||
| fcae7fbc49 | |||
| ca257a73a5 | |||
| 3f1234c46e | |||
| f81809eedd | |||
| ddf2e23d4e | |||
| 534c6964bc | |||
| 0c68486fea | |||
| c5cbede79c | |||
| cfadfc540e | |||
| d4391382fe | |||
| b6a7a94fc2 | |||
| d32a5ce10b | |||
| 9aa94b8dad | |||
| af07941725 | |||
| b435f76ae7 | |||
| e585f1546b | |||
| 5e396b98a9 | |||
| 8fe4ca5d8e | |||
| 386ddfdc0a | |||
| b1643c91de | |||
| 351b14279f | |||
| 50ea7f6e7f | |||
| 307fceebcb | |||
| de72836619 | |||
| 3c5b7a8e3d | |||
| 4cbb784d8c | |||
| a90466fcaf | |||
| 240566d744 | |||
| 8bac2ee459 | |||
| 9757bf6475 | |||
| 4e2f27527e | |||
| d1d0876283 | |||
| 377a9c7d65 | |||
| 1b4dbc9e51 | |||
| 3ddbb54442 | |||
| e32a7457eb | |||
| e0ab059ae5 | |||
| a92999c82b | |||
| b394136e1e | |||
| 75d112f851 | |||
| 249d216591 | |||
| 5f52d4fb61 | |||
| e47b9839fb | |||
| b0c7d92e1e | |||
| 8d802a7726 | |||
| 4cf95738bf | |||
| 338c6320b6 | |||
| 4b3ad926cb | |||
|
|
66030fc835 | ||
|
|
af2d3fb4e8 | ||
|
|
cc618320ab | ||
|
|
365ed04af4 | ||
|
|
d686fee958 | ||
|
|
f746d46694 | ||
|
|
8b43fe4264 | ||
|
|
70d264f9bf | ||
|
|
cfc9773ca6 | ||
| f67a2a687e | |||
| beccbebb90 | |||
|
|
1179398aed | ||
|
|
fa07b66b76 | ||
|
|
4f9475cc09 | ||
|
|
44951c4001 | ||
| 802d1dfb33 | |||
| fbd861a341 | |||
| 599dd61b5b | |||
|
|
c1a99c1af1 | ||
| a2d9dc59ff | |||
|
|
16a8090715 | ||
| b756fe3402 | |||
| d783ab38da | |||
| 708aa2fbf0 | |||
| d327badb87 | |||
| bb00590e1b | |||
| 732139ca94 | |||
| 658d0cfc4c | |||
| a565e837de | |||
| d66ebeda2f | |||
| 970bee2569 | |||
| ac1eca1f7f | |||
| b81d621ae3 | |||
| fed8ac4727 | |||
| edf0c4e70d | |||
| 749ec3dfc2 | |||
| 10289d1860 | |||
| 0cdd6fa80f | |||
| 9d51485ad4 | |||
| cc11eee1cf | |||
| 64cb5b370d | |||
| bd73ba7338 | |||
| c42ec83661 | |||
| f7bd1c37f0 | |||
| b8e4add286 | |||
| c0c6ac4259 | |||
| 64c46d545c | |||
| c252131a9e | |||
| 036c211e8e | |||
| 0563a8426a | |||
| 9fb5fee007 | |||
| 5ad11178fb | |||
| 926b6d3b89 | |||
| a55615e075 | |||
| 1effb90d02 | |||
| b6ca590121 | |||
| 99bbc3c527 | |||
| 05e7b49416 | |||
| c7e5963e40 | |||
| bd81c7c87b | |||
| 3ca3f79e19 | |||
| 4ec4e8d0ce | |||
| 0816de11bf | |||
| 369dc90288 | |||
| 4df09f3e4a | |||
| fe52447560 | |||
| 1c3d3144d3 | |||
| f375f61c25 | |||
| e6207e3be2 | |||
| 0af4672c6b | |||
| b3e435c6ae | |||
| 5543507321 | |||
| b2a73884b9 | |||
| 725785a537 | |||
| 389c2f8de7 | |||
| d60256e69d | |||
| 45ef989641 | |||
| ca18a8d22c | |||
| ceef407b17 | |||
| d0b99c9fef | |||
| 6d137928f7 | |||
| e5c0b24c60 | |||
| 1f8ecfe7c2 | |||
| 5ed06071c0 | |||
| a6a8ee0daa | |||
| 953b46a967 | |||
| 60b31bcfee | |||
| 5286cc507e | |||
| a0377c98d1 | |||
| cb025e3101 | |||
| 1abfb9faa9 | |||
| 4d999dc8d8 | |||
| 5d2e63d5b9 | |||
| 9e297befca | |||
| bbaa259028 | |||
| 19df402e4c | |||
| dc3bc57d4a | |||
| 14ca1905fa | |||
| ed597af090 | |||
| 3c5007ef1a | |||
| 9de509438d | |||
| a8845e295a | |||
| 99156ef3b8 | |||
| e1f95dcf32 | |||
| d9d082d2a4 | |||
| 5575fee327 | |||
| 703c6fce7b | |||
| 4c78ba60b0 | |||
| f976c05b39 | |||
| 4b302aa41d | |||
| 208ba1af8b | |||
| 9e38e31277 | |||
| 61147622e7 | |||
| 0cd2e91a3a | |||
| e1ba6dd2ae | |||
| a47462e20f | |||
| 212e9e1fa6 | |||
| dada2881e3 | |||
| fc2aa19ef9 | |||
| 1aa263e57b | |||
| 5264d39885 | |||
| 5b3345c0ee | |||
| c8d617670d | |||
| 9e19cf8f99 | |||
| 4c32fb65c6 | |||
| f9927a6230 | |||
| 9cbd33debd | |||
| 210d5049d9 | |||
| ec831a2847 | |||
| ce976084d2 | |||
| 293ba36730 | |||
| 0a98003e8b | |||
| d72c94723f | |||
| c2b8f32d55 | |||
| 556e688bf6 | |||
| 1e43bd6fe0 | |||
| e315369908 | |||
| 577d50748d | |||
| 0463cf3480 | |||
| ea34c3802d | |||
| e5baf0fde4 | |||
| 3453c0a161 | |||
| 8c54fb2322 | |||
| 001f46bd36 | |||
| 901a4ed5a8 | |||
| 5177025e22 | |||
| eda4bcff2b | |||
| 5619b602c2 | |||
| 361d9724fd | |||
| a3c82a91ff | |||
| de75c65a9e | |||
| 71b2cf2e0b | |||
| 90f85ee8ed | |||
| 6ef483a9ea | |||
| bb3a3fd17f | |||
| 52bbc2f2cd | |||
| 5750f324e2 | |||
| ee6833d126 | |||
| 9dc7b0ffe8 | |||
| 5c0c071212 | |||
| 8104487786 | |||
| b695a9252d | |||
| d49f1706f9 | |||
| faafe5cb27 | |||
| a662837b53 | |||
| 1c9e7dbe1b | |||
| 7545019ad6 | |||
| 3c5e85ad99 | |||
| 389c6fab91 | |||
| 6bca3a0f04 | |||
| 74beb45654 | |||
| 8b92f2404a | |||
| 31106f69e8 | |||
| 2603332be5 | |||
| c8f2747e3a | |||
| 7a35491a6e | |||
| c91243ce89 | |||
| b1cc4121b6 | |||
| 9dfa461059 | |||
| 238d4bf93a | |||
| dcd4a3b158 | |||
| 96b986a2b5 | |||
| 99350025f3 | |||
| 7d4a4a5ac5 | |||
| 950650644c | |||
| ed550a8a35 | |||
| 2084aea009 | |||
| 7183464de0 | |||
| 36b228123c | |||
| 7165a2e784 | |||
| 0c374de76c | |||
| d40409cc0f | |||
| eaba1d437e | |||
| 405b4abaea | |||
| 7d95ba4e10 | |||
| bd3b95f675 | |||
| 3a5de1d453 | |||
| 71ccaf8922 | |||
| 88c903e367 | |||
| b61677bbad | |||
| 08e822ecd4 | |||
| 5ba6b18719 | |||
| 9705114192 | |||
| 8338445c01 | |||
| 4904c6107a | |||
| 6450325b45 | |||
| 34f7dc38a2 | |||
| 8e1aa67bbe | |||
| 4ada8fcde6 | |||
| 4a7a4c69cd | |||
| 6b0a239ba9 | |||
| f1b35b198d | |||
| 3335dba9f2 | |||
| 9513de9ae4 | |||
| 898adcdc0f | |||
| 9dde84ed46 | |||
| 24de330d1c | |||
| 9409302954 | |||
| e89c75df5d | |||
| 1865096c12 | |||
| 35deab81ae | |||
| 04c46e1692 | |||
| e62601fa65 | |||
| 53e967fc6b | |||
| 31f971c602 | |||
| 04046b93c4 | |||
| 4330cedd83 | |||
| 7adfc8b5ab | |||
| 96e5bfb3de | |||
| cc1cbfa13a | |||
| d190c5a882 | |||
| 9499d5bd86 | |||
| 815178c00a | |||
| c2f0e2d84d | |||
| dbeae4ca40 | |||
| 6eca460fc9 | |||
| f053a8f96d | |||
| 6b127573bd | |||
| ee8b845c03 | |||
| 80362ef8e4 | |||
| 873480557f | |||
| 92d6b22924 | |||
| 7e6a9f15ce | |||
| d41260f255 | |||
| c278bb50d8 | |||
| e1d86cb8fb | |||
| ebbe096127 | |||
| 90b6fb4f5e | |||
| ed5b7bd392 | |||
| f141fa838c | |||
| 844c9e9fcc | |||
| e0ae27fd6c | |||
| e57168e2e8 | |||
| 75ca0b53b3 | |||
| e0e0ad229d | |||
| a2948d8f96 | |||
| 5bcd8cf4a1 | |||
| 1f7c7e0571 | |||
| 39bbe8d4ad | |||
| 8c6b982c0e | |||
| ed46839e66 | |||
| 024832eea9 | |||
| 5847b0b481 | |||
| 97342577ee | |||
| ecf7cf904c | |||
| 95c924d3cf | |||
| ee521fe887 | |||
| 9dce553f2a | |||
| 22379f7715 | |||
| 06c1b42bea | |||
| d33631e981 | |||
| b9dbb4ed78 | |||
| bceffea826 | |||
| 2aa7cf340b | |||
| 22b39e66d1 | |||
| 396dbacfad | |||
| 85ea96a24c | |||
| d47a79c120 | |||
| 1e986f99a2 | |||
| e76fca05ff | |||
| 4d2cbc8500 | |||
| 4c2f6a214c | |||
| f9abb2f32a | |||
| 202ae6ad34 | |||
| bf2ee6460c | |||
| 5303cbcb95 | |||
| bce695829e | |||
| d3796a3ed9 | |||
| eb77726f83 | |||
| 0c3f91fa16 | |||
| 8277758e90 | |||
| 36363f09c2 | |||
| 14b5a08fe2 | |||
| c549df684c | |||
| 422cb9c352 | |||
| e60bff9c6d | |||
| 59944ece4e | |||
| b29f2e2db3 | |||
| f37cff83fc | |||
| 2384178a3a | |||
| 5558b237bb |
92
BCEconomy_pl/pom.xml
Normal file
92
BCEconomy_pl/pom.xml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>net.viper</groupId>
|
||||||
|
<artifactId>BCEconomy</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spigot-repo</id>
|
||||||
|
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>vault-repo</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spigot API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot-api</artifactId>
|
||||||
|
<version>1.20.4-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Vault -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.MilkBowl</groupId>
|
||||||
|
<artifactId>VaultAPI</artifactId>
|
||||||
|
<version>1.7</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- HikariCP (wird ins JAR gepackt) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
<version>5.1.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MySQL Connector (wird ins JAR gepackt) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<version>8.3.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<!-- Alle Dependencies ins JAR shaden -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.2</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals><goal>shade</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.zaxxer.hikari</pattern>
|
||||||
|
<shadedPattern>net.viper.bceconomy.libs.hikari</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.mysql</pattern>
|
||||||
|
<shadedPattern>net.viper.bceconomy.libs.mysql</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
204
BCEconomy_pl/src/main/java/net/viper/bceconomy/BCDatabase.java
Normal file
204
BCEconomy_pl/src/main/java/net/viper/bceconomy/BCDatabase.java
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package net.viper.bceconomy;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet die MySQL-Verbindung zur bc_accounts Tabelle.
|
||||||
|
* Kein Cache – jeder Zugriff ist ein direkter DB-Query.
|
||||||
|
* Kompatibel mit der Tabelle die StatusAPI / SurvivalPlus anlegt.
|
||||||
|
*/
|
||||||
|
public class BCDatabase {
|
||||||
|
|
||||||
|
private static final String TABLE = "bc_accounts";
|
||||||
|
private static final String TABLE_NAMES = "bc_player_names";
|
||||||
|
|
||||||
|
private final Logger log;
|
||||||
|
private HikariDataSource dataSource;
|
||||||
|
|
||||||
|
public BCDatabase(Logger log, String host, int port, String database,
|
||||||
|
String user, String password) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
HikariConfig cfg = new HikariConfig();
|
||||||
|
cfg.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database
|
||||||
|
+ "?useSSL=false&autoReconnect=true"
|
||||||
|
+ "&characterEncoding=UTF-8&useUnicode=true"
|
||||||
|
+ "&allowPublicKeyRetrieval=true");
|
||||||
|
cfg.setUsername(user);
|
||||||
|
cfg.setPassword(password);
|
||||||
|
cfg.setMaximumPoolSize(5);
|
||||||
|
cfg.setMinimumIdle(1);
|
||||||
|
cfg.setConnectionTimeout(10_000);
|
||||||
|
cfg.setIdleTimeout(600_000);
|
||||||
|
cfg.setMaxLifetime(1_800_000);
|
||||||
|
cfg.setPoolName("BCEconomy");
|
||||||
|
cfg.addDataSourceProperty("cachePrepStmts", "true");
|
||||||
|
cfg.addDataSourceProperty("prepStmtCacheSize", "250");
|
||||||
|
cfg.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataSource = new HikariDataSource(cfg);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.severe("[BCEconomy] MySQL-Verbindung fehlgeschlagen: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createTables();
|
||||||
|
log.info("[BCEconomy] MySQL verbunden – Tabellen bereit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTables() {
|
||||||
|
// bc_accounts – kompatibel mit SurvivalPlus-Struktur (id + player_name + balance)
|
||||||
|
// Wir legen sie nur an wenn sie noch nicht existieren.
|
||||||
|
String createAccounts =
|
||||||
|
"CREATE TABLE IF NOT EXISTS `" + TABLE + "` (" +
|
||||||
|
" `id` INT(10) NOT NULL AUTO_INCREMENT," +
|
||||||
|
" `player_name` VARCHAR(50) NOT NULL," +
|
||||||
|
" `balance` DOUBLE(30,2) NOT NULL DEFAULT 0.00," +
|
||||||
|
" PRIMARY KEY (`id`)," +
|
||||||
|
" UNIQUE KEY `player_name` (`player_name`)" +
|
||||||
|
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
|
||||||
|
|
||||||
|
String createNames =
|
||||||
|
"CREATE TABLE IF NOT EXISTS `" + TABLE_NAMES + "` (" +
|
||||||
|
" `uuid` VARCHAR(36) NOT NULL PRIMARY KEY," +
|
||||||
|
" `name` VARCHAR(16) NOT NULL," +
|
||||||
|
" `updated` BIGINT NOT NULL" +
|
||||||
|
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
|
||||||
|
|
||||||
|
try (Connection con = dataSource.getConnection()) {
|
||||||
|
try (PreparedStatement ps = con.prepareStatement(createAccounts)) {
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
try (PreparedStatement ps = con.prepareStatement(createNames)) {
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.severe("[BCEconomy] Tabellen-Setup fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return dataSource != null && !dataSource.isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
if (isConnected()) dataSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
// Balance – direkt aus DB (kein Cache)
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest Balance direkt aus der DB.
|
||||||
|
* @return Balance oder -1 wenn kein Konto vorhanden.
|
||||||
|
*/
|
||||||
|
public double loadBalance(UUID uuid) {
|
||||||
|
if (!isConnected()) return -1;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"SELECT `balance` FROM `" + TABLE + "` WHERE `player_name` = ?")) {
|
||||||
|
ps.setString(1, uuid.toString());
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) return rs.getDouble("balance");
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.warning("[BCEconomy] loadBalance fehlgeschlagen für " + uuid + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schreibt Balance direkt in die DB (INSERT oder UPDATE).
|
||||||
|
*/
|
||||||
|
public void saveBalance(UUID uuid, double balance) {
|
||||||
|
if (!isConnected()) return;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"INSERT INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?) " +
|
||||||
|
"ON DUPLICATE KEY UPDATE `balance` = VALUES(`balance`)")) {
|
||||||
|
ps.setString(1, uuid.toString());
|
||||||
|
ps.setDouble(2, balance);
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.warning("[BCEconomy] saveBalance fehlgeschlagen für " + uuid + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legt ein neues Konto mit Startguthaben an, falls noch keines existiert.
|
||||||
|
*/
|
||||||
|
public void createAccountIfAbsent(UUID uuid, double startBalance) {
|
||||||
|
if (!isConnected()) return;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"INSERT IGNORE INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?)")) {
|
||||||
|
ps.setString(1, uuid.toString());
|
||||||
|
ps.setDouble(2, startBalance);
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.warning("[BCEconomy] createAccount fehlgeschlagen für " + uuid + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAccount(UUID uuid) {
|
||||||
|
return loadBalance(uuid) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
// Name-Mapping (für Offline-Spieler Lookup)
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void saveNameMapping(UUID uuid, String name) {
|
||||||
|
if (!isConnected()) return;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"INSERT INTO `" + TABLE_NAMES + "` (`uuid`, `name`, `updated`) VALUES (?, ?, ?) " +
|
||||||
|
"ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `updated` = VALUES(`updated`)")) {
|
||||||
|
ps.setString(1, uuid.toString());
|
||||||
|
ps.setString(2, name);
|
||||||
|
ps.setLong(3, System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.warning("[BCEconomy] saveNameMapping fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID resolveUUIDByName(String name) {
|
||||||
|
if (!isConnected()) return null;
|
||||||
|
// 1. Eigene bc_player_names Tabelle
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"SELECT `uuid` FROM `" + TABLE_NAMES + "` WHERE `name` = ? LIMIT 1")) {
|
||||||
|
ps.setString(1, name);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return UUID.fromString(rs.getString("uuid"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | IllegalArgumentException e) {
|
||||||
|
// ignorieren
|
||||||
|
}
|
||||||
|
// 2. CMI_users Fallback
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"SELECT `player_uuid` FROM `CMI_users` WHERE `username` = ? LIMIT 1")) {
|
||||||
|
ps.setString(1, name);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
String raw = rs.getString("player_uuid");
|
||||||
|
if (raw != null && !raw.isEmpty()) return UUID.fromString(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | IllegalArgumentException e) {
|
||||||
|
// CMI nicht installiert – kein Problem
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package net.viper.bceconomy;
|
||||||
|
|
||||||
|
import net.milkbowl.vault.economy.Economy;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerLoginEvent;
|
||||||
|
import org.bukkit.plugin.ServicePriority;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BCEconomy – Spigot-Plugin für Lobby, Citybuild usw.
|
||||||
|
*
|
||||||
|
* Registriert sich als Vault-Economy-Provider und liest/schreibt
|
||||||
|
* den Kontostand direkt aus der gemeinsamen MySQL-Tabelle bc_accounts.
|
||||||
|
* Kein Cache – immer aktuell, serverübergreifend korrekt.
|
||||||
|
*
|
||||||
|
* Voraussetzung: Vault muss auf dem Server installiert sein.
|
||||||
|
* SurvivalPlus ist NICHT nötig – dieses Plugin ersetzt es für Lobby/Citybuild.
|
||||||
|
*/
|
||||||
|
public class BCEconomyPlugin extends JavaPlugin implements Listener {
|
||||||
|
|
||||||
|
private BCDatabase database;
|
||||||
|
private BCEconomyProvider provider;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
// Config laden / erzeugen
|
||||||
|
saveDefaultConfig();
|
||||||
|
|
||||||
|
String host = getConfig().getString ("mysql.host", "localhost");
|
||||||
|
int port = getConfig().getInt ("mysql.port", 3306);
|
||||||
|
String database = getConfig().getString ("mysql.database", "Survival");
|
||||||
|
String user = getConfig().getString ("mysql.username", "root");
|
||||||
|
String password = getConfig().getString ("mysql.password", "");
|
||||||
|
|
||||||
|
String currName = getConfig().getString("economy.currency-name", "Dollar");
|
||||||
|
String currPlural= getConfig().getString("economy.currency-name-plural", "Dollar");
|
||||||
|
double startBal = getConfig().getDouble("economy.start-balance", 500.0);
|
||||||
|
int decimals = getConfig().getInt ("economy.decimals", 2);
|
||||||
|
|
||||||
|
// DB verbinden
|
||||||
|
this.database = new BCDatabase(getLogger(), host, port, database, user, password);
|
||||||
|
|
||||||
|
if (!this.database.isConnected()) {
|
||||||
|
getLogger().severe("[BCEconomy] Keine DB-Verbindung – Plugin wird deaktiviert.");
|
||||||
|
Bukkit.getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vault prüfen
|
||||||
|
if (Bukkit.getPluginManager().getPlugin("Vault") == null) {
|
||||||
|
getLogger().severe("[BCEconomy] Vault nicht gefunden – Plugin wird deaktiviert.");
|
||||||
|
Bukkit.getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Economy-Provider registrieren
|
||||||
|
this.provider = new BCEconomyProvider(this.database, currName, currPlural, startBal, decimals);
|
||||||
|
getServer().getServicesManager().register(
|
||||||
|
Economy.class, this.provider, this, ServicePriority.Highest);
|
||||||
|
|
||||||
|
// Login-Listener: Konto anlegen + Name-Mapping speichern
|
||||||
|
Bukkit.getPluginManager().registerEvents(this, this);
|
||||||
|
|
||||||
|
getLogger().info("[BCEconomy] Aktiviert – Vault-Economy verbunden mit " +
|
||||||
|
database + "@" + host + ":" + port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
if (database != null) {
|
||||||
|
database.close();
|
||||||
|
getLogger().info("[BCEconomy] MySQL-Verbindung geschlossen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bei jedem Login: Konto anlegen (falls neu) und Name speichern.
|
||||||
|
* Alles async – kein Blockieren des Main-Threads.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onLogin(PlayerLoginEvent event) {
|
||||||
|
if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) return;
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
|
||||||
|
database.saveNameMapping(player.getUniqueId(), player.getName());
|
||||||
|
database.createAccountIfAbsent(player.getUniqueId(),
|
||||||
|
getConfig().getDouble("economy.start-balance", 500.0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public BCDatabase getDatabase() { return database; }
|
||||||
|
public BCEconomyProvider getProvider() { return provider; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package net.viper.bceconomy;
|
||||||
|
|
||||||
|
import net.milkbowl.vault.economy.Economy;
|
||||||
|
import net.milkbowl.vault.economy.EconomyResponse;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault Economy-Implementierung.
|
||||||
|
* Jeder Aufruf geht direkt in die DB – kein Cache.
|
||||||
|
*/
|
||||||
|
public class BCEconomyProvider implements Economy {
|
||||||
|
|
||||||
|
private final BCDatabase db;
|
||||||
|
private final String currencyName;
|
||||||
|
private final String currencyNamePlural;
|
||||||
|
private final double startBalance;
|
||||||
|
private final int decimals;
|
||||||
|
|
||||||
|
public BCEconomyProvider(BCDatabase db, String currencyName,
|
||||||
|
String currencyNamePlural,
|
||||||
|
double startBalance, int decimals) {
|
||||||
|
this.db = db;
|
||||||
|
this.currencyName = currencyName;
|
||||||
|
this.currencyNamePlural = currencyNamePlural;
|
||||||
|
this.startBalance = startBalance;
|
||||||
|
this.decimals = decimals;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
// Meta
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override public boolean isEnabled() { return db.isConnected(); }
|
||||||
|
@Override public String getName() { return "BCEconomy"; }
|
||||||
|
@Override public boolean hasBankSupport() { return false; }
|
||||||
|
@Override public int fractionalDigits() { return decimals; }
|
||||||
|
@Override public String format(double amount) {
|
||||||
|
return String.format("%,." + decimals + "f " + currencyName, amount);
|
||||||
|
}
|
||||||
|
@Override public String currencyNamePlural() { return currencyNamePlural; }
|
||||||
|
@Override public String currencyNameSingular() { return currencyName; }
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
// Account
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasAccount(OfflinePlayer player) {
|
||||||
|
return db.hasAccount(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasAccount(OfflinePlayer player, String world) {
|
||||||
|
return hasAccount(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean createPlayerAccount(OfflinePlayer player) {
|
||||||
|
db.createAccountIfAbsent(player.getUniqueId(), startBalance);
|
||||||
|
if (player.getName() != null)
|
||||||
|
db.saveNameMapping(player.getUniqueId(), player.getName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean createPlayerAccount(OfflinePlayer player, String world) {
|
||||||
|
return createPlayerAccount(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
// Balance – direkt aus DB
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getBalance(OfflinePlayer player) {
|
||||||
|
double bal = db.loadBalance(player.getUniqueId());
|
||||||
|
if (bal < 0) {
|
||||||
|
// Neuer Spieler – Konto anlegen
|
||||||
|
db.createAccountIfAbsent(player.getUniqueId(), startBalance);
|
||||||
|
if (player.getName() != null)
|
||||||
|
db.saveNameMapping(player.getUniqueId(), player.getName());
|
||||||
|
return startBalance;
|
||||||
|
}
|
||||||
|
return bal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getBalance(OfflinePlayer player, String world) {
|
||||||
|
return getBalance(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean has(OfflinePlayer player, double amount) {
|
||||||
|
return getBalance(player) >= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean has(OfflinePlayer player, String world, double amount) {
|
||||||
|
return has(player, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
// Deposit / Withdraw – direkt in DB
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EconomyResponse depositPlayer(OfflinePlayer player, double amount) {
|
||||||
|
if (amount < 0)
|
||||||
|
return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE,
|
||||||
|
"Betrag darf nicht negativ sein.");
|
||||||
|
|
||||||
|
double current = db.loadBalance(player.getUniqueId());
|
||||||
|
if (current < 0) {
|
||||||
|
db.createAccountIfAbsent(player.getUniqueId(), startBalance);
|
||||||
|
current = startBalance;
|
||||||
|
}
|
||||||
|
double newBal = current + amount;
|
||||||
|
db.saveBalance(player.getUniqueId(), newBal);
|
||||||
|
return new EconomyResponse(amount, newBal, EconomyResponse.ResponseType.SUCCESS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EconomyResponse depositPlayer(OfflinePlayer player, String world, double amount) {
|
||||||
|
return depositPlayer(player, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EconomyResponse withdrawPlayer(OfflinePlayer player, double amount) {
|
||||||
|
if (amount < 0)
|
||||||
|
return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE,
|
||||||
|
"Betrag darf nicht negativ sein.");
|
||||||
|
|
||||||
|
double current = db.loadBalance(player.getUniqueId());
|
||||||
|
if (current < 0) current = 0;
|
||||||
|
if (current < amount)
|
||||||
|
return new EconomyResponse(amount, current, EconomyResponse.ResponseType.FAILURE,
|
||||||
|
"Nicht genug Guthaben.");
|
||||||
|
|
||||||
|
double newBal = current - amount;
|
||||||
|
db.saveBalance(player.getUniqueId(), newBal);
|
||||||
|
return new EconomyResponse(amount, newBal, EconomyResponse.ResponseType.SUCCESS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EconomyResponse withdrawPlayer(OfflinePlayer player, String world, double amount) {
|
||||||
|
return withdrawPlayer(player, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
// Deprecated String-Methoden (Vault-Kompatibilität)
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public boolean hasAccount(String playerName) {
|
||||||
|
UUID uuid = db.resolveUUIDByName(playerName);
|
||||||
|
return uuid != null && db.hasAccount(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public boolean hasAccount(String playerName, String world) { return hasAccount(playerName); }
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public double getBalance(String playerName) {
|
||||||
|
UUID uuid = db.resolveUUIDByName(playerName);
|
||||||
|
if (uuid == null) return 0;
|
||||||
|
double bal = db.loadBalance(uuid);
|
||||||
|
return bal < 0 ? startBalance : bal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public double getBalance(String playerName, String world) { return getBalance(playerName); }
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public boolean has(String playerName, double amount) { return getBalance(playerName) >= amount; }
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public boolean has(String playerName, String world, double amount) { return has(playerName, amount); }
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public EconomyResponse depositPlayer(String playerName, double amount) {
|
||||||
|
UUID uuid = db.resolveUUIDByName(playerName);
|
||||||
|
if (uuid == null)
|
||||||
|
return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, "Spieler nicht gefunden.");
|
||||||
|
double current = db.loadBalance(uuid);
|
||||||
|
if (current < 0) current = 0;
|
||||||
|
double newBal = current + amount;
|
||||||
|
db.saveBalance(uuid, newBal);
|
||||||
|
return new EconomyResponse(amount, newBal, EconomyResponse.ResponseType.SUCCESS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public EconomyResponse depositPlayer(String playerName, String world, double amount) {
|
||||||
|
return depositPlayer(playerName, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public EconomyResponse withdrawPlayer(String playerName, double amount) {
|
||||||
|
UUID uuid = db.resolveUUIDByName(playerName);
|
||||||
|
if (uuid == null)
|
||||||
|
return new EconomyResponse(0, 0, EconomyResponse.ResponseType.FAILURE, "Spieler nicht gefunden.");
|
||||||
|
double current = db.loadBalance(uuid);
|
||||||
|
if (current < 0) current = 0;
|
||||||
|
if (current < amount)
|
||||||
|
return new EconomyResponse(amount, current, EconomyResponse.ResponseType.FAILURE, "Nicht genug Guthaben.");
|
||||||
|
double newBal = current - amount;
|
||||||
|
db.saveBalance(uuid, newBal);
|
||||||
|
return new EconomyResponse(amount, newBal, EconomyResponse.ResponseType.SUCCESS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public EconomyResponse withdrawPlayer(String playerName, String world, double amount) {
|
||||||
|
return withdrawPlayer(playerName, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public boolean createPlayerAccount(String playerName) { return false; }
|
||||||
|
|
||||||
|
@Override @Deprecated
|
||||||
|
public boolean createPlayerAccount(String playerName, String world) { return false; }
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
// Bank (nicht unterstützt)
|
||||||
|
// ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override public EconomyResponse createBank(String name, String player) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse createBank(String name, OfflinePlayer p) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse deleteBank(String name) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse bankBalance(String name) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse bankHas(String name, double amount) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse bankWithdraw(String name, double amount) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse bankDeposit(String name, double amount) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse isBankOwner(String name, String p) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse isBankOwner(String name, OfflinePlayer p) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse isBankMember(String name, String p) { return notSupported(); }
|
||||||
|
@Override public EconomyResponse isBankMember(String name, OfflinePlayer p) { return notSupported(); }
|
||||||
|
@Override public List<String> getBanks() { return List.of(); }
|
||||||
|
|
||||||
|
private EconomyResponse notSupported() {
|
||||||
|
return new EconomyResponse(0, 0, EconomyResponse.ResponseType.NOT_IMPLEMENTED,
|
||||||
|
"Banken werden nicht unterstützt.");
|
||||||
|
}
|
||||||
|
}
|
||||||
17
BCEconomy_pl/src/main/resources/config.yml
Normal file
17
BCEconomy_pl/src/main/resources/config.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# BCEconomy – Konfiguration
|
||||||
|
# Dieselben Zugangsdaten wie in StatusAPI (verify.properties: economy.mysql.*)
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: Survival
|
||||||
|
username: root
|
||||||
|
password: ""
|
||||||
|
|
||||||
|
economy:
|
||||||
|
currency-name: Dollar
|
||||||
|
currency-name-plural: Dollar
|
||||||
|
currency-symbol: "$"
|
||||||
|
start-balance: 500.0
|
||||||
|
# Wie viele Dezimalstellen angezeigt werden (2 = xx.xx)
|
||||||
|
decimals: 2
|
||||||
12
BCEconomy_pl/src/main/resources/plugin.yml
Normal file
12
BCEconomy_pl/src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: BCEconomy
|
||||||
|
main: net.viper.bceconomy.BCEconomyPlugin
|
||||||
|
version: 1.0.0
|
||||||
|
api-version: 1.20
|
||||||
|
author: M_Viper
|
||||||
|
description: Serverübergreifendes Economy-System via MySQL (bc_accounts)
|
||||||
|
|
||||||
|
depend:
|
||||||
|
- Vault
|
||||||
|
|
||||||
|
softdepend:
|
||||||
|
- CMI
|
||||||
856
README.md
856
README.md
@@ -1,144 +1,772 @@
|
|||||||
# StatusAPI
|
# StatusAPI
|
||||||
|
|
||||||

|
<p align="center">
|
||||||

|
<img src="https://img.shields.io/badge/Version-4.1.0-0B63CE?style=for-the-badge" />
|
||||||
|
<img src="https://img.shields.io/badge/Platform-BungeeCord-D48700?style=for-the-badge" />
|
||||||
|
<img src="https://img.shields.io/badge/Minecraft-1.20+-1E9E5A?style=for-the-badge" />
|
||||||
|
<img src="https://img.shields.io/badge/Java-8+-C0392B?style=for-the-badge" />
|
||||||
|
</p>
|
||||||
|
|
||||||
Ein modulares und mächtiges Plugin für BungeeCord, das einen zentralen JSON-Status, ein globales Chat-System, WordPress-Verifizierung und dynamische Server-Navigation bereitstellt.
|
<p align="center">
|
||||||
|
Modulares BungeeCord-Plugin für Netzwerkbetrieb, Moderation und Integrationen.<br>
|
||||||
|
Live-Status · Chat & Support · Verifizierung · Forum-Bridge · Broadcasts · AntiBot · BackendJoinGuard · Scoreboard
|
||||||
|
</p>
|
||||||
|
|
||||||
## ⚡ Features
|
---
|
||||||
|
|
||||||
- **Modulares System**: Aktiviere nur die Features, die du brauchst (Chat, Stats, Verify)
|
> **Rechtlicher Hinweis**
|
||||||
- **JSON API**: Liefert Echtzeit-Server-Statistiken (Spieleranzahl, Motd, Versionen) für deine Webseite (Port 9191)
|
> Dieses Projekt sowie alle enthaltenen Module, Quelltexte und Ressourcen dürfen nicht verändert, kopiert oder weiterverbreitet werden. Jegliche Nutzung, Modifikation oder Weitergabe ist ausschließlich mit vorheriger schriftlicher Genehmigung des Entwicklers gestattet.
|
||||||
- **Global Chat**: Ein einheitlicher Chat über alle Server hinweg mit Badword-Filter und LuckPerms-Support
|
> © Entwickelt von M_Viper. Alle Rechte vorbehalten.
|
||||||
- **Server Navigation**: Automatische Generierung von Befehlen basierend auf deiner Config (z.B. `/survival` statt `/server survival`)
|
|
||||||
- **WordPress Verify**: Verifiziere Spieler direkt mit deiner WordPress-Seite (CPT Integration)
|
|
||||||
- **Auto-Updater**: Prüft automatisch auf Updates und lädt diese herunter
|
|
||||||
- **Logging**: Speichert den kompletten Chatverlauf in Dateien
|
|
||||||
|
|
||||||
## 📥 Installation
|
---
|
||||||
|
|
||||||
1. Lade die aktuellste `StatusAPI.jar` herunter
|
## Übersicht
|
||||||
2. Lege die Datei in den `plugins` Ordner deiner BungeeCord-Installation
|
|
||||||
3. Starte den Server neu
|
|
||||||
4. Die Config-Dateien werden automatisch erstellt
|
|
||||||
|
|
||||||
## ⚙️ Konfiguration
|
Dieses Repository enthält zwei aufeinander abgestimmte Plugins:
|
||||||
|
|
||||||
Alle Haupt-Einstellungen werden in der `plugins/StatusAPI/verify.properties` zentral verwaltet.
|
| Plugin | Plattform | Zweck |
|
||||||
|
|---|---|---|
|
||||||
|
| **StatusAPI** | BungeeCord (Proxy) | HTTP-API, AntiBot, Chat, Moderation, Verify, Forum, Scoreboard |
|
||||||
|
| **StatusAPIBridge** | Paper / Spigot (Backend) | Sendet Spielerdaten (Health, Kompass, Position, Ticket-Daten etc.) an StatusAPI |
|
||||||
|
| **BackendJoinGuard** | Paper / Spigot (Backend) | Verhindert Direktjoins am Proxy vorbei |
|
||||||
|
|
||||||
### verify.properties
|
BackendJoinGuard kann eigenständig betrieben werden oder seine Schutzregeln automatisch per Sync von StatusAPI beziehen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inhaltsverzeichnis
|
||||||
|
|
||||||
|
- [StatusAPI](#statusapi-1)
|
||||||
|
- [Module](#module)
|
||||||
|
- [HTTP API](#http-api)
|
||||||
|
- [Commands](#commands-statusapi)
|
||||||
|
- [Permissions](#permissions-statusapi)
|
||||||
|
- [Konfiguration](#konfiguration-statusapi)
|
||||||
|
- [ScoreboardModule](#scoreboardmodule)
|
||||||
|
- [Features](#features-scoreboard)
|
||||||
|
- [Placeholders](#placeholders)
|
||||||
|
- [Konfiguration](#konfiguration-scoreboard)
|
||||||
|
- [Commands](#commands-scoreboard)
|
||||||
|
- [StatusAPIBridge](#statusapibridge)
|
||||||
|
- [BackendJoinGuard](#backendjoinguard-1)
|
||||||
|
- [Betriebsarten](#betriebsarten)
|
||||||
|
- [Commands & Permissions](#commands--permissions-backendjoinguard)
|
||||||
|
- [Konfiguration](#konfiguration-backendjoinguard)
|
||||||
|
- [TicketSystem-Integration](#ticketsystem-integration)
|
||||||
|
- [Voraussetzungen & Installation](#voraussetzungen--installation)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## StatusAPI
|
||||||
|
|
||||||
|
### Module
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Core & HTTP-API</strong></summary>
|
||||||
|
|
||||||
|
- Eigener HTTP-Server über konfigurierbaren Port (`statusapi.port`, Standard: `9191`)
|
||||||
|
- JSON-Statusausgabe mit Spieler-, Prefix-, Bedrock-, Netzwerk- und Systemdaten
|
||||||
|
- POST-Endpunkte für Broadcast, Broadcast-Abbruch, Forum-Notify und Attack-Events
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>ScoreboardModule</strong></summary>
|
||||||
|
|
||||||
|
- Vollständig konfigurierbares BungeeCord-Sidebar-Scoreboard
|
||||||
|
- Flüssige Wave-Animation im Titel (10 fps, HSB-Farbwelle oder eigene Farben)
|
||||||
|
- Gradient-Placeholder für farbige Zeilen
|
||||||
|
- News-Ticker mit konfigurierbarem Text, Breite und Geschwindigkeit
|
||||||
|
- Zeilenrotation – bis zu 20 Varianten pro Zeile
|
||||||
|
- Separates Admin-Scoreboard und Supporter-Scoreboard mit eigenen Zeilen und Permissions
|
||||||
|
- `/sb`-Command zum Ein-/Ausblenden und Umschalten zwischen Player/Supporter/Admin-Board
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>NetworkInfo</strong></summary>
|
||||||
|
|
||||||
|
- Live-Snapshot für Proxy, Uptime, RAM, CPU, Backends und Ping
|
||||||
|
- Ingame-Befehl `/netinfo` für Statusprüfung
|
||||||
|
- Discord-Webhook-Meldungen bei: Server-Start/-Stop, hoher RAM-/Spieler-Auslastung, Attack Detected/Stopped
|
||||||
|
- Embed-Modus konfigurierbar: `compact` oder `detailed`
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>AntiBot</strong></summary>
|
||||||
|
|
||||||
|
- CPS-basierte Attack-Erkennung mit konfigurierbaren Start- und Stop-Schwellen
|
||||||
|
- Pro-IP Rate-Limit mit temporären IP-Blocks
|
||||||
|
- Optionaler VPN/Proxy/Hosting-Check via ip-api
|
||||||
|
- Profile: `strict` und `high-traffic`
|
||||||
|
- Laufzeitverwaltung über `/antibot` (Status, Blocks leeren, IP entblocken, Profil wechseln, Reload)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Chat & Moderation</strong></summary>
|
||||||
|
|
||||||
|
- Mehrere Chat-Kanäle, PM-System mit Reply, Ignore-System
|
||||||
|
- Mute/Unmute, Mentions, Emoji-Unterstützung, SocialSpy
|
||||||
|
- Chat-History und Admin-Infos
|
||||||
|
- Report-System inkl. Close-Flow
|
||||||
|
- Weiterleitung an Discord und Telegram (konfigurierbar)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Verify, Forum & CommandBlocker</strong></summary>
|
||||||
|
|
||||||
|
- Spieler-Verifizierung über Token
|
||||||
|
- Forum-Linking und Forum-Benachrichtigungen ingame
|
||||||
|
- Command-Blocker mit `/cb`-Verwaltung
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>AutoMessage & CustomCommands</strong></summary>
|
||||||
|
|
||||||
|
- Rotierende Auto-Nachrichten aus `messages.txt` (intervallgesteuert)
|
||||||
|
- Eigene Command-Mappings über `customcommands.yml`
|
||||||
|
- Optionaler `/chat`-Command (steuerbar per Konfiguration)
|
||||||
|
- Reload via `/bcmds`
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HTTP API
|
||||||
|
|
||||||
|
Basis-URL: `http://<proxy-ip>:9191` – Port konfigurierbar über `statusapi.port`.
|
||||||
|
|
||||||
|
| Methode | Pfad | Beschreibung | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `GET` | `/` | Voller Netzwerk-Status als JSON | – |
|
||||||
|
| `POST` | `/broadcast` | Broadcast senden oder planen | `x-api-key` |
|
||||||
|
| `POST` | `/broadcast/cancel` | Geplanten Broadcast abbrechen | `x-api-key` |
|
||||||
|
| `POST` | `/cancel` | Alias für `/broadcast/cancel` | `x-api-key` |
|
||||||
|
| `POST` | `/forum/notify` | Forum-Notification an Proxy | `x-api-key` / Forum-Secret |
|
||||||
|
| `POST` | `/network/attack` | Attack-Event an Discord-Webhook melden | `x-api-key` |
|
||||||
|
| `GET` | `/network/backendguard/config` | Guard-Regeln für BackendJoinGuard Sync | `x-api-key` |
|
||||||
|
| `POST` | `/scoreboard/health` | Spieler-HP aktualisieren | intern |
|
||||||
|
| `POST` | `/scoreboard/compass` | Spieler-Yaw aktualisieren | intern |
|
||||||
|
| `POST` | `/scoreboard/tps` | Server-TPS aktualisieren | intern |
|
||||||
|
| `POST` | `/player/world` | Spieler-Welt aktualisieren | intern |
|
||||||
|
| `POST` | `/player/data` | Koordinaten, Gamemode, Exp, Food, Speed | intern |
|
||||||
|
| `POST` | `/ticket/update` | TicketSystem-Daten aktualisieren (von StatusAPIBridge) | intern |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Commands (StatusAPI)
|
||||||
|
|
||||||
|
#### Netzwerk & Schutz
|
||||||
|
|
||||||
|
| Command | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `/netinfo` | Netzwerk- und Systemstatus anzeigen |
|
||||||
|
| `/antibot status` | AntiBot-Status anzeigen |
|
||||||
|
| `/antibot clearblocks` | Alle aktiven IP-Blocks entfernen |
|
||||||
|
| `/antibot unblock <ip>` | Einzelne IP entblocken |
|
||||||
|
| `/antibot profile <strict\|high-traffic>` | Profil wechseln und speichern |
|
||||||
|
| `/antibot reload` | AntiBot-Konfiguration neu laden |
|
||||||
|
|
||||||
|
#### Verify & Forum
|
||||||
|
|
||||||
|
| Command | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `/verify <token>` | Account verifizieren |
|
||||||
|
| `/forumlink <token>` | Account mit Forum verknüpfen |
|
||||||
|
| `/forum` | Forum-Benachrichtigungen anzeigen |
|
||||||
|
|
||||||
|
#### Chat, Support & Moderation
|
||||||
|
|
||||||
|
| Command | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `/channel [kanal]` | Kanal wechseln oder Liste anzeigen |
|
||||||
|
| `/helpop <text>` | Support-Anfrage an Team |
|
||||||
|
| `/msg <spieler> <text>` | Private Nachricht senden |
|
||||||
|
| `/r <text>` | Auf letzte PM antworten |
|
||||||
|
| `/ignore <spieler>` | Spieler ignorieren |
|
||||||
|
| `/unignore <spieler>` | Ignore entfernen |
|
||||||
|
| `/chatmute <spieler> [minuten]` | Spieler muten |
|
||||||
|
| `/chatunmute <spieler>` | Mute aufheben |
|
||||||
|
| `/chataus` | Chat-Empfang toggeln |
|
||||||
|
| `/broadcast <text>` | Broadcast an alle senden |
|
||||||
|
| `/emoji` | Emoji-Liste anzeigen |
|
||||||
|
| `/socialspy` | PMs mitlesen toggeln |
|
||||||
|
| `/chatreload` | Chat-Konfiguration neu laden |
|
||||||
|
| `/chatinfo <spieler>` | Admin-Infos zu einem Spieler |
|
||||||
|
| `/chathist [spieler] [anzahl]` | Chat-History anzeigen |
|
||||||
|
| `/mentions` | Mention-Benachrichtigungen toggeln |
|
||||||
|
| `/chatbypass` | Nächste Nachricht am Plugin vorbei senden |
|
||||||
|
| `/discordlink`, `/dlink` | Discord-Linktoken erstellen |
|
||||||
|
| `/telegramlink`, `/tlink` | Telegram-Linktoken erstellen |
|
||||||
|
| `/unlink <discord\|telegram\|all>` | Verknüpfung aufheben |
|
||||||
|
| `/report <spieler> <grund>` | Spieler melden |
|
||||||
|
| `/reports [all]` | Reports ansehen |
|
||||||
|
| `/reportclose <id>` | Report schließen |
|
||||||
|
|
||||||
|
#### CommandBlocker
|
||||||
|
|
||||||
|
| Command | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `/cb add <command>` | Command blockieren |
|
||||||
|
| `/cb remove <command>` | Blockierung aufheben |
|
||||||
|
| `/cb list` | Alle blockierten Commands anzeigen |
|
||||||
|
| `/cb reload` | CommandBlocker neu laden |
|
||||||
|
|
||||||
|
#### CustomCommands
|
||||||
|
|
||||||
|
| Command | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `/bcmds` | `customcommands.yml` neu laden |
|
||||||
|
| `/chat <text>` | Text als Chat senden oder Slash-Command ausführen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Permissions (StatusAPI)
|
||||||
|
|
||||||
|
| Permission | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `statusapi.update.notify` | Update-Benachrichtigungen empfangen |
|
||||||
|
| `statusapi.netinfo` | Zugriff auf `/netinfo` |
|
||||||
|
| `statusapi.antibot` | Zugriff auf `/antibot` |
|
||||||
|
| `statusapi.automessage` | Zugriff auf `/automessage reload` |
|
||||||
|
| `statusapi.bcmds` | Zugriff auf CustomCommand-Funktionen |
|
||||||
|
| `chat.channel.local` | Zugang zum Local-Kanal |
|
||||||
|
| `chat.channel.trade` | Zugang zum Trade-Kanal |
|
||||||
|
| `chat.channel.staff` | Zugang zum Staff-Kanal |
|
||||||
|
| `chat.helpop.receive` | HelpOp-Nachrichten empfangen |
|
||||||
|
| `chat.mute` | Spieler muten / unmuten |
|
||||||
|
| `chat.broadcast` | Broadcasts senden |
|
||||||
|
| `chat.socialspy` | Private Nachrichten mitlesen |
|
||||||
|
| `chat.admin.bypass` | Nicht mutbar / nicht blockierbar |
|
||||||
|
| `chat.admin.notify` | Benachrichtigungen über Mutes und Blocks |
|
||||||
|
| `chat.report` | Spieler reporten (`/report`) |
|
||||||
|
| `chat.color` | Farbcodes (`&a`, `&b`, …) im Chat nutzen |
|
||||||
|
| `chat.color.format` | Formatierungen (`&l`, `&o`, `&n`, …) im Chat nutzen |
|
||||||
|
| `chat.filter.bypass` | Anti-Spam, Caps und Blacklist umgehen |
|
||||||
|
| `commandblocker.bypass` | Command-Blocker umgehen |
|
||||||
|
| `commandblocker.admin` | CommandBlocker verwalten (`/cb`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Konfiguration (StatusAPI)
|
||||||
|
|
||||||
|
#### `verify.properties` – Core, Verify, Broadcast, Forum, Port
|
||||||
|
|
||||||
Hier konfigurierst du Server-Namen, Farben und die Verbindung zu WordPress.
|
|
||||||
```properties
|
```properties
|
||||||
# ===========================
|
statusapi.port=9191
|
||||||
# GLOBALE EINSTELLUNGEN
|
wp_verify_url=https://deine-domain.tld
|
||||||
# ===========================
|
server.<Name>.id=<id>
|
||||||
|
server.<Name>.secret=<secret>
|
||||||
# Aktiviert oder deaktiviert das globale Chat-Modul
|
broadcast.enabled=true
|
||||||
chat.enabled=true
|
broadcast.api_key=<key>
|
||||||
|
forum.enabled=true
|
||||||
# Aktiviert die automatischen Server-Switch Befehle (z.B. /survival)
|
forum.api_secret=<key>
|
||||||
navigation.enabled=true
|
automessage.enabled=true
|
||||||
|
automessage.interval=300
|
||||||
# URL deiner WordPress-Installation
|
automessage.file=messages.txt
|
||||||
wp_verify_url=https://deine-wordpress-domain.tld
|
automessage.prefix=&8[&bInfo&8]
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# SERVER KONFIGURATION
|
|
||||||
# ===========================
|
|
||||||
|
|
||||||
# Der Key-Name (z.B. bungee-server-1) MUSS exakt mit dem
|
|
||||||
# Namen in deiner BungeeCord config.yml übereinstimmen!
|
|
||||||
#
|
|
||||||
# - server.<Name>=<Anzeigename> -> Wird im Chat und als Befehl genutzt
|
|
||||||
# - server.<Name>.id=<ID> -> WordPress Server ID
|
|
||||||
# - server.<Name>.secret=<Key> -> WordPress Secret
|
|
||||||
|
|
||||||
# Beispiel: Server mit Namen "Lobby"
|
|
||||||
server.lobby=&bLobby
|
|
||||||
server.lobby.id=1606
|
|
||||||
server.lobby.secret=DeinSecretHier
|
|
||||||
|
|
||||||
# Beispiel: Server mit Namen "bungee-server-1" (Name aus BungeeConfig)
|
|
||||||
server.bungee-server-1=&aSurvival
|
|
||||||
server.bungee-server-1.id=2
|
|
||||||
server.bungee-server-1.secret=GeheimesWortFuerSurvival
|
|
||||||
|
|
||||||
# Beispiel: SkyBlock
|
|
||||||
server.skyblock=&dSkyBlock
|
|
||||||
server.skyblock.id=3
|
|
||||||
server.skyblock.secret=GeheimesWortFuerSkyBlock
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Weitere Dateien
|
#### `network-guard.properties` – NetworkInfo, Webhook, AntiBot, BackendJoinGuard
|
||||||
|
|
||||||
- **filter.yml**: Hier trägst du Badwords ein, die im Chat zensiert werden sollen
|
```properties
|
||||||
- **welcome.yml**: Hier definierst du Willkommensnachrichten für neue Spieler (nur sichtbar, wenn Welcome-Events aktiviert sind)
|
networkinfo.enabled=true
|
||||||
- **plugins/StatusAPI/logs/**: Hier werden alle Chatnachrichten als Textdateien gespeichert (Löschung nach 7 Tagen)
|
networkinfo.webhook.enabled=true
|
||||||
|
networkinfo.webhook.url=<discord_webhook>
|
||||||
|
networkinfo.webhook.embed_mode=detailed
|
||||||
|
networkinfo.attack.api_key=<attack_api_key>
|
||||||
|
|
||||||
## 💻 Befehle
|
antibot.enabled=true
|
||||||
|
antibot.profile=high-traffic
|
||||||
|
|
||||||
| Befehl | Beschreibung | Permission |
|
backendguard.enforcement_enabled=true
|
||||||
|--------|--------------|------------|
|
backendguard.log_blocked_attempts=true
|
||||||
| `/verify <token>` | Verifiziert den Spieler mit der WordPress-Seite | - |
|
backendguard.kick_message=&cBitte verbinde dich nur ueber den Proxy-Server.
|
||||||
| `/survival` / `/lobby` | Führt dich auf den entsprechenden Server um (wird dynamisch erstellt) | - |
|
backendguard.allowed_proxy_ips=127.0.0.1,::1
|
||||||
| `/globalmute` | Aktiviert oder deaktiviert den globalen Chat | `globalchat.mute` |
|
backendguard.allowed_proxy_cidrs=10.0.0.0/24
|
||||||
| `/globalreload` | Lädt Filter und Konfigurationen neu | `globalchat.reload` |
|
backendguard.sync.api_key=<sync_key>
|
||||||
| `/clearchat, cc` | Löscht den Chatverlauf | `globalchat.clear` |
|
|
||||||
| `/togglechat` | schaltet für den Spieler den Chat ab | - |
|
|
||||||
| `/support <msg>` | Sendet eine Nachricht an das Online-Team | - |
|
|
||||||
| `/reply <msg>` | Antwortet auf eine Support-Anfrage | - |
|
|
||||||
| `/info` | Zeigt Plugin-Informationen an | - |
|
|
||||||
|
|
||||||
## 🌐 JSON API (Status)
|
|
||||||
|
|
||||||
Die API läuft unter `http://DEINE_IP:9191/`.
|
|
||||||
|
|
||||||
### Beispiel Request
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:9191/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Beispiel Antwort
|
#### Weitere Konfigurationsdateien
|
||||||
```json
|
|
||||||
{
|
| Datei | Inhalt |
|
||||||
"online": true,
|
|---|---|
|
||||||
"version": "1.20.4",
|
| `chat.yml` | Kanäle, Formate, PM, Reports, Filter, Logging, Discord/Telegram, Linking |
|
||||||
"max_players": "500",
|
| `blocked-commands.yml` | Liste der geblockten Commands |
|
||||||
"motd": "Willkommen auf meinem Server!",
|
| `customcommands.yml` | Eigene Befehle, Aliase, Sender-Typen |
|
||||||
"players": [
|
| `messages.txt` | AutoMessage-Texte (Zeilen mit `#` und Leerzeilen werden ignoriert) |
|
||||||
{
|
| `scoreboard.properties` | Scoreboard-Konfiguration (siehe ScoreboardModule) |
|
||||||
"name": "Player1",
|
|
||||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
---
|
||||||
"prefix": "§aAdmin",
|
|
||||||
"server": "Lobby"
|
## ScoreboardModule
|
||||||
}
|
|
||||||
]
|
Das ScoreboardModule zeigt ein vollständig konfigurierbares Sidebar-Scoreboard auf BungeeCord-Ebene. Spielerdaten werden über **StatusAPIBridge** vom Backend-Server in Echtzeit geliefert.
|
||||||
}
|
|
||||||
|
Es gibt drei separate Scoreboards, die automatisch anhand der Spieler-Permission zugewiesen werden:
|
||||||
|
|
||||||
|
| Board | Permission | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| Spieler-Board | *(keine)* | Standard-Scoreboard für alle Spieler |
|
||||||
|
| Supporter-Board | `statusapi.scoreboard.supporter` | Zeigt Ticket-Übersicht und Support-relevante Infos |
|
||||||
|
| Admin-Board | `statusapi.scoreboard.admin` | Vollständige Server- und Ticket-Statistiken |
|
||||||
|
|
||||||
|
> Admin hat Vorrang vor Supporter – hat ein Spieler beide Permissions, sieht er das Admin-Board.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Features (Scoreboard)
|
||||||
|
|
||||||
|
- **Wave-Titel-Animation** – flüssige HSB-Farbwelle (10 fps), Farben frei konfigurierbar
|
||||||
|
- **Gradient-Placeholder** – `%gradient:FARBE1:FARBE2:TEXT%` für farbige Zeilen
|
||||||
|
- **News-Ticker** – konfigurierbarer Lauftext von rechts nach links (10 fps), nur im Spieler-Board
|
||||||
|
- **Zeilenrotation** – pro Zeile bis zu 20 Varianten, wechseln automatisch
|
||||||
|
- **Drei Scoreboards** – Spieler, Supporter, Admin – je eigenes Objective, Titel und Zeilen
|
||||||
|
- **Kompass** – Himmelsrichtungs-Anzeige mit farbigen Buchstaben, zentriert
|
||||||
|
- **Herzen & Hunger** – `♥♥♥♥♥♡♡♡♡♡` / `◆◆◆◆◆◇◇◇◇◇`
|
||||||
|
- **Separator-Styles** – frei wählbar (Striche, Wellen, Gradient, leer, …)
|
||||||
|
- **Toggle-Command** – `/sb` zum Ein-/Ausblenden, `/sb admin` / `/sb player` zum Umschalten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Placeholders
|
||||||
|
|
||||||
|
#### Spieler-Placeholders (werden von StatusAPIBridge geliefert)
|
||||||
|
|
||||||
|
| Placeholder | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `%player%` | Spielername |
|
||||||
|
| `%rank%` | Rang (LuckPerms Prefix) |
|
||||||
|
| `%server%` | Aktueller Server (erster Buchstabe groß) |
|
||||||
|
| `%health%` | Leben als Herz-Symbole `♥♥♥♡♡` |
|
||||||
|
| `%hearts%` | Leben als Zahl |
|
||||||
|
| `%food%` | Hunger als Zahl (0–20) |
|
||||||
|
| `%foodsym%` | Hunger als Symbole `◆◆◆◇◇` |
|
||||||
|
| `%compass%` | Kompass-Balken zentriert |
|
||||||
|
| `%ping%` | Ping in ms |
|
||||||
|
| `%online%` | Anzahl Online-Spieler |
|
||||||
|
| `%maxplayers%` | Max. Spieleranzahl |
|
||||||
|
| `%money%` | Kontostand (Economy) |
|
||||||
|
| `%time%` | Aktuelle Uhrzeit |
|
||||||
|
| `%date%` | Aktuelles Datum |
|
||||||
|
| `%playtime%` | Spielzeit der Session `TT HH:MM:SS` |
|
||||||
|
| `%x%` `%y%` `%z%` | Koordinaten |
|
||||||
|
| `%world%` | Welt-Name |
|
||||||
|
| `%gamemode%` | Spielmodus (SURVIVAL, CREATIVE, …) |
|
||||||
|
| `%exp%` | XP-Level |
|
||||||
|
| `%speed%` | Laufgeschwindigkeit |
|
||||||
|
| `%news%` | News-Ticker (nur Spieler-Board) |
|
||||||
|
| `%line%` | Trennlinie (konfigurierbar) |
|
||||||
|
|
||||||
|
#### Admin-Placeholders (BungeeCord-seitig)
|
||||||
|
|
||||||
|
| Placeholder | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `%tps%` | Server-TPS |
|
||||||
|
| `%ram%` | RAM-Nutzung |
|
||||||
|
| `%proxymem%` | Proxy RAM |
|
||||||
|
| `%uptime%` | Proxy-Laufzeit `HH:MM:SS` |
|
||||||
|
| `%servers%` | Anzahl verbundener Backend-Server |
|
||||||
|
|
||||||
|
#### TicketSystem-Placeholders
|
||||||
|
|
||||||
|
Werden alle 5 Sekunden von **StatusAPIBridge** über `POST /ticket/update` an StatusAPI gesendet. Voraussetzung: **TicketSystem** läuft auf demselben Backend-Server wie StatusAPIBridge.
|
||||||
|
|
||||||
|
| Placeholder | Verfügbar für | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `%ticket_my_open%` | Spieler, Supporter, Admin | Eigene aktive Tickets (OPEN + CLAIMED + FORWARDED) |
|
||||||
|
| `%ticket_open%` | Supporter, Admin | Alle unbearbeiteten Tickets (Status: OPEN) |
|
||||||
|
| `%ticket_claimed%` | Admin | Alle Tickets in Bearbeitung (Status: CLAIMED) |
|
||||||
|
| `%ticket_rating_good%` | Admin | Positive Bewertungen gesamt |
|
||||||
|
| `%ticket_rating_bad%` | Admin | Negative Bewertungen gesamt |
|
||||||
|
| `%ticket_rating_pct%` | Admin | Prozentsatz positiver Bewertungen (oder `-` wenn keine) |
|
||||||
|
|
||||||
|
#### Spezial-Placeholders
|
||||||
|
|
||||||
|
| Placeholder | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `%gradient:F1:F2:TEXT%` | Farbverlauf von F1 nach F2 über TEXT (beliebig viele Stopps) |
|
||||||
|
| `%line%` | Trennlinie aus `scoreboard.separator` |
|
||||||
|
| `%news%` | News-Ticker-Fenster |
|
||||||
|
|
||||||
|
Farben für Gradient: Hex (`#RRGGBB`, `&#RRGGBB`) oder Minecraft-Codes (`&0`–`&f`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Konfiguration (Scoreboard)
|
||||||
|
|
||||||
|
Datei: `plugins/StatusAPI/scoreboard.properties`
|
||||||
|
|
||||||
|
```properties
|
||||||
|
scoreboard.enabled=true
|
||||||
|
scoreboard.update_interval=500 # Millisekunden, min. 250
|
||||||
|
scoreboard.title=&lViper Network # Kein Farbcode → Wave übernimmt Farbe
|
||||||
|
scoreboard.admin_title=&l[Admin] Panel
|
||||||
|
scoreboard.supporter_title=&l[Support] Panel
|
||||||
|
scoreboard.admin_permission=statusapi.scoreboard.admin
|
||||||
|
scoreboard.supporter_permission=statusapi.scoreboard.supporter
|
||||||
|
|
||||||
|
# Wave-Animation
|
||||||
|
scoreboard.rainbow.enabled=true
|
||||||
|
scoreboard.rainbow.mode=wave # wave | chars | line
|
||||||
|
scoreboard.rainbow.speed=10 # 1=sehr langsam, 10=normal, 50=schnell
|
||||||
|
scoreboard.rainbow.colors=#FF0000,#FF6600,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF
|
||||||
|
|
||||||
|
# News-Ticker (nur Spieler-Board)
|
||||||
|
scoreboard.news.text=Willkommen auf Viper Network!
|
||||||
|
scoreboard.news.prefix=&8[&6News&8] &r
|
||||||
|
scoreboard.news.width=20
|
||||||
|
scoreboard.news.speed=1
|
||||||
|
|
||||||
|
# Zeilenrotation (Sekunden pro Variante)
|
||||||
|
scoreboard.rotation_interval=4
|
||||||
|
|
||||||
|
# Separator für %line%
|
||||||
|
scoreboard.separator=&8&m--------------------
|
||||||
|
|
||||||
|
# ===================================================
|
||||||
|
# SPIELER-ZEILEN (max. 15 sichtbar)
|
||||||
|
# scoreboard.lines.N = Variante 1 (dauerhaft)
|
||||||
|
# scoreboard.lines.N.2 = Variante 2 (rotiert nach rotation_interval Sekunden)
|
||||||
|
# ===================================================
|
||||||
|
scoreboard.lines.1=%line%
|
||||||
|
scoreboard.lines.2=%gradient:&b:&f:&b:&l> Player Info:%
|
||||||
|
scoreboard.lines.3=&7%rank% &f%player%
|
||||||
|
scoreboard.lines.4=
|
||||||
|
scoreboard.lines.5=&7Spielzeit: &f%playtime%
|
||||||
|
scoreboard.lines.5.2=&7Leben: &c%health%
|
||||||
|
scoreboard.lines.5.3=&7Hunger: B4513%foodsym%
|
||||||
|
scoreboard.lines.6=
|
||||||
|
scoreboard.lines.7=%gradient:&b:&f:&b:&l> Money:%
|
||||||
|
scoreboard.lines.8=&a$%money%
|
||||||
|
scoreboard.lines.9=
|
||||||
|
scoreboard.lines.10=%gradient:&b:&f:&b:&l> Server Info:%
|
||||||
|
scoreboard.lines.11=&f%server%
|
||||||
|
scoreboard.lines.11.2=&7Ping: &f%ping%ms &8| &7Online: &f%online%
|
||||||
|
scoreboard.lines.12=
|
||||||
|
scoreboard.lines.13=%news%
|
||||||
|
scoreboard.lines.14=%line%
|
||||||
|
scoreboard.lines.15=&7%compass%
|
||||||
|
|
||||||
|
# ===================================================
|
||||||
|
# SUPPORTER-ZEILEN
|
||||||
|
# ===================================================
|
||||||
|
scoreboard.supporter_lines.1=%line%
|
||||||
|
scoreboard.supporter_lines.2=%gradient:&6:&f:&6:&l> Support Panel:%
|
||||||
|
scoreboard.supporter_lines.3=&7%rank% &f%player%
|
||||||
|
scoreboard.supporter_lines.4=&7Ping: &f%ping%ms &8| &7%server%
|
||||||
|
scoreboard.supporter_lines.5=
|
||||||
|
scoreboard.supporter_lines.6=%gradient:&6:&f:&6:&l> Tickets:%
|
||||||
|
scoreboard.supporter_lines.7=&7Offen: &c%ticket_open%
|
||||||
|
scoreboard.supporter_lines.8=&7Meine Tickets: &e%ticket_my_open%
|
||||||
|
scoreboard.supporter_lines.9=
|
||||||
|
scoreboard.supporter_lines.10=%gradient:&6:&f:&6:&l> Server Info:%
|
||||||
|
scoreboard.supporter_lines.11=&7Online: &f%online% &8/ &7%maxplayers%
|
||||||
|
scoreboard.supporter_lines.12=&7Zeit: &f%time%
|
||||||
|
scoreboard.supporter_lines.13=
|
||||||
|
scoreboard.supporter_lines.14=%line%
|
||||||
|
scoreboard.supporter_lines.15=&7%compass%
|
||||||
|
|
||||||
|
# ===================================================
|
||||||
|
# ADMIN-ZEILEN
|
||||||
|
# ===================================================
|
||||||
|
scoreboard.admin_lines.1=%line%
|
||||||
|
scoreboard.admin_lines.2=%gradient:&b:&f:&b:&l> Player Info:%
|
||||||
|
scoreboard.admin_lines.3=&7%rank% &f%player%
|
||||||
|
scoreboard.admin_lines.4=&7Gamemode: &f%gamemode%
|
||||||
|
scoreboard.admin_lines.5=&7Leben: &c%health%
|
||||||
|
scoreboard.admin_lines.5.2=&7Hunger: B4513%foodsym%
|
||||||
|
scoreboard.admin_lines.6=
|
||||||
|
scoreboard.admin_lines.7=%gradient:&b:&f:&b:&l> Server Info:%
|
||||||
|
scoreboard.admin_lines.8=&f%server% &8| &7RAM: &e%ram%
|
||||||
|
scoreboard.admin_lines.8.2=&7Proxy: &f%uptime%
|
||||||
|
scoreboard.admin_lines.9=
|
||||||
|
scoreboard.admin_lines.10=&7TPS: &a%tps%
|
||||||
|
scoreboard.admin_lines.11=
|
||||||
|
scoreboard.admin_lines.12=%gradient:&b:&f:&b:&l> Tickets:%
|
||||||
|
scoreboard.admin_lines.13=&7Offen: &c%ticket_open% &8| &7Aktiv: &e%ticket_claimed%
|
||||||
|
scoreboard.admin_lines.14=&7Bewertung: &a%ticket_rating_good%&8/&c%ticket_rating_bad% &7(&f%ticket_rating_pct%&7%&7)
|
||||||
|
scoreboard.admin_lines.15=%line%
|
||||||
|
scoreboard.admin_lines.15.2=&7%compass%
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Für Entwickler
|
#### Separator-Stile
|
||||||
|
|
||||||
Die StatusAPI ist modular aufgebaut. Du kannst eigene Module erstellen, ohne den Core-Code zu berühren.
|
| Stil | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Standard | `&8&m--------------------` |
|
||||||
|
| Doppelt | `&8&m====================` |
|
||||||
|
| Wellig | `&8&m~~~~~~~~~~~~~~~~~~~~` |
|
||||||
|
| Dünn | `&8&m────────────────────` |
|
||||||
|
| Dick | `&8&m════════════════════` |
|
||||||
|
| Diamanten | `&8◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇` |
|
||||||
|
| Gradient | `%gradient:&8:&7:────────────────────%` |
|
||||||
|
| Leer | *(leer lassen)* |
|
||||||
|
|
||||||
### Beispiel
|
---
|
||||||
|
|
||||||
1. Erstelle eine Klasse, die `Module` implementiert
|
### Commands (Scoreboard)
|
||||||
2. Registriere sie in der StatusAPI Hauptdatei:
|
|
||||||
```java
|
| Command | Aliase | Beschreibung |
|
||||||
moduleManager.registerModule(new MeinModul());
|
|---|---|---|
|
||||||
|
| `/scoreboard` | `/sb`, `/togglesb` | Scoreboard ein-/ausblenden (Toggle) |
|
||||||
|
| `/sb hide` | | Scoreboard ausblenden |
|
||||||
|
| `/sb show` | | Scoreboard einblenden |
|
||||||
|
| `/sb player` | | Spieler-Scoreboard anzeigen |
|
||||||
|
| `/sb supporter` | | Supporter-Scoreboard anzeigen (Permission erforderlich) |
|
||||||
|
| `/sb admin` | | Admin-Scoreboard anzeigen (Permission erforderlich) |
|
||||||
|
|
||||||
|
**Permissions:**
|
||||||
|
|
||||||
|
| Permission | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `statusapi.scoreboard.admin` | Admin-Scoreboard + `/sb admin` |
|
||||||
|
| `statusapi.scoreboard.supporter` | Supporter-Scoreboard (automatisch, kein Command nötig) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## StatusAPIBridge
|
||||||
|
|
||||||
|
StatusAPIBridge läuft auf jedem **Backend-Server** (Spigot/Paper) und sendet Spielerdaten in Echtzeit an StatusAPI.
|
||||||
|
|
||||||
|
### Gesendete Daten
|
||||||
|
|
||||||
|
| Datenpunkt | Endpunkt | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| Health | `POST /scoreboard/health` | Aktuelle HP |
|
||||||
|
| Kompass (Yaw) | `POST /scoreboard/compass` | Blickrichtung (0–360°) |
|
||||||
|
| TPS | `POST /scoreboard/tps` | Server-TPS |
|
||||||
|
| Welt | `POST /player/world` | Welt-Name |
|
||||||
|
| Position, Gamemode, Exp, Food, Speed | `POST /player/data` | Erweiterte Spielerdaten |
|
||||||
|
| TicketSystem-Daten | `POST /ticket/update` | Ticket-Statistiken (alle 5 Sekunden, falls TicketSystem installiert) |
|
||||||
|
|
||||||
|
### TicketSystem-Daten (automatisch)
|
||||||
|
|
||||||
|
Ist das **TicketSystem**-Plugin auf demselben Backend-Server installiert, liest StatusAPIBridge die Daten per Reflection und sendet sie alle 5 Sekunden:
|
||||||
|
|
||||||
|
- Globale Werte (Anzahl offener Tickets, Tickets in Bearbeitung, Bewertungen) – nur bei Änderung
|
||||||
|
- Pro Spieler: Anzahl eigener aktiver Tickets (OPEN + CLAIMED + FORWARDED)
|
||||||
|
|
||||||
|
Es wird keine harte Abhängigkeit zum TicketSystem benötigt – fehlt es, passiert nichts.
|
||||||
|
|
||||||
|
### Konfiguration (StatusAPIBridge)
|
||||||
|
|
||||||
|
Datei: `plugins/StatusAPIBridge/config.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
statusapi-url: "http://127.0.0.1:9191"
|
||||||
|
scoreboard-sync-interval-ticks: 20 # Ticks zwischen Daten-Pushes (20 = 1s)
|
||||||
|
compass-threshold: 0.5 # Minimale Yaw-Änderung für Kompass-Update (Grad)
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Dein Modul hat Zugriff auf `onEnable` und `onDisable` des Haupt-Plugins
|
---
|
||||||
|
|
||||||
## 📝 Permissions
|
## BackendJoinGuard
|
||||||
|
|
||||||
- `globalchat.mute` - Erlaubt das Stummschalten des Chats
|
BackendJoinGuard verhindert, dass Spieler Backend-Server direkt betreten und damit den Proxy-Schutz umgehen. Das Plugin prüft beim Login die Quell-IP – nur konfigurierte Proxy-IPs oder CIDR-Netze werden durchgelassen.
|
||||||
- `globalchat.bypass` - Erlaubt das Schreiben, auch wenn der Chat gemuted ist
|
|
||||||
- `globalchat.reload` - Erlaubt das Neuladen der Konfiguration
|
|
||||||
- `globalchat.clear` - Löscht den Kompletten Chatverlauf
|
|
||||||
|
|
||||||
## 🤝 Credits
|
> **Wichtig:** BackendJoinGuard kommt auf **jeden Backend-Server**, nicht auf den Proxy.
|
||||||
|
|
||||||
Entwickelt von **M_Viper**.
|
---
|
||||||
Unterstützt durch BungeeCord und LuckPerms Community.
|
|
||||||
|
### Betriebsarten
|
||||||
|
|
||||||
|
| Modus | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| **Standalone** | Nutzt ausschließlich die lokale `config.yml` |
|
||||||
|
| **StatusAPI Sync** | Lädt Schutzregeln regelmäßig von StatusAPI; lokale Werte bleiben als Fallback erhalten |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Commands & Permissions (BackendJoinGuard)
|
||||||
|
|
||||||
|
| Command | Permission | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `/backendguard reload` | `backendguard.admin` | Config neu laden und bei aktivem Sync sofort neu abrufen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Konfiguration (BackendJoinGuard)
|
||||||
|
|
||||||
|
Datei: `plugins/BackendJoinGuard/config.yml`
|
||||||
|
|
||||||
|
#### Lokaler Schutz
|
||||||
|
|
||||||
|
```yml
|
||||||
|
enforcement-enabled: true
|
||||||
|
log-blocked-attempts: true
|
||||||
|
kick-message: "&cBitte verbinde dich nur ueber den Proxy-Server."
|
||||||
|
|
||||||
|
allowed-proxy-ips:
|
||||||
|
- "127.0.0.1"
|
||||||
|
- "::1"
|
||||||
|
- "185.123.45.67"
|
||||||
|
|
||||||
|
allowed-proxy-cidrs:
|
||||||
|
- "10.0.0.0/24"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### StatusAPI Sync
|
||||||
|
|
||||||
|
```yml
|
||||||
|
statusapi-sync:
|
||||||
|
enabled: true
|
||||||
|
base-url: "http://127.0.0.1:9191"
|
||||||
|
endpoint-path: "/network/backendguard/config"
|
||||||
|
api-key: "DEIN_SYNC_KEY"
|
||||||
|
interval-seconds: 60
|
||||||
|
log-sync-errors: true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Typische Szenarien
|
||||||
|
|
||||||
|
| Szenario | `allowed-proxy-ips` | `base-url` |
|
||||||
|
|---|---|---|
|
||||||
|
| Alles auf einer Maschine | `127.0.0.1`, `::1` | `http://127.0.0.1:9191` |
|
||||||
|
| Proxy auf separatem Host | `185.123.45.67` | `http://185.123.45.67:9191` |
|
||||||
|
| Internes Netz (CIDR) | `10.0.0.10` + CIDR `10.0.0.0/24` | `http://10.0.0.10:9191` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TicketSystem-Integration
|
||||||
|
|
||||||
|
StatusAPI und StatusAPIBridge unterstützen eine optionale Integration mit dem **TicketSystem**-Plugin. Die Verbindung ist vollständig Reflection-basiert – es sind keine harten Abhängigkeiten nötig.
|
||||||
|
|
||||||
|
### Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
TicketSystem (Bukkit)
|
||||||
|
↓ Reflection (alle 5s)
|
||||||
|
StatusAPIBridge → POST /ticket/update → StatusAPI (BungeeCord) → Scoreboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
- TicketSystem ist auf demselben Backend-Server installiert wie StatusAPIBridge
|
||||||
|
- StatusAPIBridge sendet die Daten automatisch – keine zusätzliche Konfiguration nötig
|
||||||
|
|
||||||
|
### Verfügbare Placeholder im Scoreboard
|
||||||
|
|
||||||
|
| Placeholder | Für wen | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `%ticket_my_open%` | Spieler, Supporter, Admin | Eigene aktive Tickets (OPEN + CLAIMED + FORWARDED) |
|
||||||
|
| `%ticket_open%` | Supporter, Admin | Alle offenen Tickets (noch unbearbeitet) |
|
||||||
|
| `%ticket_claimed%` | Admin | Alle Tickets in Bearbeitung |
|
||||||
|
| `%ticket_rating_good%` | Admin | Positive Bewertungen gesamt |
|
||||||
|
| `%ticket_rating_bad%` | Admin | Negative Bewertungen gesamt |
|
||||||
|
| `%ticket_rating_pct%` | Admin | Prozentsatz positiver Bewertungen |
|
||||||
|
|
||||||
|
### PlaceholderAPI (PAPI)
|
||||||
|
|
||||||
|
Das TicketSystem selbst stellt über die Klasse `TicketPlaceholderExpansion` ebenfalls PAPI-Placeholder bereit (Prefix: `ticketsystem`):
|
||||||
|
|
||||||
|
| PAPI-Placeholder | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `%ticketsystem_my_open%` | Eigene aktive Tickets des Spielers |
|
||||||
|
| `%ticketsystem_admin_open%` | Alle offenen Tickets (Status: OPEN) |
|
||||||
|
| `%ticketsystem_admin_claimed%` | Alle Tickets in Bearbeitung (Status: CLAIMED) |
|
||||||
|
| `%ticketsystem_admin_rating_good%` | Positive Bewertungen gesamt |
|
||||||
|
| `%ticketsystem_admin_rating_bad%` | Negative Bewertungen gesamt |
|
||||||
|
| `%ticketsystem_admin_rating_total%` | Alle Bewertungen gesamt |
|
||||||
|
| `%ticketsystem_admin_rating_percent%` | Prozentsatz positiver Bewertungen |
|
||||||
|
|
||||||
|
> Diese PAPI-Placeholder sind unabhängig von StatusAPI und können in jedem PAPI-kompatiblen Plugin genutzt werden (z.B. andere Scoreboard-Plugins, Chat-Plugins).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voraussetzungen & Installation
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
| Komponente | Pflicht | Optional |
|
||||||
|
|---|---|---|
|
||||||
|
| BungeeCord Proxy | ✅ | |
|
||||||
|
| Minecraft 1.20+ | ✅ | |
|
||||||
|
| Java 8+ | ✅ | |
|
||||||
|
| LuckPerms | | ✅ |
|
||||||
|
| Geyser-BungeeCord | | ✅ |
|
||||||
|
| Discord Webhook / Bot | | ✅ |
|
||||||
|
| Telegram Bot | | ✅ |
|
||||||
|
| WordPress / Forum-Backend | | ✅ |
|
||||||
|
| TicketSystem (Bukkit) | | ✅ |
|
||||||
|
| PlaceholderAPI (Bukkit) | | ✅ |
|
||||||
|
|
||||||
|
### StatusAPI installieren (Proxy)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. StatusAPI.jar → plugins/ auf dem Proxy
|
||||||
|
2. Proxy starten → Konfigurationsdateien werden erzeugt
|
||||||
|
3. Proxy stoppen
|
||||||
|
4. Konfigurationen anpassen:
|
||||||
|
verify.properties · chat.yml · network-guard.properties · scoreboard.properties
|
||||||
|
5. Proxy neu starten
|
||||||
|
```
|
||||||
|
|
||||||
|
### StatusAPIBridge installieren (Backend)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. StatusAPIBridge.jar → plugins/ auf jedem Backend-Server
|
||||||
|
2. Server starten → config.yml wird erzeugt
|
||||||
|
3. config.yml anpassen → statusapi-url auf Proxy-IP setzen
|
||||||
|
4. Server neu starten
|
||||||
|
```
|
||||||
|
|
||||||
|
### BackendJoinGuard installieren (Backend)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. BackendJoinGuard.jar → plugins/ auf jedem Backend-Server
|
||||||
|
2. Server starten → config.yml wird erzeugt
|
||||||
|
3. config.yml anpassen
|
||||||
|
4. Server neu starten oder /backendguard reload ausführen
|
||||||
|
```
|
||||||
|
|
||||||
|
### StatusPulse Companion (WordPress)
|
||||||
|
|
||||||
|
Im Repository unter `wordpress/statuspulse` liegt ein WordPress-Admin-Plugin.
|
||||||
|
Damit lassen sich StatusAPI-Verbindung, Attack-Key und Attack-Testflows direkt im WordPress-Backend bedienen.
|
||||||
|
Erfordert StatusAPI ≥ 4.1.0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### StatusAPI
|
||||||
|
|
||||||
|
| Problem | Mögliche Ursachen & Lösung |
|
||||||
|
|---|---|
|
||||||
|
| API nicht erreichbar | `statusapi.port` prüfen · Firewall / Portfreigabe prüfen · Proxy-Konsole auf HTTP-Server-Startmeldung prüfen |
|
||||||
|
| Attack-Meldungen fehlen in Discord | `networkinfo.webhook.enabled` und `.url` prüfen · `networkinfo.attack.enabled` und `api_key`-Header prüfen |
|
||||||
|
| AntiBot zu hart / zu weich | `antibot.profile` auf `strict` oder `high-traffic` setzen · Schwellwerte in `network-guard.properties` feinjustieren · `/antibot reload` ausführen |
|
||||||
|
|
||||||
|
### ScoreboardModule
|
||||||
|
|
||||||
|
| Problem | Mögliche Ursachen & Lösung |
|
||||||
|
|---|---|
|
||||||
|
| Scoreboard verschwindet nach Join | `scoreboard.update_interval` muss mind. 250ms sein – nicht auf `1` setzen |
|
||||||
|
| Wave-Titel hängt / springt | `scoreboard.rainbow.speed` reduzieren · Empfohlen: 10–30 |
|
||||||
|
| Hex-Farben werden nicht angezeigt | Titel ohne feste Farb-Codes schreiben: `&lViper Network` statt `&6&lViper Network` |
|
||||||
|
| News-Ticker stoppt kurz | Normal – 4-Zeichen-Pause zwischen Durchläufen · `scoreboard.news.speed` erhöhen |
|
||||||
|
| Kompass nicht mittig | `COMPASS_WIN` in der Quellcode-Konstante anpassen (Standard: 19) |
|
||||||
|
| `too many queued packets` | `update_interval` zu niedrig – auf mind. 500ms erhöhen |
|
||||||
|
| Supporter-Board wird nicht angezeigt | Permission `statusapi.scoreboard.supporter` prüfen · `scoreboard.supporter_permission` in `scoreboard.properties` prüfen |
|
||||||
|
| Supporter sieht Admin-Board | Normal – Admin-Permission hat Vorrang über Supporter-Permission |
|
||||||
|
|
||||||
|
### StatusAPIBridge
|
||||||
|
|
||||||
|
| Problem | Mögliche Ursachen & Lösung |
|
||||||
|
|---|---|
|
||||||
|
| Herzen zeigen immer voll | StatusAPIBridge nicht installiert oder `statusapi-url` falsch konfiguriert |
|
||||||
|
| Kompass bewegt sich nicht | Bridge läuft, aber Spieler steht still – PlayerMoveEvent sendet nur bei Bewegung |
|
||||||
|
| Ticket-Placeholder zeigen immer 0 | TicketSystem nicht auf demselben Server wie StatusAPIBridge · Konsole auf `[TicketPush]`-Fehler prüfen |
|
||||||
|
|
||||||
|
### BackendJoinGuard
|
||||||
|
|
||||||
|
| Problem | Mögliche Ursachen & Lösung |
|
||||||
|
|---|---|
|
||||||
|
| Spieler werden trotz Proxy geblockt | Proxy-IP in `allowed-proxy-ips` korrekt eintragen · Bei mehreren Segmenten CIDR-Eintrag nutzen |
|
||||||
|
| Sync mit StatusAPI funktioniert nicht | `base-url` und Port prüfen · `api-key` muss identisch zu `backendguard.sync.api_key` sein |
|
||||||
BIN
StatusAPI/lib/BungeeCord.jar
Normal file
BIN
StatusAPI/lib/BungeeCord.jar
Normal file
Binary file not shown.
107
StatusAPI/pom.xml
Normal file
107
StatusAPI/pom.xml
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>net.viper.bungee</groupId>
|
||||||
|
<artifactId>StatusAPI</artifactId>
|
||||||
|
<version>4.1.1</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>StatusAPI</name>
|
||||||
|
<description>BungeeCord Status API Plugin</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- BungeeCord API – direkt aus lokalem JAR, kein Repository noetig -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.md-5</groupId>
|
||||||
|
<artifactId>bungeecord-api</artifactId>
|
||||||
|
<version>1.20</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${project.basedir}/lib/BungeeCord.jar</systemPath>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- LuckPerms API (Optional) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.luckperms</groupId>
|
||||||
|
<artifactId>api</artifactId>
|
||||||
|
<version>5.4</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- HikariCP Connection Pool (wird ins JAR gepackt) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
<version>5.1.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MySQL JDBC Treiber (wird ins JAR gepackt) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<version>9.1.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>central</id>
|
||||||
|
<url>https://repo.maven.apache.org/maven2</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>luckperms</id>
|
||||||
|
<url>https://repo.luckperms.net/releases/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>StatusAPI</finalName>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>false</filtering>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals><goal>shade</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.zaxxer.hikari</pattern>
|
||||||
|
<shadedPattern>net.viper.status.hikari</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.mysql</pattern>
|
||||||
|
<shadedPattern>net.viper.status.mysql</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
1434
StatusAPI/src/main/java/net/viper/status/StatusAPI.java
Normal file
1434
StatusAPI/src/main/java/net/viper/status/StatusAPI.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,144 +1,142 @@
|
|||||||
package net.viper.status;
|
package net.viper.status;
|
||||||
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class UpdateChecker {
|
public class UpdateChecker {
|
||||||
|
|
||||||
private final Plugin plugin;
|
private final Plugin plugin;
|
||||||
private final String currentVersion;
|
private final String currentVersion;
|
||||||
private final int intervalHours;
|
private final int intervalHours;
|
||||||
|
|
||||||
// Neue Domain und korrekter API-Pfad für Releases
|
// Neue Domain und korrekter API-Pfad für Releases
|
||||||
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/StatusAPI/releases";
|
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/StatusAPI/releases";
|
||||||
|
|
||||||
private volatile String latestVersion = "";
|
private volatile String latestVersion = "";
|
||||||
private volatile String latestUrl = "";
|
private volatile String latestUrl = "";
|
||||||
|
|
||||||
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
|
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
|
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
|
||||||
this.intervalHours = Math.max(1, intervalHours);
|
this.intervalHours = Math.max(1, intervalHours);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkNow() {
|
public void checkNow() {
|
||||||
try {
|
try {
|
||||||
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
||||||
conn.setRequestMethod("GET");
|
conn.setRequestMethod("GET");
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
|
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
|
||||||
conn.setConnectTimeout(5000);
|
conn.setConnectTimeout(5000);
|
||||||
conn.setReadTimeout(5000);
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
int code = conn.getResponseCode();
|
int code = conn.getResponseCode();
|
||||||
if (code != 200) {
|
if (code != 200) {
|
||||||
plugin.getLogger().warning("Gitea/Forgejo API nicht erreichbar (HTTP " + code + ")");
|
plugin.getLogger().warning("Gitea/Forgejo API nicht erreichbar (HTTP " + code + ")");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = br.readLine()) != null) sb.append(line).append("\n");
|
while ((line = br.readLine()) != null) sb.append(line).append("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
String body = sb.toString();
|
String body = sb.toString();
|
||||||
|
|
||||||
// Neu: Da die API ein JSON-Array von Releases zurückgibt, nehmen wir das erste (neueste) Release
|
// Neu: Da die API ein JSON-Array von Releases zurückgibt, nehmen wir das erste (neueste) Release
|
||||||
// Wir suchen den ersten Block mit tag_name
|
// Wir suchen den ersten Block mit tag_name
|
||||||
String foundVersion = null;
|
String foundVersion = null;
|
||||||
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
|
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
|
||||||
if (tagM.find()) {
|
if (tagM.find()) {
|
||||||
foundVersion = tagM.group(1).trim();
|
foundVersion = tagM.group(1).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundVersion == null) {
|
if (foundVersion == null) {
|
||||||
plugin.getLogger().warning("Keine Version (Tag) im Release gefunden.");
|
plugin.getLogger().warning("Keine Version (Tag) im Release gefunden.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
|
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
|
||||||
foundVersion = foundVersion.substring(1);
|
foundVersion = foundVersion.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
String foundUrl = null;
|
String foundUrl = null;
|
||||||
|
|
||||||
// Wir suchen im gesamten Body nach der JAR-Datei "StatusAPI.jar"
|
// Wir suchen im gesamten Body nach der JAR-Datei "StatusAPI.jar"
|
||||||
// Da das neueste Release zuerst kommt, brechen wir ab, sobald wir eine passende JAR finden
|
// Da das neueste Release zuerst kommt, brechen wir ab, sobald wir eine passende JAR finden
|
||||||
Matcher nameMatcher = ASSET_NAME_PATTERN.matcher(body);
|
Matcher nameMatcher = ASSET_NAME_PATTERN.matcher(body);
|
||||||
Matcher downloadMatcher = DOWNLOAD_PATTERN.matcher(body);
|
Matcher downloadMatcher = DOWNLOAD_PATTERN.matcher(body);
|
||||||
|
|
||||||
java.util.List<String> names = new java.util.ArrayList<>();
|
java.util.List<String> names = new java.util.ArrayList<>();
|
||||||
java.util.List<String> urls = new java.util.ArrayList<>();
|
java.util.List<String> urls = new java.util.ArrayList<>();
|
||||||
|
|
||||||
while (nameMatcher.find()) {
|
while (nameMatcher.find()) {
|
||||||
names.add(nameMatcher.group(1));
|
names.add(nameMatcher.group(1));
|
||||||
}
|
}
|
||||||
while (downloadMatcher.find()) {
|
while (downloadMatcher.find()) {
|
||||||
urls.add(downloadMatcher.group(1));
|
urls.add(downloadMatcher.group(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
int pairs = Math.min(names.size(), urls.size());
|
int pairs = Math.min(names.size(), urls.size());
|
||||||
for (int i = 0; i < pairs; i++) {
|
for (int i = 0; i < pairs; i++) {
|
||||||
String name = names.get(i).trim();
|
String name = names.get(i).trim();
|
||||||
String url = urls.get(i);
|
String url = urls.get(i);
|
||||||
if ("StatusAPI.jar".equalsIgnoreCase(name)) {
|
if ("StatusAPI.jar".equalsIgnoreCase(name)) {
|
||||||
foundUrl = url;
|
foundUrl = url;
|
||||||
break; // Erste (also neueste) passende JAR nehmen
|
break; // Erste (also neueste) passende JAR nehmen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundUrl == null) {
|
if (foundUrl == null) {
|
||||||
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
|
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
latestVersion = foundVersion;
|
||||||
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
|
latestUrl = foundUrl;
|
||||||
latestVersion = foundVersion;
|
|
||||||
latestUrl = foundUrl;
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
|
||||||
} catch (Exception e) {
|
}
|
||||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
|
}
|
||||||
}
|
|
||||||
}
|
public String getLatestVersion() {
|
||||||
|
return latestVersion != null ? latestVersion : "";
|
||||||
public String getLatestVersion() {
|
}
|
||||||
return latestVersion != null ? latestVersion : "";
|
|
||||||
}
|
public String getLatestUrl() {
|
||||||
|
return latestUrl != null ? latestUrl : "";
|
||||||
public String getLatestUrl() {
|
}
|
||||||
return latestUrl != null ? latestUrl : "";
|
|
||||||
}
|
public boolean isUpdateAvailable(String currentVer) {
|
||||||
|
String lv = getLatestVersion();
|
||||||
public boolean isUpdateAvailable(String currentVer) {
|
if (lv.isEmpty()) return false;
|
||||||
String lv = getLatestVersion();
|
return compareVersions(lv, currentVer) > 0;
|
||||||
if (lv.isEmpty()) return false;
|
}
|
||||||
return compareVersions(lv, currentVer) > 0;
|
|
||||||
}
|
private int compareVersions(String a, String b) {
|
||||||
|
try {
|
||||||
private int compareVersions(String a, String b) {
|
String[] aa = a.split("\\.");
|
||||||
try {
|
String[] bb = b.split("\\.");
|
||||||
String[] aa = a.split("\\.");
|
int len = Math.max(aa.length, bb.length);
|
||||||
String[] bb = b.split("\\.");
|
for (int i = 0; i < len; i++) {
|
||||||
int len = Math.max(aa.length, bb.length);
|
int ai = i < aa.length ? Integer.parseInt(aa[i].replaceAll("\\D", "")) : 0;
|
||||||
for (int i = 0; i < len; i++) {
|
int bi = i < bb.length ? Integer.parseInt(bb[i].replaceAll("\\D", "")) : 0;
|
||||||
int ai = i < aa.length ? Integer.parseInt(aa[i].replaceAll("\\D", "")) : 0;
|
if (ai != bi) return Integer.compare(ai, bi);
|
||||||
int bi = i < bb.length ? Integer.parseInt(bb[i].replaceAll("\\D", "")) : 0;
|
}
|
||||||
if (ai != bi) return Integer.compare(ai, bi);
|
return 0;
|
||||||
}
|
} catch (Exception ex) {
|
||||||
return 0;
|
return a.compareTo(b);
|
||||||
} catch (Exception ex) {
|
}
|
||||||
return a.compareTo(b);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
package net.viper.status.module;
|
package net.viper.status.module;
|
||||||
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface für alle zukünftigen Erweiterungen.
|
* Interface für alle zukünftigen Erweiterungen.
|
||||||
*/
|
*/
|
||||||
public interface Module {
|
public interface Module {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wird aufgerufen, wenn die API startet.
|
* Wird aufgerufen, wenn die API startet.
|
||||||
*/
|
*/
|
||||||
void onEnable(Plugin plugin);
|
void onEnable(Plugin plugin);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wird aufgerufen, wenn die API stoppt.
|
* Wird aufgerufen, wenn die API stoppt.
|
||||||
*/
|
*/
|
||||||
void onDisable(Plugin plugin);
|
void onDisable(Plugin plugin);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eindeutiger Name des Moduls (z.B. "StatsModule").
|
* Eindeutiger Name des Moduls (z.B. "StatsModule").
|
||||||
*/
|
*/
|
||||||
String getName();
|
String getName();
|
||||||
}
|
}
|
||||||
@@ -1,60 +1,67 @@
|
|||||||
package net.viper.status.module;
|
package net.viper.status.module;
|
||||||
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.HashMap;
|
import java.util.Map;
|
||||||
import java.util.Map;
|
|
||||||
|
/**
|
||||||
/**
|
* Verwaltet alle geladenen Module.
|
||||||
* Verwaltet alle geladenen Module.
|
* Verwendet LinkedHashMap um die Registrierungsreihenfolge zu erhalten,
|
||||||
*/
|
* damit Abhängigkeiten (z.B. VanishModule → ChatModule) korrekt aufgelöst werden.
|
||||||
public class ModuleManager {
|
*/
|
||||||
|
public class ModuleManager {
|
||||||
private final Map<String, Module> modules = new HashMap<>();
|
|
||||||
|
private final Map<String, Module> modules = new LinkedHashMap<>();
|
||||||
public void registerModule(Module module) {
|
|
||||||
modules.put(module.getName().toLowerCase(), module);
|
public void registerModule(Module module) {
|
||||||
}
|
modules.put(module.getName().toLowerCase(), module);
|
||||||
|
}
|
||||||
public void enableAll(Plugin plugin) {
|
|
||||||
for (Module module : modules.values()) {
|
public void enableAll(Plugin plugin) {
|
||||||
try {
|
for (Module module : modules.values()) {
|
||||||
plugin.getLogger().info("Aktiviere Modul: " + module.getName() + "...");
|
try {
|
||||||
module.onEnable(plugin);
|
module.onEnable(plugin);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
|
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disableAll(Plugin plugin) {
|
public void disableAll(Plugin plugin) {
|
||||||
for (Module module : modules.values()) {
|
for (Module module : modules.values()) {
|
||||||
try {
|
try {
|
||||||
plugin.getLogger().info("Deaktiviere Modul: " + module.getName() + "...");
|
module.onDisable(plugin);
|
||||||
module.onDisable(plugin);
|
} catch (Exception e) {
|
||||||
} catch (Exception e) {
|
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());
|
||||||
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());
|
}
|
||||||
}
|
}
|
||||||
}
|
modules.clear();
|
||||||
modules.clear();
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
|
||||||
* Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
|
*/
|
||||||
*/
|
public Module getModule(String name) {
|
||||||
public Module getModule(String name) {
|
return modules.get(name.toLowerCase());
|
||||||
return modules.get(name.toLowerCase());
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
@SuppressWarnings("unchecked")
|
* Ersetzt ein bestehendes Modul durch eine neue Instanz (für Reload).
|
||||||
public <T extends Module> T getModule(Class<T> clazz) {
|
* Das alte Modul muss bereits deaktiviert worden sein.
|
||||||
for (Module m : modules.values()) {
|
*/
|
||||||
if (clazz.isInstance(m)) {
|
public void replaceModule(String name, Module newModule) {
|
||||||
return (T) m;
|
modules.put(name.toLowerCase(), newModule);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return null;
|
@SuppressWarnings("unchecked")
|
||||||
}
|
public <T extends Module> T getModule(Class<T> clazz) {
|
||||||
}
|
for (Module m : modules.values()) {
|
||||||
|
if (clazz.isInstance(m)) {
|
||||||
|
return (T) m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package net.viper.status.modules.AutoMessage;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AutoMessageModule
|
||||||
|
*
|
||||||
|
* Fix #5:
|
||||||
|
* - Nachrichten werden bei jedem Zyklus frisch aus der Datei gelesen,
|
||||||
|
* damit Änderungen an messages.txt sofort wirken ohne Neustart.
|
||||||
|
* - Neuer Befehl /automessage reload (Permission: statusapi.automessage)
|
||||||
|
* lädt die Konfiguration neu und setzt den Zähler zurück.
|
||||||
|
* - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes für §-Codes.
|
||||||
|
*/
|
||||||
|
public class AutoMessageModule implements Module {
|
||||||
|
|
||||||
|
private int taskId = -1;
|
||||||
|
private StatusAPI api;
|
||||||
|
private final AtomicInteger currentIndex = new AtomicInteger(0);
|
||||||
|
|
||||||
|
// Konfiguration (für Reload zugänglich)
|
||||||
|
private volatile boolean enabled = false;
|
||||||
|
private volatile int intervalSeconds = 300;
|
||||||
|
private volatile String fileName = "messages.txt";
|
||||||
|
private volatile String prefix = "";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "AutoMessage"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
this.api = (StatusAPI) plugin;
|
||||||
|
loadSettings();
|
||||||
|
ensureMessagesFileExists();
|
||||||
|
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
registerReloadCommand();
|
||||||
|
scheduleTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
cancelTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureMessagesFileExists() {
|
||||||
|
File dataFolder = api.getDataFolder();
|
||||||
|
if (!dataFolder.exists()) dataFolder.mkdirs();
|
||||||
|
|
||||||
|
File target = new File(dataFolder, fileName);
|
||||||
|
if (target.exists()) return;
|
||||||
|
|
||||||
|
// Datei aus den Plugin-Ressourcen kopieren
|
||||||
|
try (java.io.InputStream in = api.getResourceAsStream(fileName)) {
|
||||||
|
if (in != null) {
|
||||||
|
Files.copy(in, target.toPath());
|
||||||
|
api.getLogger().info("[AutoMessage] " + fileName + " wurde aus den Ressourcen erstellt.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
api.getLogger().warning("[AutoMessage] Konnte " + fileName + " nicht aus Ressourcen kopieren: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: leere Datei mit Hinweis anlegen
|
||||||
|
try {
|
||||||
|
Files.write(target.toPath(),
|
||||||
|
("# AutoMessage – eine Nachricht pro Zeile\n" +
|
||||||
|
"# Farben mit & oder §-Codes, z.B. &aGrüner Text\n" +
|
||||||
|
"# Kommentarzeilen (# ...) und Leerzeilen werden ignoriert\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
api.getLogger().info("[AutoMessage] " + fileName + " wurde als leere Vorlage erstellt.");
|
||||||
|
} catch (IOException e) {
|
||||||
|
api.getLogger().severe("[AutoMessage] Konnte " + fileName + " nicht erstellen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSettings() {
|
||||||
|
Properties props = api.getVerifyProperties();
|
||||||
|
enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
|
||||||
|
String rawInterval = props.getProperty("automessage.interval", "300");
|
||||||
|
try { intervalSeconds = Integer.parseInt(rawInterval); }
|
||||||
|
catch (NumberFormatException e) { api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s)."); intervalSeconds = 300; }
|
||||||
|
fileName = props.getProperty("automessage.file", "messages.txt");
|
||||||
|
prefix = props.getProperty("automessage.prefix", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerReloadCommand() {
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerCommand(api, new Command("automessage", "statusapi.automessage") {
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
if (args.length > 0 && "reload".equalsIgnoreCase(args[0])) {
|
||||||
|
cancelTask();
|
||||||
|
loadSettings();
|
||||||
|
currentIndex.set(0);
|
||||||
|
if (enabled) {
|
||||||
|
scheduleTask();
|
||||||
|
sender.sendMessage(ChatColor.GREEN + "[AutoMessage] Neu geladen. Intervall: " + intervalSeconds + "s");
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "[AutoMessage] Modul ist deaktiviert (automessage.enabled=false).");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/automessage reload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleTask() {
|
||||||
|
taskId = ProxyServer.getInstance().getScheduler().schedule(api, () -> {
|
||||||
|
File messageFile = new File(api.getDataFolder(), fileName);
|
||||||
|
if (!messageFile.exists()) {
|
||||||
|
api.getLogger().warning("[AutoMessage] Datei nicht gefunden: " + messageFile.getAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix #5: Datei bei jedem Tick neu einlesen → Änderungen wirken sofort
|
||||||
|
List<String> messages;
|
||||||
|
try {
|
||||||
|
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
|
||||||
|
} catch (IOException e) {
|
||||||
|
api.getLogger().severe("[AutoMessage] Fehler beim Lesen von '" + fileName + "': " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
messages.removeIf(line -> line.trim().isEmpty() || line.trim().startsWith("#"));
|
||||||
|
if (messages.isEmpty()) return;
|
||||||
|
|
||||||
|
// Index wrappen (threadsafe)
|
||||||
|
int idx = currentIndex.getAndUpdate(i -> (i + 1) % messages.size());
|
||||||
|
if (idx >= messages.size()) idx = 0;
|
||||||
|
|
||||||
|
String raw = messages.get(idx);
|
||||||
|
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', prefix) + " ";
|
||||||
|
// Fix: §-Codes direkt übersetzen (messages.txt nutzt §-Codes)
|
||||||
|
String text = prefixPart + ChatColor.translateAlternateColorCodes('&',
|
||||||
|
raw.replace("\u00a7", "&").replace("§", "&"));
|
||||||
|
|
||||||
|
ProxyServer.getInstance().broadcast(new TextComponent(text));
|
||||||
|
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelTask() {
|
||||||
|
if (taskId != -1) {
|
||||||
|
ProxyServer.getInstance().getScheduler().cancel(taskId);
|
||||||
|
taskId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,840 @@
|
|||||||
|
package net.viper.status.modules.antibot;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.connection.PendingConnection;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.event.PreLoginEvent;
|
||||||
|
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
import net.viper.status.modules.network.NetworkInfoModule;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eigenständiger AntiBot/Attack-Guard.
|
||||||
|
*
|
||||||
|
* Fixes:
|
||||||
|
* - cleanupExpired() nutzt jetzt removeIf() statt Iteration + remove() (Bug #3)
|
||||||
|
* - applyProfileDefaults() setzt korrekten attackDefaultSource aus Config
|
||||||
|
*/
|
||||||
|
public class AntiBotModule implements Module, Listener {
|
||||||
|
|
||||||
|
private static final String CONFIG_FILE_NAME = "network-guard.properties";
|
||||||
|
|
||||||
|
private StatusAPI plugin;
|
||||||
|
|
||||||
|
private boolean enabled = true;
|
||||||
|
private String profile = "high-traffic";
|
||||||
|
private int maxCps = 120;
|
||||||
|
private int attackStartCps = 220;
|
||||||
|
private int attackStopCps = 120;
|
||||||
|
private int attackCalmSeconds = 20;
|
||||||
|
private int ipConnectionsPerMinute = 18;
|
||||||
|
private int ipBlockSeconds = 600;
|
||||||
|
private String kickMessage = "Zu viele Verbindungen von deiner IP. Bitte warte kurz.";
|
||||||
|
|
||||||
|
private boolean vpnCheckEnabled = false;
|
||||||
|
private boolean vpnBlockProxy = true;
|
||||||
|
private boolean vpnBlockHosting = true;
|
||||||
|
private int vpnCacheMinutes = 30;
|
||||||
|
private int vpnTimeoutMs = 2500;
|
||||||
|
private boolean securityLogEnabled = true;
|
||||||
|
private String securityLogFileName = "antibot-security.log";
|
||||||
|
private File securityLogFile;
|
||||||
|
private final Object securityLogLock = new Object();
|
||||||
|
|
||||||
|
private boolean learningModeEnabled = true;
|
||||||
|
private int learningScoreThreshold = 100;
|
||||||
|
private int learningDecayPerSecond = 2;
|
||||||
|
private int learningStateWindowSeconds = 120;
|
||||||
|
private int learningRapidWindowMs = 1500;
|
||||||
|
private int learningRapidPoints = 12;
|
||||||
|
private int learningIpRateExceededPoints = 30;
|
||||||
|
private int learningVpnProxyPoints = 40;
|
||||||
|
private int learningVpnHostingPoints = 30;
|
||||||
|
private int learningAttackModePoints = 12;
|
||||||
|
private int learningHighCpsPoints = 10;
|
||||||
|
private int learningRecentEventLimit = 30;
|
||||||
|
|
||||||
|
private final AtomicInteger currentSecondConnections = new AtomicInteger(0);
|
||||||
|
private volatile long currentSecond = System.currentTimeMillis() / 1000L;
|
||||||
|
private volatile int lastCps = 0;
|
||||||
|
private final AtomicInteger peakCps = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private volatile boolean attackMode = false;
|
||||||
|
private volatile long attackCalmSince = 0L;
|
||||||
|
private final AtomicLong blockedConnectionsTotal = new AtomicLong(0L);
|
||||||
|
private final AtomicLong blockedConnectionsCurrentAttack = new AtomicLong(0L);
|
||||||
|
private final Set<String> blockedIpsCurrentAttack = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private final Map<String, IpWindow> perIpWindows = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Long> blockedIpsUntil = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, VpnCacheEntry> vpnCache = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, RecentPlayerIdentity> recentIdentityByIp = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, LearningProfile> learningProfiles = new ConcurrentHashMap<>();
|
||||||
|
private final Deque<String> learningRecentEvents = new ArrayDeque<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "AntiBotModule"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
if (!(plugin instanceof StatusAPI)) return;
|
||||||
|
this.plugin = (StatusAPI) plugin;
|
||||||
|
ensureModuleConfigExists();
|
||||||
|
loadConfig();
|
||||||
|
ensureSecurityLogFile();
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
StatusAPI.debugLog(this.plugin, "[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerListener(this.plugin, this);
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin, new AntiBotCommand());
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(this.plugin, this::tick, 1, 1, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
this.plugin.getLogger().fine("[AntiBotModule] aktiviert. maxCps=" + maxCps
|
||||||
|
+ ", attackStartCps=" + attackStartCps + ", ip/min=" + ipConnectionsPerMinute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
perIpWindows.clear();
|
||||||
|
blockedIpsUntil.clear();
|
||||||
|
vpnCache.clear();
|
||||||
|
learningProfiles.clear();
|
||||||
|
synchronized (learningRecentEvents) { learningRecentEvents.clear(); }
|
||||||
|
blockedIpsCurrentAttack.clear();
|
||||||
|
attackMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() { return enabled; }
|
||||||
|
|
||||||
|
private void reloadRuntimeState() {
|
||||||
|
perIpWindows.clear();
|
||||||
|
blockedIpsUntil.clear();
|
||||||
|
vpnCache.clear();
|
||||||
|
learningProfiles.clear();
|
||||||
|
synchronized (learningRecentEvents) { learningRecentEvents.clear(); }
|
||||||
|
blockedIpsCurrentAttack.clear();
|
||||||
|
attackMode = false;
|
||||||
|
attackCalmSince = 0L;
|
||||||
|
blockedConnectionsCurrentAttack.set(0L);
|
||||||
|
currentSecondConnections.set(0);
|
||||||
|
lastCps = 0;
|
||||||
|
peakCps.set(0);
|
||||||
|
loadConfig();
|
||||||
|
ensureSecurityLogFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> buildSnapshot() {
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("enabled", enabled);
|
||||||
|
out.put("profile", profile);
|
||||||
|
out.put("attack_mode", attackMode);
|
||||||
|
out.put("protection_enabled", enabled);
|
||||||
|
out.put("attack_mode_status", attackMode ? "active" : "normal");
|
||||||
|
out.put("attack_mode_display", attackMode ? "Angriff erkannt" : "Normalbetrieb");
|
||||||
|
out.put("status_message", enabled
|
||||||
|
? (attackMode ? "AntiBot aktiv: Angriff erkannt" : "AntiBot aktiv: kein Angriff erkannt")
|
||||||
|
: "AntiBot deaktiviert");
|
||||||
|
out.put("last_cps", lastCps);
|
||||||
|
out.put("peak_cps", peakCps.get());
|
||||||
|
out.put("blocked_ips_active", blockedIpsUntil.size());
|
||||||
|
out.put("blocked_connections_total", blockedConnectionsTotal.get());
|
||||||
|
out.put("vpn_check_enabled", vpnCheckEnabled);
|
||||||
|
out.put("learning_mode_enabled", learningModeEnabled);
|
||||||
|
out.put("learning_profiles", learningProfiles.size());
|
||||||
|
out.put("thresholds", buildThresholds());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildThresholds() {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("max_cps", maxCps);
|
||||||
|
m.put("attack_start_cps", attackStartCps);
|
||||||
|
m.put("attack_stop_cps", attackStopCps);
|
||||||
|
m.put("attack_calm_seconds", attackCalmSeconds);
|
||||||
|
m.put("ip_connections_per_minute", ipConnectionsPerMinute);
|
||||||
|
m.put("ip_block_seconds", ipBlockSeconds);
|
||||||
|
m.put("learning_score_threshold", learningScoreThreshold);
|
||||||
|
m.put("learning_decay_per_second", learningDecayPerSecond);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPreLogin(PreLoginEvent event) {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
String ip = extractIp(event.getConnection());
|
||||||
|
if (ip == null || ip.isEmpty()) return;
|
||||||
|
|
||||||
|
cacheRecentIdentity(ip, event.getConnection(), System.currentTimeMillis());
|
||||||
|
recordConnection();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// FIX #3: cleanupExpired verwendet removeIf statt Iteration+remove
|
||||||
|
cleanupExpired(now);
|
||||||
|
|
||||||
|
Long blockedUntil = blockedIpsUntil.get(ip);
|
||||||
|
if (blockedUntil != null && blockedUntil > now) {
|
||||||
|
logSecurityEvent("ip_block_active", ip, event.getConnection(), "blocked_until_ms=" + blockedUntil);
|
||||||
|
blockEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (learningModeEnabled) {
|
||||||
|
evaluateLearningBaseline(ip, now);
|
||||||
|
Long learningBlock = blockedIpsUntil.get(ip);
|
||||||
|
if (learningBlock != null && learningBlock > now) {
|
||||||
|
blockEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean ipRateExceeded = isIpRateExceeded(ip, now);
|
||||||
|
if (ipRateExceeded) {
|
||||||
|
if (learningModeEnabled) {
|
||||||
|
int score = addLearningScore(ip, now, learningIpRateExceededPoints, "ip-rate-exceeded", true);
|
||||||
|
logSecurityEvent("ip_rate_exceeded_scored", ip, event.getConnection(), "score=" + score + ", threshold=" + learningScoreThreshold);
|
||||||
|
if (score >= learningScoreThreshold) {
|
||||||
|
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=ip-rate-exceeded, score=" + score);
|
||||||
|
blockEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blockIp(ip, now);
|
||||||
|
logSecurityEvent("ip_rate_limit_block", ip, event.getConnection(), "mode=direct");
|
||||||
|
blockEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vpnCheckEnabled) {
|
||||||
|
VpnCheckResult info = getVpnInfo(ip, now);
|
||||||
|
if (info != null) {
|
||||||
|
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
|
||||||
|
if (shouldBlock) {
|
||||||
|
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||||
|
if (learningModeEnabled) {
|
||||||
|
if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false);
|
||||||
|
if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
|
||||||
|
int current = getLearningScore(ip, now);
|
||||||
|
if (current >= learningScoreThreshold) {
|
||||||
|
blockIp(ip, now);
|
||||||
|
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
|
||||||
|
recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
|
||||||
|
blockEvent(event);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blockIp(ip, now);
|
||||||
|
logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||||
|
blockEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPostLogin(PostLoginEvent event) {
|
||||||
|
if (!enabled || event == null || event.getPlayer() == null) return;
|
||||||
|
ProxiedPlayer player = event.getPlayer();
|
||||||
|
String ip = extractIpFromPlayer(player);
|
||||||
|
if (ip == null || ip.isEmpty()) return;
|
||||||
|
cacheRecentIdentityDirect(ip, player.getName(), player.getUniqueId(), System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void blockEvent(PreLoginEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractIp(PendingConnection conn) {
|
||||||
|
if (conn == null || conn.getAddress() == null) return null;
|
||||||
|
if (conn.getAddress() instanceof InetSocketAddress) {
|
||||||
|
InetSocketAddress sa = (InetSocketAddress) conn.getAddress();
|
||||||
|
return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString();
|
||||||
|
}
|
||||||
|
return String.valueOf(conn.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractIpFromPlayer(ProxiedPlayer player) {
|
||||||
|
if (player == null || player.getAddress() == null) return null;
|
||||||
|
if (player.getAddress() instanceof InetSocketAddress) {
|
||||||
|
InetSocketAddress sa = (InetSocketAddress) player.getAddress();
|
||||||
|
return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString();
|
||||||
|
}
|
||||||
|
return String.valueOf(player.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordConnection() {
|
||||||
|
long sec = System.currentTimeMillis() / 1000L;
|
||||||
|
if (sec != currentSecond) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (sec != currentSecond) {
|
||||||
|
currentSecond = sec;
|
||||||
|
currentSecondConnections.set(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentSecondConnections.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isIpRateExceeded(String ip, long now) {
|
||||||
|
IpWindow window = perIpWindows.computeIfAbsent(ip, k -> new IpWindow(now));
|
||||||
|
synchronized (window) {
|
||||||
|
long diff = now - window.windowStart;
|
||||||
|
if (diff > 60_000L) {
|
||||||
|
window.windowStart = now;
|
||||||
|
window.count = 0;
|
||||||
|
}
|
||||||
|
window.count++;
|
||||||
|
return window.count > Math.max(1, ipConnectionsPerMinute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void blockIp(String ip, long now) {
|
||||||
|
blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L);
|
||||||
|
blockedConnectionsTotal.incrementAndGet();
|
||||||
|
if (attackMode) {
|
||||||
|
blockedConnectionsCurrentAttack.incrementAndGet();
|
||||||
|
blockedIpsCurrentAttack.add(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIX #3: Verwendet removeIf() statt for-each + remove() um ConcurrentModificationException zu vermeiden.
|
||||||
|
*/
|
||||||
|
private void cleanupExpired(long now) {
|
||||||
|
blockedIpsUntil.entrySet().removeIf(e -> e.getValue() <= now);
|
||||||
|
vpnCache.entrySet().removeIf(e -> e.getValue().expiresAt <= now);
|
||||||
|
recentIdentityByIp.entrySet().removeIf(e -> {
|
||||||
|
RecentPlayerIdentity id = e.getValue();
|
||||||
|
return id == null || (now - id.updatedAtMs) > 600_000L;
|
||||||
|
});
|
||||||
|
if (learningModeEnabled) {
|
||||||
|
long staleAfter = Math.max(60, learningStateWindowSeconds) * 1000L;
|
||||||
|
learningProfiles.entrySet().removeIf(e -> {
|
||||||
|
LearningProfile lp = e.getValue();
|
||||||
|
return lp == null || ((now - lp.lastSeenAt) > staleAfter && lp.score <= 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tick() {
|
||||||
|
if (!enabled) return;
|
||||||
|
int cps = currentSecondConnections.getAndSet(0);
|
||||||
|
lastCps = cps;
|
||||||
|
if (cps > peakCps.get()) peakCps.set(cps);
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (!attackMode && cps >= Math.max(1, attackStartCps)) {
|
||||||
|
attackMode = true;
|
||||||
|
attackCalmSince = 0L;
|
||||||
|
blockedConnectionsCurrentAttack.set(0L);
|
||||||
|
blockedIpsCurrentAttack.clear();
|
||||||
|
sendAttackToWebhook("detected", cps, null, null, "StatusAPI AntiBot");
|
||||||
|
plugin.getLogger().warning("[AntiBotModule] Attack erkannt. CPS=" + cps);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attackMode) {
|
||||||
|
if (cps <= Math.max(1, attackStopCps)) {
|
||||||
|
if (attackCalmSince == 0L) attackCalmSince = now;
|
||||||
|
long calmFor = now - attackCalmSince;
|
||||||
|
if (calmFor >= Math.max(1, attackCalmSeconds) * 1000L) {
|
||||||
|
attackMode = false;
|
||||||
|
attackCalmSince = 0L;
|
||||||
|
int blockedIps = blockedIpsCurrentAttack.size();
|
||||||
|
long blockedConns = blockedConnectionsCurrentAttack.get();
|
||||||
|
sendAttackToWebhook("stopped", cps, blockedIps, blockedConns, "StatusAPI AntiBot");
|
||||||
|
plugin.getLogger().warning("[AntiBotModule] Attack beendet. blockedIps=" + blockedIps + ", blockedConnections=" + blockedConns);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attackCalmSince = 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAttackToWebhook(String type, Integer cps, Integer blockedIps, Long blockedConnections, String source) {
|
||||||
|
NetworkInfoModule networkInfoModule = getNetworkInfoModule();
|
||||||
|
if (networkInfoModule == null) return;
|
||||||
|
networkInfoModule.sendAttackNotification(type, cps, blockedIps, blockedConnections, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NetworkInfoModule getNetworkInfoModule() {
|
||||||
|
if (plugin == null || plugin.getModuleManager() == null) return null;
|
||||||
|
return (NetworkInfoModule) plugin.getModuleManager().getModule("NetworkInfoModule");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
|
||||||
|
if (!file.exists()) return;
|
||||||
|
|
||||||
|
Properties props = new Properties();
|
||||||
|
try (FileInputStream in = new FileInputStream(file)) {
|
||||||
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
enabled = parseBoolean(props.getProperty("antibot.enabled"), true);
|
||||||
|
profile = normalizeProfile(props.getProperty("antibot.profile", "high-traffic"));
|
||||||
|
applyProfileDefaults(profile);
|
||||||
|
|
||||||
|
maxCps = parseInt(props.getProperty("antibot.max_cps"), maxCps);
|
||||||
|
attackStartCps = parseInt(props.getProperty("antibot.attack.start_cps"), attackStartCps);
|
||||||
|
attackStopCps = parseInt(props.getProperty("antibot.attack.stop_cps"), attackStopCps);
|
||||||
|
attackCalmSeconds = parseInt(props.getProperty("antibot.attack.stop_grace_seconds"), attackCalmSeconds);
|
||||||
|
ipConnectionsPerMinute = parseInt(props.getProperty("antibot.ip.max_connections_per_minute"), ipConnectionsPerMinute);
|
||||||
|
ipBlockSeconds = parseInt(props.getProperty("antibot.ip.block_seconds"), ipBlockSeconds);
|
||||||
|
kickMessage = props.getProperty("antibot.kick_message", kickMessage);
|
||||||
|
|
||||||
|
vpnCheckEnabled = parseBoolean(props.getProperty("antibot.vpn_check.enabled"), vpnCheckEnabled);
|
||||||
|
vpnBlockProxy = parseBoolean(props.getProperty("antibot.vpn_check.block_proxy"), vpnBlockProxy);
|
||||||
|
vpnBlockHosting = parseBoolean(props.getProperty("antibot.vpn_check.block_hosting"), vpnBlockHosting);
|
||||||
|
vpnCacheMinutes = parseInt(props.getProperty("antibot.vpn_check.cache_minutes"), vpnCacheMinutes);
|
||||||
|
vpnTimeoutMs = parseInt(props.getProperty("antibot.vpn_check.timeout_ms"), vpnTimeoutMs);
|
||||||
|
securityLogEnabled = parseBoolean(props.getProperty("antibot.security_log.enabled"), securityLogEnabled);
|
||||||
|
securityLogFileName = props.getProperty("antibot.security_log.file", securityLogFileName).trim();
|
||||||
|
if (securityLogFileName.isEmpty()) securityLogFileName = "antibot-security.log";
|
||||||
|
|
||||||
|
learningModeEnabled = parseBoolean(props.getProperty("antibot.learning.enabled"), learningModeEnabled);
|
||||||
|
learningScoreThreshold = parseInt(props.getProperty("antibot.learning.score_threshold"), learningScoreThreshold);
|
||||||
|
learningDecayPerSecond = parseInt(props.getProperty("antibot.learning.decay_per_second"), learningDecayPerSecond);
|
||||||
|
learningStateWindowSeconds = parseInt(props.getProperty("antibot.learning.state_window_seconds"), learningStateWindowSeconds);
|
||||||
|
learningRapidWindowMs = parseInt(props.getProperty("antibot.learning.rapid.window_ms"), learningRapidWindowMs);
|
||||||
|
learningRapidPoints = parseInt(props.getProperty("antibot.learning.rapid.points"), learningRapidPoints);
|
||||||
|
learningIpRateExceededPoints = parseInt(props.getProperty("antibot.learning.ip_rate_exceeded.points"), learningIpRateExceededPoints);
|
||||||
|
learningVpnProxyPoints = parseInt(props.getProperty("antibot.learning.vpn_proxy.points"), learningVpnProxyPoints);
|
||||||
|
learningVpnHostingPoints = parseInt(props.getProperty("antibot.learning.vpn_hosting.points"), learningVpnHostingPoints);
|
||||||
|
learningAttackModePoints = parseInt(props.getProperty("antibot.learning.attack_mode.points"), learningAttackModePoints);
|
||||||
|
learningHighCpsPoints = parseInt(props.getProperty("antibot.learning.high_cps.points"), learningHighCpsPoints);
|
||||||
|
learningRecentEventLimit = parseInt(props.getProperty("antibot.learning.recent_events.limit"), learningRecentEventLimit);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[AntiBotModule] Fehler beim Laden von " + CONFIG_FILE_NAME + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeProfile(String raw) {
|
||||||
|
if (raw == null) return "high-traffic";
|
||||||
|
String v = raw.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return "strict".equals(v) ? "strict" : "high-traffic";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyProfileDefaults(String profileName) {
|
||||||
|
if ("strict".equals(profileName)) {
|
||||||
|
maxCps = 120; attackStartCps = 220; attackStopCps = 120; attackCalmSeconds = 20;
|
||||||
|
ipConnectionsPerMinute = 18; ipBlockSeconds = 900;
|
||||||
|
vpnCheckEnabled = true; vpnBlockProxy = true; vpnBlockHosting = true;
|
||||||
|
vpnCacheMinutes = 30; vpnTimeoutMs = 2500;
|
||||||
|
} else {
|
||||||
|
maxCps = 180; attackStartCps = 300; attackStopCps = 170; attackCalmSeconds = 25;
|
||||||
|
ipConnectionsPerMinute = 24; ipBlockSeconds = 600;
|
||||||
|
vpnCheckEnabled = false; vpnBlockProxy = true; vpnBlockHosting = true;
|
||||||
|
vpnCacheMinutes = 30; vpnTimeoutMs = 2500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSupportedProfile(String raw) {
|
||||||
|
if (raw == null) return false;
|
||||||
|
String v = raw.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return "strict".equals(v) || "high-traffic".equals(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applyProfileAndPersist(String requestedProfile) {
|
||||||
|
if (!isSupportedProfile(requestedProfile)) return false;
|
||||||
|
String normalized = normalizeProfile(requestedProfile);
|
||||||
|
profile = normalized;
|
||||||
|
applyProfileDefaults(normalized);
|
||||||
|
|
||||||
|
Map<String, String> values = new LinkedHashMap<>();
|
||||||
|
values.put("antibot.profile", normalized);
|
||||||
|
values.put("antibot.max_cps", String.valueOf(maxCps));
|
||||||
|
values.put("antibot.attack.start_cps", String.valueOf(attackStartCps));
|
||||||
|
values.put("antibot.attack.stop_cps", String.valueOf(attackStopCps));
|
||||||
|
values.put("antibot.attack.stop_grace_seconds", String.valueOf(attackCalmSeconds));
|
||||||
|
values.put("antibot.ip.max_connections_per_minute", String.valueOf(ipConnectionsPerMinute));
|
||||||
|
values.put("antibot.ip.block_seconds", String.valueOf(ipBlockSeconds));
|
||||||
|
values.put("antibot.vpn_check.enabled", String.valueOf(vpnCheckEnabled));
|
||||||
|
values.put("antibot.vpn_check.block_proxy", String.valueOf(vpnBlockProxy));
|
||||||
|
values.put("antibot.vpn_check.block_hosting", String.valueOf(vpnBlockHosting));
|
||||||
|
values.put("antibot.vpn_check.cache_minutes", String.valueOf(vpnCacheMinutes));
|
||||||
|
values.put("antibot.vpn_check.timeout_ms", String.valueOf(vpnTimeoutMs));
|
||||||
|
try { updateConfigValues(values); return true; }
|
||||||
|
catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Profil nicht speichern: " + e.getMessage()); return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void updateConfigValues(Map<String, String> keyValues) throws Exception {
|
||||||
|
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
|
||||||
|
List<String> lines = target.exists()
|
||||||
|
? Files.readAllLines(target.toPath(), StandardCharsets.UTF_8)
|
||||||
|
: new ArrayList<>();
|
||||||
|
for (Map.Entry<String, String> entry : keyValues.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
String newLine = key + "=" + entry.getValue();
|
||||||
|
boolean replaced = false;
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
if (lines.get(i).trim().startsWith(key + "=")) {
|
||||||
|
lines.set(i, newLine);
|
||||||
|
replaced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!replaced) lines.add(newLine);
|
||||||
|
}
|
||||||
|
Files.write(target.toPath(), lines, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureModuleConfigExists() {
|
||||||
|
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
|
||||||
|
if (target.exists()) return;
|
||||||
|
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
|
||||||
|
try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME);
|
||||||
|
FileOutputStream out = new FileOutputStream(target)) {
|
||||||
|
if (in == null) { plugin.getLogger().warning("[AntiBotModule] Standarddatei nicht im JAR."); return; }
|
||||||
|
byte[] buffer = new byte[4096]; int read;
|
||||||
|
while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[AntiBotModule] Konnte Config nicht erstellen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evaluateLearningBaseline(String ip, long now) {
|
||||||
|
LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
|
||||||
|
synchronized (lp) {
|
||||||
|
decayLearningProfile(lp, now);
|
||||||
|
long delta = now - lp.lastConnectionAt;
|
||||||
|
if (lp.lastConnectionAt > 0 && delta <= Math.max(250L, learningRapidWindowMs)) {
|
||||||
|
lp.rapidStreak++;
|
||||||
|
int points = learningRapidPoints + Math.min(lp.rapidStreak, 5);
|
||||||
|
lp.score += Math.max(1, points);
|
||||||
|
recordLearningEvent("IP=" + ip + " +" + points + " rapid-connect score=" + lp.score);
|
||||||
|
} else {
|
||||||
|
lp.rapidStreak = 0;
|
||||||
|
}
|
||||||
|
if (attackMode) { lp.score += Math.max(1, learningAttackModePoints); recordLearningEvent("IP=" + ip + " +" + learningAttackModePoints + " attack-mode score=" + lp.score); }
|
||||||
|
if (lastCps >= Math.max(1, maxCps)) { lp.score += Math.max(1, learningHighCpsPoints); recordLearningEvent("IP=" + ip + " +" + learningHighCpsPoints + " high-cps score=" + lp.score); }
|
||||||
|
lp.lastConnectionAt = now;
|
||||||
|
lp.lastSeenAt = now;
|
||||||
|
if (lp.score >= learningScoreThreshold) {
|
||||||
|
blockIp(ip, now);
|
||||||
|
recordLearningEvent("BLOCK " + ip + " reason=learning-threshold score=" + lp.score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int addLearningScore(String ip, long now, int points, String reason, boolean checkThreshold) {
|
||||||
|
LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
|
||||||
|
synchronized (lp) {
|
||||||
|
decayLearningProfile(lp, now);
|
||||||
|
int add = Math.max(1, points);
|
||||||
|
lp.score += add;
|
||||||
|
lp.lastSeenAt = now;
|
||||||
|
recordLearningEvent("IP=" + ip + " +" + add + " " + reason + " score=" + lp.score);
|
||||||
|
if (checkThreshold && lp.score >= learningScoreThreshold) {
|
||||||
|
blockIp(ip, now);
|
||||||
|
recordLearningEvent("BLOCK " + ip + " reason=" + reason + " score=" + lp.score);
|
||||||
|
}
|
||||||
|
return lp.score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getLearningScore(String ip, long now) {
|
||||||
|
LearningProfile lp = learningProfiles.get(ip);
|
||||||
|
if (lp == null) return 0;
|
||||||
|
synchronized (lp) { decayLearningProfile(lp, now); return lp.score; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decayLearningProfile(LearningProfile lp, long now) {
|
||||||
|
long elapsedMs = Math.max(0L, now - lp.lastScoreUpdateAt);
|
||||||
|
if (elapsedMs > 0L) {
|
||||||
|
long decay = (elapsedMs / 1000L) * Math.max(0, learningDecayPerSecond);
|
||||||
|
if (decay > 0L) lp.score = (int) Math.max(0L, lp.score - decay);
|
||||||
|
lp.lastScoreUpdateAt = now;
|
||||||
|
}
|
||||||
|
long resetAfter = Math.max(30, learningStateWindowSeconds) * 1000L;
|
||||||
|
if (lp.lastSeenAt > 0L && now - lp.lastSeenAt > resetAfter) {
|
||||||
|
lp.score = 0;
|
||||||
|
lp.rapidStreak = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordLearningEvent(String event) {
|
||||||
|
String line = new SimpleDateFormat("HH:mm:ss").format(new Date()) + " " + event;
|
||||||
|
synchronized (learningRecentEvents) {
|
||||||
|
learningRecentEvents.addLast(line);
|
||||||
|
while (learningRecentEvents.size() > Math.max(5, learningRecentEventLimit)) learningRecentEvents.pollFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureSecurityLogFile() {
|
||||||
|
if (plugin == null) return;
|
||||||
|
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
|
||||||
|
securityLogFile = new File(plugin.getDataFolder(), securityLogFileName);
|
||||||
|
try { if (!securityLogFile.exists()) securityLogFile.createNewFile(); }
|
||||||
|
catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Sicherheitslog nicht erstellen: " + e.getMessage()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logSecurityEvent(String eventType, String ip, PendingConnection conn, String details) {
|
||||||
|
if (!securityLogEnabled || plugin == null) return;
|
||||||
|
if (securityLogFile == null) { ensureSecurityLogFile(); if (securityLogFile == null) return; }
|
||||||
|
|
||||||
|
String name = extractPlayerName(conn);
|
||||||
|
String uuid = extractPlayerUuid(conn, name);
|
||||||
|
if ((name == null || name.isEmpty() || "unknown".equalsIgnoreCase(name)) && ip != null) {
|
||||||
|
RecentPlayerIdentity cached = recentIdentityByIp.get(ip);
|
||||||
|
if (cached != null) {
|
||||||
|
if (cached.playerName != null && !cached.playerName.trim().isEmpty()) name = cached.playerName;
|
||||||
|
if ((uuid == null || uuid.isEmpty()) && cached.playerUuid != null && !cached.playerUuid.trim().isEmpty()) uuid = cached.playerUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name == null || name.trim().isEmpty()) name = "unknown";
|
||||||
|
if (uuid == null || uuid.trim().isEmpty()) uuid = "unknown";
|
||||||
|
|
||||||
|
String line = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
|
||||||
|
+ " | event=" + safeLog(eventType)
|
||||||
|
+ " | ip=" + safeLog(ip)
|
||||||
|
+ " | player=" + safeLog(name)
|
||||||
|
+ " | uuid=" + safeLog(uuid)
|
||||||
|
+ " | details=" + safeLog(details);
|
||||||
|
|
||||||
|
synchronized (securityLogLock) {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(new FileWriter(securityLogFile, true))) {
|
||||||
|
bw.write(line); bw.newLine();
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[AntiBotModule] Sicherheitslog-Schreibfehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cacheRecentIdentity(String ip, PendingConnection conn, long now) {
|
||||||
|
if (ip == null || ip.isEmpty() || conn == null) return;
|
||||||
|
String name = extractPlayerName(conn);
|
||||||
|
String uuid = extractPlayerUuid(conn, name);
|
||||||
|
if ((name == null || name.isEmpty()) && (uuid == null || uuid.isEmpty())) return;
|
||||||
|
RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity());
|
||||||
|
synchronized (identity) {
|
||||||
|
if (name != null && !name.trim().isEmpty()) identity.playerName = name.trim();
|
||||||
|
if (uuid != null && !uuid.trim().isEmpty()) identity.playerUuid = uuid.trim();
|
||||||
|
identity.updatedAtMs = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cacheRecentIdentityDirect(String ip, String playerName, UUID playerUuid, long now) {
|
||||||
|
if (ip == null || ip.isEmpty()) return;
|
||||||
|
String name = playerName == null ? "" : playerName.trim();
|
||||||
|
String uuid = playerUuid == null ? "" : playerUuid.toString();
|
||||||
|
if (name.isEmpty() && uuid.isEmpty()) return;
|
||||||
|
RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity());
|
||||||
|
synchronized (identity) {
|
||||||
|
if (!name.isEmpty()) identity.playerName = name;
|
||||||
|
if (!uuid.isEmpty()) identity.playerUuid = uuid;
|
||||||
|
identity.updatedAtMs = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractPlayerName(PendingConnection conn) {
|
||||||
|
if (conn == null) return "";
|
||||||
|
try { String raw = conn.getName(); return raw == null ? "" : raw.trim(); } catch (Exception ignored) { return ""; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractPlayerUuid(PendingConnection conn, String playerName) {
|
||||||
|
if (conn != null) {
|
||||||
|
try { UUID uuid = conn.getUniqueId(); if (uuid != null) return uuid.toString(); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
if (playerName != null && !playerName.trim().isEmpty()) {
|
||||||
|
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.trim()).getBytes(StandardCharsets.UTF_8)).toString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeLog(String input) {
|
||||||
|
if (input == null || input.isEmpty()) return "-";
|
||||||
|
return input.replace("\n", " ").replace("\r", " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getRecentLearningEvents(int max) {
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
synchronized (learningRecentEvents) {
|
||||||
|
int skip = Math.max(0, learningRecentEvents.size() - Math.max(1, max));
|
||||||
|
int idx = 0;
|
||||||
|
for (String line : learningRecentEvents) {
|
||||||
|
if (idx++ < skip) continue;
|
||||||
|
out.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parseBoolean(String s, boolean fallback) {
|
||||||
|
if (s == null) return fallback;
|
||||||
|
return Boolean.parseBoolean(s.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseInt(String s, int fallback) {
|
||||||
|
try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception ignored) { return fallback; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private VpnCheckResult getVpnInfo(String ip, long now) {
|
||||||
|
VpnCacheEntry cached = vpnCache.get(ip);
|
||||||
|
if (cached != null && cached.expiresAt > now) return cached.result;
|
||||||
|
VpnCheckResult fresh = requestIpApi(ip);
|
||||||
|
if (fresh != null) {
|
||||||
|
VpnCacheEntry entry = new VpnCacheEntry();
|
||||||
|
entry.result = fresh;
|
||||||
|
entry.expiresAt = now + Math.max(1, vpnCacheMinutes) * 60_000L;
|
||||||
|
vpnCache.put(ip, entry);
|
||||||
|
}
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VpnCheckResult requestIpApi(String ip) {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
String url = "http://ip-api.com/json/" + ip + "?fields=status,proxy,hosting";
|
||||||
|
conn = (HttpURLConnection) new URL(url).openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(vpnTimeoutMs);
|
||||||
|
conn.setReadTimeout(vpnTimeoutMs);
|
||||||
|
conn.setRequestProperty("User-Agent", "StatusAPI-AntiBot/1.0");
|
||||||
|
if (conn.getResponseCode() < 200 || conn.getResponseCode() >= 300) return null;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String line; while ((line = br.readLine()) != null) sb.append(line);
|
||||||
|
}
|
||||||
|
String json = sb.toString();
|
||||||
|
if (json.isEmpty() || !json.contains("\"status\":\"success\"")) return null;
|
||||||
|
VpnCheckResult result = new VpnCheckResult();
|
||||||
|
result.proxy = json.contains("\"proxy\":true");
|
||||||
|
result.hosting = json.contains("\"hosting\":true");
|
||||||
|
return result;
|
||||||
|
} catch (Exception ignored) { return null; }
|
||||||
|
finally { if (conn != null) conn.disconnect(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Interne Klassen ---
|
||||||
|
|
||||||
|
private static class IpWindow {
|
||||||
|
long windowStart; int count;
|
||||||
|
IpWindow(long now) { this.windowStart = now; this.count = 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class VpnCacheEntry { VpnCheckResult result; long expiresAt; }
|
||||||
|
private static class VpnCheckResult { boolean proxy; boolean hosting; }
|
||||||
|
|
||||||
|
private static class LearningProfile {
|
||||||
|
long lastConnectionAt, lastScoreUpdateAt, lastSeenAt;
|
||||||
|
int rapidStreak, score;
|
||||||
|
LearningProfile(long now) { lastConnectionAt = lastScoreUpdateAt = lastSeenAt = now; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RecentPlayerIdentity { String playerName; String playerUuid; long updatedAtMs; }
|
||||||
|
|
||||||
|
// --- Command ---
|
||||||
|
|
||||||
|
private class AntiBotCommand extends Command {
|
||||||
|
AntiBotCommand() { super("antibot", "statusapi.antibot"); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
if (!enabled) { sender.sendMessage(ChatColor.RED + "AntiBotModule ist deaktiviert."); return; }
|
||||||
|
|
||||||
|
if (args.length == 0 || "status".equalsIgnoreCase(args[0])) {
|
||||||
|
sender.sendMessage(ChatColor.GOLD + "----- AntiBot Status -----");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Schutz aktiv: " + ChatColor.WHITE + enabled);
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Profil: " + ChatColor.WHITE + profile);
|
||||||
|
if (attackMode) sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.RED + "AKTIV");
|
||||||
|
else sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.GREEN + "Normal");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "CPS: " + ChatColor.WHITE + lastCps + ChatColor.GRAY + " (Peak " + peakCps.get() + ")");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Schwellen: " + ChatColor.WHITE + "start " + attackStartCps + ChatColor.GRAY + " / " + ChatColor.WHITE + "stop " + attackStopCps + ChatColor.GRAY + " CPS");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Active IP Blocks: " + ChatColor.WHITE + blockedIpsUntil.size());
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Total blocked connections: " + ChatColor.WHITE + blockedConnectionsTotal.get());
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "VPN Check: " + ChatColor.WHITE + vpnCheckEnabled);
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Learning Mode: " + ChatColor.WHITE + learningModeEnabled
|
||||||
|
+ ChatColor.GRAY + " (threshold=" + learningScoreThreshold + ")");
|
||||||
|
List<String> recent = getRecentLearningEvents(3);
|
||||||
|
if (!recent.isEmpty()) {
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Learning Events:");
|
||||||
|
for (String line : recent) sender.sendMessage(ChatColor.GRAY + "- " + line);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("clearblocks".equalsIgnoreCase(args[0])) {
|
||||||
|
blockedIpsUntil.clear();
|
||||||
|
sender.sendMessage(ChatColor.GREEN + "Alle IP-Blocks wurden entfernt.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("unblock".equalsIgnoreCase(args[0]) && args.length >= 2) {
|
||||||
|
String ip = args[1].trim();
|
||||||
|
if (blockedIpsUntil.remove(ip) != null) sender.sendMessage(ChatColor.GREEN + "IP entblockt: " + ip);
|
||||||
|
else sender.sendMessage(ChatColor.RED + "IP war nicht geblockt: " + ip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("profile".equalsIgnoreCase(args[0])) {
|
||||||
|
if (args.length < 2) {
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Aktuelles Profil: " + ChatColor.WHITE + profile);
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Benutzung: /antibot profile <strict|high-traffic>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String requested = args[1].trim().toLowerCase(Locale.ROOT);
|
||||||
|
if (!isSupportedProfile(requested)) { sender.sendMessage(ChatColor.RED + "Unbekanntes Profil. Erlaubt: strict, high-traffic"); return; }
|
||||||
|
boolean ok = applyProfileAndPersist(requested);
|
||||||
|
if (!ok) { sender.sendMessage(ChatColor.RED + "Profil konnte nicht gespeichert werden."); return; }
|
||||||
|
sender.sendMessage(ChatColor.GREEN + "AntiBot-Profil umgestellt auf: " + requested);
|
||||||
|
sender.sendMessage(ChatColor.GRAY + "Werte wurden in " + CONFIG_FILE_NAME + " gespeichert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("reload".equalsIgnoreCase(args[0])) {
|
||||||
|
reloadRuntimeState();
|
||||||
|
sender.sendMessage(ChatColor.GREEN + "AntiBot-Konfiguration neu geladen.");
|
||||||
|
sender.sendMessage(ChatColor.GRAY + "Aktives Profil: " + profile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/antibot status");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/antibot clearblocks");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/antibot unblock <ip>");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/antibot profile <strict|high-traffic>");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/antibot reload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
package net.viper.status.modules.broadcast;
|
||||||
|
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.BaseComponent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.ClickEvent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BroadcastModule
|
||||||
|
*
|
||||||
|
* Fixes:
|
||||||
|
* - loadSchedules(): ID-Split nutzt jetzt indexOf/lastIndexOf statt split("\\.") mit length==2-Check.
|
||||||
|
* Damit werden auch clientScheduleIds die Punkte enthalten korrekt geladen. (Bug #2)
|
||||||
|
* - handleBroadcast(): &-Farbcodes werden jetzt auch in der Nachricht selbst übersetzt. (Bug #3)
|
||||||
|
* - handleBroadcast(): Literal \n in der Nachricht wird als echter Zeilenumbruch gerendert. (Bug #4)
|
||||||
|
* - handleBroadcast(): URLs (http/https) werden als anklickbare TextComponents eingebettet. (Bug #5)
|
||||||
|
*/
|
||||||
|
public class BroadcastModule implements Module, Listener {
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
private boolean enabled = true;
|
||||||
|
private String requiredApiKey = "";
|
||||||
|
private String format = "%prefix% %message%";
|
||||||
|
private String fallbackPrefix = "[Broadcast]";
|
||||||
|
private String fallbackPrefixColor = "&c";
|
||||||
|
private String fallbackBracketColor = "&8";
|
||||||
|
private String fallbackMessageColor = "&f";
|
||||||
|
|
||||||
|
private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>();
|
||||||
|
private File schedulesFile;
|
||||||
|
private final SimpleDateFormat dateFormat;
|
||||||
|
|
||||||
|
public BroadcastModule() {
|
||||||
|
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||||
|
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "BroadcastModule"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules");
|
||||||
|
loadConfig();
|
||||||
|
if (!enabled) return;
|
||||||
|
try { plugin.getProxy().getPluginManager().registerListener(plugin, this); } catch (Throwable ignored) {}
|
||||||
|
plugin.getLogger().fine("[BroadcastModule] aktiviert. Format: " + format);
|
||||||
|
loadSchedules();
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
saveSchedules();
|
||||||
|
scheduledByClientId.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
File file = new File(plugin.getDataFolder(), "verify.properties");
|
||||||
|
if (!file.exists()) return;
|
||||||
|
try (InputStream in = new FileInputStream(file)) {
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
|
enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true"));
|
||||||
|
requiredApiKey = props.getProperty("broadcast.api_key", "").trim();
|
||||||
|
format = props.getProperty("broadcast.format", format).trim();
|
||||||
|
if (format.isEmpty()) format = "%prefix% %message%";
|
||||||
|
fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim();
|
||||||
|
fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim();
|
||||||
|
fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim();
|
||||||
|
fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim();
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader,
|
||||||
|
String prefix, String prefixColor, String bracketColor, String messageColor) {
|
||||||
|
loadConfig();
|
||||||
|
if (!enabled) return false;
|
||||||
|
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
|
||||||
|
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
|
||||||
|
plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == null) message = "";
|
||||||
|
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
|
||||||
|
if (type == null) type = "global";
|
||||||
|
|
||||||
|
String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix;
|
||||||
|
String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor;
|
||||||
|
String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor;
|
||||||
|
String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor;
|
||||||
|
|
||||||
|
String prefixColorCode = normalizeColorCode(usedPrefixColor);
|
||||||
|
String bracketColorCode = normalizeColorCode(usedBracketColor);
|
||||||
|
String messageColorCode = normalizeColorCode(usedMessageColor);
|
||||||
|
|
||||||
|
String finalPrefix;
|
||||||
|
if (!bracketColorCode.isEmpty()) {
|
||||||
|
String textContent = usedPrefix;
|
||||||
|
if (textContent.startsWith("[")) textContent = textContent.substring(1);
|
||||||
|
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
|
||||||
|
finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET;
|
||||||
|
} else {
|
||||||
|
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIX #1: &-Farbcodes auch in der Nachricht selbst übersetzen
|
||||||
|
String translatedMessage = ChatColor.translateAlternateColorCodes('&', message);
|
||||||
|
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + translatedMessage;
|
||||||
|
|
||||||
|
String out = format
|
||||||
|
.replace("%name%", sourceName)
|
||||||
|
.replace("%prefix%", finalPrefix)
|
||||||
|
.replace("%prefixColored%", finalPrefix)
|
||||||
|
.replace("%message%", translatedMessage)
|
||||||
|
.replace("%messageColored%",coloredMessage)
|
||||||
|
.replace("%type%", type);
|
||||||
|
|
||||||
|
// FIX #2: \r entfernen (Windows CRLF -> nur LF), Literal \\n als Fallback
|
||||||
|
out = out.replace("\r\n", "\n").replace("\r", "").replace("\\n", "\n");
|
||||||
|
|
||||||
|
// FIX #3: Nachricht mit anklickbaren URLs aufbauen
|
||||||
|
BaseComponent[] components = buildClickableComponents(out);
|
||||||
|
int sent = 0;
|
||||||
|
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
||||||
|
try { p.sendMessage(components); sent++; } catch (Throwable ignored) {}
|
||||||
|
}
|
||||||
|
StatusAPI.debugLog(plugin, "[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut ein BaseComponent-Array aus einem formatierten String.
|
||||||
|
* URLs (http/https) werden als anklickbare TextComponents eingebettet.
|
||||||
|
* Unterstützt auch echte Newlines (\n) als Zeilenumbruch.
|
||||||
|
*/
|
||||||
|
private BaseComponent[] buildClickableComponents(String text) {
|
||||||
|
// Regex für URLs
|
||||||
|
java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile(
|
||||||
|
"(https?://[^\\s\\n]+)", java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
ComponentBuilder builder = new ComponentBuilder("");
|
||||||
|
|
||||||
|
// Zeilenweise aufteilen (echte \n)
|
||||||
|
String[] lines = text.split("\n", -1);
|
||||||
|
for (int li = 0; li < lines.length; li++) {
|
||||||
|
if (li > 0) {
|
||||||
|
// Zeilenumbruch als eigene Komponente
|
||||||
|
builder.append(TextComponent.fromLegacyText("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String line = lines[li];
|
||||||
|
java.util.regex.Matcher matcher = urlPattern.matcher(line);
|
||||||
|
int lastEnd = 0;
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
// Text vor der URL (mit Minecraft-Farbcodes)
|
||||||
|
if (matcher.start() > lastEnd) {
|
||||||
|
String before = line.substring(lastEnd, matcher.start());
|
||||||
|
builder.append(TextComponent.fromLegacyText(before));
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL selbst: anklickbar + unterstrichen
|
||||||
|
String url = matcher.group(1);
|
||||||
|
TextComponent urlComponent = new TextComponent(url);
|
||||||
|
urlComponent.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url));
|
||||||
|
// Farbe der URL auf Cyan setzen damit sie sich abhebt
|
||||||
|
urlComponent.setColor(ChatColor.AQUA);
|
||||||
|
urlComponent.setUnderlined(true);
|
||||||
|
builder.append(urlComponent, ComponentBuilder.FormatRetention.NONE);
|
||||||
|
|
||||||
|
lastEnd = matcher.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restlicher Text nach der letzten URL
|
||||||
|
if (lastEnd < line.length()) {
|
||||||
|
builder.append(TextComponent.fromLegacyText(line.substring(lastEnd)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeColorCode(String code) {
|
||||||
|
if (code == null) return "";
|
||||||
|
code = code.trim();
|
||||||
|
if (code.isEmpty()) return "";
|
||||||
|
return code.contains("&") ? ChatColor.translateAlternateColorCodes('&', code) : code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveSchedules() {
|
||||||
|
Properties props = new Properties();
|
||||||
|
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
|
||||||
|
String id = entry.getKey();
|
||||||
|
ScheduledBroadcast sb = entry.getValue();
|
||||||
|
// Wir escapen den ID-Wert damit Punkte in der ID nicht den Parser verwirren
|
||||||
|
props.setProperty(id + ".nextRunMillis", String.valueOf(sb.nextRunMillis));
|
||||||
|
props.setProperty(id + ".sourceName", sb.sourceName);
|
||||||
|
props.setProperty(id + ".message", sb.message);
|
||||||
|
props.setProperty(id + ".type", sb.type);
|
||||||
|
props.setProperty(id + ".prefix", sb.prefix);
|
||||||
|
props.setProperty(id + ".prefixColor", sb.prefixColor);
|
||||||
|
props.setProperty(id + ".bracketColor", sb.bracketColor);
|
||||||
|
props.setProperty(id + ".messageColor", sb.messageColor);
|
||||||
|
props.setProperty(id + ".recur", sb.recur);
|
||||||
|
}
|
||||||
|
try (OutputStream out = new FileOutputStream(schedulesFile)) {
|
||||||
|
props.store(out, "PulseCast Scheduled Broadcasts");
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht speichern: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIX #2: Robusteres Parsen der Property-Keys.
|
||||||
|
* Statt split("\\.") mit length==2-Check nutzen wir lastIndexOf('.') um den letzten
|
||||||
|
* Punkt als Trenner zu verwenden. Damit funktionieren auch IDs die Punkte enthalten.
|
||||||
|
*
|
||||||
|
* Bekannte Felder: nextRunMillis, sourceName, message, type, prefix, prefixColor,
|
||||||
|
* bracketColor, messageColor, recur → alle ohne Punkte im Namen.
|
||||||
|
*/
|
||||||
|
private void loadSchedules() {
|
||||||
|
if (!schedulesFile.exists()) return;
|
||||||
|
Properties props = new Properties();
|
||||||
|
try (InputStream in = new FileInputStream(schedulesFile)) {
|
||||||
|
props.load(in);
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht laden: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bekannte Feld-Suffixe
|
||||||
|
Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||||
|
"nextRunMillis", "sourceName", "message", "type",
|
||||||
|
"prefix", "prefixColor", "bracketColor", "messageColor", "recur"
|
||||||
|
));
|
||||||
|
|
||||||
|
Map<String, ScheduledBroadcast> loaded = new LinkedHashMap<>();
|
||||||
|
for (String key : props.stringPropertyNames()) {
|
||||||
|
// Finde das letzte '.' das einen bekannten Feldnamen abtrennt
|
||||||
|
int lastDot = key.lastIndexOf('.');
|
||||||
|
if (lastDot < 0) continue;
|
||||||
|
String field = key.substring(lastDot + 1);
|
||||||
|
if (!knownFields.contains(field)) continue;
|
||||||
|
String id = key.substring(0, lastDot);
|
||||||
|
if (id.isEmpty()) continue;
|
||||||
|
String value = props.getProperty(key);
|
||||||
|
|
||||||
|
ScheduledBroadcast sb = loaded.computeIfAbsent(id,
|
||||||
|
k -> new ScheduledBroadcast(k, 0, "", "", "", "", "", "", "", ""));
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
case "nextRunMillis": try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {} break;
|
||||||
|
case "sourceName": sb.sourceName = value; break;
|
||||||
|
case "message": sb.message = value; break;
|
||||||
|
case "type": sb.type = value; break;
|
||||||
|
case "prefix": sb.prefix = value; break;
|
||||||
|
case "prefixColor": sb.prefixColor = value; break;
|
||||||
|
case "bracketColor": sb.bracketColor = value; break;
|
||||||
|
case "messageColor": sb.messageColor = value; break;
|
||||||
|
case "recur": sb.recur = value; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduledByClientId.putAll(loaded);
|
||||||
|
plugin.getLogger().fine("[BroadcastModule] geplante Broadcasts wiederhergestellt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type,
|
||||||
|
String apiKeyHeader, String prefix, String prefixColor, String bracketColor,
|
||||||
|
String messageColor, String recur, String clientScheduleId) {
|
||||||
|
loadConfig();
|
||||||
|
if (!enabled) return false;
|
||||||
|
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
|
||||||
|
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
|
||||||
|
plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (message == null) message = "";
|
||||||
|
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
|
||||||
|
if (type == null) type = "global";
|
||||||
|
if (recur == null) recur = "none";
|
||||||
|
|
||||||
|
String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty())
|
||||||
|
? clientScheduleId.trim() : UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (timestampMillis <= now) {
|
||||||
|
plugin.getLogger().warning("[BroadcastModule] Geplante Zeit in der Vergangenheit → sende sofort!");
|
||||||
|
return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type,
|
||||||
|
prefix, prefixColor, bracketColor, messageColor, recur);
|
||||||
|
scheduledByClientId.put(id, sb);
|
||||||
|
saveSchedules();
|
||||||
|
StatusAPI.debugLog(plugin, "[BroadcastModule] Neue geplante Nachricht registriert: " + id
|
||||||
|
+ " @ " + dateFormat.format(new Date(timestampMillis)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean cancelScheduled(String clientScheduleId) {
|
||||||
|
if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
|
||||||
|
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId);
|
||||||
|
if (removed != null) { StatusAPI.debugLog(plugin, "[BroadcastModule] Schedule abgebrochen: " + clientScheduleId); saveSchedules(); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processScheduled() {
|
||||||
|
if (scheduledByClientId.isEmpty()) return;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
List<String> toRemove = new ArrayList<>();
|
||||||
|
boolean changed = false;
|
||||||
|
|
||||||
|
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
|
||||||
|
ScheduledBroadcast sb = entry.getValue();
|
||||||
|
if (sb.nextRunMillis <= now) {
|
||||||
|
StatusAPI.debugLog(plugin, "[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ")");
|
||||||
|
handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor);
|
||||||
|
if (!"none".equalsIgnoreCase(sb.recur)) {
|
||||||
|
long next = computeNextMillis(sb.nextRunMillis, sb.recur);
|
||||||
|
if (next > 0) { sb.nextRunMillis = next; changed = true; }
|
||||||
|
else { toRemove.add(entry.getKey()); changed = true; }
|
||||||
|
} else { toRemove.add(entry.getKey()); changed = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed || !toRemove.isEmpty()) {
|
||||||
|
for (String k : toRemove) { scheduledByClientId.remove(k); }
|
||||||
|
saveSchedules();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long computeNextMillis(long currentMillis, String recur) {
|
||||||
|
switch (recur.toLowerCase(Locale.ROOT)) {
|
||||||
|
case "hourly": return currentMillis + TimeUnit.HOURS.toMillis(1);
|
||||||
|
case "daily": return currentMillis + TimeUnit.DAYS.toMillis(1);
|
||||||
|
case "weekly": return currentMillis + TimeUnit.DAYS.toMillis(7);
|
||||||
|
default: return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ScheduledBroadcast {
|
||||||
|
final String clientId;
|
||||||
|
long nextRunMillis;
|
||||||
|
String sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur;
|
||||||
|
|
||||||
|
ScheduledBroadcast(String clientId, long nextRunMillis, String sourceName, String message, String type,
|
||||||
|
String prefix, String prefixColor, String bracketColor, String messageColor, String recur) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.nextRunMillis = nextRunMillis;
|
||||||
|
this.sourceName = sourceName;
|
||||||
|
this.message = message;
|
||||||
|
this.type = type;
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.prefixColor = prefixColor;
|
||||||
|
this.bracketColor = bracketColor;
|
||||||
|
this.messageColor = messageColor;
|
||||||
|
this.recur = recur == null ? "none" : recur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet die Verknüpfung von Minecraft-Accounts mit Discord/Telegram.
|
||||||
|
*
|
||||||
|
* Ablauf:
|
||||||
|
* 1. Spieler tippt /linkdiscord oder /linktelegram → Token wird generiert
|
||||||
|
* 2. Spieler schickt Token an den Bot
|
||||||
|
* 3. Bot-Polling erkennt Token → Verknüpfung wird gespeichert
|
||||||
|
*
|
||||||
|
* Speicherformat (chat_links.dat):
|
||||||
|
* minecraft:<uuid>|name:<spielername>|discord:<discord-user-id>|telegram:<telegram-user-id>
|
||||||
|
*/
|
||||||
|
public class AccountLinkManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
// UUID → verknüpfte Accounts
|
||||||
|
private final ConcurrentHashMap<UUID, LinkedAccount> links = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Ausstehende Token: token → UUID (läuft nach 10 Min ab)
|
||||||
|
private final ConcurrentHashMap<String, PendingToken> pendingTokens = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public AccountLinkManager(File dataFolder, Logger logger) {
|
||||||
|
this.file = new File(dataFolder, "chat_links.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Datenklassen =====
|
||||||
|
|
||||||
|
public static class LinkedAccount {
|
||||||
|
public UUID minecraftUUID;
|
||||||
|
public String minecraftName;
|
||||||
|
public String discordUserId = ""; // leer = nicht verknüpft
|
||||||
|
public String telegramUserId = ""; // leer = nicht verknüpft
|
||||||
|
public String telegramUsername = ""; // @username für Anzeige
|
||||||
|
public String discordUsername = ""; // für Anzeige
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PendingToken {
|
||||||
|
UUID uuid;
|
||||||
|
String playerName;
|
||||||
|
String type; // "discord" oder "telegram"
|
||||||
|
long expiresAt; // Unix-Millis
|
||||||
|
|
||||||
|
PendingToken(UUID uuid, String playerName, String type) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.playerName = playerName;
|
||||||
|
this.type = type;
|
||||||
|
this.expiresAt = System.currentTimeMillis() + (10 * 60 * 1000L); // 10 Min
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExpired() {
|
||||||
|
return System.currentTimeMillis() > expiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Token-Generierung =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert einen neuen Verknüpfungs-Token für einen Spieler.
|
||||||
|
* Bestehende Token für denselben Spieler+Typ werden überschrieben.
|
||||||
|
*
|
||||||
|
* @param uuid UUID des Spielers
|
||||||
|
* @param playerName Anzeigename
|
||||||
|
* @param type "discord" oder "telegram"
|
||||||
|
* @return 6-stelliger alphanumerischer Token (z.B. "A3F9K2")
|
||||||
|
*/
|
||||||
|
public String generateToken(UUID uuid, String playerName, String type) {
|
||||||
|
// Alte Token für diesen Spieler+Typ entfernen
|
||||||
|
pendingTokens.entrySet().removeIf(e ->
|
||||||
|
e.getValue().uuid.equals(uuid) && e.getValue().type.equals(type));
|
||||||
|
|
||||||
|
// Abgelaufene Token bereinigen
|
||||||
|
pendingTokens.entrySet().removeIf(e -> e.getValue().isExpired());
|
||||||
|
|
||||||
|
String token;
|
||||||
|
do {
|
||||||
|
token = generateRandomToken();
|
||||||
|
} while (pendingTokens.containsKey(token));
|
||||||
|
|
||||||
|
pendingTokens.put(token, new PendingToken(uuid, playerName, type));
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateRandomToken() {
|
||||||
|
String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne I,O,0,1 (Verwechslungsgefahr)
|
||||||
|
Random rnd = new Random();
|
||||||
|
StringBuilder sb = new StringBuilder(6);
|
||||||
|
for (int i = 0; i < 6; i++) sb.append(chars.charAt(rnd.nextInt(chars.length())));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Token einlösen =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versucht einen Token einzulösen (aufgerufen wenn Bot eine Nachricht empfängt).
|
||||||
|
*
|
||||||
|
* @param token Der eingesendete Token
|
||||||
|
* @param externalId Discord User-ID oder Telegram User-ID (als String)
|
||||||
|
* @param externalName Discord-Username oder Telegram-@username
|
||||||
|
* @return LinkedAccount wenn erfolgreich, null wenn Token ungültig/abgelaufen
|
||||||
|
*/
|
||||||
|
public LinkedAccount redeemToken(String token, String externalId, String externalName, String type) {
|
||||||
|
token = token.trim().toUpperCase();
|
||||||
|
PendingToken pending = pendingTokens.get(token);
|
||||||
|
|
||||||
|
if (pending == null || pending.isExpired()) {
|
||||||
|
pendingTokens.remove(token);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Typ muss übereinstimmen
|
||||||
|
if (!pending.type.equals(type)) return null;
|
||||||
|
|
||||||
|
pendingTokens.remove(token);
|
||||||
|
|
||||||
|
// Bestehenden Account holen oder neu anlegen
|
||||||
|
LinkedAccount account = links.computeIfAbsent(pending.uuid, k -> {
|
||||||
|
LinkedAccount a = new LinkedAccount();
|
||||||
|
a.minecraftUUID = pending.uuid;
|
||||||
|
a.minecraftName = pending.playerName;
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
account.minecraftName = pending.playerName; // aktuell halten
|
||||||
|
|
||||||
|
if ("discord".equals(pending.type)) {
|
||||||
|
account.discordUserId = externalId;
|
||||||
|
account.discordUsername = externalName;
|
||||||
|
} else if ("telegram".equals(pending.type)) {
|
||||||
|
account.telegramUserId = externalId;
|
||||||
|
account.telegramUsername = externalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
save();
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Lookup =====
|
||||||
|
|
||||||
|
public LinkedAccount getByUUID(UUID uuid) {
|
||||||
|
return links.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkedAccount getByDiscordId(String discordUserId) {
|
||||||
|
for (LinkedAccount a : links.values()) {
|
||||||
|
if (discordUserId.equals(a.discordUserId)) return a;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkedAccount getByTelegramId(String telegramUserId) {
|
||||||
|
for (LinkedAccount a : links.values()) {
|
||||||
|
if (telegramUserId.equals(a.telegramUserId)) return a;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den Minecraft-Namen für eine Discord-User-ID zurück, oder null. */
|
||||||
|
public String getMinecraftNameByDiscordId(String discordUserId) {
|
||||||
|
LinkedAccount a = getByDiscordId(discordUserId);
|
||||||
|
return a != null ? a.minecraftName : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den Minecraft-Namen für eine Telegram-User-ID zurück, oder null. */
|
||||||
|
public String getMinecraftNameByTelegramId(String telegramUserId) {
|
||||||
|
LinkedAccount a = getByTelegramId(telegramUserId);
|
||||||
|
return a != null ? a.minecraftName : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prüft ob ein Token gerade aussteht (für Tab-Complete etc.). */
|
||||||
|
public boolean hasPendingToken(UUID uuid, String type) {
|
||||||
|
for (PendingToken t : pendingTokens.values()) {
|
||||||
|
if (t.uuid.equals(uuid) && t.type.equals(type) && !t.isExpired()) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Verknüpfung aufheben =====
|
||||||
|
|
||||||
|
public boolean unlinkDiscord(UUID uuid) {
|
||||||
|
LinkedAccount a = links.get(uuid);
|
||||||
|
if (a == null || a.discordUserId.isEmpty()) return false;
|
||||||
|
a.discordUserId = "";
|
||||||
|
a.discordUsername = "";
|
||||||
|
cleanupEmpty(uuid);
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean unlinkTelegram(UUID uuid) {
|
||||||
|
LinkedAccount a = links.get(uuid);
|
||||||
|
if (a == null || a.telegramUserId.isEmpty()) return false;
|
||||||
|
a.telegramUserId = "";
|
||||||
|
a.telegramUsername = "";
|
||||||
|
cleanupEmpty(uuid);
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupEmpty(UUID uuid) {
|
||||||
|
LinkedAccount a = links.get(uuid);
|
||||||
|
if (a != null && a.discordUserId.isEmpty() && a.telegramUserId.isEmpty()) {
|
||||||
|
links.remove(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Convenience-Methoden für Bridges =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst einen Telegram-Token ein.
|
||||||
|
* Wrapper für redeemToken mit type="telegram".
|
||||||
|
*/
|
||||||
|
public LinkedAccount redeemTelegram(String token, String telegramUserId, String telegramUsername) {
|
||||||
|
return redeemToken(token, telegramUserId, telegramUsername, "telegram");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst einen Discord-Token ein.
|
||||||
|
* Wrapper für redeemToken mit type="discord".
|
||||||
|
*/
|
||||||
|
public LinkedAccount redeemDiscord(String token, String discordUserId, String discordUsername) {
|
||||||
|
return redeemToken(token, discordUserId, discordUsername, "discord");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Anzeigenamen für einen Telegram-Nutzer zurück.
|
||||||
|
* Wenn verknüpft: "MinecraftName (@telegram)", sonst: "@telegram"
|
||||||
|
*/
|
||||||
|
public String resolveTelegramName(String telegramUserId, String fallbackName) {
|
||||||
|
String mc = getMinecraftNameByTelegramId(telegramUserId);
|
||||||
|
return mc != null ? mc : fallbackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Anzeigenamen für einen Discord-Nutzer zurück.
|
||||||
|
* Wenn verknüpft: Minecraft-Name, sonst: Discord-Username
|
||||||
|
*/
|
||||||
|
public String resolveDiscordName(String discordUserId, String fallbackName) {
|
||||||
|
String mc = getMinecraftNameByDiscordId(discordUserId);
|
||||||
|
return mc != null ? mc : fallbackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt das korrekte Format-String zurück abhängig ob Account verknüpft.
|
||||||
|
* linked=true → linkedFormat (mit {player}), false → unlinkedFormat (mit {user})
|
||||||
|
*/
|
||||||
|
public boolean isLinkedTelegram(String telegramUserId) {
|
||||||
|
return getByTelegramId(telegramUserId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLinkedDiscord(String discordUserId) {
|
||||||
|
return getByDiscordId(discordUserId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Persistenz =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
for (LinkedAccount a : links.values()) {
|
||||||
|
bw.write(a.minecraftUUID
|
||||||
|
+ "|" + esc(a.minecraftName)
|
||||||
|
+ "|" + esc(a.discordUserId)
|
||||||
|
+ "|" + esc(a.discordUsername)
|
||||||
|
+ "|" + esc(a.telegramUserId)
|
||||||
|
+ "|" + esc(a.telegramUsername));
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Speichern der Account-Links: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
links.clear();
|
||||||
|
if (!file.exists()) return;
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String[] p = line.split("\\|", -1);
|
||||||
|
if (p.length < 6) continue;
|
||||||
|
try {
|
||||||
|
LinkedAccount a = new LinkedAccount();
|
||||||
|
a.minecraftUUID = UUID.fromString(p[0]);
|
||||||
|
a.minecraftName = unesc(p[1]);
|
||||||
|
a.discordUserId = unesc(p[2]);
|
||||||
|
a.discordUsername = unesc(p[3]);
|
||||||
|
a.telegramUserId = unesc(p[4]);
|
||||||
|
a.telegramUsername = unesc(p[5]);
|
||||||
|
if (!a.discordUserId.isEmpty() || !a.telegramUserId.isEmpty()) {
|
||||||
|
links.put(a.minecraftUUID, a);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Laden der Account-Links: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String esc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("|", "\\p");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String unesc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\p", "|").replace("\\\\", "\\");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet den gegenseitigen Blockier-/Ignore-Status zwischen Spielern.
|
||||||
|
*
|
||||||
|
* Admins/OPs mit dem Bypass-Permission sind nicht blockierbar.
|
||||||
|
*
|
||||||
|
* Format der Speicherdatei:
|
||||||
|
* <blocker-uuid>|<blocked-uuid1>,<blocked-uuid2>,...
|
||||||
|
*/
|
||||||
|
public class BlockManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
// blocker UUID → Set der blockierten UUIDs
|
||||||
|
private final ConcurrentHashMap<UUID, Set<UUID>> blocked = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public BlockManager(File dataFolder, Logger logger) {
|
||||||
|
this.file = new File(dataFolder, "chat_blocked.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Block-Logik =====
|
||||||
|
|
||||||
|
/** Spieler `blocker` blockiert Spieler `target`. */
|
||||||
|
public void block(UUID blocker, UUID target) {
|
||||||
|
blocked.computeIfAbsent(blocker, k -> Collections.newSetFromMap(new ConcurrentHashMap<>()))
|
||||||
|
.add(target);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spieler `blocker` hebt den Block für `target` auf. */
|
||||||
|
public void unblock(UUID blocker, UUID target) {
|
||||||
|
Set<UUID> set = blocked.get(blocker);
|
||||||
|
if (set != null) {
|
||||||
|
set.remove(target);
|
||||||
|
if (set.isEmpty()) blocked.remove(blocker);
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob `blocker` den Spieler `target` blockiert hat.
|
||||||
|
* Admins (isAdmin=true) sind niemals blockiert.
|
||||||
|
*/
|
||||||
|
public boolean isBlocked(UUID blocker, UUID target) {
|
||||||
|
Set<UUID> set = blocked.get(blocker);
|
||||||
|
return set != null && set.contains(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob eine Nachricht von `sender` an `receiver` zugestellt werden soll.
|
||||||
|
* Gibt false zurück, wenn einer der beiden den anderen blockiert.
|
||||||
|
*/
|
||||||
|
public boolean canReceive(UUID sender, UUID receiver) {
|
||||||
|
// receiver hat sender blockiert → keine Nachricht
|
||||||
|
if (isBlocked(receiver, sender)) return false;
|
||||||
|
// sender hat receiver blockiert → keine Nachricht (Komfort)
|
||||||
|
if (isBlocked(sender, receiver)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle UUIDs zurück, die `blocker` blockiert hat. */
|
||||||
|
public Set<UUID> getBlockedBy(UUID blocker) {
|
||||||
|
Set<UUID> set = blocked.get(blocker);
|
||||||
|
if (set == null) return Collections.emptySet();
|
||||||
|
return Collections.unmodifiableSet(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Persistenz =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
for (Map.Entry<UUID, Set<UUID>> e : blocked.entrySet()) {
|
||||||
|
if (e.getValue().isEmpty()) continue;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(e.getKey()).append("|");
|
||||||
|
Iterator<UUID> it = e.getValue().iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
sb.append(it.next());
|
||||||
|
if (it.hasNext()) sb.append(",");
|
||||||
|
}
|
||||||
|
bw.write(sb.toString());
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Speichern der Block-Liste: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
blocked.clear();
|
||||||
|
if (!file.exists()) return;
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String[] parts = line.split("\\|", 2);
|
||||||
|
if (parts.length < 2) continue;
|
||||||
|
try {
|
||||||
|
UUID blocker = UUID.fromString(parts[0]);
|
||||||
|
Set<UUID> targets = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
for (String rawUUID : parts[1].split(",")) {
|
||||||
|
rawUUID = rawUUID.trim();
|
||||||
|
if (!rawUUID.isEmpty()) {
|
||||||
|
try { targets.add(UUID.fromString(rawUUID)); }
|
||||||
|
catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targets.isEmpty()) blocked.put(blocker, targets);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Laden der Block-Liste: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen Chat-Kanal mit allen zugehörigen Einstellungen.
|
||||||
|
*/
|
||||||
|
public class ChatChannel {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String name;
|
||||||
|
private final String symbol;
|
||||||
|
private final String permission;
|
||||||
|
private final String color;
|
||||||
|
private final String format;
|
||||||
|
private final boolean localOnly;
|
||||||
|
|
||||||
|
// Bridge-Einstellungen
|
||||||
|
private final String discordWebhook;
|
||||||
|
private final String discordChannelId;
|
||||||
|
private final String telegramChatId;
|
||||||
|
private final int telegramThreadId; // 0 = kein Thema, >0 = Themen-ID
|
||||||
|
private final boolean useAdminBridge;
|
||||||
|
|
||||||
|
public ChatChannel(String id, String name, String symbol, String permission,
|
||||||
|
String color, String format, boolean localOnly,
|
||||||
|
String discordWebhook, String discordChannelId,
|
||||||
|
String telegramChatId, int telegramThreadId, boolean useAdminBridge) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.symbol = symbol;
|
||||||
|
this.permission = permission;
|
||||||
|
this.color = color;
|
||||||
|
this.format = format;
|
||||||
|
this.localOnly = localOnly;
|
||||||
|
this.discordWebhook = discordWebhook;
|
||||||
|
this.discordChannelId = discordChannelId;
|
||||||
|
this.telegramChatId = telegramChatId;
|
||||||
|
this.telegramThreadId = telegramThreadId;
|
||||||
|
this.useAdminBridge = useAdminBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() { return id; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public String getSymbol() { return symbol; }
|
||||||
|
public String getPermission() { return permission; }
|
||||||
|
public String getColor() { return color; }
|
||||||
|
public String getFormat() { return format; }
|
||||||
|
public boolean isLocalOnly() { return localOnly; }
|
||||||
|
public String getDiscordWebhook() { return discordWebhook; }
|
||||||
|
public String getDiscordChannelId() { return discordChannelId; }
|
||||||
|
public String getTelegramChatId() { return telegramChatId; }
|
||||||
|
public int getTelegramThreadId() { return telegramThreadId; }
|
||||||
|
public boolean isUseAdminBridge() { return useAdminBridge; }
|
||||||
|
|
||||||
|
/** Prüft ob der Kanal eine Permission erfordert. */
|
||||||
|
public boolean hasPermission() {
|
||||||
|
return permission != null && !permission.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt das formatierte Kanalprefix zurück, z.B. §a[G] */
|
||||||
|
public String getFormattedTag() {
|
||||||
|
return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&',
|
||||||
|
color + "[" + symbol + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ChatChannel{id=" + id + ", name=" + name + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,496 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.config.Configuration;
|
||||||
|
import net.md_5.bungee.config.ConfigurationProvider;
|
||||||
|
import net.md_5.bungee.config.YamlConfiguration;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt und verwaltet die chat.yml Konfiguration.
|
||||||
|
*
|
||||||
|
* Fix #8: Rate-Limit-Werte aus anti-spam werden nicht mehr durch nachfolgende
|
||||||
|
* Berechnungen überschrieben. Der rate-limit.chat-Block hat jetzt Vorrang.
|
||||||
|
* Die Reihenfolge ist: erst rate-limit.chat einlesen, dann ggf. durch anti-spam
|
||||||
|
* als Fallback ergänzen, nicht umgekehrt.
|
||||||
|
*/
|
||||||
|
public class ChatConfig {
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
private Configuration config;
|
||||||
|
|
||||||
|
private final Map<String, ChatChannel> channels = new LinkedHashMap<>();
|
||||||
|
private String defaultChannel;
|
||||||
|
|
||||||
|
private String helpopFormat, helpopPermission, helpopConfirm, helpopDiscordWebhook, helpopTelegramChatId;
|
||||||
|
private int helpopCooldown;
|
||||||
|
|
||||||
|
private String broadcastFormat, broadcastPermission;
|
||||||
|
|
||||||
|
private boolean pmEnabled;
|
||||||
|
private String pmFormatSender, pmFormatReceiver, pmFormatSpy, pmSpyPermission, pmRateLimitMessage;
|
||||||
|
private boolean pmRateLimitEnabled;
|
||||||
|
private long pmRateLimitWindowMs;
|
||||||
|
private int pmRateLimitMaxActions;
|
||||||
|
private long pmRateLimitBlockMs;
|
||||||
|
|
||||||
|
private int defaultMuteDuration;
|
||||||
|
private String mutedMessage;
|
||||||
|
|
||||||
|
private boolean emojiEnabled, emojiBedrockSupport;
|
||||||
|
private final Map<String, String> emojiMappings = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
private boolean discordEnabled;
|
||||||
|
private String discordBotToken, discordGuildId, discordFromFormat, discordAdminChannelId, discordEmbedColor;
|
||||||
|
private int discordPollInterval;
|
||||||
|
|
||||||
|
private boolean telegramEnabled;
|
||||||
|
private String telegramBotToken, telegramFromFormat, telegramAdminChatId;
|
||||||
|
private int telegramPollInterval, telegramChatTopicId, telegramAdminTopicId;
|
||||||
|
|
||||||
|
private boolean linkingEnabled;
|
||||||
|
private String linkDiscordMessage, linkTelegramMessage, linkSuccessDiscord, linkSuccessTelegram;
|
||||||
|
private String linkBotSuccessDiscord, linkBotSuccessTelegram, linkedDiscordFormat, linkedTelegramFormat;
|
||||||
|
private int telegramAdminThreadId;
|
||||||
|
|
||||||
|
private String adminBypassPermission, adminNotifyPermission;
|
||||||
|
|
||||||
|
private boolean chatlogEnabled;
|
||||||
|
private int chatlogRetentionDays;
|
||||||
|
|
||||||
|
private final Map<String, String> serverColors = new LinkedHashMap<>();
|
||||||
|
private final Map<String, String> serverDisplayNames = new LinkedHashMap<>();
|
||||||
|
private String serverColorDefault;
|
||||||
|
|
||||||
|
private boolean reportsEnabled, reportWebhookEnabled;
|
||||||
|
private String reportConfirm, reportPermission, reportClosePermission, reportViewPermission;
|
||||||
|
private String reportDiscordWebhook, reportTelegramChatId;
|
||||||
|
private int reportCooldown;
|
||||||
|
|
||||||
|
private ChatFilter.ChatFilterConfig filterConfig = new ChatFilter.ChatFilterConfig();
|
||||||
|
|
||||||
|
private boolean mentionsEnabled, mentionsAllowToggle;
|
||||||
|
private String mentionsHighlightColor, mentionsSound, mentionsNotifyPrefix;
|
||||||
|
|
||||||
|
private int historyMaxLines, historyDefaultLines;
|
||||||
|
|
||||||
|
private boolean joinLeaveEnabled, vanishShowToAdmins;
|
||||||
|
private String joinFormat, leaveFormat, vanishJoinFormat, vanishLeaveFormat;
|
||||||
|
private String joinLeaveDiscordWebhook, joinLeaveTelegramChatId;
|
||||||
|
private int joinLeaveTelegramThreadId;
|
||||||
|
|
||||||
|
public ChatConfig(Plugin plugin) { this.plugin = plugin; }
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
File file = new File(plugin.getDataFolder(), "chat.yml");
|
||||||
|
if (!file.exists()) {
|
||||||
|
plugin.getDataFolder().mkdirs();
|
||||||
|
InputStream in = plugin.getResourceAsStream("chat.yml");
|
||||||
|
if (in != null) {
|
||||||
|
try { Files.copy(in, file.toPath()); }
|
||||||
|
catch (IOException e) { plugin.getLogger().severe("[ChatModule] Konnte chat.yml nicht erstellen: " + e.getMessage()); }
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().warning("[ChatModule] chat.yml nicht in JAR, erstelle leere Datei.");
|
||||||
|
try { file.createNewFile(); } catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try { config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file); }
|
||||||
|
catch (IOException e) {
|
||||||
|
plugin.getLogger().severe("[ChatModule] Fehler beim Laden der chat.yml: " + e.getMessage());
|
||||||
|
config = new Configuration();
|
||||||
|
}
|
||||||
|
parseConfig();
|
||||||
|
plugin.getLogger().fine("[ChatModule] " + channels.size() + " Kanäle geladen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseConfig() {
|
||||||
|
defaultChannel = config.getString("default-channel", "global");
|
||||||
|
|
||||||
|
// --- Kanäle ---
|
||||||
|
channels.clear();
|
||||||
|
Configuration chSection = config.getSection("channels");
|
||||||
|
if (chSection != null) {
|
||||||
|
for (String id : chSection.getKeys()) {
|
||||||
|
Configuration ch = chSection.getSection(id);
|
||||||
|
if (ch == null) continue;
|
||||||
|
channels.put(id.toLowerCase(), new ChatChannel(
|
||||||
|
id.toLowerCase(),
|
||||||
|
ch.getString("name", id),
|
||||||
|
ch.getString("symbol", id.substring(0, 1).toUpperCase()),
|
||||||
|
ch.getString("permission", ""),
|
||||||
|
ch.getString("color", "&f"),
|
||||||
|
ch.getString("format", "&8[&7{server}&8] {prefix}&r{player}{suffix}&8: &f{message}"),
|
||||||
|
ch.getBoolean("local-only", false),
|
||||||
|
ch.getString("discord-webhook", ""),
|
||||||
|
ch.getString("discord-channel-id", ""),
|
||||||
|
ch.getString("telegram-chat-id", ""),
|
||||||
|
ch.getInt("telegram-thread-id", 0),
|
||||||
|
ch.getBoolean("use-admin-bridge", false)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!channels.containsKey("global")) {
|
||||||
|
channels.put("global", new ChatChannel("global", "Global", "G", "", "&a",
|
||||||
|
"&8[&a{server}&8] {prefix}&r{player}{suffix}&8: &f{message}", false, "", "", "", 0, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HelpOp ---
|
||||||
|
Configuration ho = config.getSection("helpop");
|
||||||
|
if (ho != null) {
|
||||||
|
helpopFormat = ho.getString("format", "&8[&eHELPOP&8] &f{player}&8@&7{server}&8: &e{message}");
|
||||||
|
helpopPermission = ho.getString("receive-permission", "chat.helpop.receive");
|
||||||
|
helpopCooldown = ho.getInt("cooldown", 30);
|
||||||
|
helpopConfirm = ho.getString("confirm-message", "&aHilferuf gesendet!");
|
||||||
|
helpopDiscordWebhook = ho.getString("discord-webhook", "");
|
||||||
|
helpopTelegramChatId = ho.getString("telegram-chat-id", "");
|
||||||
|
} else {
|
||||||
|
helpopFormat = "&8[&eHELPOP&8] &f{player}&8@&7{server}&8: &e{message}";
|
||||||
|
helpopPermission = "chat.helpop.receive"; helpopCooldown = 30;
|
||||||
|
helpopConfirm = "&aHilferuf gesendet!"; helpopDiscordWebhook = ""; helpopTelegramChatId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Broadcast ---
|
||||||
|
Configuration bc = config.getSection("broadcast");
|
||||||
|
broadcastFormat = bc != null ? bc.getString("format", "&c[&6Broadcast&c] &e{message}") : "&c[&6Broadcast&c] &e{message}";
|
||||||
|
broadcastPermission = bc != null ? bc.getString("permission", "chat.broadcast") : "chat.broadcast";
|
||||||
|
|
||||||
|
// --- Private Messages ---
|
||||||
|
Configuration pm = config.getSection("private-messages");
|
||||||
|
pmEnabled = pm == null || pm.getBoolean("enabled", true);
|
||||||
|
pmFormatSender = pm != null ? pm.getString("format-sender", "&8[&7Du &8→ &b{player}&8] &f{message}") : "&8[&7Du &8→ &b{player}&8] &f{message}";
|
||||||
|
pmFormatReceiver = pm != null ? pm.getString("format-receiver", "&8[&b{player} &8→ &7Dir&8] &f{message}") : "&8[&b{player} &8→ &7Dir&8] &f{message}";
|
||||||
|
pmFormatSpy = pm != null ? pm.getString("format-social-spy","&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}") : "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}";
|
||||||
|
pmSpyPermission = pm != null ? pm.getString("social-spy-permission", "chat.socialspy") : "chat.socialspy";
|
||||||
|
|
||||||
|
// --- Mute ---
|
||||||
|
Configuration mu = config.getSection("mute");
|
||||||
|
defaultMuteDuration = mu != null ? mu.getInt("default-duration-minutes", 60) : 60;
|
||||||
|
mutedMessage = mu != null ? mu.getString("muted-message", "&cDu bist stummgeschaltet. Noch: &f{time}") : "&cDu bist stummgeschaltet. Noch: &f{time}";
|
||||||
|
|
||||||
|
// --- Emoji ---
|
||||||
|
Configuration em = config.getSection("emoji");
|
||||||
|
if (em != null) {
|
||||||
|
emojiEnabled = em.getBoolean("enabled", true);
|
||||||
|
emojiBedrockSupport = em.getBoolean("bedrock-support", true);
|
||||||
|
emojiMappings.clear();
|
||||||
|
Configuration map = em.getSection("mappings");
|
||||||
|
if (map != null) { for (String key : map.getKeys()) emojiMappings.put(key, map.getString(key, key)); }
|
||||||
|
} else { emojiEnabled = true; emojiBedrockSupport = true; }
|
||||||
|
|
||||||
|
// --- Discord ---
|
||||||
|
Configuration dc = config.getSection("discord");
|
||||||
|
if (dc != null) {
|
||||||
|
discordEnabled = dc.getBoolean("enabled", false);
|
||||||
|
discordBotToken = dc.getString("bot-token", "");
|
||||||
|
discordGuildId = dc.getString("guild-id", "");
|
||||||
|
discordPollInterval = dc.getInt("poll-interval", 3);
|
||||||
|
discordFromFormat = dc.getString("from-discord-format", "&9[Discord] &b{user}&8: &f{message}");
|
||||||
|
discordAdminChannelId = dc.getString("admin-channel-id", "");
|
||||||
|
discordEmbedColor = dc.getString("embed-color", "5865F2");
|
||||||
|
} else { discordEnabled = false; discordBotToken = ""; discordGuildId = ""; discordPollInterval = 3; discordFromFormat = "&9[Discord] &b{user}&8: &f{message}"; discordAdminChannelId = ""; discordEmbedColor = "5865F2"; }
|
||||||
|
|
||||||
|
// --- Telegram ---
|
||||||
|
Configuration tg = config.getSection("telegram");
|
||||||
|
if (tg != null) {
|
||||||
|
telegramEnabled = tg.getBoolean("enabled", false);
|
||||||
|
telegramBotToken = tg.getString("bot-token", "");
|
||||||
|
telegramPollInterval = tg.getInt("poll-interval", 3);
|
||||||
|
telegramFromFormat = tg.getString("from-telegram-format", "&3[Telegram] &b{user}&8: &f{message}");
|
||||||
|
telegramAdminChatId = tg.getString("admin-chat-id", "");
|
||||||
|
telegramChatTopicId = tg.getInt("chat-topic-id", 0);
|
||||||
|
telegramAdminTopicId = tg.getInt("admin-topic-id", 0);
|
||||||
|
} else { telegramEnabled = false; telegramBotToken = ""; telegramPollInterval = 3; telegramFromFormat = "&3[Telegram] &b{user}&8: &f{message}"; telegramAdminChatId = ""; telegramChatTopicId = 0; telegramAdminTopicId = 0; }
|
||||||
|
|
||||||
|
// --- Account-Linking ---
|
||||||
|
Configuration al = config.getSection("account-linking");
|
||||||
|
linkingEnabled = al == null || al.getBoolean("enabled", true);
|
||||||
|
linkDiscordMessage = al != null ? al.getString("discord-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
|
||||||
|
linkTelegramMessage = al != null ? al.getString("telegram-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
|
||||||
|
linkSuccessDiscord = al != null ? al.getString("success-discord", "&aDiscord verknüpft!") : "&aDiscord verknüpft!";
|
||||||
|
linkSuccessTelegram = al != null ? al.getString("success-telegram", "&aTelegram verknüpft!") : "&aTelegram verknüpft!";
|
||||||
|
linkBotSuccessDiscord = al != null ? al.getString("bot-success-discord", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
|
||||||
|
linkBotSuccessTelegram = al != null ? al.getString("bot-success-telegram", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
|
||||||
|
linkedDiscordFormat = al != null ? al.getString("linked-discord-format", "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}") : "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}";
|
||||||
|
linkedTelegramFormat = al != null ? al.getString("linked-telegram-format", "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}") : "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}";
|
||||||
|
|
||||||
|
// --- Chat-Filter ---
|
||||||
|
filterConfig = new ChatFilter.ChatFilterConfig();
|
||||||
|
Configuration cf = config.getSection("chat-filter");
|
||||||
|
if (cf != null) {
|
||||||
|
Configuration spam = cf.getSection("anti-spam");
|
||||||
|
if (spam != null) {
|
||||||
|
filterConfig.antiSpamEnabled = spam.getBoolean("enabled", true);
|
||||||
|
filterConfig.spamCooldownMs = spam.getInt("cooldown-ms", 1500);
|
||||||
|
filterConfig.spamMaxMessages = spam.getInt("max-messages", 3);
|
||||||
|
filterConfig.spamMessage = spam.getString("message", "&cNicht so schnell!");
|
||||||
|
// FIX #8: Fallback-Werte aus anti-spam werden NUR gesetzt wenn rate-limit.chat nicht
|
||||||
|
// konfiguriert ist. Wir setzen die Werte hier als Vorbelegung und überschreiben sie
|
||||||
|
// unten mit dem rate-limit.chat-Block wenn vorhanden.
|
||||||
|
filterConfig.globalRateLimitWindowMs = Math.max(500L, filterConfig.spamCooldownMs);
|
||||||
|
filterConfig.globalRateLimitMaxActions = Math.max(1, filterConfig.spamMaxMessages);
|
||||||
|
filterConfig.globalRateLimitBlockMs = Math.max(2000L, filterConfig.spamCooldownMs * 4L);
|
||||||
|
}
|
||||||
|
Configuration dup = cf.getSection("duplicate-check");
|
||||||
|
if (dup != null) {
|
||||||
|
filterConfig.duplicateCheckEnabled = dup.getBoolean("enabled", true);
|
||||||
|
filterConfig.duplicateMessage = dup.getString("message", "&cKeine identischen Nachrichten.");
|
||||||
|
}
|
||||||
|
Configuration bl = cf.getSection("blacklist");
|
||||||
|
if (bl != null) {
|
||||||
|
filterConfig.blacklistEnabled = bl.getBoolean("enabled", true);
|
||||||
|
filterConfig.blacklistWords.clear();
|
||||||
|
loadFilterWords(filterConfig.blacklistWords);
|
||||||
|
try {
|
||||||
|
java.util.List<?> wordList = bl.getList("words");
|
||||||
|
if (wordList != null) {
|
||||||
|
for (Object o : wordList) {
|
||||||
|
if (o != null && !o.toString().trim().isEmpty()) {
|
||||||
|
String w = o.toString().trim();
|
||||||
|
if (!filterConfig.blacklistWords.contains(w)) filterConfig.blacklistWords.add(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
Configuration caps = cf.getSection("caps-filter");
|
||||||
|
if (caps != null) {
|
||||||
|
filterConfig.capsFilterEnabled = caps.getBoolean("enabled", true);
|
||||||
|
filterConfig.capsMinLength = caps.getInt("min-length", 6);
|
||||||
|
filterConfig.capsMaxPercent = caps.getInt("max-percent", 70);
|
||||||
|
}
|
||||||
|
Configuration antiAd = cf.getSection("anti-ad");
|
||||||
|
if (antiAd != null) {
|
||||||
|
filterConfig.antiAdEnabled = antiAd.getBoolean("enabled", true);
|
||||||
|
filterConfig.antiAdMessage = antiAd.getString("message", "&cWerbung ist nicht erlaubt!");
|
||||||
|
java.util.List<?> wl = antiAd.getList("whitelist");
|
||||||
|
if (wl != null) { filterConfig.antiAdWhitelist.clear(); for (Object o : wl) if (o != null) filterConfig.antiAdWhitelist.add(o.toString()); }
|
||||||
|
java.util.List<?> tlds = antiAd.getList("blocked-tlds");
|
||||||
|
if (tlds != null) { filterConfig.antiAdBlockedTlds.clear(); for (Object o : tlds) if (o != null) filterConfig.antiAdBlockedTlds.add(o.toString()); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rate-Limit (FIX #8: dieser Block setzt die endgültigen Werte, hat Vorrang) ---
|
||||||
|
pmRateLimitEnabled = true;
|
||||||
|
pmRateLimitWindowMs = 5000L;
|
||||||
|
pmRateLimitMaxActions = 4;
|
||||||
|
pmRateLimitBlockMs = 10000L;
|
||||||
|
pmRateLimitMessage = "&cDu sendest zu viele private Nachrichten. Bitte warte kurz.";
|
||||||
|
|
||||||
|
Configuration rl = config.getSection("rate-limit");
|
||||||
|
if (rl != null) {
|
||||||
|
Configuration rlChat = rl.getSection("chat");
|
||||||
|
if (rlChat != null) {
|
||||||
|
// FIX #8: rate-limit.chat überschreibt die anti-spam-Fallbacks vollständig
|
||||||
|
filterConfig.globalRateLimitEnabled = rlChat.getBoolean("enabled", true);
|
||||||
|
filterConfig.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs);
|
||||||
|
filterConfig.globalRateLimitMaxActions = rlChat.getInt("max-actions", filterConfig.globalRateLimitMaxActions);
|
||||||
|
filterConfig.globalRateLimitBlockMs = rlChat.getLong("block-ms", filterConfig.globalRateLimitBlockMs);
|
||||||
|
filterConfig.spamMessage = rlChat.getString("message", filterConfig.spamMessage);
|
||||||
|
}
|
||||||
|
Configuration rlPm = rl.getSection("private-messages");
|
||||||
|
if (rlPm != null) {
|
||||||
|
pmRateLimitEnabled = rlPm.getBoolean("enabled", pmRateLimitEnabled);
|
||||||
|
pmRateLimitWindowMs = rlPm.getLong("window-ms", pmRateLimitWindowMs);
|
||||||
|
pmRateLimitMaxActions = rlPm.getInt("max-actions", pmRateLimitMaxActions);
|
||||||
|
pmRateLimitBlockMs = rlPm.getLong("block-ms", pmRateLimitBlockMs);
|
||||||
|
pmRateLimitMessage = rlPm.getString("message", pmRateLimitMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mentions ---
|
||||||
|
Configuration mn = config.getSection("mentions");
|
||||||
|
mentionsEnabled = mn == null || mn.getBoolean("enabled", true);
|
||||||
|
mentionsHighlightColor = mn != null ? mn.getString("highlight-color", "&e&l") : "&e&l";
|
||||||
|
mentionsSound = mn != null ? mn.getString("sound", "ENTITY_EXPERIENCE_ORB_PICKUP") : "ENTITY_EXPERIENCE_ORB_PICKUP";
|
||||||
|
mentionsAllowToggle = mn == null || mn.getBoolean("allow-toggle", true);
|
||||||
|
mentionsNotifyPrefix = mn != null ? mn.getString("notify-prefix", "&e&l[Mention] &r") : "&e&l[Mention] &r";
|
||||||
|
|
||||||
|
// --- Chat-History ---
|
||||||
|
Configuration ch = config.getSection("chat-history");
|
||||||
|
historyMaxLines = ch != null ? ch.getInt("max-lines", 50) : 50;
|
||||||
|
historyDefaultLines = ch != null ? ch.getInt("default-lines", 10) : 10;
|
||||||
|
|
||||||
|
// --- Admin ---
|
||||||
|
Configuration adm = config.getSection("admin");
|
||||||
|
adminBypassPermission = adm != null ? adm.getString("bypass-permission", "chat.admin.bypass") : "chat.admin.bypass";
|
||||||
|
adminNotifyPermission = adm != null ? adm.getString("notify-permission", "chat.admin.notify") : "chat.admin.notify";
|
||||||
|
|
||||||
|
// --- Server-Farben ---
|
||||||
|
serverColors.clear(); serverDisplayNames.clear();
|
||||||
|
Configuration sc = config.getSection("server-colors");
|
||||||
|
if (sc != null) {
|
||||||
|
serverColorDefault = sc.getString("default", "&7");
|
||||||
|
for (String key : sc.getKeys()) {
|
||||||
|
if (key.equals("default")) continue;
|
||||||
|
Configuration sub = sc.getSection(key);
|
||||||
|
if (sub != null) {
|
||||||
|
serverColors.put(key.toLowerCase(), sub.getString("color", "&7"));
|
||||||
|
String display = sub.getString("display", "");
|
||||||
|
if (!display.isEmpty()) serverDisplayNames.put(key.toLowerCase(), display);
|
||||||
|
} else {
|
||||||
|
serverColors.put(key.toLowerCase(), sc.getString(key, "&7"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { serverColorDefault = "&7"; }
|
||||||
|
|
||||||
|
// --- Chatlog ---
|
||||||
|
Configuration cl = config.getSection("chatlog");
|
||||||
|
chatlogEnabled = cl == null || cl.getBoolean("enabled", true);
|
||||||
|
int raw = cl != null ? cl.getInt("retention-days", 7) : 7;
|
||||||
|
chatlogRetentionDays = (raw == 14) ? 14 : 7;
|
||||||
|
|
||||||
|
// --- Reports ---
|
||||||
|
Configuration rp = config.getSection("reports");
|
||||||
|
if (rp != null) {
|
||||||
|
reportsEnabled = rp.getBoolean("enabled", true);
|
||||||
|
reportWebhookEnabled = rp.getBoolean("webhook-enabled", false);
|
||||||
|
reportConfirm = rp.getString("confirm-message", "&aDein Report &8({id}) &awurde eingereicht. Danke!");
|
||||||
|
reportPermission = rp.getString("report-permission", "");
|
||||||
|
reportClosePermission = rp.getString("close-permission", "chat.admin.bypass");
|
||||||
|
reportViewPermission = rp.getString("view-permission", "chat.admin.bypass");
|
||||||
|
reportCooldown = rp.getInt("cooldown", 60);
|
||||||
|
reportDiscordWebhook = rp.getString("discord-webhook", "");
|
||||||
|
reportTelegramChatId = rp.getString("telegram-chat-id", "");
|
||||||
|
} else {
|
||||||
|
reportsEnabled = true; reportWebhookEnabled = false;
|
||||||
|
reportConfirm = "&aDein Report &8({id}) &awurde eingereicht. Danke!";
|
||||||
|
reportPermission = ""; reportClosePermission = "chat.admin.bypass"; reportViewPermission = "chat.admin.bypass";
|
||||||
|
reportCooldown = 60; reportDiscordWebhook = ""; reportTelegramChatId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Join / Leave ---
|
||||||
|
Configuration jl = config.getSection("join-leave");
|
||||||
|
if (jl != null) {
|
||||||
|
joinLeaveEnabled = jl.getBoolean("enabled", true);
|
||||||
|
joinFormat = jl.getString("join-format", "&8[&a+&8] {prefix}&a{player}&r &7hat das Netzwerk betreten.");
|
||||||
|
leaveFormat = jl.getString("leave-format", "&8[&c-&8] {prefix}&c{player}&r &7hat das Netzwerk verlassen.");
|
||||||
|
vanishShowToAdmins = jl.getBoolean("vanish-show-to-admins", true);
|
||||||
|
vanishJoinFormat = jl.getString("vanish-join-format", "&8[&7+&8] &8{player} &7hat das Netzwerk betreten. &8(Vanish)");
|
||||||
|
vanishLeaveFormat = jl.getString("vanish-leave-format", "&8[&7-&8] &8{player} &7hat das Netzwerk verlassen. &8(Vanish)");
|
||||||
|
joinLeaveDiscordWebhook = jl.getString("discord-webhook", "");
|
||||||
|
joinLeaveTelegramChatId = jl.getString("telegram-chat-id", "");
|
||||||
|
joinLeaveTelegramThreadId = jl.getInt("telegram-thread-id", 0);
|
||||||
|
} else {
|
||||||
|
joinLeaveEnabled = true;
|
||||||
|
joinFormat = "&8[&a+&8] {prefix}&a{player}&r &7hat das Netzwerk betreten.";
|
||||||
|
leaveFormat = "&8[&c-&8] {prefix}&c{player}&r &7hat das Netzwerk verlassen.";
|
||||||
|
vanishShowToAdmins = true;
|
||||||
|
vanishJoinFormat = "&8[&7+&8] &8{player} &7hat das Netzwerk betreten. &8(Vanish)";
|
||||||
|
vanishLeaveFormat = "&8[&7-&8] &8{player} &7hat das Netzwerk verlassen. &8(Vanish)";
|
||||||
|
joinLeaveDiscordWebhook = ""; joinLeaveTelegramChatId = ""; joinLeaveTelegramThreadId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFilterWords(java.util.List<String> target) {
|
||||||
|
File filterFile = new File(plugin.getDataFolder(), "filter.yml");
|
||||||
|
if (!filterFile.exists()) {
|
||||||
|
try {
|
||||||
|
plugin.getDataFolder().mkdirs();
|
||||||
|
try (java.io.FileWriter fw = new java.io.FileWriter(filterFile)) {
|
||||||
|
fw.write("# StatusAPI - Wort-Blacklist\n# words:\n# - beispielwort\nwords:\n");
|
||||||
|
}
|
||||||
|
plugin.getLogger().fine("[ChatModule] filter.yml erstellt.");
|
||||||
|
} catch (IOException e) { plugin.getLogger().warning("[ChatModule] Konnte filter.yml nicht erstellen: " + e.getMessage()); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Configuration fc = ConfigurationProvider.getProvider(YamlConfiguration.class).load(filterFile);
|
||||||
|
java.util.List<?> words = fc.getList("words");
|
||||||
|
if (words != null) {
|
||||||
|
for (Object o : words) {
|
||||||
|
if (o != null && !o.toString().trim().isEmpty()) target.add(o.toString().trim().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) { plugin.getLogger().warning("[ChatModule] Fehler beim Laden der filter.yml: " + e.getMessage()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Getter =====
|
||||||
|
|
||||||
|
public Map<String, ChatChannel> getChannels() { return Collections.unmodifiableMap(channels); }
|
||||||
|
public ChatChannel getChannel(String id) { return channels.get(id == null ? defaultChannel : id.toLowerCase()); }
|
||||||
|
public ChatChannel getDefaultChannel() { return channels.getOrDefault(defaultChannel, channels.values().iterator().next()); }
|
||||||
|
public String getDefaultChannelId() { return defaultChannel; }
|
||||||
|
public String getHelpopFormat() { return helpopFormat; }
|
||||||
|
public String getHelpopPermission() { return helpopPermission; }
|
||||||
|
public int getHelpopCooldown() { return helpopCooldown; }
|
||||||
|
public String getHelpopConfirm() { return helpopConfirm; }
|
||||||
|
public String getHelpopDiscordWebhook() { return helpopDiscordWebhook; }
|
||||||
|
public String getHelpopTelegramChatId() { return helpopTelegramChatId; }
|
||||||
|
public String getBroadcastFormat() { return broadcastFormat; }
|
||||||
|
public String getBroadcastPermission() { return broadcastPermission; }
|
||||||
|
public boolean isPmEnabled() { return pmEnabled; }
|
||||||
|
public String getPmFormatSender() { return pmFormatSender; }
|
||||||
|
public String getPmFormatReceiver() { return pmFormatReceiver; }
|
||||||
|
public String getPmFormatSpy() { return pmFormatSpy; }
|
||||||
|
public String getPmSpyPermission() { return pmSpyPermission; }
|
||||||
|
public boolean isPmRateLimitEnabled() { return pmRateLimitEnabled; }
|
||||||
|
public long getPmRateLimitWindowMs() { return pmRateLimitWindowMs; }
|
||||||
|
public int getPmRateLimitMaxActions() { return pmRateLimitMaxActions; }
|
||||||
|
public long getPmRateLimitBlockMs() { return pmRateLimitBlockMs; }
|
||||||
|
public String getPmRateLimitMessage() { return pmRateLimitMessage; }
|
||||||
|
public int getDefaultMuteDuration() { return defaultMuteDuration; }
|
||||||
|
public String getMutedMessage() { return mutedMessage; }
|
||||||
|
public boolean isEmojiEnabled() { return emojiEnabled; }
|
||||||
|
public boolean isEmojiBedrockSupport() { return emojiBedrockSupport; }
|
||||||
|
public Map<String, String> getEmojiMappings() { return Collections.unmodifiableMap(emojiMappings); }
|
||||||
|
public boolean isDiscordEnabled() { return discordEnabled; }
|
||||||
|
public String getDiscordBotToken() { return discordBotToken; }
|
||||||
|
public String getDiscordGuildId() { return discordGuildId; }
|
||||||
|
public int getDiscordPollInterval() { return discordPollInterval; }
|
||||||
|
public String getDiscordFromFormat() { return discordFromFormat; }
|
||||||
|
public String getDiscordAdminChannelId() { return discordAdminChannelId; }
|
||||||
|
public String getDiscordEmbedColor() { return discordEmbedColor; }
|
||||||
|
public boolean isTelegramEnabled() { return telegramEnabled; }
|
||||||
|
public String getTelegramBotToken() { return telegramBotToken; }
|
||||||
|
public int getTelegramPollInterval() { return telegramPollInterval; }
|
||||||
|
public String getTelegramFromFormat() { return telegramFromFormat; }
|
||||||
|
public String getTelegramAdminChatId() { return telegramAdminChatId; }
|
||||||
|
public int getTelegramChatTopicId() { return telegramChatTopicId; }
|
||||||
|
public int getTelegramAdminTopicId() { return telegramAdminTopicId; }
|
||||||
|
public boolean isLinkingEnabled() { return linkingEnabled; }
|
||||||
|
public String getLinkDiscordMessage() { return linkDiscordMessage; }
|
||||||
|
public String getLinkTelegramMessage() { return linkTelegramMessage; }
|
||||||
|
public String getLinkSuccessDiscord() { return linkSuccessDiscord; }
|
||||||
|
public String getLinkSuccessTelegram() { return linkSuccessTelegram; }
|
||||||
|
public String getLinkBotSuccessDiscord() { return linkBotSuccessDiscord; }
|
||||||
|
public String getLinkBotSuccessTelegram() { return linkBotSuccessTelegram; }
|
||||||
|
public String getLinkedDiscordFormat() { return linkedDiscordFormat; }
|
||||||
|
public String getLinkedTelegramFormat() { return linkedTelegramFormat; }
|
||||||
|
public String getAdminBypassPermission() { return adminBypassPermission; }
|
||||||
|
public String getAdminNotifyPermission() { return adminNotifyPermission; }
|
||||||
|
public String getServerColor(String serverName) { if (serverName == null) return serverColorDefault; String c = serverColors.get(serverName.toLowerCase()); return c != null ? c : serverColorDefault; }
|
||||||
|
public Map<String, String> getServerColors() { return Collections.unmodifiableMap(serverColors); }
|
||||||
|
public String getServerColorDefault() { return serverColorDefault; }
|
||||||
|
public String getServerDisplay(String serverName) { if (serverName == null) return ""; String d = serverDisplayNames.get(serverName.toLowerCase()); return d != null ? d : serverName; }
|
||||||
|
public boolean isChatlogEnabled() { return chatlogEnabled; }
|
||||||
|
public int getChatlogRetentionDays() { return chatlogRetentionDays; }
|
||||||
|
public boolean isReportsEnabled() { return reportsEnabled; }
|
||||||
|
public String getReportConfirm() { return reportConfirm; }
|
||||||
|
public String getReportPermission() { return reportPermission; }
|
||||||
|
public String getReportClosePermission() { return reportClosePermission; }
|
||||||
|
public String getReportViewPermission() { return reportViewPermission; }
|
||||||
|
public int getReportCooldown() { return reportCooldown; }
|
||||||
|
public String getReportDiscordWebhook() { return reportDiscordWebhook; }
|
||||||
|
public String getReportTelegramChatId() { return reportTelegramChatId; }
|
||||||
|
public boolean isReportWebhookEnabled() { return reportWebhookEnabled; }
|
||||||
|
public ChatFilter.ChatFilterConfig getFilterConfig() { return filterConfig; }
|
||||||
|
public boolean isMentionsEnabled() { return mentionsEnabled; }
|
||||||
|
public String getMentionsHighlightColor() { return mentionsHighlightColor; }
|
||||||
|
public String getMentionsSound() { return mentionsSound; }
|
||||||
|
public boolean isMentionsAllowToggle() { return mentionsAllowToggle; }
|
||||||
|
public String getMentionsNotifyPrefix() { return mentionsNotifyPrefix; }
|
||||||
|
public int getHistoryMaxLines() { return historyMaxLines; }
|
||||||
|
public int getHistoryDefaultLines() { return historyDefaultLines; }
|
||||||
|
public boolean isJoinLeaveEnabled() { return joinLeaveEnabled; }
|
||||||
|
public String getJoinFormat() { return joinFormat; }
|
||||||
|
public String getLeaveFormat() { return leaveFormat; }
|
||||||
|
public boolean isVanishShowToAdmins() { return vanishShowToAdmins; }
|
||||||
|
public String getVanishJoinFormat() { return vanishJoinFormat; }
|
||||||
|
public String getVanishLeaveFormat() { return vanishLeaveFormat; }
|
||||||
|
public String getJoinLeaveDiscordWebhook() { return joinLeaveDiscordWebhook; }
|
||||||
|
public String getJoinLeaveTelegramChatId() { return joinLeaveTelegramChatId; }
|
||||||
|
public int getJoinLeaveTelegramThreadId() { return joinLeaveTelegramThreadId; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import net.viper.status.ratelimit.GlobalRateLimitFramework;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat-Filter: Anti-Spam, Caps-Filter, Wort-Blacklist, Farbcode-Filter.
|
||||||
|
*
|
||||||
|
* Reihenfolge der Prüfungen in processChat():
|
||||||
|
* 1. Spam-Cooldown (zu schnell geschrieben?)
|
||||||
|
* 2. Gleiche Nachricht wiederholt?
|
||||||
|
* 3. Zu viele Großbuchstaben?
|
||||||
|
* 4. Verbotene Wörter → ersetzen durch ****
|
||||||
|
* 5. Farbcodes (& Codes) → nur mit Permission erlaubt
|
||||||
|
*/
|
||||||
|
public class ChatFilter {
|
||||||
|
|
||||||
|
private final ChatFilterConfig cfg;
|
||||||
|
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
||||||
|
|
||||||
|
// UUID → letzte Nachricht (für Duplikat-Check)
|
||||||
|
private final Map<UUID, String> lastMessageText = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Kompilierte Regex-Pattern für Blacklist-Wörter
|
||||||
|
private final List<Pattern> blacklistPatterns = new ArrayList<>();
|
||||||
|
|
||||||
|
public ChatFilter(ChatFilterConfig cfg) {
|
||||||
|
this.cfg = cfg;
|
||||||
|
compilePatterns();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void compilePatterns() {
|
||||||
|
blacklistPatterns.clear();
|
||||||
|
for (String word : cfg.blacklistWords) {
|
||||||
|
// Case-insensitiv, ganzes Wort oder Teilwort je nach Config
|
||||||
|
blacklistPatterns.add(Pattern.compile(
|
||||||
|
"(?i)" + Pattern.quote(word)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Ergebnis-Klasse =====
|
||||||
|
|
||||||
|
public enum FilterResult {
|
||||||
|
ALLOWED, // Nachricht darf durch
|
||||||
|
BLOCKED, // Nachricht blockiert (Spam/Flood)
|
||||||
|
MODIFIED // Nachricht wurde verändert (Wörter ersetzt / Caps reduziert)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FilterResponse {
|
||||||
|
public final FilterResult result;
|
||||||
|
public final String message; // ggf. modifizierte Nachricht
|
||||||
|
public final String denyReason; // Nachricht an den Spieler wenn BLOCKED
|
||||||
|
|
||||||
|
FilterResponse(FilterResult result, String message, String denyReason) {
|
||||||
|
this.result = result;
|
||||||
|
this.message = message;
|
||||||
|
this.denyReason = denyReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Haupt-Filtermethode =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wendet alle aktiven Filter auf eine Nachricht an.
|
||||||
|
*
|
||||||
|
* @param uuid UUID des sendenden Spielers
|
||||||
|
* @param message Originalnachricht
|
||||||
|
* @param isAdmin true → Farbcodes und Caps-Filter überspringen
|
||||||
|
* @param hasColorPerm true → &-Farbcodes erlaubt
|
||||||
|
* @param hasFormatPerm true → &l, &o etc. erlaubt
|
||||||
|
* @return FilterResponse mit Ergebnis und ggf. modifizierter Nachricht
|
||||||
|
*/
|
||||||
|
public FilterResponse filter(UUID uuid, String message, boolean isAdmin,
|
||||||
|
boolean hasColorPerm, boolean hasFormatPerm) {
|
||||||
|
|
||||||
|
// ── 1. Spam-Cooldown ──
|
||||||
|
if (cfg.antiSpamEnabled && !isAdmin) {
|
||||||
|
if (cfg.globalRateLimitEnabled) {
|
||||||
|
GlobalRateLimitFramework.Result rl = rateLimiter.check(
|
||||||
|
"chat.message",
|
||||||
|
uuid.toString(),
|
||||||
|
new GlobalRateLimitFramework.Rule(
|
||||||
|
true,
|
||||||
|
cfg.globalRateLimitWindowMs,
|
||||||
|
cfg.globalRateLimitMaxActions,
|
||||||
|
cfg.globalRateLimitBlockMs
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (rl.isBlocked()) {
|
||||||
|
return new FilterResponse(FilterResult.BLOCKED, message, cfg.spamMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Duplikat-Check ──
|
||||||
|
if (cfg.duplicateCheckEnabled && !isAdmin) {
|
||||||
|
String lastText = lastMessageText.get(uuid);
|
||||||
|
if (message.equalsIgnoreCase(lastText)) {
|
||||||
|
return new FilterResponse(FilterResult.BLOCKED, message, cfg.duplicateMessage);
|
||||||
|
}
|
||||||
|
lastMessageText.put(uuid, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
String result = message;
|
||||||
|
boolean modified = false;
|
||||||
|
|
||||||
|
// ── 3. Blacklist ──
|
||||||
|
if (cfg.blacklistEnabled) {
|
||||||
|
String filtered = applyBlacklist(result);
|
||||||
|
if (!filtered.equals(result)) {
|
||||||
|
result = filtered;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Caps-Filter ──
|
||||||
|
if (cfg.capsFilterEnabled && !isAdmin) {
|
||||||
|
String capped = applyCapsFilter(result);
|
||||||
|
if (!capped.equals(result)) {
|
||||||
|
result = capped;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5. Farbcodes filtern (nur wenn keine Permission) ──
|
||||||
|
if (!isAdmin) {
|
||||||
|
String colorFiltered = applyColorFilter(result, hasColorPerm, hasFormatPerm);
|
||||||
|
if (!colorFiltered.equals(result)) {
|
||||||
|
result = colorFiltered;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 6. Anti-Werbung ──
|
||||||
|
if (cfg.antiAdEnabled && !isAdmin) {
|
||||||
|
if (containsAdvertisement(result)) {
|
||||||
|
return new FilterResponse(FilterResult.BLOCKED, result, cfg.antiAdMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterResponse(
|
||||||
|
modified ? FilterResult.MODIFIED : FilterResult.ALLOWED,
|
||||||
|
result,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Einzelne Filter =====
|
||||||
|
|
||||||
|
private String applyBlacklist(String message) {
|
||||||
|
String result = message;
|
||||||
|
for (Pattern p : blacklistPatterns) {
|
||||||
|
result = p.matcher(result).replaceAll(buildStars(p.pattern()
|
||||||
|
.replace("(?i)", "").replace("\\Q", "").replace("\\E", "").length()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String applyCapsFilter(String message) {
|
||||||
|
// Zähle Großbuchstaben
|
||||||
|
int total = 0, upper = 0;
|
||||||
|
for (char c : message.toCharArray()) {
|
||||||
|
if (Character.isLetter(c)) { total++; if (Character.isUpperCase(c)) upper++; }
|
||||||
|
}
|
||||||
|
if (total < cfg.capsMinLength) return message; // Kurze Nachrichten ignorieren
|
||||||
|
double ratio = total > 0 ? (double) upper / total : 0;
|
||||||
|
if (ratio < cfg.capsMaxPercent / 100.0) return message;
|
||||||
|
// Zu viele Caps → alles lowercase
|
||||||
|
return message.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt &-Farbcodes je nach Permission.
|
||||||
|
* hasColorPerm → &0-&9, &a-&f erlaubt
|
||||||
|
* hasFormatPerm → &l, &o, &n, &m, &k erlaubt
|
||||||
|
* Beide false → alle &-Codes entfernen
|
||||||
|
*/
|
||||||
|
private String applyColorFilter(String message, boolean hasColorPerm, boolean hasFormatPerm) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < message.length(); i++) {
|
||||||
|
char c = message.charAt(i);
|
||||||
|
if (c == '&' && i + 1 < message.length()) {
|
||||||
|
char next = Character.toLowerCase(message.charAt(i + 1));
|
||||||
|
boolean isColor = (next >= '0' && next <= '9') || (next >= 'a' && next <= 'f');
|
||||||
|
boolean isFormat = "lonmkr".indexOf(next) >= 0;
|
||||||
|
boolean isHex = next == '#';
|
||||||
|
|
||||||
|
if (isColor && hasColorPerm) { sb.append(c); continue; }
|
||||||
|
if (isFormat && hasFormatPerm) { sb.append(c); continue; }
|
||||||
|
if (isHex && hasColorPerm) { sb.append(c); continue; }
|
||||||
|
|
||||||
|
// Kein Recht → & und nächstes Zeichen überspringen
|
||||||
|
if (isColor || isFormat) { i++; continue; }
|
||||||
|
// Hex: &# + 6 Zeichen überspringen (i zeigt auf &, +1 = #, +2..+7 = RRGGBB)
|
||||||
|
if (isHex && i + 7 <= message.length()) { i += 7; continue; }
|
||||||
|
}
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Anti-Werbung =====
|
||||||
|
|
||||||
|
// Vorkompilierte Patterns (einmalig beim Classload)
|
||||||
|
private static final Pattern PATTERN_IP =
|
||||||
|
Pattern.compile("\\b(\\d{1,3}[.,]){3}\\d{1,3}(:\\d{1,5})?\\b");
|
||||||
|
|
||||||
|
private static final Pattern PATTERN_DOMAIN_GENERIC =
|
||||||
|
Pattern.compile("(?i)\\b[a-z0-9-]{2,63}\\.[a-z]{2,10}(?:[/:\\d]\\S*)?\\b");
|
||||||
|
|
||||||
|
private static final Pattern PATTERN_URL_PREFIX =
|
||||||
|
Pattern.compile("(?i)(https?://|www\\.)\\S+");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob die Nachricht Werbung enthält (IP, URL, fremde Domain).
|
||||||
|
* Domains auf der Whitelist werden ignoriert.
|
||||||
|
*
|
||||||
|
* Erkennt:
|
||||||
|
* - http:// / https:// / www. Prefixe
|
||||||
|
* - IPv4-Adressen (auch mit Port)
|
||||||
|
* - Domain-Namen mit konfigurierten TLDs (z.B. .net, .de, .com)
|
||||||
|
* - Verschleierungsversuche mit Leerzeichen um Punkte ("play . server . net")
|
||||||
|
*/
|
||||||
|
private boolean containsAdvertisement(String message) {
|
||||||
|
// Normalisierung: "play . server . net" → "play.server.net"
|
||||||
|
String normalized = message.replaceAll("\\s*\\.\\s*", ".");
|
||||||
|
|
||||||
|
// 1. Explizite URL-Prefixe
|
||||||
|
if (PATTERN_URL_PREFIX.matcher(normalized).find()) {
|
||||||
|
return !allMatchesWhitelisted(normalized, PATTERN_URL_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. IP-Adressen (werden nie whitelisted)
|
||||||
|
if (PATTERN_IP.matcher(normalized).find()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Domains mit bekannten TLDs
|
||||||
|
if (!cfg.antiAdBlockedTlds.isEmpty()) {
|
||||||
|
java.util.regex.Matcher m = PATTERN_DOMAIN_GENERIC.matcher(normalized);
|
||||||
|
while (m.find()) {
|
||||||
|
String match = m.group();
|
||||||
|
String tld = extractTld(match);
|
||||||
|
if (cfg.antiAdBlockedTlds.contains(tld.toLowerCase())) {
|
||||||
|
if (!isOnWhitelist(match)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** true wenn ALLE Treffer des Patterns auf der Whitelist stehen. */
|
||||||
|
private boolean allMatchesWhitelisted(String message, Pattern pattern) {
|
||||||
|
java.util.regex.Matcher m = pattern.matcher(message);
|
||||||
|
while (m.find()) {
|
||||||
|
if (!isOnWhitelist(m.group())) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOnWhitelist(String match) {
|
||||||
|
String lower = match.toLowerCase();
|
||||||
|
for (String entry : cfg.antiAdWhitelist) {
|
||||||
|
if (lower.contains(entry.toLowerCase())) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractTld(String domain) {
|
||||||
|
String clean = domain.split("[/:]")[0];
|
||||||
|
int dot = clean.lastIndexOf('.');
|
||||||
|
return dot >= 0 ? clean.substring(dot + 1) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildStars(int length) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < Math.max(length, 4); i++) sb.append('*');
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Cleanup beim Logout =====
|
||||||
|
|
||||||
|
public void cleanup(UUID uuid) {
|
||||||
|
lastMessageText.remove(uuid);
|
||||||
|
rateLimiter.clearActor(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Konfigurationsklasse =====
|
||||||
|
|
||||||
|
public static class ChatFilterConfig {
|
||||||
|
// Anti-Spam
|
||||||
|
public boolean antiSpamEnabled = true;
|
||||||
|
public long spamCooldownMs = 1500; // Legacy-Feld fuer Kompatibilitaet
|
||||||
|
public int spamMaxMessages = 3; // Legacy-Feld fuer Kompatibilitaet
|
||||||
|
public String spamMessage = "&cBitte nicht so schnell schreiben!";
|
||||||
|
|
||||||
|
// Globales Rate-Limit-Framework
|
||||||
|
public boolean globalRateLimitEnabled = true;
|
||||||
|
public long globalRateLimitWindowMs = 2500;
|
||||||
|
public int globalRateLimitMaxActions = 3;
|
||||||
|
public long globalRateLimitBlockMs = 6000;
|
||||||
|
|
||||||
|
// Duplikat
|
||||||
|
public boolean duplicateCheckEnabled = true;
|
||||||
|
public String duplicateMessage = "&cBitte keine identischen Nachrichten senden.";
|
||||||
|
|
||||||
|
// Blacklist
|
||||||
|
public boolean blacklistEnabled = true;
|
||||||
|
public List<String> blacklistWords = new ArrayList<>();
|
||||||
|
|
||||||
|
// Caps
|
||||||
|
public boolean capsFilterEnabled = true;
|
||||||
|
public int capsMinLength = 6; // Mindestlänge für Caps-Check
|
||||||
|
public int capsMaxPercent = 70; // Max. % Großbuchstaben
|
||||||
|
|
||||||
|
// Anti-Werbung
|
||||||
|
public boolean antiAdEnabled = true;
|
||||||
|
public String antiAdMessage = "&cWerbung ist in diesem Chat nicht erlaubt!";
|
||||||
|
// Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
|
||||||
|
public List<String> antiAdWhitelist = new ArrayList<>();
|
||||||
|
// TLDs die als Werbung gewertet werden (leer = alle TLDs prüfen)
|
||||||
|
public List<String> antiAdBlockedTlds = new ArrayList<>(Arrays.asList(
|
||||||
|
"net", "com", "de", "org", "gg", "io", "eu", "tv", "xyz",
|
||||||
|
"info", "me", "cc", "co", "app", "online", "site", "fun"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protokolliert alle Chat-Nachrichten in tagesweise rotierende Logdateien.
|
||||||
|
*
|
||||||
|
* Verzeichnis: plugins/StatusAPI/chatlogs/chatlog_YYYY-MM-DD.log
|
||||||
|
* Format: [HH:mm:ss] [MSG-XXXXXX] [SERVER] [CHANNEL] Spieler: Nachricht
|
||||||
|
*
|
||||||
|
* Alte Logs werden beim Start und täglich automatisch bereinigt.
|
||||||
|
* Die Aufbewahrungsdauer ist in der chat.yml konfigurierbar (7 oder 14 Tage).
|
||||||
|
*/
|
||||||
|
public class ChatLogger {
|
||||||
|
|
||||||
|
private final File logDir;
|
||||||
|
private final Logger logger;
|
||||||
|
private final int retentionDays;
|
||||||
|
private final AtomicInteger counter = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
|
private static final SimpleDateFormat TIME_FMT = new SimpleDateFormat("HH:mm:ss");
|
||||||
|
|
||||||
|
public ChatLogger(File dataFolder, Logger logger, int retentionDays) {
|
||||||
|
this.logDir = new File(dataFolder, "chatlogs");
|
||||||
|
this.logger = logger;
|
||||||
|
this.retentionDays = Math.max(1, retentionDays);
|
||||||
|
this.logDir.mkdirs();
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Nachrichten-ID =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert eine eindeutige Nachrichten-ID (z.B. MSG-A3F2B1).
|
||||||
|
* Kombiniert Zeitstempel + inkrementellen Zähler für Eindeutigkeit.
|
||||||
|
*/
|
||||||
|
public String generateMessageId() {
|
||||||
|
int seq = counter.incrementAndGet();
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
int hash = (int)(ts ^ (ts >>> 32)) ^ (seq * 0x9E3779B9);
|
||||||
|
return "MSG-" + String.format("%06X", hash & 0xFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Logging =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loggt eine Nachricht und gibt die generierte Nachrichten-ID zurück.
|
||||||
|
*
|
||||||
|
* @param msgId Vorher generierte ID (aus generateMessageId())
|
||||||
|
* @param server Servername des Absenders
|
||||||
|
* @param channel Kanal-ID
|
||||||
|
* @param player Spielername
|
||||||
|
* @param message Nachrichtentext (Rohtext, ohne Farbcodes)
|
||||||
|
*/
|
||||||
|
public void log(String msgId, String server, String channel, String player, String message) {
|
||||||
|
String date = DATE_FMT.format(new Date());
|
||||||
|
String time = TIME_FMT.format(new Date());
|
||||||
|
|
||||||
|
// Minecraft-Farbcodes aus dem Log entfernen
|
||||||
|
String cleanMsg = stripColor(message);
|
||||||
|
|
||||||
|
String line = "[" + time + "] [" + msgId + "] [" + server + "] [" + channel + "] "
|
||||||
|
+ player + ": " + cleanMsg;
|
||||||
|
|
||||||
|
File logFile = new File(logDir, "chatlog_" + date + ".log");
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(logFile, true), "UTF-8"))) {
|
||||||
|
bw.write(line);
|
||||||
|
bw.newLine();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatLogger] Fehler beim Schreiben: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Cleanup =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löscht Log-Dateien, die älter als retentionDays Tage sind.
|
||||||
|
* Wird beim Start und kann manuell aufgerufen werden.
|
||||||
|
*/
|
||||||
|
public void cleanup() {
|
||||||
|
if (!logDir.exists()) return;
|
||||||
|
long cutoff = System.currentTimeMillis() - ((long) retentionDays * 24L * 60L * 60L * 1000L);
|
||||||
|
File[] files = logDir.listFiles((dir, name) ->
|
||||||
|
name.startsWith("chatlog_") && name.endsWith(".log"));
|
||||||
|
if (files == null) return;
|
||||||
|
for (File f : files) {
|
||||||
|
if (f.lastModified() < cutoff) {
|
||||||
|
if (f.delete()) {
|
||||||
|
logger.info("[ChatLogger] Altes Log gelöscht: " + f.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Hilfsmethoden =====
|
||||||
|
|
||||||
|
/** Entfernt §-Farbcodes aus dem Text. */
|
||||||
|
private static String stripColor(String input) {
|
||||||
|
if (input == null) return "";
|
||||||
|
return input.replaceAll("(?i)§[0-9A-FK-OR]", "")
|
||||||
|
.replaceAll("(?i)&[0-9A-FK-OR]", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRetentionDays() { return retentionDays; }
|
||||||
|
public File getLogDir() { return logDir; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest die letzten `maxLines` Zeilen aus dem heutigen Chatlog.
|
||||||
|
* Wenn ein Spielername angegeben ist, werden nur seine Zeilen zurückgegeben.
|
||||||
|
*
|
||||||
|
* @param playerFilter Spielername (case-insensitiv) oder null für alle
|
||||||
|
* @param maxLines Maximale Anzahl zurückgegebener Zeilen
|
||||||
|
* @return Liste der Logzeilen (älteste zuerst)
|
||||||
|
*/
|
||||||
|
public List<String> readLastLines(String playerFilter, int maxLines) {
|
||||||
|
String date = DATE_FMT.format(new Date());
|
||||||
|
File logFile = new File(logDir, "chatlog_" + date + ".log");
|
||||||
|
if (!logFile.exists()) return Collections.emptyList();
|
||||||
|
|
||||||
|
List<String> allLines = new ArrayList<>();
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(new FileInputStream(logFile), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (line.trim().isEmpty()) continue;
|
||||||
|
// Spieler-Filter: Format ist [...] [...] [...] [...] Spieler: Nachricht
|
||||||
|
if (playerFilter != null) {
|
||||||
|
// Spielername steht nach dem 4. [...]-Block
|
||||||
|
int lastBracket = line.indexOf("] ", line.lastIndexOf("["));
|
||||||
|
if (lastBracket >= 0) {
|
||||||
|
String rest = line.substring(lastBracket + 2);
|
||||||
|
String name = rest.contains(":") ? rest.substring(0, rest.indexOf(":")).trim() : "";
|
||||||
|
if (!name.equalsIgnoreCase(playerFilter)) continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allLines.add(line);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatLogger] Fehler beim Lesen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Letzte maxLines zurückgeben
|
||||||
|
if (allLines.size() <= maxLines) return allLines;
|
||||||
|
return allLines.subList(allLines.size() - maxLines, allLines.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ersetzt Emoji-Shortcuts (:smile:, :heart:, …) durch Unicode-Zeichen.
|
||||||
|
*
|
||||||
|
* Bedrock-Spieler (Geyser) unterstützen Unicode-Emojis ebenfalls,
|
||||||
|
* da sie als reguläre UTF-8 Zeichen in TextComponents übertragen werden.
|
||||||
|
*/
|
||||||
|
public class EmojiParser {
|
||||||
|
|
||||||
|
private final Map<String, String> mappings;
|
||||||
|
private final boolean enabled;
|
||||||
|
|
||||||
|
public EmojiParser(Map<String, String> mappings, boolean enabled) {
|
||||||
|
this.mappings = mappings;
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert alle bekannten Emoji-Shortcuts in der Nachricht zu Unicode.
|
||||||
|
* Nicht erkannte Shortcuts bleiben unverändert.
|
||||||
|
*
|
||||||
|
* @param message Die Originalnachricht des Spielers
|
||||||
|
* @return Nachricht mit ersetzten Emojis
|
||||||
|
*/
|
||||||
|
public String parse(String message) {
|
||||||
|
if (!enabled || message == null || message.isEmpty()) return message;
|
||||||
|
|
||||||
|
String result = message;
|
||||||
|
for (Map.Entry<String, String> entry : mappings.entrySet()) {
|
||||||
|
result = result.replace(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt eine lesbare Liste aller Emojis zurück (für /emoji list).
|
||||||
|
*/
|
||||||
|
public String buildEmojiList() {
|
||||||
|
if (mappings.isEmpty()) return "&cKeine Emojis konfiguriert.";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("&eVerfügbare Emojis:\n");
|
||||||
|
int i = 0;
|
||||||
|
for (Map.Entry<String, String> entry : mappings.entrySet()) {
|
||||||
|
sb.append("&7").append(entry.getKey()).append(" &f→ ").append(entry.getValue());
|
||||||
|
if (i < mappings.size() - 1) sb.append(" ");
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet Mutes von Spielern.
|
||||||
|
* Speichert: UUID → Ablaufzeitpunkt (Unix-Sekunden, 0 = permanent)
|
||||||
|
*
|
||||||
|
* Admins/OPs mit dem Bypass-Permission können nicht gemutet werden.
|
||||||
|
*/
|
||||||
|
public class MuteManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
// UUID → Ablaufzeitpunkt (0 = permanent)
|
||||||
|
private final ConcurrentHashMap<UUID, Long> mutes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public MuteManager(File dataFolder, Logger logger) {
|
||||||
|
this.file = new File(dataFolder, "chat_mutes.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Mute-Logik =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutet einen Spieler für durationMinutes Minuten.
|
||||||
|
* durationMinutes = 0 → permanent
|
||||||
|
*/
|
||||||
|
public void mute(UUID uuid, int durationMinutes) {
|
||||||
|
long expiry = (durationMinutes <= 0)
|
||||||
|
? 0L
|
||||||
|
: (System.currentTimeMillis() / 1000L) + ((long) durationMinutes * 60);
|
||||||
|
mutes.put(uuid, expiry);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hebt den Mute auf. */
|
||||||
|
public void unmute(UUID uuid) {
|
||||||
|
mutes.remove(uuid);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prüft ob ein Spieler aktuell gemutet ist. */
|
||||||
|
public boolean isMuted(UUID uuid) {
|
||||||
|
Long expiry = mutes.get(uuid);
|
||||||
|
if (expiry == null) return false;
|
||||||
|
if (expiry == 0L) return true; // permanent
|
||||||
|
if (System.currentTimeMillis() / 1000L >= expiry) {
|
||||||
|
// Abgelaufen → entfernen
|
||||||
|
mutes.remove(uuid);
|
||||||
|
save();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die verbleibende Zeit als lesbaren String zurück.
|
||||||
|
* Gibt "permanent" zurück bei dauerhaftem Mute.
|
||||||
|
*/
|
||||||
|
public String getRemainingTime(UUID uuid) {
|
||||||
|
Long expiry = mutes.get(uuid);
|
||||||
|
if (expiry == null) return "0";
|
||||||
|
if (expiry == 0L) return "permanent";
|
||||||
|
|
||||||
|
long remaining = expiry - (System.currentTimeMillis() / 1000L);
|
||||||
|
if (remaining <= 0) return "0";
|
||||||
|
|
||||||
|
long hours = remaining / 3600;
|
||||||
|
long minutes = (remaining % 3600) / 60;
|
||||||
|
long seconds = remaining % 60;
|
||||||
|
|
||||||
|
if (hours > 0) return hours + "h " + minutes + "m";
|
||||||
|
if (minutes > 0) return minutes + "m " + seconds + "s";
|
||||||
|
return seconds + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Persistenz =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
for (Map.Entry<UUID, Long> e : mutes.entrySet()) {
|
||||||
|
// Nur aktive Mutes speichern
|
||||||
|
if (e.getValue() == 0L || e.getValue() > now) {
|
||||||
|
bw.write(e.getKey() + "|" + e.getValue());
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Speichern der Mutes: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
mutes.clear();
|
||||||
|
if (!file.exists()) return;
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
if (parts.length < 2) continue;
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(parts[0]);
|
||||||
|
long expiry = Long.parseLong(parts[1]);
|
||||||
|
// Nur laden wenn noch aktiv
|
||||||
|
if (expiry == 0L || expiry > now) {
|
||||||
|
mutes.put(uuid, expiry);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Laden der Mutes: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.viper.status.ratelimit.GlobalRateLimitFramework;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet private Nachrichten (/msg, /r) und Social-Spy.
|
||||||
|
*/
|
||||||
|
public class PrivateMsgManager {
|
||||||
|
|
||||||
|
private final BlockManager blockManager;
|
||||||
|
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
||||||
|
|
||||||
|
// UUID → letzte PM-Gesprächspartner UUID (für /r)
|
||||||
|
private final Map<UUID, UUID> lastPartner = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// UUIDs die Social-Spy aktiviert haben
|
||||||
|
private final java.util.Set<UUID> spyEnabled =
|
||||||
|
java.util.Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
|
public PrivateMsgManager(BlockManager blockManager) {
|
||||||
|
this.blockManager = blockManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine private Nachricht von `sender` an `receiver`.
|
||||||
|
*
|
||||||
|
* @param sender Der sendende Spieler
|
||||||
|
* @param receiver Der empfangende Spieler
|
||||||
|
* @param message Die Nachricht
|
||||||
|
* @param config Chat-Konfiguration (Formate)
|
||||||
|
* @param bypassPermission Permission für Admin-Bypass (kann nicht geblockt werden)
|
||||||
|
* @return true wenn erfolgreich gesendet
|
||||||
|
*/
|
||||||
|
public boolean send(ProxiedPlayer sender, ProxiedPlayer receiver,
|
||||||
|
String message, ChatConfig config, String bypassPermission) {
|
||||||
|
|
||||||
|
// Selbst anschreiben verhindern
|
||||||
|
if (sender.getUniqueId().equals(receiver.getUniqueId())) {
|
||||||
|
sender.sendMessage(color("&cDu kannst dir nicht selbst schreiben."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin-Bypass: Wenn Sender Admin ist, kann er nicht geblockt werden
|
||||||
|
boolean senderIsAdmin = sender.hasPermission(bypassPermission);
|
||||||
|
|
||||||
|
// Block-Check (nur wenn Sender kein Admin)
|
||||||
|
if (!senderIsAdmin) {
|
||||||
|
if (!blockManager.canReceive(sender.getUniqueId(), receiver.getUniqueId())) {
|
||||||
|
sender.sendMessage(color("&cDieser Spieler hat dich blockiert oder du hast ihn blockiert."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isPmRateLimitEnabled()) {
|
||||||
|
GlobalRateLimitFramework.Result result = rateLimiter.check(
|
||||||
|
"chat.pm",
|
||||||
|
sender.getUniqueId().toString(),
|
||||||
|
new GlobalRateLimitFramework.Rule(
|
||||||
|
true,
|
||||||
|
config.getPmRateLimitWindowMs(),
|
||||||
|
config.getPmRateLimitMaxActions(),
|
||||||
|
config.getPmRateLimitBlockMs()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isBlocked()) {
|
||||||
|
sender.sendMessage(color(config.getPmRateLimitMessage()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatierung
|
||||||
|
String toSender = format(config.getPmFormatSender(), sender.getName(), receiver.getName(), message, true);
|
||||||
|
String toReceiver = format(config.getPmFormatReceiver(), sender.getName(), receiver.getName(), message, false);
|
||||||
|
|
||||||
|
sender.sendMessage(color(toSender));
|
||||||
|
receiver.sendMessage(color(toReceiver));
|
||||||
|
|
||||||
|
// Letzte Partner speichern (für /r)
|
||||||
|
lastPartner.put(sender.getUniqueId(), receiver.getUniqueId());
|
||||||
|
lastPartner.put(receiver.getUniqueId(), sender.getUniqueId());
|
||||||
|
|
||||||
|
// Social Spy
|
||||||
|
String spyMsg = format(config.getPmFormatSpy(), sender.getName(), receiver.getName(), message, true);
|
||||||
|
broadcastSpy(spyMsg, config.getPmSpyPermission(), sender.getUniqueId(), receiver.getUniqueId());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Antwort-Funktion (/r).
|
||||||
|
* Sucht den letzten Gesprächspartner des Senders.
|
||||||
|
*/
|
||||||
|
public void reply(ProxiedPlayer sender, String message, ChatConfig config, String bypassPermission) {
|
||||||
|
UUID partnerUuid = lastPartner.get(sender.getUniqueId());
|
||||||
|
if (partnerUuid == null) {
|
||||||
|
sender.sendMessage(color("&cDu hast noch keine Nachricht erhalten."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProxiedPlayer partner = ProxyServer.getInstance().getPlayer(partnerUuid);
|
||||||
|
if (partner == null || !partner.isConnected()) {
|
||||||
|
sender.sendMessage(color("&cDieser Spieler ist nicht mehr online."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(sender, partner, message, config, bypassPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Social-Spy umschalten. */
|
||||||
|
public boolean toggleSpy(UUID uuid) {
|
||||||
|
if (spyEnabled.contains(uuid)) {
|
||||||
|
spyEnabled.remove(uuid);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
spyEnabled.add(uuid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastSpy(String formatted, String spyPermission, UUID... exclude) {
|
||||||
|
java.util.Set<UUID> excl = new java.util.HashSet<>(java.util.Arrays.asList(exclude));
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
if (excl.contains(p.getUniqueId())) continue;
|
||||||
|
// Spy muss entweder via Permission aktiv oder manuell aktiviert haben
|
||||||
|
boolean hasPerm = p.hasPermission(spyPermission);
|
||||||
|
boolean hasToggle= spyEnabled.contains(p.getUniqueId());
|
||||||
|
if (hasPerm || hasToggle) {
|
||||||
|
p.sendMessage(color(formatted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert eine PM-Nachricht.
|
||||||
|
* {sender} → Name des Absenders
|
||||||
|
* {receiver} → Name des Empfängers
|
||||||
|
* {player} → Gesprächspartner aus Sicht des jeweiligen Empfängers:
|
||||||
|
* Beim Sender: der Empfänger (an wen schreibt er?)
|
||||||
|
* Beim Empfänger: der Sender (von wem kommt es?)
|
||||||
|
*
|
||||||
|
* @param viewerIsSender true wenn der aktuelle Betrachter der Absender ist
|
||||||
|
*/
|
||||||
|
private String format(String template, String sender, String receiver,
|
||||||
|
String message, boolean viewerIsSender) {
|
||||||
|
String partner = viewerIsSender ? receiver : sender;
|
||||||
|
return template
|
||||||
|
.replace("{sender}", sender)
|
||||||
|
.replace("{receiver}", receiver)
|
||||||
|
.replace("{player}", partner) // Gesprächspartner aus Sicht des Betrachters
|
||||||
|
.replace("{message}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextComponent color(String text) {
|
||||||
|
return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet Spieler-Reports (/report).
|
||||||
|
*
|
||||||
|
* Reports werden mit einer eindeutigen ID (z.B. RPT-0001) gespeichert und
|
||||||
|
* bleiben offen, bis ein Admin sie explizit mit /reportclose <ID> schließt.
|
||||||
|
*
|
||||||
|
* Online-Admins werden sofort benachrichtigt.
|
||||||
|
* Offline-Admins erhalten eine verzögerte Benachrichtigung beim nächsten Login
|
||||||
|
* (gesteuert von außen via getPendingNotificationFor()).
|
||||||
|
*
|
||||||
|
* Speicherformat (chat_reports.dat):
|
||||||
|
* id|reporter|reporterUUID|reported|server|messageContext|reason|timestamp|closed|closedBy
|
||||||
|
*/
|
||||||
|
public class ReportManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
/** Alle Reports (offen und geschlossen). */
|
||||||
|
private final ConcurrentHashMap<String, ChatReport> reports = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** Zähler für Report-IDs. Wird beim Laden synchronisiert. */
|
||||||
|
private final AtomicInteger idCounter = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
|
||||||
|
|
||||||
|
// ===== Report-Datenklasse =====
|
||||||
|
|
||||||
|
public static class ChatReport {
|
||||||
|
public String id;
|
||||||
|
public String reporterName;
|
||||||
|
public UUID reporterUUID;
|
||||||
|
public String reportedName;
|
||||||
|
public String server;
|
||||||
|
public String messageContext; // letzte bekannte Chatnachricht des Gemeldeten
|
||||||
|
public String reason;
|
||||||
|
public long timestamp;
|
||||||
|
public boolean closed;
|
||||||
|
public String closedBy; // Name des schließenden Admins (oder leer)
|
||||||
|
|
||||||
|
public String getFormattedTime() {
|
||||||
|
return DATE_FMT.format(new Date(timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Konstruktor =====
|
||||||
|
|
||||||
|
public ReportManager(File dataFolder, Logger logger) {
|
||||||
|
this.file = new File(dataFolder, "chat_reports.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Report-Logik =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen neuen Report.
|
||||||
|
*
|
||||||
|
* @param reporterName Name des meldenden Spielers
|
||||||
|
* @param reporterUUID UUID des meldenden Spielers
|
||||||
|
* @param reportedName Name des gemeldeten Spielers
|
||||||
|
* @param server Server, auf dem sich der Reporter befand
|
||||||
|
* @param messageContext Letzte bekannte Nachricht des Gemeldeten (für Kontext)
|
||||||
|
* @param reason Freitext-Begründung
|
||||||
|
* @return die neue Report-ID (z.B. RPT-0001)
|
||||||
|
*/
|
||||||
|
public String createReport(String reporterName, UUID reporterUUID,
|
||||||
|
String reportedName, String server,
|
||||||
|
String messageContext, String reason) {
|
||||||
|
String id = String.format("RPT-%04d", idCounter.incrementAndGet());
|
||||||
|
|
||||||
|
ChatReport report = new ChatReport();
|
||||||
|
report.id = id;
|
||||||
|
report.reporterName = reporterName;
|
||||||
|
report.reporterUUID = reporterUUID;
|
||||||
|
report.reportedName = reportedName;
|
||||||
|
report.server = server;
|
||||||
|
report.messageContext = messageContext != null ? messageContext : "";
|
||||||
|
report.reason = reason;
|
||||||
|
report.timestamp = System.currentTimeMillis();
|
||||||
|
report.closed = false;
|
||||||
|
report.closedBy = "";
|
||||||
|
|
||||||
|
reports.put(id, report);
|
||||||
|
save();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schließt einen Report.
|
||||||
|
*
|
||||||
|
* @param id Report-ID (z.B. RPT-0001, case-insensitiv)
|
||||||
|
* @param adminName Name des Admins, der den Report schließt
|
||||||
|
* @return true wenn erfolgreich geschlossen, false wenn nicht gefunden / bereits geschlossen
|
||||||
|
*/
|
||||||
|
public boolean closeReport(String id, String adminName) {
|
||||||
|
ChatReport report = getReport(id);
|
||||||
|
if (report == null || report.closed) return false;
|
||||||
|
report.closed = true;
|
||||||
|
report.closedBy = adminName;
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt einen Report nach ID zurück (case-insensitiv). */
|
||||||
|
public ChatReport getReport(String id) {
|
||||||
|
if (id == null) return null;
|
||||||
|
return reports.get(id.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle offenen Reports chronologisch (älteste zuerst) zurück. */
|
||||||
|
public List<ChatReport> getOpenReports() {
|
||||||
|
List<ChatReport> list = new ArrayList<>();
|
||||||
|
for (ChatReport r : reports.values()) {
|
||||||
|
if (!r.closed) list.add(r);
|
||||||
|
}
|
||||||
|
list.sort(Comparator.comparingLong(r -> r.timestamp));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle Reports chronologisch zurück (auch geschlossene). */
|
||||||
|
public List<ChatReport> getAllReports() {
|
||||||
|
List<ChatReport> list = new ArrayList<>(reports.values());
|
||||||
|
list.sort(Comparator.comparingLong(r -> r.timestamp));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anzahl offener Reports. */
|
||||||
|
public int getOpenCount() {
|
||||||
|
int count = 0;
|
||||||
|
for (ChatReport r : reports.values()) if (!r.closed) count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Persistenz =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
for (ChatReport r : reports.values()) {
|
||||||
|
bw.write(
|
||||||
|
esc(r.id) + "|" +
|
||||||
|
esc(r.reporterName) + "|" +
|
||||||
|
r.reporterUUID + "|" +
|
||||||
|
esc(r.reportedName) + "|" +
|
||||||
|
esc(r.server) + "|" +
|
||||||
|
esc(r.messageContext) + "|" +
|
||||||
|
esc(r.reason) + "|" +
|
||||||
|
r.timestamp + "|" +
|
||||||
|
r.closed + "|" +
|
||||||
|
esc(r.closedBy != null ? r.closedBy : "")
|
||||||
|
);
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Speichern der Reports: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
reports.clear();
|
||||||
|
if (!file.exists()) return;
|
||||||
|
|
||||||
|
int maxNum = 0;
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String[] p = line.split("\\|", -1);
|
||||||
|
if (p.length < 10) continue;
|
||||||
|
try {
|
||||||
|
ChatReport r = new ChatReport();
|
||||||
|
r.id = unesc(p[0]);
|
||||||
|
r.reporterName = unesc(p[1]);
|
||||||
|
r.reporterUUID = UUID.fromString(p[2]);
|
||||||
|
r.reportedName = unesc(p[3]);
|
||||||
|
r.server = unesc(p[4]);
|
||||||
|
r.messageContext = unesc(p[5]);
|
||||||
|
r.reason = unesc(p[6]);
|
||||||
|
r.timestamp = Long.parseLong(p[7]);
|
||||||
|
r.closed = Boolean.parseBoolean(p[8]);
|
||||||
|
r.closedBy = unesc(p[9]);
|
||||||
|
reports.put(r.id.toUpperCase(), r);
|
||||||
|
|
||||||
|
// Zähler auf höchste bekannte Nummer synchronisieren
|
||||||
|
if (r.id.toUpperCase().startsWith("RPT-")) {
|
||||||
|
try {
|
||||||
|
int num = Integer.parseInt(r.id.substring(4));
|
||||||
|
if (num > maxNum) maxNum = num;
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Laden der Reports: " + e.getMessage());
|
||||||
|
}
|
||||||
|
idCounter.set(maxNum);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbrüche escapen) =====
|
||||||
|
|
||||||
|
private static String esc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\")
|
||||||
|
.replace("|", "\\p")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String unesc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\n", "\n")
|
||||||
|
.replace("\\p", "|")
|
||||||
|
.replace("\\\\", "\\");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zentrale Schnittstelle zwischen dem VanishModule und dem ChatModule.
|
||||||
|
*
|
||||||
|
* Das VanishModule (oder jedes andere Modul) ruft {@link #setVanished} auf
|
||||||
|
* um Spieler als unsichtbar zu markieren. Das ChatModule prüft via
|
||||||
|
* {@link #isVanished} bevor es Join-/Leave-Nachrichten sendet oder
|
||||||
|
* Privat-Nachrichten zulässt.
|
||||||
|
*
|
||||||
|
* Verwendung im VanishModule:
|
||||||
|
* VanishProvider.setVanished(player.getUniqueId(), true); // beim Verschwinden
|
||||||
|
* VanishProvider.setVanished(player.getUniqueId(), false); // beim Erscheinen / Disconnect
|
||||||
|
*/
|
||||||
|
public final class VanishProvider {
|
||||||
|
|
||||||
|
private VanishProvider() {}
|
||||||
|
|
||||||
|
/** Intern verwaltete Menge aller aktuell unsichtbaren Spieler-UUIDs. */
|
||||||
|
private static final Set<UUID> vanishedPlayers =
|
||||||
|
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
|
// ===== Schreib-API (wird vom VanishModule aufgerufen) =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert einen Spieler als sichtbar oder unsichtbar.
|
||||||
|
*
|
||||||
|
* @param uuid UUID des Spielers
|
||||||
|
* @param vanished true = unsichtbar, false = sichtbar
|
||||||
|
*/
|
||||||
|
public static void setVanished(UUID uuid, boolean vanished) {
|
||||||
|
if (vanished) {
|
||||||
|
vanishedPlayers.add(uuid);
|
||||||
|
} else {
|
||||||
|
vanishedPlayers.remove(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt einen Spieler beim Disconnect aus der Vanish-Liste.
|
||||||
|
* Sollte vom ChatModule (onDisconnect) aufgerufen werden, damit
|
||||||
|
* kein toter Eintrag verbleibt.
|
||||||
|
*/
|
||||||
|
public static void cleanup(UUID uuid) {
|
||||||
|
vanishedPlayers.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Lese-API (wird vom ChatModule aufgerufen) =====
|
||||||
|
|
||||||
|
/** @return true wenn der Spieler aktuell als unsichtbar markiert ist. */
|
||||||
|
public static boolean isVanished(ProxiedPlayer player) {
|
||||||
|
return player != null && vanishedPlayers.contains(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true wenn der Spieler mit der angegebenen UUID unsichtbar ist. */
|
||||||
|
public static boolean isVanished(UUID uuid) {
|
||||||
|
return uuid != null && vanishedPlayers.contains(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Snapshot der aktuell unsichtbaren Spieler (für Debugging / Logs). */
|
||||||
|
public static Set<UUID> getVanishedPlayers() {
|
||||||
|
return Collections.unmodifiableSet(vanishedPlayers);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package net.viper.status.modules.chat.bridge;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.viper.status.modules.chat.AccountLinkManager;
|
||||||
|
import net.viper.status.modules.chat.ChatConfig;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord-Brücke für bidirektionale Kommunikation.
|
||||||
|
*
|
||||||
|
* Fix #12: extractJsonString() behandelt Escape-Sequenzen jetzt korrekt.
|
||||||
|
* Statt Zeichenvergleich mit dem Vorgänger-Char wird ein expliziter Escape-Flag verwendet.
|
||||||
|
*/
|
||||||
|
public class DiscordBridge {
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final ChatConfig config;
|
||||||
|
private final Logger logger;
|
||||||
|
private AccountLinkManager linkManager;
|
||||||
|
|
||||||
|
private final java.util.Map<String, AtomicLong> lastMessageIds = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
private volatile boolean running = false;
|
||||||
|
|
||||||
|
public DiscordBridge(Plugin plugin, ChatConfig config) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.config = config;
|
||||||
|
this.logger = plugin.getLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLinkManager(AccountLinkManager linkManager) { this.linkManager = linkManager; }
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (!config.isDiscordEnabled()
|
||||||
|
|| config.getDiscordBotToken().isEmpty()
|
||||||
|
|| config.getDiscordBotToken().equals("YOUR_BOT_TOKEN_HERE")) {
|
||||||
|
logger.warning("[ChatModule-Discord] Bot-Token nicht konfiguriert. Discord-Empfang deaktiviert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
running = true;
|
||||||
|
int interval = Math.max(2, config.getDiscordPollInterval());
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, interval, interval, TimeUnit.SECONDS);
|
||||||
|
logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s).");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() { running = false; }
|
||||||
|
|
||||||
|
// ===== Minecraft → Discord =====
|
||||||
|
|
||||||
|
public void sendToDiscord(String webhookUrl, String username, String message, String avatarUrl) {
|
||||||
|
if (webhookUrl == null || webhookUrl.isEmpty()) return;
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
|
try {
|
||||||
|
String payload = "{\"username\":\"" + escapeJson(username) + "\""
|
||||||
|
+ (avatarUrl != null && !avatarUrl.isEmpty() ? ",\"avatar_url\":\"" + avatarUrl + "\"" : "")
|
||||||
|
+ ",\"content\":\"" + escapeJson(message) + "\"}";
|
||||||
|
postJson(webhookUrl, payload, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warning("[ChatModule-Discord] Webhook-Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendEmbedToDiscord(String webhookUrl, String title, String description, String colorHex) {
|
||||||
|
if (webhookUrl == null || webhookUrl.isEmpty()) return;
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
|
try {
|
||||||
|
int color = 0x5865F2;
|
||||||
|
try { color = Integer.parseInt(colorHex.replace("#", ""), 16); } catch (Exception ignored) {}
|
||||||
|
String payload = "{\"embeds\":[{\"title\":\"" + escapeJson(title) + "\""
|
||||||
|
+ ",\"description\":\"" + escapeJson(description) + "\""
|
||||||
|
+ ",\"color\":" + color + "}]}";
|
||||||
|
postJson(webhookUrl, payload, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warning("[ChatModule-Discord] Embed-Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendToChannel(String channelId, String message) {
|
||||||
|
if (channelId == null || channelId.isEmpty()) return;
|
||||||
|
if (config.getDiscordBotToken().isEmpty()) return;
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
|
try {
|
||||||
|
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages";
|
||||||
|
postJson(url, "{\"content\":\"" + escapeJson(message) + "\"}", "Bot " + config.getDiscordBotToken());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warning("[ChatModule-Discord] Send-to-Channel-Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Discord → Minecraft (Polling) =====
|
||||||
|
|
||||||
|
private void pollAllChannels() {
|
||||||
|
if (!running) return;
|
||||||
|
java.util.Set<String> channelIds = new java.util.LinkedHashSet<>();
|
||||||
|
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
||||||
|
if (!ch.getDiscordChannelId().isEmpty()) channelIds.add(ch.getDiscordChannelId());
|
||||||
|
}
|
||||||
|
if (!config.getDiscordAdminChannelId().isEmpty()) channelIds.add(config.getDiscordAdminChannelId());
|
||||||
|
for (String channelId : channelIds) pollChannel(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pollChannel(String channelId) {
|
||||||
|
try {
|
||||||
|
AtomicLong lastId = lastMessageIds.computeIfAbsent(channelId, k -> new AtomicLong(0L));
|
||||||
|
if (lastId.get() == 0L) {
|
||||||
|
String initResp = getJson("https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1",
|
||||||
|
"Bot " + config.getDiscordBotToken());
|
||||||
|
if (initResp != null && !initResp.equals("[]") && !initResp.isEmpty()) {
|
||||||
|
java.util.List<DiscordMessage> initMsgs = parseMessages(initResp);
|
||||||
|
if (!initMsgs.isEmpty()) lastId.set(initMsgs.get(0).id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages?after=" + lastId.get() + "&limit=10";
|
||||||
|
String response = getJson(url, "Bot " + config.getDiscordBotToken());
|
||||||
|
if (response == null || response.equals("[]") || response.isEmpty()) return;
|
||||||
|
|
||||||
|
java.util.List<DiscordMessage> messages = parseMessages(response);
|
||||||
|
messages.sort(java.util.Comparator.comparingLong(m -> m.id));
|
||||||
|
|
||||||
|
for (DiscordMessage msg : messages) {
|
||||||
|
if (msg.id <= lastId.get()) continue;
|
||||||
|
if (msg.isBot) continue;
|
||||||
|
if (msg.content.isEmpty()) continue;
|
||||||
|
lastId.set(msg.id);
|
||||||
|
|
||||||
|
if (msg.content.startsWith("!link ")) {
|
||||||
|
String token = msg.content.substring(6).trim().toUpperCase();
|
||||||
|
if (linkManager != null) {
|
||||||
|
AccountLinkManager.LinkedAccount acc = linkManager.redeemDiscord(token, msg.authorId, msg.authorName);
|
||||||
|
if (acc != null) sendToChannel(channelId, "✅ Verknüpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
|
||||||
|
else sendToChannel(channelId, "❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen.");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String displayName = (linkManager != null)
|
||||||
|
? linkManager.resolveDiscordName(msg.authorId, msg.authorName) : msg.authorName;
|
||||||
|
String mcFormat = resolveFormat(channelId);
|
||||||
|
if (mcFormat == null) continue;
|
||||||
|
|
||||||
|
String formatted = ChatColor.translateAlternateColorCodes('&',
|
||||||
|
mcFormat.replace("{user}", displayName).replace("{message}", msg.content));
|
||||||
|
ProxyServer.getInstance().getScheduler().runAsync(plugin,
|
||||||
|
() -> ProxyServer.getInstance().broadcast(new TextComponent(formatted)));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveFormat(String channelId) {
|
||||||
|
if (channelId.equals(config.getDiscordAdminChannelId())) return config.getDiscordFromFormat();
|
||||||
|
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
||||||
|
if (channelId.equals(ch.getDiscordChannelId())) return config.getDiscordFromFormat();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== HTTP =====
|
||||||
|
|
||||||
|
private void postJson(String urlStr, String payload, String authorization) throws Exception {
|
||||||
|
HttpURLConnection conn = openConnection(urlStr, "POST", authorization);
|
||||||
|
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
|
||||||
|
conn.setRequestProperty("Content-Length", String.valueOf(data.length));
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
try (OutputStream os = conn.getOutputStream()) { os.write(data); }
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code >= 400) logger.warning("[ChatModule-Discord] HTTP " + code + ": " + readStream(conn.getErrorStream()));
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getJson(String urlStr, String authorization) throws Exception {
|
||||||
|
HttpURLConnection conn = openConnection(urlStr, "GET", authorization);
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code != 200) { conn.disconnect(); return null; }
|
||||||
|
String result = readStream(conn.getInputStream());
|
||||||
|
conn.disconnect();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpURLConnection openConnection(String urlStr, String method, String authorization) throws Exception {
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
|
||||||
|
conn.setRequestMethod(method);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(8000);
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0");
|
||||||
|
if (authorization != null && !authorization.isEmpty()) conn.setRequestProperty("Authorization", authorization);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readStream(InputStream in) throws IOException {
|
||||||
|
if (in == null) return "";
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||||
|
StringBuilder sb = new StringBuilder(); String line;
|
||||||
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== JSON Mini-Parser =====
|
||||||
|
|
||||||
|
private static class DiscordMessage {
|
||||||
|
long id;
|
||||||
|
String authorId = "", authorName = "", content = "";
|
||||||
|
boolean isBot = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.util.List<DiscordMessage> parseMessages(String json) {
|
||||||
|
java.util.List<DiscordMessage> result = new java.util.ArrayList<>();
|
||||||
|
int depth = 0, start = -1;
|
||||||
|
for (int i = 0; i < json.length(); i++) {
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '{') { if (depth++ == 0) start = i; }
|
||||||
|
else if (c == '}') {
|
||||||
|
if (--depth == 0 && start != -1) {
|
||||||
|
DiscordMessage msg = parseMessage(json.substring(start, i + 1));
|
||||||
|
if (msg != null) result.add(msg);
|
||||||
|
start = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiscordMessage parseMessage(String obj) {
|
||||||
|
try {
|
||||||
|
DiscordMessage msg = new DiscordMessage();
|
||||||
|
msg.id = Long.parseLong(extractJsonString(obj, "id"));
|
||||||
|
msg.content = unescapeJson(extractJsonString(obj, "content"));
|
||||||
|
|
||||||
|
// Webhook-Nachrichten als Bot markieren (Echo-Loop verhindern)
|
||||||
|
if (!extractJsonString(obj, "webhook_id").isEmpty()) {
|
||||||
|
msg.isBot = true;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
int authStart = obj.indexOf("\"author\"");
|
||||||
|
if (authStart >= 0) {
|
||||||
|
String authBlock = extractJsonObject(obj, authStart);
|
||||||
|
msg.authorId = extractJsonString(authBlock, "id");
|
||||||
|
msg.authorName = unescapeJson(extractJsonString(authBlock, "username"));
|
||||||
|
msg.isBot = "true".equals(extractJsonString(authBlock, "bot"));
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIX #12: Escape-Sequenzen werden korrekt mit einem Escape-Flag behandelt
|
||||||
|
* statt den Vorgänger-Char zu vergleichen (der bei '\\' + '"' versagt).
|
||||||
|
* Gibt immer einen leeren String zurück wenn der Key nicht gefunden wird (nie null).
|
||||||
|
*/
|
||||||
|
private String extractJsonString(String json, String key) {
|
||||||
|
if (json == null || key == null) return "";
|
||||||
|
String fullKey = "\"" + key + "\"";
|
||||||
|
int keyIdx = json.indexOf(fullKey);
|
||||||
|
if (keyIdx < 0) return "";
|
||||||
|
int colon = json.indexOf(':', keyIdx + fullKey.length());
|
||||||
|
if (colon < 0) return "";
|
||||||
|
int valStart = colon + 1;
|
||||||
|
while (valStart < json.length() && json.charAt(valStart) == ' ') valStart++;
|
||||||
|
if (valStart >= json.length()) return "";
|
||||||
|
char first = json.charAt(valStart);
|
||||||
|
if (first == '"') {
|
||||||
|
// FIX: Expliziter Escape-Flag statt Vorgänger-Char-Vergleich
|
||||||
|
int end = valStart + 1;
|
||||||
|
boolean escaped = false;
|
||||||
|
while (end < json.length()) {
|
||||||
|
char ch = json.charAt(end);
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (ch == '\\') {
|
||||||
|
escaped = true;
|
||||||
|
} else if (ch == '"') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
return json.substring(valStart + 1, end);
|
||||||
|
} else {
|
||||||
|
int end = valStart;
|
||||||
|
while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++;
|
||||||
|
return json.substring(valStart, end).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractJsonObject(String json, int fromIndex) {
|
||||||
|
int depth = 0, start = -1;
|
||||||
|
for (int i = fromIndex; i < json.length(); i++) {
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '{') { if (depth++ == 0) start = i; }
|
||||||
|
else if (c == '}') { if (--depth == 0 && start >= 0) return json.substring(start, i + 1); }
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String unescapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\\"", "\"").replace("\\n", "\n").replace("\\r", "\r").replace("\\\\", "\\");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
package net.viper.status.modules.chat.bridge;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.viper.status.modules.chat.AccountLinkManager;
|
||||||
|
import net.viper.status.modules.chat.ChatConfig;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Telegram-Brücke für bidirektionale Kommunikation.
|
||||||
|
*
|
||||||
|
* Minecraft → Telegram: Via Bot API (sendMessage)
|
||||||
|
* Telegram → Minecraft: Via Long-Polling (getUpdates)
|
||||||
|
*
|
||||||
|
* Voraussetzungen:
|
||||||
|
* - Telegram Bot via @BotFather erstellen
|
||||||
|
* - Bot-Token in chat.yml eintragen
|
||||||
|
* - Bot in die gewünschten Gruppen/Kanäle einladen
|
||||||
|
* - Bot zu Admin machen (für Gruppen-Nachrichten empfangen)
|
||||||
|
*/
|
||||||
|
public class TelegramBridge {
|
||||||
|
|
||||||
|
private static final String API_BASE = "https://api.telegram.org/bot";
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final ChatConfig config;
|
||||||
|
private final Logger logger;
|
||||||
|
private AccountLinkManager linkManager; // wird nach dem Start gesetzt
|
||||||
|
|
||||||
|
// Letztes verarbeitetes Update-ID (für getUpdates Offset)
|
||||||
|
private final AtomicLong lastUpdateId = new AtomicLong(0L);
|
||||||
|
|
||||||
|
private volatile boolean running = false;
|
||||||
|
|
||||||
|
public TelegramBridge(Plugin plugin, ChatConfig config) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.config = config;
|
||||||
|
this.logger = plugin.getLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setzt den AccountLinkManager – muss vor start() aufgerufen werden. */
|
||||||
|
public void setLinkManager(AccountLinkManager linkManager) {
|
||||||
|
this.linkManager = linkManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (!config.isTelegramEnabled()
|
||||||
|
|| config.getTelegramBotToken().isEmpty()
|
||||||
|
|| config.getTelegramBotToken().equals("YOUR_TELEGRAM_BOT_TOKEN")) {
|
||||||
|
logger.warning("[ChatModule-Telegram] Bot-Token nicht konfiguriert. Telegram-Empfang deaktiviert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
int interval = Math.max(2, config.getTelegramPollInterval());
|
||||||
|
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, this::pollUpdates,
|
||||||
|
interval, interval, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
logger.info("[ChatModule-Telegram] Brücke gestartet (Poll-Intervall: " + interval + "s).");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Minecraft → Telegram =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine Nachricht an eine Telegram-Chat-ID.
|
||||||
|
* Unterstützt Themen-Gruppen via message_thread_id.
|
||||||
|
*/
|
||||||
|
public void sendToTelegram(String chatId, String message) {
|
||||||
|
sendToTelegram(chatId, 0, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendToTelegram(String chatId, int threadId, String message) {
|
||||||
|
if (chatId == null || chatId.isEmpty()) return;
|
||||||
|
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
|
try {
|
||||||
|
String cleanMessage = ChatColor.stripColor(
|
||||||
|
ChatColor.translateAlternateColorCodes('&', message));
|
||||||
|
|
||||||
|
String url = API_BASE + config.getTelegramBotToken()
|
||||||
|
+ "/sendMessage?chat_id=" + URLEncoder.encode(chatId, "UTF-8")
|
||||||
|
+ "&text=" + URLEncoder.encode(cleanMessage, "UTF-8")
|
||||||
|
+ "&parse_mode=HTML"
|
||||||
|
+ (threadId > 0 ? "&message_thread_id=" + threadId : "");
|
||||||
|
|
||||||
|
getJson(url);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warning("[ChatModule-Telegram] Sende-Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine formatierte HelpOp/Broadcast-Nachricht an Telegram.
|
||||||
|
* Unterstützt Themen-Gruppen via message_thread_id.
|
||||||
|
*/
|
||||||
|
public void sendFormattedToTelegram(String chatId, String header, String content) {
|
||||||
|
sendFormattedToTelegram(chatId, 0, header, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendFormattedToTelegram(String chatId, int threadId, String header, String content) {
|
||||||
|
if (chatId == null || chatId.isEmpty()) return;
|
||||||
|
String text = "<b>" + escapeHtml(header) + "</b>\n" + escapeHtml(content);
|
||||||
|
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
|
try {
|
||||||
|
String url = API_BASE + config.getTelegramBotToken()
|
||||||
|
+ "/sendMessage?chat_id=" + URLEncoder.encode(chatId, "UTF-8")
|
||||||
|
+ "&text=" + URLEncoder.encode(text, "UTF-8")
|
||||||
|
+ "&parse_mode=HTML"
|
||||||
|
+ (threadId > 0 ? "&message_thread_id=" + threadId : "");
|
||||||
|
getJson(url);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warning("[ChatModule-Telegram] Format-Sende-Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Telegram → Minecraft (Polling) =====
|
||||||
|
|
||||||
|
private void pollUpdates() {
|
||||||
|
if (!running) return;
|
||||||
|
try {
|
||||||
|
// Beim ersten Poll: nur den aktuellen Offset holen, keine alten Updates verarbeiten
|
||||||
|
if (lastUpdateId.get() == 0L) {
|
||||||
|
String initUrl = API_BASE + config.getTelegramBotToken()
|
||||||
|
+ "/getUpdates?limit=1&offset=-1";
|
||||||
|
String initResp = getJson(initUrl);
|
||||||
|
if (initResp != null && initResp.contains("\"ok\":true")) {
|
||||||
|
java.util.List<TelegramUpdate> initUpdates = parseUpdates(initResp);
|
||||||
|
if (!initUpdates.isEmpty()) {
|
||||||
|
lastUpdateId.set(initUpdates.get(initUpdates.size() - 1).updateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return; // Erster Poll nur zum Initialisieren
|
||||||
|
}
|
||||||
|
|
||||||
|
long offset = lastUpdateId.get() + 1;
|
||||||
|
String url = API_BASE + config.getTelegramBotToken()
|
||||||
|
+ "/getUpdates?timeout=2&limit=10"
|
||||||
|
+ (offset > 0 ? "&offset=" + offset : "");
|
||||||
|
|
||||||
|
String response = getJson(url);
|
||||||
|
if (response == null || !response.contains("\"ok\":true")) return;
|
||||||
|
|
||||||
|
java.util.List<TelegramUpdate> updates = parseUpdates(response);
|
||||||
|
|
||||||
|
for (TelegramUpdate update : updates) {
|
||||||
|
if (update.updateId > lastUpdateId.get()) {
|
||||||
|
lastUpdateId.set(update.updateId);
|
||||||
|
}
|
||||||
|
if (update.text == null || update.text.isEmpty()) continue;
|
||||||
|
if (update.isBot) continue;
|
||||||
|
|
||||||
|
// ── Token-Einlösung: /link <TOKEN> ──
|
||||||
|
if (update.text.startsWith("/link ") || update.text.startsWith("/link@")) {
|
||||||
|
String[] parts = update.text.split("\\s+", 2);
|
||||||
|
if (parts.length == 2 && linkManager != null) {
|
||||||
|
String token = parts[1].trim().toUpperCase();
|
||||||
|
AccountLinkManager.LinkedAccount acc =
|
||||||
|
linkManager.redeemTelegram(token, update.fromId, update.fromName);
|
||||||
|
if (acc != null) {
|
||||||
|
sendToTelegram(update.chatId, update.threadId,
|
||||||
|
"✅ Verknüpfung erfolgreich! Minecraft-Account: <b>"
|
||||||
|
+ escapeHtml(acc.minecraftName) + "</b>");
|
||||||
|
} else {
|
||||||
|
sendToTelegram(update.chatId, update.threadId,
|
||||||
|
"❌ Ungültiger oder abgelaufener Token. Bitte /telegramlink im Spiel erneut ausführen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue; // Nicht als Chat-Nachricht weiterleiten
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bot-Befehle ignorieren
|
||||||
|
if (update.text.startsWith("/")) continue;
|
||||||
|
|
||||||
|
// ── Account-Name auflösen ──
|
||||||
|
String displayName = (linkManager != null)
|
||||||
|
? linkManager.resolveTelegramName(update.fromId, update.fromName)
|
||||||
|
: update.fromName;
|
||||||
|
|
||||||
|
// Welchem Minecraft-Kanal gehört diese Telegram-Chat-ID + Thread?
|
||||||
|
final boolean isAdminChat = update.chatId.equals(config.getTelegramAdminChatId())
|
||||||
|
&& (config.getTelegramAdminTopicId() == 0
|
||||||
|
|| config.getTelegramAdminTopicId() == update.threadId);
|
||||||
|
|
||||||
|
// Prüfen ob die Nachricht zu einem konfigurierten Kanal-Thema gehört
|
||||||
|
final boolean matchesChannel = isAdminChat || matchesTelegramChannel(update);
|
||||||
|
|
||||||
|
if (!matchesChannel && !isAdminChat) continue;
|
||||||
|
|
||||||
|
final String format = config.getTelegramFromFormat();
|
||||||
|
final String finalDisplay = displayName;
|
||||||
|
final String formatted = ChatColor.translateAlternateColorCodes('&',
|
||||||
|
format.replace("{user}", finalDisplay)
|
||||||
|
.replace("{message}", update.text));
|
||||||
|
|
||||||
|
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||||
|
if (isAdminChat) {
|
||||||
|
for (net.md_5.bungee.api.connection.ProxiedPlayer p :
|
||||||
|
ProxyServer.getInstance().getPlayers()) {
|
||||||
|
if (p.hasPermission("chat.admin.bypass")) {
|
||||||
|
p.sendMessage(new TextComponent(formatted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ProxyServer.getInstance().broadcast(new TextComponent(formatted));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.fine("[ChatModule-Telegram] Poll-Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== HTTP-Hilfsmethoden =====
|
||||||
|
|
||||||
|
private String getJson(String urlStr) throws Exception {
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(6000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0");
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
String result = readStream(code == 200 ? conn.getInputStream() : conn.getErrorStream());
|
||||||
|
conn.disconnect();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readStream(InputStream in) throws IOException {
|
||||||
|
if (in == null) return "";
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== JSON Mini-Parser =====
|
||||||
|
|
||||||
|
private static class TelegramUpdate {
|
||||||
|
long updateId;
|
||||||
|
String chatId = "";
|
||||||
|
String fromId = ""; // Telegram User-ID (für Account-Link)
|
||||||
|
String fromName = "";
|
||||||
|
String text = "";
|
||||||
|
boolean isBot = false;
|
||||||
|
int threadId = 0; // message_thread_id für Themen-Gruppen (0 = kein Thema)
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.util.List<TelegramUpdate> parseUpdates(String json) {
|
||||||
|
java.util.List<TelegramUpdate> result = new java.util.ArrayList<>();
|
||||||
|
// Suche nach "result":[...]
|
||||||
|
int resultStart = json.indexOf("\"result\":[");
|
||||||
|
if (resultStart < 0) return result;
|
||||||
|
|
||||||
|
// Extrahiere alle Update-Objekte
|
||||||
|
int depth = 0, start = -1;
|
||||||
|
boolean inResult = false;
|
||||||
|
for (int i = resultStart + 10; i < json.length(); i++) {
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '[' && !inResult) { inResult = true; continue; }
|
||||||
|
if (!inResult) continue;
|
||||||
|
if (c == '{') { if (depth++ == 0) start = i; }
|
||||||
|
else if (c == '}') {
|
||||||
|
if (--depth == 0 && start >= 0) {
|
||||||
|
TelegramUpdate upd = parseUpdate(json.substring(start, i + 1));
|
||||||
|
if (upd != null) result.add(upd);
|
||||||
|
start = -1;
|
||||||
|
}
|
||||||
|
} else if (c == ']' && depth == 0) break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prüft ob ein Update zu einem konfigurierten Kanal-Thema gehört. */
|
||||||
|
private boolean matchesTelegramChannel(TelegramUpdate update) {
|
||||||
|
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
||||||
|
if (!ch.getTelegramChatId().equals(update.chatId)) continue;
|
||||||
|
// Thema konfiguriert? → Thread-ID muss übereinstimmen
|
||||||
|
if (ch.getTelegramThreadId() > 0 && ch.getTelegramThreadId() != update.threadId) continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TelegramUpdate parseUpdate(String obj) {
|
||||||
|
try {
|
||||||
|
TelegramUpdate upd = new TelegramUpdate();
|
||||||
|
upd.updateId = Long.parseLong(extractValue(obj, "update_id"));
|
||||||
|
|
||||||
|
// message-Block
|
||||||
|
int msgIdx = obj.indexOf("\"message\"");
|
||||||
|
if (msgIdx < 0) return null;
|
||||||
|
String msgBlock = extractObject(obj, msgIdx);
|
||||||
|
|
||||||
|
upd.text = unescapeJson(extractString(msgBlock, "text"));
|
||||||
|
|
||||||
|
// message_thread_id (Themen-Gruppen)
|
||||||
|
String threadIdStr = extractValue(msgBlock, "message_thread_id");
|
||||||
|
if (!threadIdStr.isEmpty()) {
|
||||||
|
try { upd.threadId = Integer.parseInt(threadIdStr); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// from-Block (Absender)
|
||||||
|
int fromIdx = msgBlock.indexOf("\"from\"");
|
||||||
|
if (fromIdx >= 0) {
|
||||||
|
String fromBlock = extractObject(msgBlock, fromIdx);
|
||||||
|
String firstName = unescapeJson(extractString(fromBlock, "first_name"));
|
||||||
|
String lastName = unescapeJson(extractString(fromBlock, "last_name"));
|
||||||
|
String username = unescapeJson(extractString(fromBlock, "username"));
|
||||||
|
upd.fromId = extractValue(fromBlock, "id");
|
||||||
|
upd.fromName = !username.isEmpty() ? "@" + username
|
||||||
|
: (firstName + (lastName.isEmpty() ? "" : " " + lastName)).trim();
|
||||||
|
String botFlag = extractValue(fromBlock, "is_bot");
|
||||||
|
upd.isBot = "true".equals(botFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// chat-Block (Chat-ID)
|
||||||
|
int chatIdx = msgBlock.indexOf("\"chat\"");
|
||||||
|
if (chatIdx >= 0) {
|
||||||
|
String chatBlock = extractObject(msgBlock, chatIdx);
|
||||||
|
upd.chatId = extractValue(chatBlock, "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
return upd;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractValue(String json, String key) {
|
||||||
|
String fullKey = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(fullKey);
|
||||||
|
if (idx < 0) return "";
|
||||||
|
int colon = json.indexOf(':', idx + fullKey.length());
|
||||||
|
if (colon < 0) return "";
|
||||||
|
int valStart = colon + 1;
|
||||||
|
while (valStart < json.length() && json.charAt(valStart) == ' ') valStart++;
|
||||||
|
if (valStart >= json.length()) return "";
|
||||||
|
char first = json.charAt(valStart);
|
||||||
|
if (first == '"') {
|
||||||
|
return extractString(json.substring(valStart - 1 - key.length()), key);
|
||||||
|
}
|
||||||
|
int end = valStart;
|
||||||
|
while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++;
|
||||||
|
return json.substring(valStart, end).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractString(String json, String key) {
|
||||||
|
String fullKey = "\"" + key + "\":\"";
|
||||||
|
int idx = json.indexOf(fullKey);
|
||||||
|
if (idx < 0) return "";
|
||||||
|
int start = idx + fullKey.length();
|
||||||
|
int end = start;
|
||||||
|
while (end < json.length()) {
|
||||||
|
if (json.charAt(end) == '"' && json.charAt(end - 1) != '\\') break;
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
return json.substring(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractObject(String json, int fromIndex) {
|
||||||
|
int depth = 0, start = -1;
|
||||||
|
for (int i = fromIndex; i < json.length(); i++) {
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '{') { if (depth++ == 0) start = i; }
|
||||||
|
else if (c == '}') { if (--depth == 0 && start >= 0) return json.substring(start, i + 1); }
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String unescapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\\"", "\"").replace("\\n", "\n")
|
||||||
|
.replace("\\r", "\r").replace("\\\\", "\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeHtml(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package net.viper.status.modules.commandblocker;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.event.ChatEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
import net.viper.status.ratelimit.GlobalRateLimitFramework;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
public class CommandBlockerModule implements Module, Listener {
|
||||||
|
|
||||||
|
private StatusAPI plugin;
|
||||||
|
private boolean enabled = true; // Standardmäßig aktiv
|
||||||
|
private String bypassPermission = "commandblocker.bypass"; // Standard Permission
|
||||||
|
|
||||||
|
private File file;
|
||||||
|
private Set<String> blocked = new HashSet<>();
|
||||||
|
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
||||||
|
|
||||||
|
private boolean commandRateLimitEnabled = true;
|
||||||
|
private long commandRateLimitWindowMs = 3000L;
|
||||||
|
private int commandRateLimitMaxActions = 8;
|
||||||
|
private long commandRateLimitBlockMs = 6000L;
|
||||||
|
private String commandRateLimitMessage = "&cZu viele Befehle in kurzer Zeit. Bitte warte kurz.";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "CommandBlockerModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
if (!(plugin instanceof StatusAPI)) return; // Sicherheit
|
||||||
|
this.plugin = (StatusAPI) plugin;
|
||||||
|
|
||||||
|
// Datei laden
|
||||||
|
file = new File(this.plugin.getDataFolder(), "blocked-commands.yml");
|
||||||
|
loadFile();
|
||||||
|
|
||||||
|
// Listener registrieren
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerListener(this.plugin, this);
|
||||||
|
|
||||||
|
// /cb Befehl registrieren
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin,
|
||||||
|
new net.md_5.bungee.api.plugin.Command("cb", "commandblocker.admin") {
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
handleCommand(sender, args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.plugin.getLogger().fine("[CommandBlocker] aktiviert (" + blocked.size() + " Commands).");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
blocked.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onCommand(ChatEvent event) {
|
||||||
|
if (!enabled || !event.isCommand()) return;
|
||||||
|
|
||||||
|
if (!(event.getSender() instanceof ProxiedPlayer)) return;
|
||||||
|
ProxiedPlayer player = (ProxiedPlayer) event.getSender();
|
||||||
|
|
||||||
|
if (player.hasPermission(bypassPermission)) return;
|
||||||
|
|
||||||
|
String msg = event.getMessage();
|
||||||
|
if (msg == null || msg.length() <= 1) return;
|
||||||
|
|
||||||
|
if (commandRateLimitEnabled) {
|
||||||
|
GlobalRateLimitFramework.Result result = rateLimiter.check(
|
||||||
|
"chat.command",
|
||||||
|
player.getUniqueId().toString(),
|
||||||
|
new GlobalRateLimitFramework.Rule(
|
||||||
|
true,
|
||||||
|
commandRateLimitWindowMs,
|
||||||
|
commandRateLimitMaxActions,
|
||||||
|
commandRateLimitBlockMs
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isBlocked()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
player.sendMessage(ChatColor.translateAlternateColorCodes('&', commandRateLimitMessage));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String cmd = msg.substring(1).toLowerCase(Locale.ROOT);
|
||||||
|
String base = cmd.split(" ")[0];
|
||||||
|
|
||||||
|
if (blocked.contains(base)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
player.sendMessage(ChatColor.RED + "Dieser Befehl ist auf diesem Netzwerk blockiert.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCommand(CommandSender sender, String[] args) {
|
||||||
|
if (args == null || args.length == 0) {
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/cb add <command>");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/cb remove <command>");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/cb list");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/cb reload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String action = args[0].toLowerCase();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "add":
|
||||||
|
if (args.length < 2) break;
|
||||||
|
blocked.add(args[1].toLowerCase());
|
||||||
|
saveFile();
|
||||||
|
sender.sendMessage(ChatColor.GREEN + "Command blockiert: " + args[1]);
|
||||||
|
break;
|
||||||
|
case "remove":
|
||||||
|
if (args.length < 2) break;
|
||||||
|
blocked.remove(args[1].toLowerCase());
|
||||||
|
saveFile();
|
||||||
|
sender.sendMessage(ChatColor.GREEN + "Command freigegeben: " + args[1]);
|
||||||
|
break;
|
||||||
|
case "list":
|
||||||
|
sender.sendMessage(ChatColor.GOLD + "Blockierte Commands:");
|
||||||
|
for (String c : blocked) {
|
||||||
|
sender.sendMessage(ChatColor.RED + "- " + c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "reload":
|
||||||
|
loadFile();
|
||||||
|
sender.sendMessage(ChatColor.GREEN + "CommandBlocker neu geladen.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sender.sendMessage(ChatColor.RED + "Unbekannter Unterbefehl.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFile() {
|
||||||
|
try {
|
||||||
|
if (!file.exists()) {
|
||||||
|
File parent = file.getParentFile();
|
||||||
|
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||||
|
file.createNewFile();
|
||||||
|
saveFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
Yaml yaml = new Yaml();
|
||||||
|
Map<String, Object> data = null;
|
||||||
|
FileInputStream fis = null;
|
||||||
|
try {
|
||||||
|
fis = new FileInputStream(file);
|
||||||
|
data = yaml.loadAs(fis, Map.class);
|
||||||
|
} finally {
|
||||||
|
if (fis != null) try { fis.close(); } catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked.clear();
|
||||||
|
if (data != null && data.containsKey("blocked")) {
|
||||||
|
Object obj = data.get("blocked");
|
||||||
|
if (obj instanceof List) {
|
||||||
|
List list = (List) obj;
|
||||||
|
for (Object o : list) {
|
||||||
|
if (o != null) blocked.add(String.valueOf(o).toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data != null && data.containsKey("rate-limit")) {
|
||||||
|
Object rlObj = data.get("rate-limit");
|
||||||
|
if (rlObj instanceof Map) {
|
||||||
|
Map rl = (Map) rlObj;
|
||||||
|
commandRateLimitEnabled = parseBoolean(rl.get("enabled"), commandRateLimitEnabled);
|
||||||
|
commandRateLimitWindowMs = parseLong(rl.get("window-ms"), commandRateLimitWindowMs);
|
||||||
|
commandRateLimitMaxActions = (int) parseLong(rl.get("max-actions"), commandRateLimitMaxActions);
|
||||||
|
commandRateLimitBlockMs = parseLong(rl.get("block-ms"), commandRateLimitBlockMs);
|
||||||
|
Object msgObj = rl.get("message");
|
||||||
|
if (msgObj != null) {
|
||||||
|
commandRateLimitMessage = String.valueOf(msgObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[CommandBlocker] Fehler beim Laden: " + e.getMessage());
|
||||||
|
else System.err.println("[CommandBlocker] Fehler beim Laden: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveFile() {
|
||||||
|
try {
|
||||||
|
Yaml yaml = new Yaml();
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("blocked", new ArrayList<>(blocked));
|
||||||
|
|
||||||
|
Map<String, Object> rl = new LinkedHashMap<>();
|
||||||
|
rl.put("enabled", commandRateLimitEnabled);
|
||||||
|
rl.put("window-ms", commandRateLimitWindowMs);
|
||||||
|
rl.put("max-actions", commandRateLimitMaxActions);
|
||||||
|
rl.put("block-ms", commandRateLimitBlockMs);
|
||||||
|
rl.put("message", commandRateLimitMessage);
|
||||||
|
out.put("rate-limit", rl);
|
||||||
|
|
||||||
|
FileWriter fw = null;
|
||||||
|
try {
|
||||||
|
fw = new FileWriter(file);
|
||||||
|
yaml.dump(out, fw);
|
||||||
|
} finally {
|
||||||
|
if (fw != null) try { fw.close(); } catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (plugin != null) plugin.getLogger().severe("[CommandBlocker] Fehler beim Speichern: " + e.getMessage());
|
||||||
|
else System.err.println("[CommandBlocker] Fehler beim Speichern: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parseBoolean(Object obj, boolean fallback) {
|
||||||
|
if (obj == null) return fallback;
|
||||||
|
return Boolean.parseBoolean(String.valueOf(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseLong(Object obj, long fallback) {
|
||||||
|
if (obj == null) return fallback;
|
||||||
|
try {
|
||||||
|
return Long.parseLong(String.valueOf(obj));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package net.viper.status.modules.customcommands;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.CopyOption;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.api.config.ServerInfo;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.event.ChatEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin; // Import für das Interface Argument
|
||||||
|
import net.md_5.bungee.config.Configuration;
|
||||||
|
import net.md_5.bungee.config.ConfigurationProvider;
|
||||||
|
import net.md_5.bungee.config.YamlConfiguration;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
public class CustomCommandModule implements Module, Listener {
|
||||||
|
|
||||||
|
private StatusAPI plugin;
|
||||||
|
private Configuration config;
|
||||||
|
private Command chatCommand;
|
||||||
|
|
||||||
|
public CustomCommandModule() {
|
||||||
|
// Leerer Konstruktor
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "CustomCommandModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
// Hier casten wir 'Plugin' zu 'StatusAPI', da wir wissen, dass es das ist
|
||||||
|
this.plugin = (StatusAPI) plugin;
|
||||||
|
|
||||||
|
this.plugin.getLogger().fine("Lade CustomCommandModule...");
|
||||||
|
reloadConfig();
|
||||||
|
if (this.config == null) {
|
||||||
|
this.config = new Configuration();
|
||||||
|
this.plugin.getLogger().warning("customcommands.yml konnte nicht geladen werden. Verwende leere Konfiguration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// /bcmds Reload Befehl registrieren
|
||||||
|
this.plugin.getProxy().getPluginManager().registerCommand(this.plugin, new Command("bcmds") {
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
if (!sender.hasPermission("statusapi.bcmds")) {
|
||||||
|
sender.sendMessage(new TextComponent(ChatColor.RED + "You don't have permission."));
|
||||||
|
} else {
|
||||||
|
reloadConfig();
|
||||||
|
sender.sendMessage(new TextComponent(ChatColor.GREEN + "Config reloaded."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// /chat Befehl registrieren (falls aktiviert)
|
||||||
|
if (config.getBoolean("chat-command", true)) {
|
||||||
|
chatCommand = new Command("chat") {
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
if (sender instanceof ProxiedPlayer) {
|
||||||
|
ProxiedPlayer player = (ProxiedPlayer) sender;
|
||||||
|
if (player.getServer() == null) {
|
||||||
|
player.sendMessage(new TextComponent(ChatColor.RED + "Konnte deinen Server nicht ermitteln. Bitte versuche es erneut."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String msg = String.join(" ", args);
|
||||||
|
if (msg.trim().isEmpty()) {
|
||||||
|
player.sendMessage(new TextComponent(ChatColor.RED + "Bitte gib eine Nachricht an."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChatEvent e = new ChatEvent(player, player.getServer(), msg);
|
||||||
|
ProxyServer.getInstance().getPluginManager().callEvent(e);
|
||||||
|
if (!e.isCancelled()) {
|
||||||
|
if (!e.isCommand() || !ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, msg.substring(1))) {
|
||||||
|
player.chat(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String msg = String.join(" ", args);
|
||||||
|
if(msg.startsWith("/")) {
|
||||||
|
ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, msg.substring(1));
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(new TextComponent("Console cannot send chat messages via /chat usually."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.plugin.getProxy().getPluginManager().registerCommand(this.plugin, chatCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugin.getProxy().getPluginManager().registerListener(this.plugin, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
// Optional: Cleanup logic, falls nötig.
|
||||||
|
// Wir nutzen hier das übergebene 'plugin' Argument (oder this.plugin, ist egal)
|
||||||
|
// Listener und Commands werden automatisch entfernt, wenn das Plugin stoppt.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadConfig() {
|
||||||
|
try {
|
||||||
|
if (!this.plugin.getDataFolder().exists()) {
|
||||||
|
this.plugin.getDataFolder().mkdirs();
|
||||||
|
}
|
||||||
|
File file = new File(this.plugin.getDataFolder(), "customcommands.yml");
|
||||||
|
if (!file.exists()) {
|
||||||
|
// Kopieren aus Resources
|
||||||
|
InputStream in = this.plugin.getResourceAsStream("customcommands.yml");
|
||||||
|
if (in == null) {
|
||||||
|
this.plugin.getLogger().warning("customcommands.yml nicht in JAR gefunden. Erstelle leere Datei.");
|
||||||
|
file.createNewFile();
|
||||||
|
} else {
|
||||||
|
Files.copy(in, file.toPath(), new CopyOption[0]);
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
this.plugin.getLogger().severe("Konnte customcommands.yml nicht laden!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = 64)
|
||||||
|
public void onCommand(ChatEvent e) {
|
||||||
|
if (!e.isCommand()) return;
|
||||||
|
if (!(e.getSender() instanceof ProxiedPlayer)) return;
|
||||||
|
|
||||||
|
final ProxiedPlayer player = (ProxiedPlayer) e.getSender();
|
||||||
|
String[] split = e.getMessage().split(" ");
|
||||||
|
String label = split[0].substring(1);
|
||||||
|
|
||||||
|
final List<String> args = new ArrayList<>(Arrays.asList(split));
|
||||||
|
args.remove(0);
|
||||||
|
|
||||||
|
Configuration cmds = config.getSection("commands");
|
||||||
|
if (cmds == null) return;
|
||||||
|
|
||||||
|
Configuration section = null;
|
||||||
|
String foundKey = null;
|
||||||
|
|
||||||
|
for (String key : cmds.getKeys()) {
|
||||||
|
Configuration cmdSection = cmds.getSection(key);
|
||||||
|
if (key.equalsIgnoreCase(label)) {
|
||||||
|
section = cmdSection;
|
||||||
|
foundKey = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (String alias : cmdSection.getStringList("aliases")) {
|
||||||
|
if (alias.equalsIgnoreCase(label)) {
|
||||||
|
section = cmdSection;
|
||||||
|
foundKey = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (section != null) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section == null) return;
|
||||||
|
|
||||||
|
String type = section.getString("type", "line");
|
||||||
|
String sendertype = section.getString("sender", "default");
|
||||||
|
String permission = section.getString("permission", "");
|
||||||
|
final List<String> commands = section.getStringList("commands");
|
||||||
|
|
||||||
|
if (!permission.isEmpty() && !player.hasPermission(permission)) {
|
||||||
|
player.sendMessage(new TextComponent(ChatColor.RED + "You don't have permission."));
|
||||||
|
e.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.setCancelled(true);
|
||||||
|
|
||||||
|
final CommandSender target;
|
||||||
|
if (sendertype.equals("default")) {
|
||||||
|
target = player;
|
||||||
|
} else if (sendertype.equals("admin")) {
|
||||||
|
target = new ForwardSender(player, true);
|
||||||
|
} else if (sendertype.equals("console")) {
|
||||||
|
target = ProxyServer.getInstance().getConsole();
|
||||||
|
} else {
|
||||||
|
ProxiedPlayer targetPlayer = ProxyServer.getInstance().getPlayer(sendertype);
|
||||||
|
if (targetPlayer == null || !targetPlayer.isConnected()) {
|
||||||
|
player.sendMessage(new TextComponent(ChatColor.RED + "Player " + sendertype + " is not online."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target = targetPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
String argsString = args.size() >= 1 ? String.join(" ", args) : "";
|
||||||
|
final String finalArgs = argsString;
|
||||||
|
final String senderName = player.getName();
|
||||||
|
|
||||||
|
if (type.equals("random")) {
|
||||||
|
int randomIndex = new Random().nextInt(commands.size());
|
||||||
|
String rawCommand = commands.get(randomIndex);
|
||||||
|
executeCommand(target, rawCommand, finalArgs, senderName);
|
||||||
|
} else if (type.equals("line")) {
|
||||||
|
ProxyServer.getInstance().getScheduler().runAsync(this.plugin, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (String rawCommand : commands) {
|
||||||
|
executeCommand(target, rawCommand, finalArgs, senderName);
|
||||||
|
try {
|
||||||
|
Thread.sleep(100L);
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.plugin.getLogger().warning("Unknown type '" + type + "' for command " + foundKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeCommand(CommandSender sender, String rawCommand, String args, String playerName) {
|
||||||
|
String parsed = rawCommand
|
||||||
|
.replace("%args%", args)
|
||||||
|
.replace("%sender%", playerName);
|
||||||
|
|
||||||
|
String commandToDispatch = parsed.startsWith("/") ? parsed.substring(1) : parsed;
|
||||||
|
ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, commandToDispatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package net.viper.status.modules.customcommands;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.util.Collection;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.chat.BaseComponent;
|
||||||
|
import net.md_5.bungee.api.connection.Connection;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.connection.Connection.Unsafe;
|
||||||
|
|
||||||
|
public class ForwardSender implements CommandSender, Connection {
|
||||||
|
private ProxiedPlayer target;
|
||||||
|
private Boolean admin;
|
||||||
|
|
||||||
|
public ForwardSender(ProxiedPlayer sender, Boolean admin) {
|
||||||
|
this.target = sender;
|
||||||
|
this.admin = admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProxiedPlayer target() {
|
||||||
|
return this.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.target.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(String message) {
|
||||||
|
this.target.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessages(String... messages) {
|
||||||
|
this.target.sendMessages(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(BaseComponent... message) {
|
||||||
|
this.target.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(BaseComponent message) {
|
||||||
|
this.target.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getGroups() {
|
||||||
|
return this.target.getGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addGroups(String... groups) {
|
||||||
|
this.target.addGroups(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeGroups(String... groups) {
|
||||||
|
this.target.removeGroups(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(String permission) {
|
||||||
|
return this.admin ? true : this.target.hasPermission(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPermission(String permission, boolean value) {
|
||||||
|
this.target.setPermission(permission, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getPermissions() {
|
||||||
|
Collection<String> perms = new java.util.ArrayList<>(this.target.getPermissions());
|
||||||
|
if (this.admin) {
|
||||||
|
perms.add("*");
|
||||||
|
}
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InetSocketAddress getAddress() {
|
||||||
|
return this.target.getAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SocketAddress getSocketAddress() {
|
||||||
|
return this.target.getSocketAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnect(String reason) {
|
||||||
|
this.target.disconnect(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnect(BaseComponent... reason) {
|
||||||
|
this.target.disconnect(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnect(BaseComponent reason) {
|
||||||
|
this.target.disconnect(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConnected() {
|
||||||
|
return this.target.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Unsafe unsafe() {
|
||||||
|
return this.target.unsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package net.viper.status.modules.economy;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /ecoadmin – wird NICHT mehr auf BungeeCord registriert.
|
||||||
|
* NexEco /eco auf dem Spigot-Server übernimmt Admin-Befehle.
|
||||||
|
*/
|
||||||
|
public class EcoAdminCommand extends Command {
|
||||||
|
|
||||||
|
public EcoAdminCommand(Plugin plugin, EconomyManager manager) {
|
||||||
|
super("ecoadmin_disabled_nexeco", "economy.admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
package net.viper.status.modules.economy;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet die MySQL-Verbindung (HikariCP) und die Tabelle bc_accounts.
|
||||||
|
*
|
||||||
|
* Fixes:
|
||||||
|
* - balance-Spalte als DOUBLE(30,2) statt VARCHAR → kompatibel mit NexEco & SurvivalPlus
|
||||||
|
* - atomare Transaktion für withdraw+deposit → kein Geldverlust bei Absturz
|
||||||
|
* - FOR UPDATE Lock → kein Race-Condition-Bug bei gleichzeitigen Überweisungen
|
||||||
|
*/
|
||||||
|
public class EconomyDatabase {
|
||||||
|
|
||||||
|
private static final String TABLE = "bc_accounts";
|
||||||
|
private static final String TABLE_NAMES = "bc_player_names";
|
||||||
|
|
||||||
|
private final Logger log;
|
||||||
|
private HikariDataSource dataSource;
|
||||||
|
|
||||||
|
public EconomyDatabase(Plugin plugin, String host, int port, String database, String user, String password) {
|
||||||
|
this.log = plugin.getLogger();
|
||||||
|
|
||||||
|
HikariConfig cfg = new HikariConfig();
|
||||||
|
java.util.logging.Logger.getLogger("com.zaxxer.hikari").setLevel(java.util.logging.Level.WARNING);
|
||||||
|
cfg.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database
|
||||||
|
+ "?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&useUnicode=true"
|
||||||
|
+ "&allowPublicKeyRetrieval=true");
|
||||||
|
cfg.setUsername(user);
|
||||||
|
cfg.setPassword(password);
|
||||||
|
cfg.setMaximumPoolSize(5);
|
||||||
|
cfg.setMinimumIdle(1);
|
||||||
|
cfg.setConnectionTimeout(10_000);
|
||||||
|
cfg.setIdleTimeout(600_000);
|
||||||
|
cfg.setMaxLifetime(1_800_000);
|
||||||
|
cfg.setPoolName("StatusAPI-Economy");
|
||||||
|
cfg.addDataSourceProperty("cachePrepStmts", "true");
|
||||||
|
cfg.addDataSourceProperty("prepStmtCacheSize", "250");
|
||||||
|
cfg.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataSource = new HikariDataSource(cfg);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.severe("[Economy] MySQL-Verbindung fehlgeschlagen: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── bc_accounts: balance als DOUBLE – kompatibel mit NexEco & SurvivalPlus ──
|
||||||
|
try (Connection con = dataSource.getConnection(); Statement st = con.createStatement()) {
|
||||||
|
st.executeUpdate(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `" + TABLE + "` (" +
|
||||||
|
" `player_name` VARCHAR(36) NOT NULL," +
|
||||||
|
" `balance` DOUBLE(30,2) NOT NULL DEFAULT 0.00," +
|
||||||
|
" PRIMARY KEY (`player_name`)" +
|
||||||
|
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
||||||
|
);
|
||||||
|
// Falls Tabelle existiert aber balance noch VARCHAR ist → konvertieren
|
||||||
|
st.executeUpdate(
|
||||||
|
"ALTER TABLE `" + TABLE + "` " +
|
||||||
|
"MODIFY COLUMN `balance` DOUBLE(30,2) NOT NULL DEFAULT 0.00"
|
||||||
|
);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// ALTER schlägt fehl wenn Typ bereits korrekt ist – kein Problem
|
||||||
|
if (!e.getMessage().contains("Duplicate") && !e.getMessage().contains("doesn't exist")) {
|
||||||
|
log.warning("[Economy] Tabellen-Setup bc_accounts: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── bc_player_names ────────────────────────────────────────────────────
|
||||||
|
try (Connection con = dataSource.getConnection(); Statement st = con.createStatement()) {
|
||||||
|
st.executeUpdate(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `" + TABLE_NAMES + "` (" +
|
||||||
|
" `uuid` VARCHAR(36) NOT NULL PRIMARY KEY," +
|
||||||
|
" `name` VARCHAR(16) NOT NULL," +
|
||||||
|
" `updated` BIGINT NOT NULL" +
|
||||||
|
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
||||||
|
);
|
||||||
|
if (StatusAPI.DEBUG) log.info("[Economy] MySQL verbunden – Tabellen bereit.");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.severe("[Economy] Tabellen-Setup bc_player_names fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return dataSource != null && !dataSource.isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
if (dataSource != null && !dataSource.isClosed()) dataSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kontostand ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Lädt den Kontostand direkt aus der DB. Gibt -1 zurück wenn kein Eintrag. */
|
||||||
|
public double load(UUID uuid) {
|
||||||
|
if (!isConnected()) return -1;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"SELECT `balance` FROM `" + TABLE + "` WHERE `player_name` = ?")) {
|
||||||
|
ps.setString(1, uuid.toString());
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) return rs.getDouble("balance");
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.warning("[Economy] Load fehlgeschlagen für " + uuid + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schreibt einen Kontostand in die DB (INSERT oder UPDATE). */
|
||||||
|
public void save(UUID uuid, double balance) {
|
||||||
|
if (!isConnected()) return;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"INSERT INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?) " +
|
||||||
|
"ON DUPLICATE KEY UPDATE `balance` = VALUES(`balance`)")) {
|
||||||
|
ps.setString(1, uuid.toString());
|
||||||
|
ps.setDouble(2, balance);
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.warning("[Economy] Save fehlgeschlagen für " + uuid + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomare Überweisung von → to.
|
||||||
|
* Nutzt eine SQL-Transaktion mit FOR UPDATE Lock – race-condition-sicher.
|
||||||
|
* Gibt false zurück wenn Sender nicht genug Guthaben hat.
|
||||||
|
*/
|
||||||
|
public boolean transfer(UUID from, UUID to, double amount, double startBalance) {
|
||||||
|
if (!isConnected()) return false;
|
||||||
|
Connection con = null;
|
||||||
|
try {
|
||||||
|
con = dataSource.getConnection();
|
||||||
|
con.setAutoCommit(false);
|
||||||
|
|
||||||
|
// Sender sperren und Balance lesen
|
||||||
|
double fromBalance;
|
||||||
|
try (PreparedStatement ps = con.prepareStatement(
|
||||||
|
"SELECT `balance` FROM `" + TABLE + "` WHERE `player_name` = ? FOR UPDATE")) {
|
||||||
|
ps.setString(1, from.toString());
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
fromBalance = rs.next() ? rs.getDouble("balance") : startBalance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromBalance < amount) { con.rollback(); return false; }
|
||||||
|
|
||||||
|
// Sender abziehen
|
||||||
|
try (PreparedStatement ps = con.prepareStatement(
|
||||||
|
"INSERT INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?) " +
|
||||||
|
"ON DUPLICATE KEY UPDATE `balance` = VALUES(`balance`)")) {
|
||||||
|
ps.setString(1, from.toString());
|
||||||
|
ps.setDouble(2, fromBalance - amount);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empfänger gutschreiben (Konto anlegen falls nötig)
|
||||||
|
try (PreparedStatement ps = con.prepareStatement(
|
||||||
|
"INSERT INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?) " +
|
||||||
|
"ON DUPLICATE KEY UPDATE `balance` = `balance` + ?")) {
|
||||||
|
ps.setString(1, to.toString());
|
||||||
|
ps.setDouble(2, startBalance + amount);
|
||||||
|
ps.setDouble(3, amount);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
con.commit();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.warning("[Economy] Transfer fehlgeschlagen: " + e.getMessage());
|
||||||
|
try { if (con != null) con.rollback(); } catch (SQLException ex) { /* ignore */ }
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
try { if (con != null) con.close(); } catch (SQLException ex) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Name-Lookup ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void saveNameMapping(UUID uuid, String name) {
|
||||||
|
if (!isConnected()) return;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"INSERT INTO `" + TABLE_NAMES + "` (`uuid`, `name`, `updated`) VALUES (?, ?, ?) " +
|
||||||
|
"ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `updated` = VALUES(`updated`)")) {
|
||||||
|
ps.setString(1, uuid.toString());
|
||||||
|
ps.setString(2, name);
|
||||||
|
ps.setLong(3, System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.warning("[Economy] Name-Mapping fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID findUUIDByNameOwn(String name) {
|
||||||
|
if (!isConnected()) return null;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"SELECT `uuid` FROM `" + TABLE_NAMES + "` WHERE `name` = ? LIMIT 1")) {
|
||||||
|
ps.setString(1, name);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) return UUID.fromString(rs.getString("uuid"));
|
||||||
|
}
|
||||||
|
} catch (SQLException | IllegalArgumentException e) { /* ignorieren */ }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID findUUIDByName(String name) {
|
||||||
|
if (!isConnected()) return null;
|
||||||
|
try (Connection con = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = con.prepareStatement(
|
||||||
|
"SELECT `player_uuid` FROM `CMI_users` WHERE `username` = ? LIMIT 1")) {
|
||||||
|
ps.setString(1, name);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
String s = rs.getString("player_uuid");
|
||||||
|
if (s != null && !s.isEmpty()) return UUID.fromString(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | IllegalArgumentException e) { /* CMI nicht vorhanden – kein Problem */ }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package net.viper.status.modules.economy;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||||
|
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EconomyListener – nur noch Aufräumen der playerBalances Map.
|
||||||
|
*
|
||||||
|
* Das Befüllen der Map geschieht ausschließlich durch die StatusAPIBridge
|
||||||
|
* (Spigot) die über Vault/NexEco den Kontostand per HTTP an die StatusAPI sendet.
|
||||||
|
*/
|
||||||
|
public class EconomyListener implements Listener {
|
||||||
|
|
||||||
|
public EconomyListener(Plugin plugin, EconomyManager manager) {
|
||||||
|
// EconomyManager wird nicht mehr benötigt
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onLogin(PostLoginEvent event) {
|
||||||
|
// Wird von StatusAPIBridge befüllt – nichts zu tun beim Login
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onDisconnect(PlayerDisconnectEvent event) {
|
||||||
|
// Beim Logout aus der Map entfernen
|
||||||
|
StatusAPI.playerBalances.remove(event.getPlayer().getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelTasks() {
|
||||||
|
// Kein periodischer Task mehr nötig
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package net.viper.status.modules.economy;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EconomyManager – Stub, nicht mehr aktiv.
|
||||||
|
* Economy wird ausschließlich über NexEco (Spigot) verwaltet.
|
||||||
|
*/
|
||||||
|
public class EconomyManager {
|
||||||
|
|
||||||
|
public EconomyManager(Plugin plugin, EconomyDatabase db, double startBalance) {}
|
||||||
|
|
||||||
|
public void saveNameMapping(UUID uuid, String name) {}
|
||||||
|
|
||||||
|
public UUID resolveUUID(String name) { return null; }
|
||||||
|
|
||||||
|
public double getBalance(UUID uuid) { return 0.0; }
|
||||||
|
|
||||||
|
public void setBalance(UUID uuid, double amount) {}
|
||||||
|
|
||||||
|
public boolean deposit(UUID uuid, double amount) { return false; }
|
||||||
|
|
||||||
|
public boolean withdraw(UUID uuid, double amount) { return false; }
|
||||||
|
|
||||||
|
public boolean transfer(UUID from, UUID to, double amount) { return false; }
|
||||||
|
|
||||||
|
public boolean hasAccount(UUID uuid) { return false; }
|
||||||
|
|
||||||
|
public double getStartBalance() { return 0.0; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package net.viper.status.modules.economy;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EconomyModule – DEAKTIVIERT.
|
||||||
|
*
|
||||||
|
* Die Economy wird ausschließlich über NexEco (Spigot) verwaltet.
|
||||||
|
* Die StatusAPIBridge (Spigot-Plugin) liest den Kontostand über Vault/NexEco
|
||||||
|
* und pushed ihn per HTTP an die StatusAPI → playerBalances Map.
|
||||||
|
*
|
||||||
|
* Damit gibt es nur EINE Datenquelle für Kontostände: NexEco / money_accounts.
|
||||||
|
* Das alte EconomyModule schrieb in bc_accounts – das führte zu doppelten,
|
||||||
|
* inkonsistenten Kontoständen.
|
||||||
|
*/
|
||||||
|
public class EconomyModule implements Module {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "EconomyModule"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
plugin.getLogger().info("[Economy] EconomyModule ist deaktiviert – NexEco ist zuständig.");
|
||||||
|
plugin.getLogger().info("[Economy] Kontostände kommen via StatusAPIBridge (Vault → NexEco → HTTP).");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {}
|
||||||
|
|
||||||
|
public EconomyManager getManager() { return null; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package net.viper.status.modules.economy;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /pay – wird NICHT mehr auf BungeeCord registriert.
|
||||||
|
* NexEco auf dem Spigot-Server übernimmt /pay direkt.
|
||||||
|
* Diese Klasse existiert nur noch für Kompilier-Kompatibilität.
|
||||||
|
*/
|
||||||
|
public class PayCommand extends Command {
|
||||||
|
|
||||||
|
public PayCommand(Plugin plugin, EconomyManager manager) {
|
||||||
|
super("pay_disabled_nexeco", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
package net.viper.status.modules.forum;
|
||||||
|
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.ClickEvent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.HoverEvent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
|
||||||
|
*
|
||||||
|
* Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zurück.
|
||||||
|
* Alle Aufrufer müssen nicht mehr auf null prüfen, was NullPointerExceptions verhindert.
|
||||||
|
*/
|
||||||
|
public class ForumBridgeModule implements Module, Listener {
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
private ForumNotifStorage storage;
|
||||||
|
|
||||||
|
private boolean enabled = true;
|
||||||
|
private String wpBaseUrl = "";
|
||||||
|
private String apiSecret = "";
|
||||||
|
private int loginDelaySeconds = 3;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ForumBridgeModule"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
loadConfig(plugin);
|
||||||
|
if (!enabled) { StatusAPI.debugLog(plugin, "ForumBridgeModule ist deaktiviert."); return; }
|
||||||
|
|
||||||
|
storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger());
|
||||||
|
storage.load();
|
||||||
|
|
||||||
|
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand());
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand());
|
||||||
|
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
|
try { storage.save(); } catch (Exception e) { plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage()); }
|
||||||
|
}, 10, 10, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, () -> storage.purgeOld(30), 1, 24, TimeUnit.HOURS);
|
||||||
|
plugin.getLogger().fine("ForumBridgeModule aktiviert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
if (storage != null) { storage.save(); StatusAPI.debugLog(plugin, "Forum-Benachrichtigungen gespeichert."); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig(Plugin plugin) {
|
||||||
|
try {
|
||||||
|
Properties props = new Properties();
|
||||||
|
File configFile = new File(plugin.getDataFolder(), "verify.properties");
|
||||||
|
if (configFile.exists()) {
|
||||||
|
try (FileInputStream fis = new FileInputStream(configFile)) { props.load(fis); }
|
||||||
|
}
|
||||||
|
this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true"));
|
||||||
|
this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", ""));
|
||||||
|
this.apiSecret = props.getProperty("forum.api_secret", "");
|
||||||
|
this.loginDelaySeconds = parseInt(props.getProperty("forum.login_delay_seconds", "3"), 3);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("Fehler beim Laden der ForumBridge-Config: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseInt(String s, int def) { try { return Integer.parseInt(s); } catch (Exception e) { return def; } }
|
||||||
|
|
||||||
|
// ===== HTTP HANDLER =====
|
||||||
|
|
||||||
|
public String handleNotify(String body, String apiKeyHeader) {
|
||||||
|
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) {
|
||||||
|
return "{\"success\":false,\"error\":\"unauthorized\"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIX #13: extractJsonString gibt "" statt null → kein NullPointerException möglich
|
||||||
|
String playerUuid = extractJsonString(body, "player_uuid");
|
||||||
|
String type = extractJsonString(body, "type");
|
||||||
|
String title = extractJsonString(body, "title");
|
||||||
|
String author = extractJsonString(body, "author");
|
||||||
|
String url = extractJsonString(body, "url");
|
||||||
|
|
||||||
|
if (playerUuid.isEmpty()) return "{\"success\":false,\"error\":\"missing_player_uuid\"}";
|
||||||
|
|
||||||
|
java.util.UUID uuid;
|
||||||
|
try { uuid = java.util.UUID.fromString(playerUuid); }
|
||||||
|
catch (Exception e) { return "{\"success\":false,\"error\":\"invalid_uuid\"}"; }
|
||||||
|
|
||||||
|
if ("thread".equalsIgnoreCase(type) && title.toLowerCase().contains("umfrage")) type = "poll";
|
||||||
|
if (type.isEmpty()) type = "reply";
|
||||||
|
|
||||||
|
// Alle Werte sind garantiert nicht null (extractJsonString gibt "" zurück)
|
||||||
|
ForumNotification notification = new ForumNotification(uuid, type, title, author, url);
|
||||||
|
|
||||||
|
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
|
||||||
|
if (online != null && online.isConnected()) {
|
||||||
|
deliverNotification(online, notification);
|
||||||
|
notification.setDelivered(true);
|
||||||
|
return "{\"success\":true,\"delivered\":true}";
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.add(notification);
|
||||||
|
return "{\"success\":true,\"delivered\":false}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String handleUnlink(String body, String apiKeyHeader) {
|
||||||
|
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) return "{\"success\":false,\"error\":\"unauthorized\"}";
|
||||||
|
return "{\"success\":true}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String handleStatus() {
|
||||||
|
String version = "unknown";
|
||||||
|
try { if (plugin.getDescription() != null) version = plugin.getDescription().getVersion(); } catch (Exception ignored) {}
|
||||||
|
return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== NOTIFICATION =====
|
||||||
|
|
||||||
|
private void deliverNotification(ProxiedPlayer player, ForumNotification notif) {
|
||||||
|
String color = notif.getTypeColor();
|
||||||
|
String label = notif.getTypeLabel();
|
||||||
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
|
player.sendMessage(new TextComponent("§6§l✉ Forum §8» " + color + label));
|
||||||
|
if (!notif.getTitle().isEmpty()) player.sendMessage(new TextComponent("§7 " + notif.getTitle()));
|
||||||
|
if (!notif.getAuthor().isEmpty()) player.sendMessage(new TextComponent("§7 von §f" + notif.getAuthor()));
|
||||||
|
if (!notif.getUrl().isEmpty()) {
|
||||||
|
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
|
||||||
|
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
|
||||||
|
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||||
|
new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create()));
|
||||||
|
player.sendMessage(link);
|
||||||
|
}
|
||||||
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deliverPending(ProxiedPlayer player) {
|
||||||
|
List<ForumNotification> pending = storage.getPending(player.getUniqueId());
|
||||||
|
if (pending.isEmpty()) return;
|
||||||
|
int count = pending.size();
|
||||||
|
if (count > 3) {
|
||||||
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
|
player.sendMessage(new TextComponent("§6§l✉ Forum §8» §fDu hast §e" + count + " §fneue Benachrichtigungen!"));
|
||||||
|
player.sendMessage(new TextComponent("§7 Tippe §e/forum §7um sie anzuzeigen."));
|
||||||
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
|
} else {
|
||||||
|
for (ForumNotification n : pending) deliverNotification(player, n);
|
||||||
|
}
|
||||||
|
storage.markAllDelivered(player.getUniqueId());
|
||||||
|
storage.clearDelivered(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onJoin(PostLoginEvent e) {
|
||||||
|
ProxiedPlayer player = e.getPlayer();
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
|
if (player.isConnected()) deliverPending(player);
|
||||||
|
}, loginDelaySeconds, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== COMMANDS =====
|
||||||
|
|
||||||
|
private class ForumLinkCommand extends Command {
|
||||||
|
public ForumLinkCommand() { super("forumlink", null, "fl"); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
|
||||||
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
|
if (args.length != 1) {
|
||||||
|
p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>"));
|
||||||
|
p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String token = args[0].trim().toUpperCase();
|
||||||
|
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert.")); return; }
|
||||||
|
p.sendMessage(new TextComponent("§7Überprüfe Token..."));
|
||||||
|
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
|
try {
|
||||||
|
String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link";
|
||||||
|
String payload = "{\"token\":\"" + escapeJson(token) + "\","
|
||||||
|
+ "\"mc_uuid\":\"" + p.getUniqueId() + "\","
|
||||||
|
+ "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}";
|
||||||
|
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(7000);
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
if (!apiSecret.isEmpty()) conn.setRequestProperty("X-Api-Key", apiSecret);
|
||||||
|
|
||||||
|
Charset utf8 = Charset.forName("UTF-8");
|
||||||
|
try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); }
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
String resp = code >= 200 && code < 300
|
||||||
|
? streamToString(conn.getInputStream(), utf8)
|
||||||
|
: streamToString(conn.getErrorStream(), utf8);
|
||||||
|
|
||||||
|
if (resp != null && resp.contains("\"success\":true")) {
|
||||||
|
String displayName = extractJsonString(resp, "display_name");
|
||||||
|
String username = extractJsonString(resp, "username");
|
||||||
|
String show = !displayName.isEmpty() ? displayName : username;
|
||||||
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
|
p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!"));
|
||||||
|
if (!show.isEmpty()) p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
|
||||||
|
p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen."));
|
||||||
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
|
} else {
|
||||||
|
String error = extractJsonString(resp, "error");
|
||||||
|
String message = extractJsonString(resp, "message");
|
||||||
|
if ("token_expired".equals(error)) p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen."));
|
||||||
|
else if ("uuid_already_linked".equals(error)) p.sendMessage(new TextComponent("§c✗ " + (!message.isEmpty() ? message : "Diese UUID ist bereits verknüpft.")));
|
||||||
|
else if ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ungültiger Token."));
|
||||||
|
else p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler")));
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
|
||||||
|
plugin.getLogger().warning("ForumLink Fehler: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ForumCommand extends Command {
|
||||||
|
public ForumCommand() { super("forum"); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
|
||||||
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
|
List<ForumNotification> pending = storage.getPending(p.getUniqueId());
|
||||||
|
|
||||||
|
if (pending.isEmpty()) {
|
||||||
|
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
|
||||||
|
if (!wpBaseUrl.isEmpty()) {
|
||||||
|
TextComponent link = new TextComponent("§a➜ Forum öffnen");
|
||||||
|
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
|
||||||
|
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
|
||||||
|
p.sendMessage(link);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
|
p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)"));
|
||||||
|
p.sendMessage(new TextComponent(""));
|
||||||
|
int shown = 0;
|
||||||
|
for (ForumNotification n : pending) {
|
||||||
|
if (shown >= 10) { p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere")); break; }
|
||||||
|
String color = n.getTypeColor();
|
||||||
|
TextComponent line = new TextComponent(color + " • " + n.getTypeLabel() + "§7: ");
|
||||||
|
TextComponent detail = new TextComponent(!n.getTitle().isEmpty() ? "§f" + n.getTitle() : "§fvon " + n.getAuthor());
|
||||||
|
if (!n.getUrl().isEmpty()) {
|
||||||
|
detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl()));
|
||||||
|
detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke zum Öffnen").create()));
|
||||||
|
}
|
||||||
|
line.addExtra(detail);
|
||||||
|
p.sendMessage(line);
|
||||||
|
shown++;
|
||||||
|
}
|
||||||
|
p.sendMessage(new TextComponent(""));
|
||||||
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
|
storage.markAllDelivered(p.getUniqueId());
|
||||||
|
storage.clearDelivered(p.getUniqueId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== HELPER =====
|
||||||
|
|
||||||
|
public ForumNotifStorage getStorage() { return storage; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIX #13: Gibt immer einen leeren String zurück, niemals null.
|
||||||
|
* Verhindert NullPointerExceptions in allen Aufrufern.
|
||||||
|
*/
|
||||||
|
private static String extractJsonString(String json, String key) {
|
||||||
|
if (json == null || key == null) return "";
|
||||||
|
String search = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(search);
|
||||||
|
if (idx < 0) return "";
|
||||||
|
int colon = json.indexOf(':', idx + search.length());
|
||||||
|
if (colon < 0) return "";
|
||||||
|
int i = colon + 1;
|
||||||
|
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
|
||||||
|
if (i >= json.length()) return "";
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '"') {
|
||||||
|
i++;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
boolean escape = false;
|
||||||
|
while (i < json.length()) {
|
||||||
|
char ch = json.charAt(i++);
|
||||||
|
if (escape) { sb.append(ch); escape = false; }
|
||||||
|
else { if (ch == '\\') escape = true; else if (ch == '"') break; else sb.append(ch); }
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String streamToString(InputStream in, Charset charset) throws IOException {
|
||||||
|
if (in == null) return "";
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
|
||||||
|
StringBuilder sb = new StringBuilder(); String line;
|
||||||
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package net.viper.status.modules.forum;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert ausstehende Forum-Benachrichtigungen (Datei-basiert).
|
||||||
|
* Benachrichtigungen die nicht sofort zugestellt werden konnten (Spieler offline)
|
||||||
|
* werden hier gespeichert und beim nächsten Login zugestellt.
|
||||||
|
*/
|
||||||
|
public class ForumNotifStorage {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
// UUID -> Liste ausstehender Notifications
|
||||||
|
private final ConcurrentHashMap<UUID, CopyOnWriteArrayList<ForumNotification>> pending = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public ForumNotifStorage(File pluginFolder, Logger logger) {
|
||||||
|
if (!pluginFolder.exists()) pluginFolder.mkdirs();
|
||||||
|
this.file = new File(pluginFolder, "forum_notifications.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt eine Benachrichtigung hinzu.
|
||||||
|
*/
|
||||||
|
public void add(ForumNotification notification) {
|
||||||
|
pending.computeIfAbsent(notification.getPlayerUuid(), k -> new CopyOnWriteArrayList<>())
|
||||||
|
.add(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt alle ausstehenden (nicht zugestellten) Benachrichtigungen eines Spielers zurück.
|
||||||
|
*/
|
||||||
|
public List<ForumNotification> getPending(UUID playerUuid) {
|
||||||
|
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||||
|
if (list == null) return Collections.emptyList();
|
||||||
|
List<ForumNotification> result = new ArrayList<>();
|
||||||
|
for (ForumNotification n : list) {
|
||||||
|
if (!n.isDelivered()) result.add(n);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anzahl ausstehender Benachrichtigungen.
|
||||||
|
*/
|
||||||
|
public int getPendingCount(UUID playerUuid) {
|
||||||
|
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||||
|
if (list == null) return 0;
|
||||||
|
int count = 0;
|
||||||
|
for (ForumNotification n : list) {
|
||||||
|
if (!n.isDelivered()) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert alle Benachrichtigungen eines Spielers als zugestellt und entfernt sie.
|
||||||
|
*/
|
||||||
|
public void clearDelivered(UUID playerUuid) {
|
||||||
|
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||||
|
if (list == null) return;
|
||||||
|
list.removeIf(ForumNotification::isDelivered);
|
||||||
|
if (list.isEmpty()) pending.remove(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markiert alle als zugestellt.
|
||||||
|
*/
|
||||||
|
public void markAllDelivered(UUID playerUuid) {
|
||||||
|
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||||
|
if (list == null) return;
|
||||||
|
for (ForumNotification n : list) {
|
||||||
|
n.setDelivered(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt Benachrichtigungen die älter als maxDays Tage sind.
|
||||||
|
*/
|
||||||
|
public void purgeOld(int maxDays) {
|
||||||
|
long cutoff = System.currentTimeMillis() - ((long) maxDays * 24 * 60 * 60 * 1000);
|
||||||
|
for (Map.Entry<UUID, CopyOnWriteArrayList<ForumNotification>> entry : pending.entrySet()) {
|
||||||
|
entry.getValue().removeIf(n -> n.getTimestamp() < cutoff);
|
||||||
|
if (entry.getValue().isEmpty()) pending.remove(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Datei-Operationen =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
for (CopyOnWriteArrayList<ForumNotification> list : pending.values()) {
|
||||||
|
for (ForumNotification n : list) {
|
||||||
|
if (!n.isDelivered()) {
|
||||||
|
bw.write(n.toLine());
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bw.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("Fehler beim Speichern der Forum-Benachrichtigungen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
if (!file.exists()) return;
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
ForumNotification n = ForumNotification.fromLine(line);
|
||||||
|
if (n != null && !n.isDelivered()) {
|
||||||
|
pending.computeIfAbsent(n.getPlayerUuid(), k -> new CopyOnWriteArrayList<>())
|
||||||
|
.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("Fehler beim Laden der Forum-Benachrichtigungen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package net.viper.status.modules.forum;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine einzelne Forum-Benachrichtigung.
|
||||||
|
*/
|
||||||
|
public class ForumNotification {
|
||||||
|
|
||||||
|
private final UUID playerUuid;
|
||||||
|
private final String type; // reply, mention, message
|
||||||
|
private final String title;
|
||||||
|
private final String author;
|
||||||
|
private final String url;
|
||||||
|
private final long timestamp;
|
||||||
|
private boolean delivered;
|
||||||
|
|
||||||
|
public ForumNotification(UUID playerUuid, String type, String title, String author, String url) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
this.type = type != null ? type : "reply";
|
||||||
|
this.title = title != null ? title : "";
|
||||||
|
this.author = author != null ? author : "Unbekannt";
|
||||||
|
this.url = url != null ? url : "";
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
this.delivered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interner Konstruktor für Deserialisierung */
|
||||||
|
ForumNotification(UUID playerUuid, String type, String title, String author, String url, long timestamp, boolean delivered) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
this.type = type;
|
||||||
|
this.title = title;
|
||||||
|
this.author = author;
|
||||||
|
this.url = url;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.delivered = delivered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Getter ---
|
||||||
|
|
||||||
|
public UUID getPlayerUuid() { return playerUuid; }
|
||||||
|
public String getType() { return type; }
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public String getAuthor() { return author; }
|
||||||
|
public String getUrl() { return url; }
|
||||||
|
public long getTimestamp() { return timestamp; }
|
||||||
|
public boolean isDelivered() { return delivered; }
|
||||||
|
public void setDelivered(boolean d) { this.delivered = d; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deutsches Label für den Benachrichtigungstyp.
|
||||||
|
*/
|
||||||
|
public String getTypeLabel() {
|
||||||
|
switch (type) {
|
||||||
|
case "reply": return "Neue Antwort";
|
||||||
|
case "mention": return "Erwähnung";
|
||||||
|
case "message": return "Neue PN";
|
||||||
|
case "thread": return "Neuer Thread";
|
||||||
|
case "poll": return "Neue Umfrage";
|
||||||
|
case "answer": return "Antwort auf deinen Thread";
|
||||||
|
default: return "Benachrichtigung";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Farbcode (Minecraft) je nach Typ.
|
||||||
|
*/
|
||||||
|
public String getTypeColor() {
|
||||||
|
switch (type) {
|
||||||
|
case "reply": return "§b"; // Aqua
|
||||||
|
case "mention": return "§e"; // Gelb
|
||||||
|
case "message": return "§d"; // Rosa
|
||||||
|
case "thread": return "§a"; // Grün
|
||||||
|
case "poll": return "§3"; // Dunkel-Aqua
|
||||||
|
case "answer": return "§2"; // Dunkel-Grün
|
||||||
|
default: return "§f"; // Weiß
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialisierung für Datei-Speicherung.
|
||||||
|
* Format: uuid|type|title|author|url|timestamp|delivered
|
||||||
|
*/
|
||||||
|
public String toLine() {
|
||||||
|
return playerUuid.toString() + "|"
|
||||||
|
+ type + "|"
|
||||||
|
+ title.replace("|", "_") + "|"
|
||||||
|
+ author.replace("|", "_") + "|"
|
||||||
|
+ url.replace("|", "_") + "|"
|
||||||
|
+ timestamp + "|"
|
||||||
|
+ (delivered ? "1" : "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialisierung aus einer Zeile.
|
||||||
|
*/
|
||||||
|
public static ForumNotification fromLine(String line) {
|
||||||
|
if (line == null || line.trim().isEmpty()) return null;
|
||||||
|
String[] parts = line.split("\\|", -1);
|
||||||
|
if (parts.length < 7) return null;
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(parts[0]);
|
||||||
|
String type = parts[1];
|
||||||
|
String title = parts[2];
|
||||||
|
String author = parts[3];
|
||||||
|
String url = parts[4];
|
||||||
|
long timestamp = Long.parseLong(parts[5]);
|
||||||
|
boolean delivered = "1".equals(parts[6]);
|
||||||
|
return new ForumNotification(uuid, type, title, author, url, timestamp, delivered);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,854 @@
|
|||||||
|
package net.viper.status.modules.network;
|
||||||
|
|
||||||
|
import com.sun.management.OperatingSystemMXBean;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.config.ServerInfo;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.api.scheduler.ScheduledTask;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert erweiterte Proxy- und Systeminformationen für API und Ingame-Debug.
|
||||||
|
*/
|
||||||
|
public class NetworkInfoModule implements Module {
|
||||||
|
|
||||||
|
private static final String CONFIG_FILE_NAME = "network-guard.properties";
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
private long startedAtMillis;
|
||||||
|
|
||||||
|
private boolean enabled = true;
|
||||||
|
private boolean commandEnabled = true;
|
||||||
|
private boolean includePlayerNames = false;
|
||||||
|
|
||||||
|
private boolean webhookEnabled = false;
|
||||||
|
private String webhookUrl = "";
|
||||||
|
private String webhookUsername = "StatusAPI";
|
||||||
|
private String webhookThumbnailUrl = "";
|
||||||
|
private boolean webhookNotifyStartStop = true;
|
||||||
|
private String webhookEmbedMode = "detailed";
|
||||||
|
private int webhookCheckSeconds = 30;
|
||||||
|
private int alertMemoryPercent = 90;
|
||||||
|
private int alertPlayerPercent = 95;
|
||||||
|
private int alertCooldownSeconds = 300;
|
||||||
|
private boolean alertTpsEnabled = true;
|
||||||
|
private double alertTpsThreshold = 18.0D;
|
||||||
|
private boolean attackNotificationsEnabled = true;
|
||||||
|
private String attackApiKey = "";
|
||||||
|
private String attackDefaultSource = "Viper-Network";
|
||||||
|
private long lastMemoryAlertAt = 0L;
|
||||||
|
private long lastPlayerAlertAt = 0L;
|
||||||
|
private long lastTpsAlertAt = 0L;
|
||||||
|
private volatile double currentProxyTps = 20.0D;
|
||||||
|
|
||||||
|
/** FIX: Öffentlicher Getter damit ScoreboardModule als TPS-Fallback darauf zugreifen kann */
|
||||||
|
public double getProxyTps() { return currentProxyTps; }
|
||||||
|
private long lastTpsSampleAtMs = 0L;
|
||||||
|
private ScheduledTask alertTask;
|
||||||
|
private ScheduledTask tpsSamplerTask;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "NetworkInfoModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.startedAtMillis = System.currentTimeMillis();
|
||||||
|
ensureModuleConfigExists();
|
||||||
|
loadConfig();
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandEnabled) {
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new NetInfoCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
tpsSamplerTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::sampleProxyTps, 1, 1, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (webhookEnabled && !webhookUrl.isEmpty()) {
|
||||||
|
if (webhookNotifyStartStop) {
|
||||||
|
boolean delivered = sendLifecycleStartNotification();
|
||||||
|
if (!delivered) {
|
||||||
|
plugin.getLogger().warning("[NetworkInfoModule] Start-Webhook konnte nicht direkt zugestellt werden. Wiederhole in 10 Sekunden.");
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
|
boolean retryDelivered = sendLifecycleStartNotification();
|
||||||
|
if (!retryDelivered) {
|
||||||
|
plugin.getLogger().warning("[NetworkInfoModule] Start-Webhook auch beim zweiten Versuch fehlgeschlagen.");
|
||||||
|
}
|
||||||
|
}, 10L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int interval = Math.max(10, webhookCheckSeconds);
|
||||||
|
alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
if (alertTask != null) {
|
||||||
|
alertTask.cancel();
|
||||||
|
alertTask = null;
|
||||||
|
}
|
||||||
|
if (tpsSamplerTask != null) {
|
||||||
|
tpsSamplerTask.cancel();
|
||||||
|
tpsSamplerTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled && webhookEnabled && webhookNotifyStartStop && webhookUrl != null && !webhookUrl.isEmpty()) {
|
||||||
|
boolean delivered = sendLifecycleStopNotification();
|
||||||
|
if (!delivered) {
|
||||||
|
plugin.getLogger().warning("[NetworkInfoModule] Stop-Webhook konnte nicht zugestellt werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendLifecycleStartNotification() {
|
||||||
|
if (isCompactEmbedMode()) {
|
||||||
|
return sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"✅ NetworkInfo gestartet",
|
||||||
|
"Proxy: **" + ProxyServer.getInstance().getName() + "**\nÜberwachung und Webhook-Alerts sind jetzt aktiv.",
|
||||||
|
0x2ECC71,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder fields = new StringBuilder();
|
||||||
|
appendEmbedField(fields, "Proxy", ProxyServer.getInstance().getName(), true);
|
||||||
|
appendEmbedField(fields, "Modus", "Detailed", true);
|
||||||
|
appendEmbedField(fields, "Check-Intervall", Math.max(10, webhookCheckSeconds) + "s", true);
|
||||||
|
return sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"✅ NetworkInfo gestartet",
|
||||||
|
"Überwachung und Webhook-Alerts sind jetzt aktiv.",
|
||||||
|
0x2ECC71,
|
||||||
|
fields.toString(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendLifecycleStopNotification() {
|
||||||
|
if (isCompactEmbedMode()) {
|
||||||
|
return sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"🛑 NetworkInfo gestoppt",
|
||||||
|
"Die NetworkInfo-Überwachung wurde gestoppt.\nKeine weiteren Auto-Alerts bis zum nächsten Start.",
|
||||||
|
0xE74C3C,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder fields = new StringBuilder();
|
||||||
|
appendEmbedField(fields, "Proxy", ProxyServer.getInstance().getName(), true);
|
||||||
|
appendEmbedField(fields, "Modus", "Detailed", true);
|
||||||
|
appendEmbedField(fields, "Status", "Monitoring pausiert", false);
|
||||||
|
return sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"🛑 NetworkInfo gestoppt",
|
||||||
|
"Die NetworkInfo-Überwachung wurde gestoppt.",
|
||||||
|
0xE74C3C,
|
||||||
|
fields.toString(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAttackNotificationsEnabled() {
|
||||||
|
return enabled && attackNotificationsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAttackApiKeyValid(String providedKey) {
|
||||||
|
if (attackApiKey == null || attackApiKey.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return providedKey != null && attackApiKey.equals(providedKey.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sendAttackNotification(String eventType,
|
||||||
|
Integer connectionsPerSecond,
|
||||||
|
Integer blockedIps,
|
||||||
|
Long blockedConnections,
|
||||||
|
String source) {
|
||||||
|
if (!isAttackNotificationsEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String usedSource = (source == null || source.trim().isEmpty()) ? attackDefaultSource : source.trim();
|
||||||
|
String type = eventType == null ? "detected" : eventType.trim().toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
|
String title;
|
||||||
|
String shortText;
|
||||||
|
int color;
|
||||||
|
if ("stopped".equals(type) || "mitigated".equals(type)) {
|
||||||
|
title = "✅ Attack Stopped";
|
||||||
|
shortText = "Traffic hat sich normalisiert.";
|
||||||
|
color = 0x2ECC71;
|
||||||
|
} else {
|
||||||
|
title = "🚨 Attack Detected";
|
||||||
|
shortText = "Ungewöhnlich hoher Verbindungs-Traffic erkannt.";
|
||||||
|
color = 0xE74C3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder fields = new StringBuilder();
|
||||||
|
appendEmbedField(fields, "Source", usedSource, true);
|
||||||
|
appendEmbedField(fields, "Event", type.toUpperCase(Locale.ROOT), true);
|
||||||
|
|
||||||
|
if (connectionsPerSecond != null && connectionsPerSecond >= 0) {
|
||||||
|
appendEmbedField(fields, "Connections / Second", String.valueOf(connectionsPerSecond), true);
|
||||||
|
}
|
||||||
|
if (blockedIps != null && blockedIps >= 0) {
|
||||||
|
appendEmbedField(fields, "Blocked IPs", String.valueOf(blockedIps), true);
|
||||||
|
}
|
||||||
|
if (blockedConnections != null && blockedConnections >= 0L) {
|
||||||
|
appendEmbedField(fields, "Blocked Connections", String.valueOf(blockedConnections), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWebhookAttackEmbed(webhookUrl, title, shortText, color, fields.toString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> buildSnapshot() {
|
||||||
|
Map<String, Object> out = new LinkedHashMap<String, Object>();
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long uptimeMs = Math.max(0L, now - startedAtMillis);
|
||||||
|
|
||||||
|
Runtime rt = Runtime.getRuntime();
|
||||||
|
long maxMemory = rt.maxMemory();
|
||||||
|
long totalMemory = rt.totalMemory();
|
||||||
|
long freeMemory = rt.freeMemory();
|
||||||
|
long usedMemory = totalMemory - freeMemory;
|
||||||
|
|
||||||
|
Map<String, Object> memory = new LinkedHashMap<String, Object>();
|
||||||
|
memory.put("used_mb", bytesToMb(usedMemory));
|
||||||
|
memory.put("free_mb", bytesToMb(freeMemory));
|
||||||
|
memory.put("total_mb", bytesToMb(totalMemory));
|
||||||
|
memory.put("max_mb", bytesToMb(maxMemory));
|
||||||
|
memory.put("usage_percent", percent(usedMemory, Math.max(1L, maxMemory)));
|
||||||
|
|
||||||
|
int onlinePlayers = ProxyServer.getInstance().getPlayers().size();
|
||||||
|
int maxPlayers = ProxyServer.getInstance().getConfig().getPlayerLimit();
|
||||||
|
|
||||||
|
// getPlayerLimit() liefert -1 wenn kein globales Limit gesetzt ist.
|
||||||
|
// In diesem Fall den Listener-Wert (angezeigte Max-Spielerzahl im Server-Ping) nutzen.
|
||||||
|
if (maxPlayers <= 0) {
|
||||||
|
try {
|
||||||
|
java.util.Iterator<net.md_5.bungee.api.config.ListenerInfo> listenerIt =
|
||||||
|
ProxyServer.getInstance().getConfig().getListeners().iterator();
|
||||||
|
if (listenerIt.hasNext()) {
|
||||||
|
int listenerMax = listenerIt.next().getMaxPlayers();
|
||||||
|
if (listenerMax > 0) {
|
||||||
|
maxPlayers = listenerMax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> ping = buildPingSummary(ProxyServer.getInstance().getPlayers());
|
||||||
|
|
||||||
|
Map<String, Object> players = new LinkedHashMap<String, Object>();
|
||||||
|
players.put("online", onlinePlayers);
|
||||||
|
players.put("max", maxPlayers);
|
||||||
|
players.put("occupancy_percent", percent(onlinePlayers, Math.max(1, maxPlayers)));
|
||||||
|
players.put("bedrock_online", countBedrockPlayers());
|
||||||
|
players.put("ping", ping);
|
||||||
|
|
||||||
|
List<Map<String, Object>> backend = buildBackendDistribution();
|
||||||
|
|
||||||
|
Map<String, Object> system = new LinkedHashMap<String, Object>();
|
||||||
|
system.put("java_version", System.getProperty("java.version"));
|
||||||
|
system.put("java_vendor", System.getProperty("java.vendor"));
|
||||||
|
system.put("os_name", System.getProperty("os.name"));
|
||||||
|
system.put("os_arch", System.getProperty("os.arch"));
|
||||||
|
system.put("available_processors", Runtime.getRuntime().availableProcessors());
|
||||||
|
system.put("system_load_percent", getSystemLoadPercent());
|
||||||
|
system.put("proxy_tps", roundDouble(currentProxyTps, 2));
|
||||||
|
|
||||||
|
out.put("enabled", true);
|
||||||
|
out.put("timestamp_unix", now / 1000L);
|
||||||
|
out.put("uptime_seconds", uptimeMs / 1000L);
|
||||||
|
out.put("uptime_human", formatDuration(uptimeMs));
|
||||||
|
out.put("players", players);
|
||||||
|
out.put("backend_servers", backend);
|
||||||
|
out.put("memory", memory);
|
||||||
|
out.put("system", system);
|
||||||
|
out.put("features", buildFeatureSummary());
|
||||||
|
|
||||||
|
if (includePlayerNames) {
|
||||||
|
List<String> names = new ArrayList<String>();
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
names.add(p.getName());
|
||||||
|
}
|
||||||
|
out.put("player_names", names);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildPingSummary(Collection<ProxiedPlayer> players) {
|
||||||
|
Map<String, Object> ping = new LinkedHashMap<String, Object>();
|
||||||
|
if (players.isEmpty()) {
|
||||||
|
ping.put("avg_ms", 0);
|
||||||
|
ping.put("min_ms", 0);
|
||||||
|
ping.put("max_ms", 0);
|
||||||
|
return ping;
|
||||||
|
}
|
||||||
|
|
||||||
|
long sum = 0L;
|
||||||
|
int min = Integer.MAX_VALUE;
|
||||||
|
int max = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
for (ProxiedPlayer p : players) {
|
||||||
|
int ms = Math.max(0, p.getPing());
|
||||||
|
sum += ms;
|
||||||
|
if (ms < min) min = ms;
|
||||||
|
if (ms > max) max = ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
ping.put("avg_ms", Math.round((double) sum / (double) players.size()));
|
||||||
|
ping.put("min_ms", min == Integer.MAX_VALUE ? 0 : min);
|
||||||
|
ping.put("max_ms", max == Integer.MIN_VALUE ? 0 : max);
|
||||||
|
return ping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> buildBackendDistribution() {
|
||||||
|
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (Map.Entry<String, ServerInfo> entry : ProxyServer.getInstance().getServers().entrySet()) {
|
||||||
|
ServerInfo info = entry.getValue();
|
||||||
|
|
||||||
|
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
||||||
|
row.put("name", entry.getKey());
|
||||||
|
row.put("online_players", info.getPlayers().size());
|
||||||
|
row.put("address", String.valueOf(info.getAddress()));
|
||||||
|
list.add(row);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildFeatureSummary() {
|
||||||
|
Map<String, Object> features = new LinkedHashMap<String, Object>();
|
||||||
|
features.put("luckperms", ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null);
|
||||||
|
features.put("floodgate", isFloodgateAvailable());
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFloodgateAvailable() {
|
||||||
|
try {
|
||||||
|
Class.forName("org.geysermc.floodgate.api.FloodgateApi");
|
||||||
|
return true;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countBedrockPlayers() {
|
||||||
|
try {
|
||||||
|
Class<?> apiClass = Class.forName("org.geysermc.floodgate.api.FloodgateApi");
|
||||||
|
Object api = apiClass.getMethod("getInstance").invoke(null);
|
||||||
|
int count = 0;
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
Boolean isBedrock = (Boolean) api.getClass().getMethod("isBedrockPlayer", UUID.class).invoke(api, p.getUniqueId());
|
||||||
|
if (Boolean.TRUE.equals(isBedrock)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer getSystemLoadPercent() {
|
||||||
|
try {
|
||||||
|
java.lang.management.OperatingSystemMXBean bean = ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
if (bean instanceof OperatingSystemMXBean) {
|
||||||
|
double load = ((OperatingSystemMXBean) bean).getSystemCpuLoad();
|
||||||
|
if (load >= 0D) {
|
||||||
|
return (int) Math.round(load * 100D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int bytesToMb(long bytes) {
|
||||||
|
return (int) (bytes / (1024L * 1024L));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int percent(long value, long max) {
|
||||||
|
if (max <= 0L) return 0;
|
||||||
|
return (int) Math.min(100L, Math.round((value * 100.0D) / max));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDuration(long ms) {
|
||||||
|
long totalSeconds = ms / 1000L;
|
||||||
|
long days = totalSeconds / 86400L;
|
||||||
|
long hours = (totalSeconds % 86400L) / 3600L;
|
||||||
|
long minutes = (totalSeconds % 3600L) / 60L;
|
||||||
|
long seconds = totalSeconds % 60L;
|
||||||
|
return String.format(Locale.ROOT, "%dd %02dh %02dm %02ds", days, hours, minutes, seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
|
||||||
|
if (!file.exists()) {
|
||||||
|
enabled = true;
|
||||||
|
commandEnabled = true;
|
||||||
|
includePlayerNames = false;
|
||||||
|
webhookEnabled = false;
|
||||||
|
webhookUrl = "";
|
||||||
|
webhookUsername = "StatusAPI";
|
||||||
|
webhookThumbnailUrl = "";
|
||||||
|
webhookNotifyStartStop = true;
|
||||||
|
webhookEmbedMode = "detailed";
|
||||||
|
webhookCheckSeconds = 30;
|
||||||
|
alertMemoryPercent = 90;
|
||||||
|
alertPlayerPercent = 95;
|
||||||
|
alertCooldownSeconds = 300;
|
||||||
|
alertTpsEnabled = true;
|
||||||
|
alertTpsThreshold = 18.0D;
|
||||||
|
attackNotificationsEnabled = true;
|
||||||
|
attackApiKey = "";
|
||||||
|
attackDefaultSource = "BetterBungee";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Properties props = new Properties();
|
||||||
|
try (FileInputStream in = new FileInputStream(file)) {
|
||||||
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
|
enabled = Boolean.parseBoolean(props.getProperty("networkinfo.enabled", "true"));
|
||||||
|
commandEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.command.enabled", "true"));
|
||||||
|
includePlayerNames = Boolean.parseBoolean(props.getProperty("networkinfo.include_player_names", "false"));
|
||||||
|
|
||||||
|
webhookEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.webhook.enabled", "false"));
|
||||||
|
webhookUrl = props.getProperty("networkinfo.webhook.url", "").trim();
|
||||||
|
webhookUsername = props.getProperty("networkinfo.webhook.username", "StatusAPI").trim();
|
||||||
|
webhookThumbnailUrl = props.getProperty("networkinfo.webhook.thumbnail_url", "").trim();
|
||||||
|
webhookNotifyStartStop = Boolean.parseBoolean(props.getProperty("networkinfo.webhook.notify_start_stop", "true"));
|
||||||
|
webhookEmbedMode = props.getProperty("networkinfo.webhook.embed_mode", "detailed").trim();
|
||||||
|
webhookCheckSeconds = parseInt(props.getProperty("networkinfo.webhook.check_seconds", "30"), 30);
|
||||||
|
alertMemoryPercent = parseInt(props.getProperty("networkinfo.alert.memory_percent", "90"), 90);
|
||||||
|
alertPlayerPercent = parseInt(props.getProperty("networkinfo.alert.player_percent", "95"), 95);
|
||||||
|
alertCooldownSeconds = parseInt(props.getProperty("networkinfo.alert.cooldown_seconds", "300"), 300);
|
||||||
|
alertTpsEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.alert.tps_enabled", "true"));
|
||||||
|
alertTpsThreshold = parseDouble(props.getProperty("networkinfo.alert.tps_threshold", "18.0"), 18.0D);
|
||||||
|
attackNotificationsEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.attack.enabled", "true"));
|
||||||
|
attackApiKey = props.getProperty("networkinfo.attack.api_key", "").trim();
|
||||||
|
attackDefaultSource = props.getProperty("networkinfo.attack.source", "BetterBungee").trim();
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[NetworkInfoModule] Fehler beim Laden von " + CONFIG_FILE_NAME + ": " + e.getMessage());
|
||||||
|
enabled = true;
|
||||||
|
commandEnabled = true;
|
||||||
|
includePlayerNames = false;
|
||||||
|
webhookEnabled = false;
|
||||||
|
webhookUrl = "";
|
||||||
|
webhookUsername = "StatusAPI";
|
||||||
|
webhookThumbnailUrl = "";
|
||||||
|
webhookNotifyStartStop = true;
|
||||||
|
webhookEmbedMode = "detailed";
|
||||||
|
webhookCheckSeconds = 30;
|
||||||
|
alertMemoryPercent = 90;
|
||||||
|
alertPlayerPercent = 95;
|
||||||
|
alertCooldownSeconds = 300;
|
||||||
|
alertTpsEnabled = true;
|
||||||
|
alertTpsThreshold = 18.0D;
|
||||||
|
attackNotificationsEnabled = true;
|
||||||
|
attackApiKey = "";
|
||||||
|
attackDefaultSource = "BetterBungee";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureModuleConfigExists() {
|
||||||
|
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
|
||||||
|
if (target.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plugin.getDataFolder().exists()) {
|
||||||
|
plugin.getDataFolder().mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME);
|
||||||
|
OutputStream out = new FileOutputStream(target)) {
|
||||||
|
if (in == null) {
|
||||||
|
plugin.getLogger().warning("[NetworkInfoModule] Standarddatei " + CONFIG_FILE_NAME + " nicht im JAR gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int read;
|
||||||
|
while ((read = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evaluateAndSendAlerts() {
|
||||||
|
if (!enabled || !webhookEnabled || webhookUrl == null || webhookUrl.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Map<String, Object> snapshot = buildSnapshot();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> memory = (Map<String, Object>) snapshot.get("memory");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> players = (Map<String, Object>) snapshot.get("players");
|
||||||
|
|
||||||
|
int memoryPercent = toInt(memory.get("usage_percent"));
|
||||||
|
int playerPercent = toInt(players.get("occupancy_percent"));
|
||||||
|
double proxyTps = currentProxyTps;
|
||||||
|
|
||||||
|
if (memoryPercent >= Math.max(1, alertMemoryPercent) && canSend(lastMemoryAlertAt, now)) {
|
||||||
|
lastMemoryAlertAt = now;
|
||||||
|
if (isCompactEmbedMode()) {
|
||||||
|
sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"⚠️ Hohe RAM-Auslastung",
|
||||||
|
"Aktuell: **" + memoryPercent + "%**\nVerbrauch: **" + memory.get("used_mb") + " MB / " + memory.get("max_mb") + " MB**\nSchwelle: **" + alertMemoryPercent + "%**",
|
||||||
|
0xF39C12
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
StringBuilder fields = new StringBuilder();
|
||||||
|
appendEmbedField(fields, "RAM-Nutzung", memoryPercent + "%", true);
|
||||||
|
appendEmbedField(fields, "Schwelle", alertMemoryPercent + "%", true);
|
||||||
|
appendEmbedField(fields, "Verbrauch", memory.get("used_mb") + " MB / " + memory.get("max_mb") + " MB", false);
|
||||||
|
sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"⚠️ Hohe RAM-Auslastung",
|
||||||
|
"Ein Schwellwert wurde überschritten.",
|
||||||
|
0xF39C12,
|
||||||
|
fields.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerPercent >= Math.max(1, alertPlayerPercent) && canSend(lastPlayerAlertAt, now)) {
|
||||||
|
lastPlayerAlertAt = now;
|
||||||
|
if (isCompactEmbedMode()) {
|
||||||
|
sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"📈 Hohe Spieler-Auslastung",
|
||||||
|
"Auslastung: **" + playerPercent + "%**\nSpieler: **" + players.get("online") + "/" + players.get("max") + "**\nSchwelle: **" + alertPlayerPercent + "%**",
|
||||||
|
0x3498DB
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
StringBuilder fields = new StringBuilder();
|
||||||
|
appendEmbedField(fields, "Auslastung", playerPercent + "%", true);
|
||||||
|
appendEmbedField(fields, "Schwelle", alertPlayerPercent + "%", true);
|
||||||
|
appendEmbedField(fields, "Spieler", players.get("online") + "/" + players.get("max"), true);
|
||||||
|
sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"📈 Hohe Spieler-Auslastung",
|
||||||
|
"Die Spielerlast ist aktuell sehr hoch.",
|
||||||
|
0x3498DB,
|
||||||
|
fields.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertTpsEnabled && proxyTps > 0D && proxyTps < Math.max(1D, alertTpsThreshold) && canSend(lastTpsAlertAt, now)) {
|
||||||
|
lastTpsAlertAt = now;
|
||||||
|
String tpsText = String.format(Locale.ROOT, "%.2f", proxyTps);
|
||||||
|
String thresholdText = String.format(Locale.ROOT, "%.2f", Math.max(1D, alertTpsThreshold));
|
||||||
|
|
||||||
|
if (isCompactEmbedMode()) {
|
||||||
|
sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"🟥 Niedrige Proxy-TPS",
|
||||||
|
"Aktuell: **" + tpsText + " TPS**\nSchwelle: **" + thresholdText + " TPS**",
|
||||||
|
0xE74C3C
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
StringBuilder fields = new StringBuilder();
|
||||||
|
appendEmbedField(fields, "Proxy TPS", tpsText, true);
|
||||||
|
appendEmbedField(fields, "Schwelle", thresholdText, true);
|
||||||
|
appendEmbedField(fields, "Check-Intervall", Math.max(10, webhookCheckSeconds) + "s", true);
|
||||||
|
sendWebhookEmbed(
|
||||||
|
webhookUrl,
|
||||||
|
"🟥 Niedrige Proxy-TPS",
|
||||||
|
"Die gemessene Proxy-TPS liegt unter der konfigurierten Schwelle.",
|
||||||
|
0xE74C3C,
|
||||||
|
fields.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sampleProxyTps() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (lastTpsSampleAtMs <= 0L) {
|
||||||
|
lastTpsSampleAtMs = now;
|
||||||
|
currentProxyTps = 20.0D;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long deltaMs = now - lastTpsSampleAtMs;
|
||||||
|
lastTpsSampleAtMs = now;
|
||||||
|
if (deltaMs <= 0L) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1s Scheduler-Tick sollte etwa 20 TPS entsprechen. Abweichung zeigt Main-Thread-Lag.
|
||||||
|
double instantTps = (1000.0D / (double) deltaMs) * 20.0D;
|
||||||
|
instantTps = Math.max(0.1D, Math.min(20.0D, instantTps));
|
||||||
|
currentProxyTps = (currentProxyTps * 0.7D) + (instantTps * 0.3D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCompactEmbedMode() {
|
||||||
|
return "compact".equalsIgnoreCase(webhookEmbedMode == null ? "" : webhookEmbedMode.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canSend(long lastSentAt, long now) {
|
||||||
|
long cooldownMs = Math.max(10, alertCooldownSeconds) * 1000L;
|
||||||
|
return (now - lastSentAt) >= cooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseInt(String s, int fallback) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(s == null ? "" : s.trim());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double parseDouble(String s, double fallback) {
|
||||||
|
try {
|
||||||
|
return Double.parseDouble(s == null ? "" : s.trim());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int toInt(Object o) {
|
||||||
|
if (o instanceof Number) {
|
||||||
|
return ((Number) o).intValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(String.valueOf(o));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double roundDouble(double value, int digits) {
|
||||||
|
double factor = Math.pow(10D, Math.max(0, digits));
|
||||||
|
return Math.round(value * factor) / factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color) {
|
||||||
|
return sendWebhookEmbed(targetWebhookUrl, title, description, color, null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color, String fieldsJson) {
|
||||||
|
return sendWebhookEmbed(targetWebhookUrl, title, description, color, fieldsJson, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color, String fieldsJson, boolean async) {
|
||||||
|
if (targetWebhookUrl == null || targetWebhookUrl.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder embed = new StringBuilder();
|
||||||
|
embed.append("{\"username\":\"").append(escapeJson(webhookUsername)).append("\",")
|
||||||
|
.append("\"embeds\":[{\"title\":\"").append(escapeJson(title)).append("\",")
|
||||||
|
.append("\"description\":\"").append(escapeJson(description)).append("\",")
|
||||||
|
.append("\"color\":").append(color).append(",");
|
||||||
|
|
||||||
|
if (fieldsJson != null && !fieldsJson.trim().isEmpty()) {
|
||||||
|
embed.append("\"fields\":[").append(fieldsJson).append("],");
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.append("\"footer\":{\"text\":\"StatusPulse • NetworkInfo\"},")
|
||||||
|
.append("\"timestamp\":\"").append(Instant.now().toString()).append("\"");
|
||||||
|
|
||||||
|
if (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()) {
|
||||||
|
embed.append(",\"thumbnail\":{\"url\":\"").append(escapeJson(webhookThumbnailUrl)).append("\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.append("}]}");
|
||||||
|
return postWebhookPayload(targetWebhookUrl, embed.toString(), async);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendWebhookAttackEmbed(String targetWebhookUrl,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
int color,
|
||||||
|
String fieldsJson) {
|
||||||
|
if (targetWebhookUrl == null || targetWebhookUrl.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder embed = new StringBuilder();
|
||||||
|
embed.append("{\"username\":\"").append(escapeJson(webhookUsername)).append("\",")
|
||||||
|
.append("\"embeds\":[{\"title\":\"").append(escapeJson(title)).append("\",")
|
||||||
|
.append("\"description\":\"").append(escapeJson(description)).append("\",")
|
||||||
|
.append("\"color\":").append(color).append(",")
|
||||||
|
.append("\"fields\":[").append(fieldsJson).append("],")
|
||||||
|
.append("\"footer\":{\"text\":\"StatusPulse • Network Guard\"},")
|
||||||
|
.append("\"timestamp\":\"").append(Instant.now().toString()).append("\"");
|
||||||
|
|
||||||
|
if (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()) {
|
||||||
|
embed.append(",\"thumbnail\":{\"url\":\"").append(escapeJson(webhookThumbnailUrl)).append("\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.append("}]}");
|
||||||
|
postWebhookPayload(targetWebhookUrl, embed.toString(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendEmbedField(StringBuilder out, String name, String value, boolean inline) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (out.length() > 0) {
|
||||||
|
out.append(",");
|
||||||
|
}
|
||||||
|
out.append("{\"name\":\"").append(escapeJson(name)).append("\",")
|
||||||
|
.append("\"value\":\"").append(escapeJson(value.trim())).append("\",")
|
||||||
|
.append("\"inline\":").append(inline ? "true" : "false")
|
||||||
|
.append("}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean postWebhookPayload(String targetWebhookUrl, String payload, boolean async) {
|
||||||
|
if (async) {
|
||||||
|
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> executeWebhookPost(targetWebhookUrl, payload));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return executeWebhookPost(targetWebhookUrl, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean executeWebhookPost(String targetWebhookUrl, String payload) {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
byte[] bytes = payload.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
conn = (HttpURLConnection) new URL(targetWebhookUrl).openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(8000);
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
conn.setRequestProperty("Content-Length", String.valueOf(bytes.length));
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code >= 200 && code < 300) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().warning("[NetworkInfoModule] Discord Webhook HTTP " + code + ": " + readErrorBody(conn));
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[NetworkInfoModule] Discord Webhook Fehler: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (conn != null) {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readErrorBody(HttpURLConnection conn) {
|
||||||
|
if (conn == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try (InputStream errorStream = conn.getErrorStream()) {
|
||||||
|
if (errorStream == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
byte[] bytes = new byte[1024];
|
||||||
|
int read = errorStream.read(bytes);
|
||||||
|
if (read <= 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return new String(bytes, 0, read, StandardCharsets.UTF_8).trim();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NetInfoCommand extends Command {
|
||||||
|
|
||||||
|
NetInfoCommand() {
|
||||||
|
super("netinfo", "statusapi.netinfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
if (!enabled) {
|
||||||
|
sender.sendMessage(ChatColor.RED + "NetworkInfoModule ist deaktiviert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> snapshot = buildSnapshot();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> players = (Map<String, Object>) snapshot.get("players");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> memory = (Map<String, Object>) snapshot.get("memory");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> system = (Map<String, Object>) snapshot.get("system");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> ping = (Map<String, Object>) players.get("ping");
|
||||||
|
|
||||||
|
sender.sendMessage(ChatColor.GOLD + "----- StatusAPI NetworkInfo -----");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Uptime: " + ChatColor.WHITE + snapshot.get("uptime_human"));
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Spieler: " + ChatColor.WHITE + players.get("online") + "/" + players.get("max") + ChatColor.GRAY + " (Bedrock: " + players.get("bedrock_online") + ")");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Ping: " + ChatColor.WHITE + "avg " + ping.get("avg_ms") + "ms, min " + ping.get("min_ms") + "ms, max " + ping.get("max_ms") + "ms");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "RAM: " + ChatColor.WHITE + memory.get("used_mb") + "MB / " + memory.get("max_mb") + "MB" + ChatColor.GRAY + " (" + memory.get("usage_percent") + "%)");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Proxy TPS: " + ChatColor.WHITE + system.get("proxy_tps") + ChatColor.GRAY + " (Alert < " + String.format(Locale.ROOT, "%.2f", alertTpsThreshold) + ")");
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "Backends: " + ChatColor.WHITE + ((List<?>) snapshot.get("backend_servers")).size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,287 @@
|
|||||||
|
package net.viper.status.modules.serverswitcher;
|
||||||
|
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.ClickEvent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.HoverEvent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.config.ServerInfo;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.event.TabCompleteEvent;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class ServerSwitcherModule implements Module {
|
||||||
|
|
||||||
|
private static final String CONFIG_FILE = "serverswitcher.properties";
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
private boolean enabled = true;
|
||||||
|
private String permission = "serverswitcher.use";
|
||||||
|
private String commandName = "go";
|
||||||
|
private List<String> aliases = new ArrayList<>(Arrays.asList("wechsel", "switch"));
|
||||||
|
private List<String> serverWhitelist = new ArrayList<>();
|
||||||
|
|
||||||
|
private String colorHeader = "&8&m---&r &6&lServer-Menü &8&m---";
|
||||||
|
private String colorEntry = "&7>> &e";
|
||||||
|
private String colorOnline = "&a";
|
||||||
|
private String colorOffline = "&c";
|
||||||
|
private String colorSelf = "&7(Aktuell)";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "ServerSwitcherModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
ensureConfigExists();
|
||||||
|
loadConfig();
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
StatusAPI.debugLog(plugin, "[ServerSwitcherModule] Deaktiviert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] aliasArray = aliases.toArray(new String[0]);
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin,
|
||||||
|
new GoCommand(commandName, permission, aliasArray));
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerListener(plugin,
|
||||||
|
new GoTabListener());
|
||||||
|
|
||||||
|
plugin.getLogger().fine("[ServerSwitcherModule] Aktiviert. Command: /" + commandName
|
||||||
|
+ " | Aliases: " + aliases + " | Permission: " + permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureConfigExists() {
|
||||||
|
File target = new File(plugin.getDataFolder(), CONFIG_FILE);
|
||||||
|
if (target.exists()) return;
|
||||||
|
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
|
||||||
|
|
||||||
|
String defaults =
|
||||||
|
"# ServerSwitcherModule Konfiguration\n" +
|
||||||
|
"serverswitcher.enabled=true\n\n" +
|
||||||
|
"serverswitcher.command=go\n" +
|
||||||
|
"serverswitcher.aliases=wechsel,switch\n" +
|
||||||
|
"serverswitcher.permission=serverswitcher.use\n\n" +
|
||||||
|
"# Optionale Whitelist (leer = alle BungeeCord-Server)\n" +
|
||||||
|
"# Beispiel: serverswitcher.servers=lobby,citybuild,survival\n" +
|
||||||
|
"serverswitcher.servers=\n\n" +
|
||||||
|
"serverswitcher.color.header=&8&m---&r &6&lServer-Menü &8&m---\n" +
|
||||||
|
"serverswitcher.color.entry=&7>> &e\n" +
|
||||||
|
"serverswitcher.color.online=&a\n" +
|
||||||
|
"serverswitcher.color.offline=&c\n" +
|
||||||
|
"serverswitcher.color.self=&7(Aktuell)\n";
|
||||||
|
|
||||||
|
try (OutputStream out = new FileOutputStream(target)) {
|
||||||
|
out.write(defaults.getBytes(StandardCharsets.UTF_8));
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[ServerSwitcherModule] Konnte " + CONFIG_FILE + " nicht erstellen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
File file = new File(plugin.getDataFolder(), CONFIG_FILE);
|
||||||
|
if (!file.exists()) return;
|
||||||
|
|
||||||
|
Properties props = new Properties();
|
||||||
|
try (FileInputStream fis = new FileInputStream(file)) {
|
||||||
|
props.load(new InputStreamReader(fis, StandardCharsets.UTF_8));
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[ServerSwitcherModule] Fehler beim Laden: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled = Boolean.parseBoolean(props.getProperty("serverswitcher.enabled", "true"));
|
||||||
|
commandName = props.getProperty("serverswitcher.command", "go").trim();
|
||||||
|
permission = props.getProperty("serverswitcher.permission", "serverswitcher.use").trim();
|
||||||
|
|
||||||
|
aliases.clear();
|
||||||
|
for (String a : props.getProperty("serverswitcher.aliases", "wechsel,switch").split(",")) {
|
||||||
|
String t = a.trim();
|
||||||
|
if (!t.isEmpty()) aliases.add(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
serverWhitelist.clear();
|
||||||
|
for (String s : props.getProperty("serverswitcher.servers", "").split(",")) {
|
||||||
|
String t = s.trim().toLowerCase();
|
||||||
|
if (!t.isEmpty()) serverWhitelist.add(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
colorHeader = props.getProperty("serverswitcher.color.header", colorHeader);
|
||||||
|
colorEntry = props.getProperty("serverswitcher.color.entry", colorEntry);
|
||||||
|
colorOnline = props.getProperty("serverswitcher.color.online", colorOnline);
|
||||||
|
colorOffline = props.getProperty("serverswitcher.color.offline", colorOffline);
|
||||||
|
colorSelf = props.getProperty("serverswitcher.color.self", colorSelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getServerList() {
|
||||||
|
if (!serverWhitelist.isEmpty()) return new ArrayList<>(serverWhitelist);
|
||||||
|
List<String> list = new ArrayList<>(ProxyServer.getInstance().getServers().keySet());
|
||||||
|
list.sort(String.CASE_INSENSITIVE_ORDER);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String c(String text) {
|
||||||
|
return ChatColor.translateAlternateColorCodes('&', text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String capitalize(String s) {
|
||||||
|
if (s == null || s.isEmpty()) return s;
|
||||||
|
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Command ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private class GoCommand extends Command {
|
||||||
|
|
||||||
|
GoCommand(String name, String permission, String[] aliases) {
|
||||||
|
super(name, permission.isEmpty() ? null : permission, aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
if (!(sender instanceof ProxiedPlayer)) {
|
||||||
|
sender.sendMessage(c("&cDieser Befehl ist nur für Spieler verfügbar."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxiedPlayer player = (ProxiedPlayer) sender;
|
||||||
|
|
||||||
|
if (args.length >= 1) {
|
||||||
|
String target = args[0].trim();
|
||||||
|
ServerInfo server = null;
|
||||||
|
for (Map.Entry<String, ServerInfo> entry : ProxyServer.getInstance().getServers().entrySet()) {
|
||||||
|
if (entry.getKey().equalsIgnoreCase(target)) {
|
||||||
|
server = entry.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server == null) {
|
||||||
|
player.sendMessage(c("&cServer &e" + args[0] + " &cnicht gefunden."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.getServer() != null
|
||||||
|
&& player.getServer().getInfo().getName().equalsIgnoreCase(server.getName())) {
|
||||||
|
player.sendMessage(c("&7Du bist bereits auf &e" + server.getName() + "&7."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendMessage(c("&7Verbinde mit &e" + server.getName() + "&7..."));
|
||||||
|
player.connect(server);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendServerMenu(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendServerMenu(ProxiedPlayer player) {
|
||||||
|
player.sendMessage(c(colorHeader));
|
||||||
|
|
||||||
|
for (String serverName : getServerList()) {
|
||||||
|
ServerInfo info = ProxyServer.getInstance().getServerInfo(serverName);
|
||||||
|
if (info == null) continue;
|
||||||
|
|
||||||
|
boolean isCurrent = player.getServer() != null
|
||||||
|
&& player.getServer().getInfo().getName().equalsIgnoreCase(serverName);
|
||||||
|
int count = info.getPlayers().size();
|
||||||
|
|
||||||
|
TextComponent line = new TextComponent(c(colorEntry));
|
||||||
|
TextComponent btn = new TextComponent(c((isCurrent ? colorOffline : colorOnline) + capitalize(serverName)));
|
||||||
|
|
||||||
|
if (!isCurrent) {
|
||||||
|
btn.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
|
||||||
|
"/" + commandName + " " + serverName));
|
||||||
|
btn.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||||
|
new ComponentBuilder(c("&7Klicken zum Verbinden\n&7Online: &a" + count + " Spieler")).create()));
|
||||||
|
}
|
||||||
|
|
||||||
|
line.addExtra(btn);
|
||||||
|
line.addExtra(new TextComponent(c(" &8(&7" + count + " online&8)")));
|
||||||
|
if (isCurrent) line.addExtra(new TextComponent(c(" " + colorSelf)));
|
||||||
|
|
||||||
|
player.sendMessage(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendMessage(c("&8&m----------------------------"));
|
||||||
|
player.sendMessage(c("&7Tipp: &e/" + commandName + " <Server> &7für direkten Wechsel"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab-Completion ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class GoTabListener implements Listener {
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onTabComplete(TabCompleteEvent event) {
|
||||||
|
String cursor = event.getCursor();
|
||||||
|
if (cursor == null) return;
|
||||||
|
|
||||||
|
String lower = cursor.toLowerCase();
|
||||||
|
boolean matches = lower.startsWith("/" + commandName.toLowerCase() + " ");
|
||||||
|
if (!matches) {
|
||||||
|
for (String alias : aliases) {
|
||||||
|
if (lower.startsWith("/" + alias.toLowerCase() + " ")) {
|
||||||
|
matches = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matches) return;
|
||||||
|
|
||||||
|
if (event.getSender() instanceof ProxiedPlayer) {
|
||||||
|
ProxiedPlayer p = (ProxiedPlayer) event.getSender();
|
||||||
|
if (!permission.isEmpty() && !p.hasPermission(permission)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int spaceIdx = cursor.indexOf(' ');
|
||||||
|
String input = spaceIdx >= 0 ? cursor.substring(spaceIdx + 1).toLowerCase() : "";
|
||||||
|
|
||||||
|
List<String> suggestions = new ArrayList<>();
|
||||||
|
for (String server : getServerList()) {
|
||||||
|
if (server.toLowerCase().startsWith(input)) suggestions.add(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.getSuggestions().clear();
|
||||||
|
event.getSuggestions().addAll(suggestions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,844 @@
|
|||||||
|
package net.viper.status.modules.tablist;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.api.config.ListenerInfo;
|
||||||
|
import net.md_5.bungee.api.config.ServerInfo;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||||
|
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||||
|
import net.md_5.bungee.api.event.ServerSwitchEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.api.scheduler.ScheduledTask;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.md_5.bungee.protocol.packet.PlayerListItem;
|
||||||
|
import net.md_5.bungee.protocol.packet.PlayerListItem.Item;
|
||||||
|
import net.md_5.bungee.protocol.packet.PlayerListItemUpdate;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class TablistModule implements Module, Listener {
|
||||||
|
|
||||||
|
private static final String CONFIG_FILE = "tablist.properties";
|
||||||
|
|
||||||
|
// Leerer Skin (grauer Kopf) für Platzhalter-Slots
|
||||||
|
private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = {
|
||||||
|
new net.md_5.bungee.protocol.data.Property(
|
||||||
|
"textures",
|
||||||
|
"ewogICJ0aW1lc3RhbXAiIDogMTY0NDcwNTExNjQ2OCwKICAicHJvZmlsZUlkIiA6ICJmZDQ3Y2I4YjgzNjQ0YmY3YWIyYmUxODZkYjI1ZmMwZCIsCiAgInByb2ZpbGVOYW1lIiA6ICJDVUNGTDEyIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2ZmOWJiOWU1NjEyNWM4MjI3Yjk0YmJkYTlmNmUwZjg2MjkzMWMyMjkyNTViYThmMTIwNWQxM2M0NGMxYmI1NjEiCiAgICB9CiAgfQp9",
|
||||||
|
"D24yzbg+aBETxe5e+acQR8xJwBkhf8+CdkNYi1ufu3NgXk6YK67dIij8o3QtMx/y3rR6xupRq7bKHUGGgkw+joCC/mtG6yDdLbD32s//VAhA+VVDbIQq/CJrJ8oYarerElTjOF08zxQCw8n97cfI10gkoZvdTDouRfTfQYIIo6vvG9kTGyAJv7mIriTvxE/nwP3m6WlwRmtKWOqDhiMRNoWwo9btCp5JTZR9HVFaZdsNQvh6gUmjBqHoKtr/xWOVveEhQ5mc8WZh0dAiiC3Astfr0VIx7HW1+xNu+Z7xvRMgbZ+SbKuRwotW2KHCN+BDymTbiQ3GBljjXDjwFao0sBHQ24DjafWQcuEEWNsDnhDHtmG3tKdvGQbZ1bYhh97EjRYKXG+eZKMrFGG4jr9oCg0JD3JMBc88Z0mJWyKzPF9B+klFocmrFBF/UgkQnzkNShfkpC6RjUfCymrnAFAoV6XBcznbKQzyKKAMeNE3LPFZ3iS2Tygbrqo2Sjmq9zGpjva04RxWHJ1oeKzROQkge0z96AOO7ChTFTXqnNnAjdkfW2TjK7pSIwS0vMGsUgm1C/amzMpZdJuI0FXFEzz1jhFi5cdwHXSQY1gVpa4VTLNQvu1xgcnbOVJaV0Ty+AebI2s6CLt6OcpI3QKY+KPlITuwj5HydMiQvfYldhiHPjc="
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grid – rows ist IMMER 20 (Minecraft-Client-Layout: N Slots → ceil(N/20) Spalten à 20 Zeilen)
|
||||||
|
private static final int ROWS = 20;
|
||||||
|
private int rows = ROWS, columns = 3, total = 60, tabSizeMax = 60;
|
||||||
|
private int configuredTabSize = 0; // 0 = auto-detect aus BungeeCord
|
||||||
|
private UUID[] fakeUuids;
|
||||||
|
|
||||||
|
// Skin-Cache (pro Spieler)
|
||||||
|
private final ConcurrentHashMap<UUID, net.md_5.bungee.protocol.data.Property[]> skinCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Config
|
||||||
|
private boolean enabled = true;
|
||||||
|
private int updateInterval = 5;
|
||||||
|
private String layoutMode = "compact";
|
||||||
|
|
||||||
|
private String headerLine1 = "&8&m" + rep('\u2501', 53);
|
||||||
|
private String headerLine2 = " &6&lViper Network";
|
||||||
|
private String headerLine3 = "&8&m" + rep('\u2501', 53);
|
||||||
|
private String footerLine1 = "&8&m" + rep('\u2501', 53);
|
||||||
|
private String footerLine2 = " &7Discord: &ediscord.viper-network.de &8| &7Shop: &eviper-network.de/shop";
|
||||||
|
private String footerLine3 = "&8&m" + rep('\u2501', 53);
|
||||||
|
|
||||||
|
private String compactHeader1 = "&6&lViper Network &8• &7%online% Spieler online";
|
||||||
|
private String compactHeader2 = "";
|
||||||
|
private String compactHeader3 = "";
|
||||||
|
private boolean compactHeader2Spacer = false;
|
||||||
|
private boolean compactHeader3Spacer = false;
|
||||||
|
private String compactFooter1 = "";
|
||||||
|
private String compactFooter2 = "&7Zeit: &f%time% &8| &7Spieler: &f%online% &8| &7Ping: &f%ping%ms";
|
||||||
|
private String compactFooter3 = "&7Kontostand: &a$%balance% &8| &7Server: &f%server% &8| &7Welt: &f%world%";
|
||||||
|
private String compactFooter4 = "";
|
||||||
|
private boolean compactFooter1Spacer = false;
|
||||||
|
private boolean compactFooter4Spacer = false;
|
||||||
|
|
||||||
|
private String colorSrvHeader = "&6&l";
|
||||||
|
private boolean showFooterServerList = true;
|
||||||
|
private String columnHeaderMode = "none";
|
||||||
|
private final Map<String, String> serverSymbols = new LinkedHashMap<>();
|
||||||
|
private String timeFormat = "HH:mm:ss / h:mm a";
|
||||||
|
private String timeZone = "Europe/Berlin";
|
||||||
|
private SimpleDateFormat sdf;
|
||||||
|
private List<String> serverOrder = new ArrayList<>();
|
||||||
|
private Set<String> hiddenServers = new HashSet<>();
|
||||||
|
private List<String> rankOrder = new ArrayList<>();
|
||||||
|
|
||||||
|
// Info-Spalte
|
||||||
|
private static class InfoEntry {
|
||||||
|
String label, type, value; boolean enabled;
|
||||||
|
InfoEntry(String l, String t, String v, boolean e) { label=l; type=t; value=v; enabled=e; }
|
||||||
|
}
|
||||||
|
private List<InfoEntry> infoEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
// State
|
||||||
|
private Plugin plugin;
|
||||||
|
private ScheduledTask updateTask;
|
||||||
|
private Method sendPacketQueuedMethod;
|
||||||
|
|
||||||
|
@Override public String getName() { return "TablistModule"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
plugin.getLogger().info("[TablistModule] Starte...");
|
||||||
|
ensureConfigExists();
|
||||||
|
loadConfig();
|
||||||
|
plugin.getLogger().info("[TablistModule] Config geladen. Layout=" + layoutMode + " enabled=" + enabled);
|
||||||
|
if (!enabled) { plugin.getLogger().info("[TablistModule] Deaktiviert."); return; }
|
||||||
|
try {
|
||||||
|
initGridSize();
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[TablistModule] initGridSize Fehler: " + e.getMessage() + " – nutze Fallback 3x20");
|
||||||
|
int fbSize = configuredTabSize > 0 ? configuredTabSize : 60;
|
||||||
|
tabSizeMax = fbSize; rows = ROWS; columns = Math.min(Math.max(3, fbSize / ROWS), 8); total = ROWS * columns;
|
||||||
|
}
|
||||||
|
initUuids();
|
||||||
|
try {
|
||||||
|
Class<?> uc = Class.forName("net.md_5.bungee.UserConnection");
|
||||||
|
sendPacketQueuedMethod = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class);
|
||||||
|
sendPacketQueuedMethod.setAccessible(true);
|
||||||
|
plugin.getLogger().info("[TablistModule] sendPacketQueued gefunden.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("[TablistModule] sendPacketQueued NICHT gefunden: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
||||||
|
updateTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, Math.max(1, updateInterval), TimeUnit.SECONDS);
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
|
plugin.getLogger().info("[TablistModule] Alle BungeeCord-Server: " + new ArrayList<>(ProxyServer.getInstance().getServers().keySet()));
|
||||||
|
plugin.getLogger().info("[TablistModule] Tablist-Spalten: " + getServerOrder());
|
||||||
|
recalculateGrid();
|
||||||
|
}, 3L, TimeUnit.SECONDS);
|
||||||
|
plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + " layout=" + layoutMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
if (updateTask != null) { updateTask.cancel(); updateTask = null; }
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
try { removeFakeSlots(p); p.setTabHeader(new TextComponent(""), new TextComponent("")); }
|
||||||
|
catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initGridSize() {
|
||||||
|
int tabSize = 60;
|
||||||
|
try {
|
||||||
|
for (ListenerInfo li : ProxyServer.getInstance().getConfig().getListeners()) {
|
||||||
|
try { Object v = li.getClass().getMethod("getTabSize").invoke(li);
|
||||||
|
if (v instanceof Number && ((Number)v).intValue() > 0) { tabSize = ((Number)v).intValue(); break; }
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
if (configuredTabSize > 0) tabSize = configuredTabSize; // manuell gesetzt in tablist.properties
|
||||||
|
tabSizeMax = tabSize;
|
||||||
|
rows = ROWS; // immer 20 – Minecraft-Client-Pflicht
|
||||||
|
boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode);
|
||||||
|
int serverCount = getServerOrder().size();
|
||||||
|
int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount);
|
||||||
|
// Spalten = benötigte Spalten, aber max was tab-size erlaubt (tab-size/20)
|
||||||
|
columns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSize / ROWS));
|
||||||
|
total = ROWS * columns;
|
||||||
|
if (needed > tabSize / ROWS) {
|
||||||
|
plugin.getLogger().warning("[TablistModule] Nicht alle Server passen in die Tablist! "
|
||||||
|
+ "Erhöhe tab-size in der BungeeCord config.yml auf mindestens " + (needed * ROWS)
|
||||||
|
+ " (aktuell: " + tabSize + ")");
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("[TablistModule] tab_size=" + tabSize + " -> " + columns + "x" + ROWS + "=" + total + " (" + serverCount + " Server)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initUuids() {
|
||||||
|
fakeUuids = new UUID[total];
|
||||||
|
for (int i = 0; i < total; i++) fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onLogin(PostLoginEvent e) {
|
||||||
|
if (!enabled) return;
|
||||||
|
ProxiedPlayer p = e.getPlayer();
|
||||||
|
net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p);
|
||||||
|
if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
|
updateTablist(p);
|
||||||
|
// Nach 2s nochmals für alle damit der neue Spieler mit Kopf erscheint
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, TimeUnit.SECONDS);
|
||||||
|
}, 2L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onSwitch(ServerSwitchEvent e) {
|
||||||
|
if (!enabled) return;
|
||||||
|
ProxiedPlayer switched = e.getPlayer();
|
||||||
|
|
||||||
|
// Skin sofort cachen (noch auf dem alten Server, LoginProfile noch verfügbar)
|
||||||
|
net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(switched);
|
||||||
|
if (skin != null && skin.length > 0) skinCache.put(switched.getUniqueId(), skin);
|
||||||
|
|
||||||
|
// Nach 1s: alle Fake-Slots bei allen Viewern entfernen → erzwingt frisches ADD_PLAYER mit neuem Skin
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
|
// Skin nochmals versuchen (jetzt auf neuem Server)
|
||||||
|
net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched);
|
||||||
|
if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin);
|
||||||
|
|
||||||
|
// Alle Slots bei allen Viewern entfernen
|
||||||
|
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
try { removeFakeSlots(viewer); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sofort neu aufbauen (kein weiterer Delay nötig da removeFakeSlots synchron ist)
|
||||||
|
updateAll();
|
||||||
|
|
||||||
|
// Nochmal nach 2s als Sicherheit
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
|
net.md_5.bungee.protocol.data.Property[] s2 = fetchSkin(switched);
|
||||||
|
if (s2 != null && s2.length > 0) skinCache.put(switched.getUniqueId(), s2);
|
||||||
|
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
try { removeFakeSlots(viewer); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
updateAll();
|
||||||
|
}, 2L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
}, 1L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onDisconnect(PlayerDisconnectEvent e) {
|
||||||
|
if (!enabled) return;
|
||||||
|
skinCache.remove(e.getPlayer().getUniqueId());
|
||||||
|
// Erst alle Fake-Slots entfernen, dann nach kurzer Pause neu aufbauen
|
||||||
|
// So verschwindet der Kopf des Spielers zuverlässig
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
|
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
try { removeFakeSlots(viewer); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
ProxyServer.getInstance().getScheduler().schedule(plugin,
|
||||||
|
this::updateAll, 1L, TimeUnit.SECONDS);
|
||||||
|
}, 1L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Core ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateAll() {
|
||||||
|
recalculateGrid();
|
||||||
|
// Fehlende Skins nachladen
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
if (!skinCache.containsKey(p.getUniqueId())) {
|
||||||
|
net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p);
|
||||||
|
if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) updateTablist(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recalculateGrid() {
|
||||||
|
boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode);
|
||||||
|
int serverCount = getServerOrder().size();
|
||||||
|
int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount);
|
||||||
|
// Spalten = benötigte Spalten, aber max was tab-size erlaubt (tab-size/20)
|
||||||
|
int newColumns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSizeMax / ROWS));
|
||||||
|
int newTotal = ROWS * newColumns;
|
||||||
|
if (newColumns == columns && newTotal == total) return;
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
try { removeFakeSlots(p); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
rows = ROWS;
|
||||||
|
columns = newColumns;
|
||||||
|
total = newTotal;
|
||||||
|
initUuids();
|
||||||
|
plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + serverCount + " Server)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTablist(ProxiedPlayer viewer) {
|
||||||
|
if (viewer == null || !viewer.isConnected()) return;
|
||||||
|
try {
|
||||||
|
String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014";
|
||||||
|
String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world");
|
||||||
|
String rank = getRank(viewer);
|
||||||
|
String time = sdf.format(new Date());
|
||||||
|
String balance = getBalance(viewer);
|
||||||
|
int online = ProxyServer.getInstance().getOnlineCount();
|
||||||
|
|
||||||
|
String header, footer;
|
||||||
|
if ("compact".equalsIgnoreCase(layoutMode)) {
|
||||||
|
header = buildCompactHeader(viewer, srv, world, rank, time, balance, online);
|
||||||
|
footer = buildCompactFooter(viewer, srv, world, rank, time, balance, online);
|
||||||
|
} else {
|
||||||
|
header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3);
|
||||||
|
footer = c(footerLine1) + "\n" + c(footerLine2) + "\n" + c(footerLine3);
|
||||||
|
}
|
||||||
|
viewer.setTabHeader(new TextComponent(header), new TextComponent(footer));
|
||||||
|
hideRealPlayers(viewer);
|
||||||
|
sendSlots(viewer, buildItems(viewer));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
plugin.getLogger().warning("[TablistModule] " + viewer.getName() + ": " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header / Footer ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private String buildCompactHeader(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
appendLine(sb, compactHeader1, false, viewer, srv, world, rank, time, balance, online);
|
||||||
|
appendLine(sb, compactHeader2, compactHeader2Spacer, viewer, srv, world, rank, time, balance, online);
|
||||||
|
appendLine(sb, compactHeader3, compactHeader3Spacer, viewer, srv, world, rank, time, balance, online);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildCompactFooter(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
appendLine(sb, compactFooter1, compactFooter1Spacer, viewer, srv, world, rank, time, balance, online);
|
||||||
|
List<String> servers = getServerOrder();
|
||||||
|
if (showFooterServerList && !servers.isEmpty()) {
|
||||||
|
StringBuilder sLine = new StringBuilder();
|
||||||
|
for (String sName : servers) {
|
||||||
|
ServerInfo si = ProxyServer.getInstance().getServerInfo(sName);
|
||||||
|
int cnt = si != null ? si.getPlayers().size() : 0;
|
||||||
|
if (sLine.length() > 0) sLine.append(" &8| ");
|
||||||
|
sLine.append(c(colorSrvHeader)).append(capitalize(sName)).append(" &8\u25cf &7").append(cnt);
|
||||||
|
}
|
||||||
|
if (sb.length() > 0) sb.append("\n");
|
||||||
|
sb.append(c(sLine.toString()));
|
||||||
|
}
|
||||||
|
appendLine(sb, compactFooter2, false, viewer, srv, world, rank, time, balance, online);
|
||||||
|
appendLine(sb, compactFooter3, false, viewer, srv, world, rank, time, balance, online);
|
||||||
|
appendLine(sb, compactFooter4, compactFooter4Spacer, viewer, srv, world, rank, time, balance, online);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendLine(StringBuilder sb, String line, boolean spacer, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
|
||||||
|
boolean empty = line == null || line.trim().isEmpty();
|
||||||
|
if (empty && !spacer) return;
|
||||||
|
if (sb.length() > 0) sb.append("\n");
|
||||||
|
sb.append(empty ? " " : c(replacePlaceholders(line, viewer, srv, world, rank, time, balance, online)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Items ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Item[] buildItems(ProxiedPlayer viewer) {
|
||||||
|
String[] texts = new String[total];
|
||||||
|
net.md_5.bungee.protocol.data.Property[][] skins = new net.md_5.bungee.protocol.data.Property[total][];
|
||||||
|
int[] pings = new int[total];
|
||||||
|
for (int i = 0; i < total; i++) { texts[i] = " "; skins[i] = EMPTY_SKIN; pings[i] = 0; }
|
||||||
|
|
||||||
|
boolean compact = "compact".equalsIgnoreCase(layoutMode);
|
||||||
|
boolean useSlotHeader = "full".equalsIgnoreCase(columnHeaderMode);
|
||||||
|
|
||||||
|
// Info-Spalte (nur classic)
|
||||||
|
if (!compact) {
|
||||||
|
int base = 0, row = 0;
|
||||||
|
String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014";
|
||||||
|
String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world");
|
||||||
|
String rank = getRank(viewer); String time = sdf.format(new Date());
|
||||||
|
String balance = getBalance(viewer); int online = ProxyServer.getInstance().getOnlineCount();
|
||||||
|
for (InfoEntry entry : infoEntries) {
|
||||||
|
if (!entry.enabled || row + 1 >= rows) continue;
|
||||||
|
if (entry.label != null && !entry.label.isEmpty())
|
||||||
|
row = set(texts, base, row, c(replacePlaceholders(entry.label, viewer, srv, world, rank, time, balance, online)));
|
||||||
|
String val;
|
||||||
|
switch (entry.type) {
|
||||||
|
case "name": val = "&f" + viewer.getName(); break;
|
||||||
|
case "rank": val = "&f" + rank; break;
|
||||||
|
case "server": val = "&f" + srv; break;
|
||||||
|
case "world": val = "&f" + world; break;
|
||||||
|
case "time": val = "&f[" + time + "]"; break;
|
||||||
|
case "balance": val = "&f" + balance; break;
|
||||||
|
case "online": val = "&f" + online; break;
|
||||||
|
default: val = replacePlaceholders(entry.value, viewer, srv, world, rank, time, balance, online); break;
|
||||||
|
}
|
||||||
|
row = set(texts, base, row, c(val));
|
||||||
|
row = set(texts, base, row, " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-Spalten
|
||||||
|
List<String> servers = getServerOrder();
|
||||||
|
int startCol = compact ? 0 : 1;
|
||||||
|
for (int col = startCol; col < columns && (col - startCol) < servers.size(); col++) {
|
||||||
|
int base = col * rows;
|
||||||
|
int row = 0;
|
||||||
|
String sName = servers.get(col - startCol);
|
||||||
|
if (useSlotHeader) row = set(texts, base, row, c(colorSrvHeader + capitalize(sName)));
|
||||||
|
ServerInfo si = ProxyServer.getInstance().getServerInfo(sName);
|
||||||
|
if (si != null) {
|
||||||
|
for (ProxiedPlayer p : sortPlayersByRank(new ArrayList<>(si.getPlayers()))) {
|
||||||
|
if (row >= rows) break;
|
||||||
|
String prefix = getLuckPermsPrefix(p);
|
||||||
|
String symbol = getServerSymbol(p);
|
||||||
|
String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol);
|
||||||
|
set(texts, base, row, prefix.isEmpty() ? c("&7" + nameStr) : c(prefix + "&r " + nameStr));
|
||||||
|
net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
|
||||||
|
skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
|
||||||
|
pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item[] items = new Item[total];
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
|
Item item = new Item();
|
||||||
|
item.setUuid(fakeUuids[i]);
|
||||||
|
item.setUsername(fakeName(i));
|
||||||
|
item.setProperties(skins[i]);
|
||||||
|
item.setGamemode(0);
|
||||||
|
item.setPing(pings[i]);
|
||||||
|
item.setListed(true);
|
||||||
|
item.setDisplayName(new TextComponent(texts[i] == null || texts[i].isEmpty() ? " " : texts[i]));
|
||||||
|
items[i] = item;
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pakete ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void hideRealPlayers(ProxiedPlayer viewer) {
|
||||||
|
if (sendPacketQueuedMethod == null) return;
|
||||||
|
try {
|
||||||
|
Collection<ProxiedPlayer> online = ProxyServer.getInstance().getPlayers();
|
||||||
|
List<UUID> toHide = new ArrayList<>();
|
||||||
|
for (ProxiedPlayer p : online) toHide.add(p.getUniqueId());
|
||||||
|
for (String srvName : ProxyServer.getInstance().getServers().keySet()) {
|
||||||
|
try {
|
||||||
|
toHide.add(UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8)));
|
||||||
|
toHide.add(UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
if (toHide.isEmpty()) return;
|
||||||
|
PlayerListItemUpdate pkt = new PlayerListItemUpdate();
|
||||||
|
pkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED));
|
||||||
|
Item[] items = new Item[toHide.size()];
|
||||||
|
int idx = 0;
|
||||||
|
for (UUID uuid : toHide) { Item it = new Item(); it.setUuid(uuid); it.setListed(false); items[idx++] = it; }
|
||||||
|
pkt.setItems(items);
|
||||||
|
sendPacketQueuedMethod.invoke(viewer, pkt);
|
||||||
|
} catch (Exception e) { plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void sendSlots(ProxiedPlayer viewer, Item[] items) {
|
||||||
|
if (sendPacketQueuedMethod == null) return;
|
||||||
|
// Immer vollständiges ADD_PLAYER – einfach und zuverlässig
|
||||||
|
PlayerListItemUpdate pkt = new PlayerListItemUpdate();
|
||||||
|
pkt.setActions(EnumSet.of(
|
||||||
|
PlayerListItemUpdate.Action.ADD_PLAYER,
|
||||||
|
PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME,
|
||||||
|
PlayerListItemUpdate.Action.UPDATE_LISTED,
|
||||||
|
PlayerListItemUpdate.Action.UPDATE_LATENCY));
|
||||||
|
pkt.setItems(items);
|
||||||
|
try { sendPacketQueuedMethod.invoke(viewer, pkt); }
|
||||||
|
catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendSlots: " + e.getMessage()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeFakeSlots(ProxiedPlayer viewer) {
|
||||||
|
if (sendPacketQueuedMethod == null || fakeUuids == null) return;
|
||||||
|
try {
|
||||||
|
Class<?> cls = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove");
|
||||||
|
Object pkt = cls.getDeclaredConstructor().newInstance();
|
||||||
|
cls.getMethod("setUuids", UUID[].class).invoke(pkt, (Object) fakeUuids.clone());
|
||||||
|
sendPacketQueuedMethod.invoke(viewer, pkt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
try {
|
||||||
|
PlayerListItem rem = new PlayerListItem();
|
||||||
|
rem.setAction(PlayerListItem.Action.REMOVE_PLAYER);
|
||||||
|
Item[] items = new Item[total];
|
||||||
|
for (int i = 0; i < total; i++) { Item it = new Item(); it.setUuid(fakeUuids[i]); items[i] = it; }
|
||||||
|
rem.setItems(items);
|
||||||
|
sendPacketQueuedMethod.invoke(viewer, rem);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private String getServerSymbol(ProxiedPlayer player) {
|
||||||
|
if (serverSymbols.isEmpty() || player.getServer() == null) return "";
|
||||||
|
String raw = serverSymbols.get(player.getServer().getInfo().getName().toLowerCase());
|
||||||
|
if (raw == null || raw.isEmpty()) return "";
|
||||||
|
return c(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private net.md_5.bungee.protocol.data.Property[] fetchSkin(ProxiedPlayer player) {
|
||||||
|
try {
|
||||||
|
Object pending = player.getPendingConnection();
|
||||||
|
net.md_5.bungee.connection.LoginResult profile =
|
||||||
|
(net.md_5.bungee.connection.LoginResult) pending.getClass().getMethod("getLoginProfile").invoke(pending);
|
||||||
|
if (profile != null && profile.getProperties() != null && profile.getProperties().length > 0)
|
||||||
|
return profile.getProperties();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return new net.md_5.bungee.protocol.data.Property[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getServerOrder() {
|
||||||
|
List<String> list;
|
||||||
|
if (!serverOrder.isEmpty()) {
|
||||||
|
list = new ArrayList<>(serverOrder);
|
||||||
|
// Versteckte Server auch aus manueller Liste entfernen
|
||||||
|
list.removeIf(s -> hiddenServers.contains(s.toLowerCase()));
|
||||||
|
} else {
|
||||||
|
list = new ArrayList<>();
|
||||||
|
final String[] lobbyKey = {null};
|
||||||
|
for (String key : ProxyServer.getInstance().getServers().keySet())
|
||||||
|
if (key.equalsIgnoreCase("lobby")) { lobbyKey[0] = key; break; }
|
||||||
|
if (lobbyKey[0] != null) list.add(lobbyKey[0]);
|
||||||
|
ProxyServer.getInstance().getServers().keySet().stream()
|
||||||
|
.filter(s -> lobbyKey[0] == null || !s.equalsIgnoreCase(lobbyKey[0]))
|
||||||
|
.filter(s -> !hiddenServers.contains(s.toLowerCase()))
|
||||||
|
.sorted(String.CASE_INSENSITIVE_ORDER).forEach(list::add);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ProxiedPlayer> sortPlayersByRank(List<ProxiedPlayer> players) {
|
||||||
|
if (rankOrder.isEmpty()) return players;
|
||||||
|
players.sort((a, b) -> { int ia = getRankIndex(a), ib = getRankIndex(b);
|
||||||
|
return ia != ib ? Integer.compare(ia, ib) : a.getName().compareToIgnoreCase(b.getName()); });
|
||||||
|
return players;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getRankIndex(ProxiedPlayer player) {
|
||||||
|
try {
|
||||||
|
Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
|
||||||
|
Object api = prov.getMethod("get").invoke(null);
|
||||||
|
Object um = api.getClass().getMethod("getUserManager").invoke(api);
|
||||||
|
Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
|
||||||
|
if (usr != null) {
|
||||||
|
Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr);
|
||||||
|
if (pg != null) { String g = pg.toString().toLowerCase();
|
||||||
|
for (int i = 0; i < rankOrder.size(); i++) if (rankOrder.get(i).equalsIgnoreCase(g)) return i; }
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return rankOrder.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRank(ProxiedPlayer player) {
|
||||||
|
try {
|
||||||
|
Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
|
||||||
|
Object api = prov.getMethod("get").invoke(null);
|
||||||
|
Object um = api.getClass().getMethod("getUserManager").invoke(api);
|
||||||
|
Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
|
||||||
|
if (usr != null) {
|
||||||
|
Class<?> qo = Class.forName("net.luckperms.api.query.QueryOptions");
|
||||||
|
Object opts = qo.getMethod("defaultContextualOptions").invoke(null);
|
||||||
|
Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
|
||||||
|
Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts);
|
||||||
|
Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
|
||||||
|
if (pfx != null && !pfx.toString().isEmpty()) return pfx.toString();
|
||||||
|
Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr);
|
||||||
|
if (pg != null && !pg.toString().isEmpty()) return "[" + pg.toString().toUpperCase() + "]";
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return "NONE";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLuckPermsPrefix(ProxiedPlayer player) {
|
||||||
|
try {
|
||||||
|
Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
|
||||||
|
Object api = prov.getMethod("get").invoke(null);
|
||||||
|
Object um = api.getClass().getMethod("getUserManager").invoke(api);
|
||||||
|
Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
|
||||||
|
if (usr != null) {
|
||||||
|
Class<?> qo = Class.forName("net.luckperms.api.query.QueryOptions");
|
||||||
|
Object opts = qo.getMethod("defaultContextualOptions").invoke(null);
|
||||||
|
Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
|
||||||
|
Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts);
|
||||||
|
Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
|
||||||
|
if (pfx != null) return c(pfx.toString());
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBalance(ProxiedPlayer player) {
|
||||||
|
try {
|
||||||
|
Map<?,?> balances = (Map<?,?>) net.viper.status.StatusAPI.class.getField("playerBalances").get(null);
|
||||||
|
Object val = balances.get(player.getUniqueId());
|
||||||
|
if (val != null) return String.format("%,.2f", ((Number) val).doubleValue());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return "0.00";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String replacePlaceholders(String text, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
|
||||||
|
if (text == null) return "";
|
||||||
|
// PAPI zuerst, native Tokens danach (überschreiben PAPI-Werte falls gleicher Name)
|
||||||
|
String result = resolvePapiPlaceholders(text, viewer.getUniqueId());
|
||||||
|
result = result.replace("%player%", viewer.getName()).replace("%rank%", rank)
|
||||||
|
.replace("%server%", srv).replace("%world%", world).replace("%time%", time)
|
||||||
|
.replace("%balance%", balance).replace("%ping%", String.valueOf(viewer.getPing()))
|
||||||
|
.replace("%online%", String.valueOf(online));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolvePapiPlaceholders(String text, UUID uuid) {
|
||||||
|
if (text == null || !text.contains("%")) return text;
|
||||||
|
java.util.Map<String, String> papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid);
|
||||||
|
if (papiMap == null || papiMap.isEmpty()) return text;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int i = 0;
|
||||||
|
while (i < text.length()) {
|
||||||
|
int start = text.indexOf('%', i);
|
||||||
|
if (start < 0) { sb.append(text.substring(i)); break; }
|
||||||
|
int end = text.indexOf('%', start + 1);
|
||||||
|
if (end < 0) { sb.append(text.substring(i)); break; }
|
||||||
|
String token = text.substring(start + 1, end);
|
||||||
|
if (papiMap.containsKey(token)) {
|
||||||
|
sb.append(text, i, start);
|
||||||
|
sb.append(papiMap.get(token));
|
||||||
|
i = end + 1;
|
||||||
|
} else {
|
||||||
|
sb.append(text, i, end + 1);
|
||||||
|
i = end + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int set(String[] arr, int base, int row, String text) {
|
||||||
|
if (base + row < total) arr[base + row] = text == null ? " " : text; return row + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String c(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
s = replaceHexColors(s);
|
||||||
|
return ChatColor.translateAlternateColorCodes('&', s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String replaceHexColors(String text) {
|
||||||
|
if (text == null) return null;
|
||||||
|
if (!text.contains("&#") && !text.contains("{#") && !text.contains("<#")) return text;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int i = 0;
|
||||||
|
while (i < text.length()) {
|
||||||
|
if (i + 7 <= text.length() && text.charAt(i) == '&' && text.charAt(i+1) == '#') {
|
||||||
|
String hex = text.substring(i+2, i+8);
|
||||||
|
if (hex.matches("[0-9a-fA-F]{6}")) {
|
||||||
|
sb.append('\u00A7').append('x');
|
||||||
|
for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch);
|
||||||
|
i += 8; continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i + 8 < text.length() && text.charAt(i) == '{' && text.charAt(i+1) == '#') {
|
||||||
|
int end = text.indexOf('}', i+2);
|
||||||
|
if (end == i+8) {
|
||||||
|
String hex = text.substring(i+2, i+8);
|
||||||
|
if (hex.matches("[0-9a-fA-F]{6}")) {
|
||||||
|
sb.append('\u00A7').append('x');
|
||||||
|
for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch);
|
||||||
|
i += 9; continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Format 3: <#RRGGBB>
|
||||||
|
if (i + 8 < text.length() && text.charAt(i) == '<' && text.charAt(i+1) == '#') {
|
||||||
|
int end = text.indexOf('>', i+2);
|
||||||
|
if (end == i+8) {
|
||||||
|
String hex = text.substring(i+2, i+8);
|
||||||
|
if (hex.matches("[0-9a-fA-F]{6}")) {
|
||||||
|
sb.append('\u00A7').append('x');
|
||||||
|
for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch);
|
||||||
|
i += 9; continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append(text.charAt(i)); i++;
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fakeName(int i) { return String.format("~vt%03d", i); }
|
||||||
|
private static String capitalize(String s){ return s==null||s.isEmpty()?s:Character.toUpperCase(s.charAt(0))+s.substring(1); }
|
||||||
|
private static String rep(char ch, int n) { StringBuilder sb=new StringBuilder(n); for(int i=0;i<n;i++) sb.append(ch); return sb.toString(); }
|
||||||
|
private int parseInt(String s, int fb) { try{return Integer.parseInt(s==null?"":s.trim());}catch(Exception e){return fb;} }
|
||||||
|
|
||||||
|
// ── Config ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void ensureConfigExists() {
|
||||||
|
File f = new File(plugin.getDataFolder(), CONFIG_FILE);
|
||||||
|
if (f.exists()) return;
|
||||||
|
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
|
||||||
|
String sep = rep('\u2501', 53);
|
||||||
|
String content =
|
||||||
|
"# TablistModule Konfiguration\n" +
|
||||||
|
"tablist.enabled=true\n" +
|
||||||
|
"tablist.tab_size=160\n" +
|
||||||
|
"tablist.update_interval=5\n" +
|
||||||
|
"# Layout-Modus: classic oder compact\n" +
|
||||||
|
"tablist.layout=compact\n" +
|
||||||
|
"tablist.server_order=\n" +
|
||||||
|
"tablist.hidden_servers=\n" +
|
||||||
|
"tablist.rank_order=owner,mod,primo,vip,scout,bewohner\n\n" +
|
||||||
|
"tablist.header.line1=&8&m" + sep + "\n" +
|
||||||
|
"tablist.header.line2= &6&lViper Network\n" +
|
||||||
|
"tablist.header.line3=&8&m" + sep + "\n\n" +
|
||||||
|
"tablist.footer.line1=&8&m" + sep + "\n" +
|
||||||
|
"tablist.footer.line2= &7Discord: &ediscord.viper-network.de &8| &7Shop: &eviper-network.de/shop\n" +
|
||||||
|
"tablist.footer.line3=&8&m" + sep + "\n\n" +
|
||||||
|
"# ── Compact Layout ──────────────────────────────────────────────────\n" +
|
||||||
|
"# Platzhalter: %player% %rank% %server% %world% %time% %balance% %ping% %online%\n" +
|
||||||
|
"# spacer=true: leere Zeile = Abstand | spacer=false: leere Zeile = überspringen\n" +
|
||||||
|
"tablist.compact.header.line1=&6&lViper Network &8• &2Hallo, &a%player%&7!\n" +
|
||||||
|
"tablist.compact.header.line2=&dCitybuild &8• &aSurvival &8• &eMinigames\n" +
|
||||||
|
"tablist.compact.header.line2.spacer=false\n" +
|
||||||
|
"tablist.compact.header.line3=\n" +
|
||||||
|
"tablist.compact.header.line3.spacer=false\n\n" +
|
||||||
|
"tablist.compact.footer.line1=\n" +
|
||||||
|
"tablist.compact.footer.line1.spacer=false\n" +
|
||||||
|
"tablist.compact.footer.line2=&7Zeit: &f%time% &8| &7Spieler: &f%online% &8| &7Ping: &f%ping%ms\n" +
|
||||||
|
"tablist.compact.footer.line2.spacer=false\n" +
|
||||||
|
"tablist.compact.footer.line3=&7Kontostand: &a$%balance% &8| &7Server: &f%server% &8| &7Welt: &f%world%\n" +
|
||||||
|
"tablist.compact.footer.line3.spacer=false\n" +
|
||||||
|
"tablist.compact.footer.line4=\n" +
|
||||||
|
"tablist.compact.footer.line4.spacer=false\n\n" +
|
||||||
|
"tablist.color.server_header=&6&l\n" +
|
||||||
|
"# column_header: full=großer Header | none=kein Header (Zeile 0 frei) | small=nur im Footer\n" +
|
||||||
|
"tablist.column_header=none\n" +
|
||||||
|
"# Server-Liste im Footer anzeigen (true/false)\n" +
|
||||||
|
"tablist.compact.footer.serverlist=true\n" +
|
||||||
|
"tablist.time_format=HH:mm:ss / h:mm a\n" +
|
||||||
|
"tablist.timezone=Europe/Berlin\n\n" +
|
||||||
|
"# ── Info-Spalte (nur classic) ────────────────────────────────────────\n" +
|
||||||
|
"tablist.info.order=website,name,rank,server,world,time,teamspeak\n\n" +
|
||||||
|
"tablist.info.website.enabled=true\n" +
|
||||||
|
"tablist.info.website.label=&b&lWebsite:\n" +
|
||||||
|
"tablist.info.website.type=website\n" +
|
||||||
|
"tablist.info.website.value=&fviper-network.de\n\n" +
|
||||||
|
"tablist.info.name.enabled=true\n" +
|
||||||
|
"tablist.info.name.label=&b&lName:\n" +
|
||||||
|
"tablist.info.name.type=name\n\n" +
|
||||||
|
"tablist.info.rank.enabled=true\n" +
|
||||||
|
"tablist.info.rank.label=&b&lRank:\n" +
|
||||||
|
"tablist.info.rank.type=rank\n\n" +
|
||||||
|
"tablist.info.server.enabled=true\n" +
|
||||||
|
"tablist.info.server.label=&b&lServer:\n" +
|
||||||
|
"tablist.info.server.type=server\n\n" +
|
||||||
|
"tablist.info.world.enabled=true\n" +
|
||||||
|
"tablist.info.world.label=&b&lWorld:\n" +
|
||||||
|
"tablist.info.world.type=world\n\n" +
|
||||||
|
"tablist.info.time.enabled=true\n" +
|
||||||
|
"tablist.info.time.label=&b&lTime:\n" +
|
||||||
|
"tablist.info.time.type=time\n\n" +
|
||||||
|
"tablist.info.teamspeak.enabled=true\n" +
|
||||||
|
"tablist.info.teamspeak.label=&b&lTeamspeak:\n" +
|
||||||
|
"tablist.info.teamspeak.type=teamspeak\n" +
|
||||||
|
"tablist.info.teamspeak.value=&fts.viper-network.de\n" +
|
||||||
|
"\n# Server-Symbole hinter dem Spielernamen\n" +
|
||||||
|
"# Format: tablist.symbol.<servername>=&FarbCode Symbol\n" +
|
||||||
|
"tablist.symbol.lobby=&f\uD83C\uDFE0\n" +
|
||||||
|
"tablist.symbol.sv1=&6\u26CF\uFE0F\n";
|
||||||
|
try (OutputStream out = new FileOutputStream(f)) { out.write(content.getBytes(StandardCharsets.UTF_8)); }
|
||||||
|
catch (Exception e) { plugin.getLogger().warning("[TablistModule] Config: " + e.getMessage()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
File file = new File(plugin.getDataFolder(), CONFIG_FILE);
|
||||||
|
Map<String, String> map = new LinkedHashMap<>();
|
||||||
|
if (file.exists()) {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
int eq = line.indexOf('=');
|
||||||
|
if (eq < 1) continue;
|
||||||
|
map.put(line.substring(0, eq).trim(), line.substring(eq + 1));
|
||||||
|
}
|
||||||
|
} catch (Exception e) { plugin.getLogger().warning("[TablistModule] Ladefehler: " + e.getMessage()); }
|
||||||
|
}
|
||||||
|
java.util.function.BiFunction<String,String,String> get = (k,d) -> map.getOrDefault(k,d);
|
||||||
|
|
||||||
|
configuredTabSize = parseInt(get.apply("tablist.tab_size", "0"), 0);
|
||||||
|
enabled = Boolean.parseBoolean(get.apply("tablist.enabled", "true"));
|
||||||
|
updateInterval = parseInt(get.apply("tablist.update_interval", "5"), 5);
|
||||||
|
layoutMode = get.apply("tablist.layout", "compact").trim().toLowerCase();
|
||||||
|
headerLine1 = get.apply("tablist.header.line1", headerLine1);
|
||||||
|
headerLine2 = get.apply("tablist.header.line2", headerLine2);
|
||||||
|
headerLine3 = get.apply("tablist.header.line3", headerLine3);
|
||||||
|
footerLine1 = get.apply("tablist.footer.line1", footerLine1);
|
||||||
|
footerLine2 = get.apply("tablist.footer.line2", footerLine2);
|
||||||
|
footerLine3 = get.apply("tablist.footer.line3", footerLine3);
|
||||||
|
compactHeader1 = get.apply("tablist.compact.header.line1", compactHeader1);
|
||||||
|
compactHeader2 = get.apply("tablist.compact.header.line2", compactHeader2);
|
||||||
|
compactHeader3 = get.apply("tablist.compact.header.line3", compactHeader3);
|
||||||
|
compactHeader2Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line2.spacer", "false"));
|
||||||
|
compactHeader3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line3.spacer", "false"));
|
||||||
|
compactFooter1 = get.apply("tablist.compact.footer.line1", compactFooter1);
|
||||||
|
compactFooter2 = get.apply("tablist.compact.footer.line2", compactFooter2);
|
||||||
|
compactFooter3 = get.apply("tablist.compact.footer.line3", compactFooter3);
|
||||||
|
compactFooter4 = get.apply("tablist.compact.footer.line4", compactFooter4);
|
||||||
|
compactFooter1Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line1.spacer", "false"));
|
||||||
|
compactFooter4Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line4.spacer", "false"));
|
||||||
|
colorSrvHeader = get.apply("tablist.color.server_header", colorSrvHeader);
|
||||||
|
showFooterServerList = Boolean.parseBoolean(get.apply("tablist.compact.footer.serverlist", "true"));
|
||||||
|
columnHeaderMode = get.apply("tablist.column_header", "none").trim().toLowerCase();
|
||||||
|
timeFormat = get.apply("tablist.time_format", timeFormat);
|
||||||
|
timeZone = get.apply("tablist.timezone", timeZone);
|
||||||
|
try { sdf = new SimpleDateFormat(timeFormat); sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone)); }
|
||||||
|
catch (Exception e) { sdf = new SimpleDateFormat("HH:mm:ss / h:mm a"); }
|
||||||
|
|
||||||
|
rankOrder.clear();
|
||||||
|
String rankRaw = get.apply("tablist.rank_order", "").trim();
|
||||||
|
if (!rankRaw.isEmpty()) for (String s : rankRaw.split(",")) { String t=s.trim(); if(!t.isEmpty()) rankOrder.add(t.toLowerCase()); }
|
||||||
|
|
||||||
|
serverOrder.clear();
|
||||||
|
String raw = get.apply("tablist.server_order", "").trim();
|
||||||
|
if (!raw.isEmpty()) for (String s : raw.split(",")) { String t=s.trim(); if(!t.isEmpty()) serverOrder.add(t.toLowerCase()); }
|
||||||
|
|
||||||
|
hiddenServers.clear();
|
||||||
|
String hRaw = get.apply("tablist.hidden_servers", "").trim();
|
||||||
|
if (!hRaw.isEmpty()) for (String s : hRaw.split(",")) { String t=s.trim().toLowerCase(); if(!t.isEmpty()) hiddenServers.add(t); }
|
||||||
|
|
||||||
|
infoEntries.clear();
|
||||||
|
String orderRaw = get.apply("tablist.info.order", "website,name,rank,server,world,time,teamspeak").trim();
|
||||||
|
for (String id : orderRaw.split(",")) {
|
||||||
|
id = id.trim(); if (id.isEmpty()) continue;
|
||||||
|
boolean en = Boolean.parseBoolean(get.apply("tablist.info." + id + ".enabled", "true"));
|
||||||
|
String label = get.apply("tablist.info." + id + ".label", "");
|
||||||
|
String type = get.apply("tablist.info." + id + ".type", "custom");
|
||||||
|
String value = get.apply("tablist.info." + id + ".value", "");
|
||||||
|
infoEntries.add(new InfoEntry(label, type, value, en));
|
||||||
|
}
|
||||||
|
if (infoEntries.isEmpty()) {
|
||||||
|
infoEntries.add(new InfoEntry("&b&lWebsite:", "website", "&fviper-network.de", true));
|
||||||
|
infoEntries.add(new InfoEntry("&b&lName:", "name", "", true));
|
||||||
|
infoEntries.add(new InfoEntry("&b&lRank:", "rank", "", true));
|
||||||
|
infoEntries.add(new InfoEntry("&b&lServer:", "server", "", true));
|
||||||
|
infoEntries.add(new InfoEntry("&b&lWorld:", "world", "", true));
|
||||||
|
infoEntries.add(new InfoEntry("&b&lTime:", "time", "", true));
|
||||||
|
infoEntries.add(new InfoEntry("&b&lTeamspeak:", "teamspeak", "&fts.viper-network.de", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-Symbole
|
||||||
|
serverSymbols.clear();
|
||||||
|
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
if (key.startsWith("tablist.symbol.")) {
|
||||||
|
String srvName = key.substring("tablist.symbol.".length()).trim().toLowerCase();
|
||||||
|
String symbol = entry.getValue().trim();
|
||||||
|
if (!srvName.isEmpty() && !symbol.isEmpty())
|
||||||
|
serverSymbols.put(srvName, symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package net.viper.status.modules.vanish;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.CommandSender;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||||
|
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.md_5.bungee.event.EventPriority;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
import net.viper.status.modules.chat.VanishProvider;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VanishModule für StatusAPI (BungeeCord)
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - /vanish zum Ein-/Ausschalten
|
||||||
|
* - /vanish <Spieler> für Admin-Vanish anderer Spieler
|
||||||
|
* - /vanishlist – zeigt alle aktuell unsichtbaren Spieler
|
||||||
|
* - Vanish-Status wird persistent in vanish.dat gespeichert
|
||||||
|
* - Beim Login wird gespeicherter Status wiederhergestellt
|
||||||
|
* - Volle Integration mit VanishProvider → ChatModule sieht den Status
|
||||||
|
*
|
||||||
|
* Permission:
|
||||||
|
* - vanish.use → darf vanishen
|
||||||
|
* - vanish.other → darf andere Spieler vanishen
|
||||||
|
* - vanish.list → darf /vanishlist nutzen
|
||||||
|
* - chat.admin.bypass → sieht Vanish-Join/Leave-Meldungen im Chat
|
||||||
|
*/
|
||||||
|
public class VanishModule implements Module, Listener {
|
||||||
|
|
||||||
|
private static final String PERMISSION = "chat.admin.bypass";
|
||||||
|
private static final String PERMISSION_OTHER = "chat.admin.bypass";
|
||||||
|
private static final String PERMISSION_LIST = "chat.admin.bypass";
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
|
||||||
|
// Persistente Vanish-UUIDs (werden in vanish.dat gespeichert)
|
||||||
|
private final Set<UUID> persistentVanished =
|
||||||
|
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
|
private File dataFile;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "VanishModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.dataFile = new File(plugin.getDataFolder(), "vanish.dat");
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
||||||
|
registerCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
save();
|
||||||
|
// Alle als sichtbar markieren beim Shutdown (damit beim nächsten Start
|
||||||
|
// der VanishProvider sauber ist – load() setzt sie beim Login neu)
|
||||||
|
for (UUID uuid : persistentVanished) {
|
||||||
|
VanishProvider.setVanished(uuid, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// EVENTS
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beim Login: Wenn der Spieler persistent gevanisht war, sofort
|
||||||
|
* in den VanishProvider eintragen – BEVOR das ChatModule die
|
||||||
|
* Join-Nachricht nach 2 Sekunden sendet.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.LOWEST)
|
||||||
|
public void onLogin(PostLoginEvent e) {
|
||||||
|
ProxiedPlayer player = e.getPlayer();
|
||||||
|
if (persistentVanished.contains(player.getUniqueId())) {
|
||||||
|
VanishProvider.setVanished(player.getUniqueId(), true);
|
||||||
|
// Kurze Bestätigung an den Spieler selbst (nach kurzem Delay damit
|
||||||
|
// der Client bereit ist)
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
|
if (player.isConnected()) {
|
||||||
|
player.sendMessage(color("&8[&7Vanish&8] &7Du bist &cUnsichtbar&7."));
|
||||||
|
}
|
||||||
|
}, 1, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onDisconnect(PlayerDisconnectEvent e) {
|
||||||
|
// VanishProvider cleanup – der Eintrag in persistentVanished bleibt
|
||||||
|
// erhalten damit der Status beim nächsten Login wiederhergestellt wird
|
||||||
|
VanishProvider.cleanup(e.getPlayer().getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// COMMANDS
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
private void registerCommands() {
|
||||||
|
|
||||||
|
// /vanish [spieler]
|
||||||
|
plugin.getProxy().getPluginManager().registerCommand(plugin,
|
||||||
|
new Command("vanish", PERMISSION, "v") {
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
|
||||||
|
if (args.length == 0) {
|
||||||
|
// Sich selbst vanishen
|
||||||
|
if (!(sender instanceof ProxiedPlayer)) {
|
||||||
|
sender.sendMessage(color("&cNur Spieler!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleVanish((ProxiedPlayer) sender, (ProxiedPlayer) sender);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Anderen Spieler vanishen
|
||||||
|
if (!sender.hasPermission(PERMISSION_OTHER)) {
|
||||||
|
sender.sendMessage(color("&cDu hast keine Berechtigung für /vanish <Spieler>."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]);
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleVanish(sender, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// /vanishlist
|
||||||
|
plugin.getProxy().getPluginManager().registerCommand(plugin,
|
||||||
|
new Command("vanishlist", PERMISSION_LIST, "vlist") {
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
|
Set<UUID> vanished = VanishProvider.getVanishedPlayers();
|
||||||
|
if (vanished.isEmpty()) {
|
||||||
|
sender.sendMessage(color("&8[Vanish] &7Keine unsichtbaren Spieler."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender.sendMessage(color("&8[Vanish] &7Unsichtbare Spieler &8(" + vanished.size() + ")&7:"));
|
||||||
|
for (UUID uuid : vanished) {
|
||||||
|
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
|
||||||
|
String name = p != null ? p.getName() : uuid.toString().substring(0, 8) + "...";
|
||||||
|
String online = p != null ? " &8(online)" : " &8(offline/persistent)";
|
||||||
|
sender.sendMessage(color(" &8- &7" + name + online));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// VANISH-LOGIK
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schaltet den Vanish-Status eines Spielers um.
|
||||||
|
*
|
||||||
|
* @param executor Der Befehlsgeber (für Feedback-Nachrichten)
|
||||||
|
* @param target Der betroffene Spieler
|
||||||
|
*/
|
||||||
|
private void toggleVanish(CommandSender executor, ProxiedPlayer target) {
|
||||||
|
boolean nowVanished = !VanishProvider.isVanished(target);
|
||||||
|
setVanished(target, nowVanished);
|
||||||
|
|
||||||
|
String statusMsg = nowVanished
|
||||||
|
? "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &cUnsichtbar&7."
|
||||||
|
: "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &aSichtbar&7.";
|
||||||
|
|
||||||
|
// Feedback an den Ausführenden
|
||||||
|
executor.sendMessage(color(statusMsg));
|
||||||
|
|
||||||
|
// Falls jemand anderes gevanisht wurde, auch dem Ziel Bescheid geben
|
||||||
|
if (!executor.equals(target)) {
|
||||||
|
String selfMsg = nowVanished
|
||||||
|
? "&8[&7Vanish&8] &7Du wurdest &cUnsichtbar &7gemacht."
|
||||||
|
: "&8[&7Vanish&8] &7Du wurdest &aSichtbar &7gemacht.";
|
||||||
|
target.sendMessage(color(selfMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admins mit chat.admin.bypass informieren (außer dem Ausführenden)
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
if (p.equals(executor) || p.equals(target)) continue;
|
||||||
|
if (p.hasPermission("chat.admin.bypass")) {
|
||||||
|
p.sendMessage(color(statusMsg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt den Vanish-Status direkt (ohne Toggle).
|
||||||
|
* Aktualisiert VanishProvider UND die persistente Liste.
|
||||||
|
*/
|
||||||
|
public void setVanished(ProxiedPlayer player, boolean vanished) {
|
||||||
|
VanishProvider.setVanished(player.getUniqueId(), vanished);
|
||||||
|
if (vanished) {
|
||||||
|
persistentVanished.add(player.getUniqueId());
|
||||||
|
} else {
|
||||||
|
persistentVanished.remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffentliche API für andere Module.
|
||||||
|
*/
|
||||||
|
public boolean isVanished(ProxiedPlayer player) {
|
||||||
|
return VanishProvider.isVanished(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PERSISTENZ
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
private void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(dataFile), "UTF-8"))) {
|
||||||
|
for (UUID uuid : persistentVanished) {
|
||||||
|
bw.write(uuid.toString());
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("[VanishModule] Fehler beim Speichern: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
persistentVanished.clear();
|
||||||
|
if (!dataFile.exists()) return;
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(new FileInputStream(dataFile), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
try {
|
||||||
|
persistentVanished.add(UUID.fromString(line));
|
||||||
|
} catch (IllegalArgumentException ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("[VanishModule] Fehler beim Laden: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// HILFSMETHODEN
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
private TextComponent color(String text) {
|
||||||
|
return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,248 +1,196 @@
|
|||||||
package net.viper.status.modules.verify;
|
package net.viper.status.modules.verify;
|
||||||
|
|
||||||
import net.md_5.bungee.api.ChatColor;
|
import net.viper.status.StatusAPI;
|
||||||
import net.md_5.bungee.api.CommandSender;
|
import net.md_5.bungee.api.ChatColor;
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
import net.viper.status.StatusAPI;
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
import net.md_5.bungee.api.CommandSender;
|
||||||
import net.md_5.bungee.api.plugin.Command;
|
import net.viper.status.StatusAPI;
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
import net.viper.status.module.Module;
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
import javax.crypto.Mac;
|
import net.viper.status.StatusAPI;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
import java.io.*;
|
import net.viper.status.StatusAPI;
|
||||||
import java.net.HttpURLConnection;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
import java.net.URL;
|
import net.viper.status.module.Module;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.util.HashMap;
|
import javax.crypto.Mac;
|
||||||
import java.util.Map;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.util.Properties;
|
import java.io.*;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
/**
|
import java.net.URL;
|
||||||
* VerifyModule: Multi-Server Support.
|
import java.nio.charset.Charset;
|
||||||
* Liest pro Server die passende ID und das Secret aus der verify.properties.
|
import java.util.HashMap;
|
||||||
*/
|
import java.util.Map;
|
||||||
public class VerifyModule implements Module {
|
import java.util.Properties;
|
||||||
|
|
||||||
private String wpVerifyUrl;
|
/**
|
||||||
// Speichert für jeden Servernamen (z.B. "Lobby") die passende Konfiguration
|
* VerifyModule: Multi-Server Support.
|
||||||
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
|
*
|
||||||
|
* Fix #7: Servernamen werden jetzt case-insensitiv verglichen.
|
||||||
@Override
|
* Keys in serverConfigs werden beim Laden auf lowercase normalisiert
|
||||||
public String getName() {
|
* und die Suche erfolgt ebenfalls lowercase.
|
||||||
return "VerifyModule";
|
*/
|
||||||
}
|
public class VerifyModule implements Module {
|
||||||
|
|
||||||
@Override
|
private String wpVerifyUrl;
|
||||||
public void onEnable(Plugin plugin) {
|
// Keys sind lowercase normalisiert für case-insensitiven Vergleich
|
||||||
loadConfig(plugin);
|
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
|
||||||
|
|
||||||
// Befehl registrieren
|
@Override
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
|
public String getName() { return "VerifyModule"; }
|
||||||
|
|
||||||
plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
|
@Override
|
||||||
}
|
public void onEnable(Plugin plugin) {
|
||||||
|
loadConfig(plugin);
|
||||||
@Override
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
|
||||||
public void onDisable(Plugin plugin) {
|
plugin.getLogger().fine("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
|
||||||
// Befehl muss nicht manuell entfernt werden, BungeeCord übernimmt das beim Plugin-Stop
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
// --- Konfiguration Laden & Kopieren ---
|
public void onDisable(Plugin plugin) {}
|
||||||
private void loadConfig(Plugin plugin) {
|
|
||||||
String fileName = "verify.properties";
|
private void loadConfig(Plugin plugin) {
|
||||||
File configFile = new File(plugin.getDataFolder(), fileName);
|
String fileName = "verify.properties";
|
||||||
Properties props = new Properties();
|
File configFile = new File(plugin.getDataFolder(), fileName);
|
||||||
|
Properties props = new Properties();
|
||||||
// 1. Datei kopieren, falls sie noch nicht existiert
|
|
||||||
if (!configFile.exists()) {
|
if (!configFile.exists()) {
|
||||||
plugin.getDataFolder().mkdirs();
|
plugin.getDataFolder().mkdirs();
|
||||||
try (InputStream in = plugin.getResourceAsStream(fileName);
|
try (InputStream in = plugin.getResourceAsStream(fileName);
|
||||||
OutputStream out = new FileOutputStream(configFile)) {
|
OutputStream out = new FileOutputStream(configFile)) {
|
||||||
if (in == null) {
|
if (in == null) { plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR."); return; }
|
||||||
plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR gefunden. Erstelle manuell.");
|
byte[] buffer = new byte[1024]; int length;
|
||||||
return;
|
while ((length = in.read(buffer)) > 0) out.write(buffer, 0, length);
|
||||||
}
|
StatusAPI.debugLog(plugin, "Konfigurationsdatei '" + fileName + "' erstellt.");
|
||||||
byte[] buffer = new byte[1024];
|
} catch (Exception e) { plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage()); return; }
|
||||||
int length;
|
}
|
||||||
while ((length = in.read(buffer)) > 0) {
|
|
||||||
out.write(buffer, 0, length);
|
try (InputStream in = new FileInputStream(configFile)) {
|
||||||
}
|
props.load(in);
|
||||||
plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt.");
|
} catch (IOException e) { e.printStackTrace(); return; }
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage());
|
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
|
||||||
return;
|
|
||||||
}
|
// FIX #7: Keys beim Laden auf lowercase normalisieren
|
||||||
}
|
this.serverConfigs.clear();
|
||||||
|
for (String key : props.stringPropertyNames()) {
|
||||||
// 2. Eigentliche Config laden
|
if (key.startsWith("server.")) {
|
||||||
try (InputStream in = new FileInputStream(configFile)) {
|
String[] parts = key.split("\\.");
|
||||||
props.load(in);
|
if (parts.length == 3) {
|
||||||
} catch (IOException e) {
|
// Servername lowercase → case-insensitiver Lookup
|
||||||
e.printStackTrace();
|
String serverName = parts[1].toLowerCase();
|
||||||
return;
|
String type = parts[2];
|
||||||
}
|
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
|
||||||
|
if ("id".equalsIgnoreCase(type)) {
|
||||||
// Globale URL
|
try { config.serverId = Integer.parseInt(props.getProperty(key)); }
|
||||||
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
|
catch (NumberFormatException e) { plugin.getLogger().warning("Ungültige Server ID für " + serverName); }
|
||||||
|
} else if ("secret".equalsIgnoreCase(type)) {
|
||||||
// Server-Configs parsen (z.B. server.Lobby.id)
|
config.sharedSecret = props.getProperty(key);
|
||||||
this.serverConfigs.clear();
|
}
|
||||||
for (String key : props.stringPropertyNames()) {
|
}
|
||||||
if (key.startsWith("server.")) {
|
}
|
||||||
// Key Struktur: server.<ServerName>.id oder .secret
|
}
|
||||||
String[] parts = key.split("\\.");
|
}
|
||||||
if (parts.length == 3) {
|
|
||||||
String serverName = parts[1];
|
private static class ServerConfig {
|
||||||
String type = parts[2];
|
int serverId = 0;
|
||||||
|
String sharedSecret = "";
|
||||||
// Eintrag in der Map erstellen oder holen
|
}
|
||||||
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
|
|
||||||
|
private class VerifyCommand extends Command {
|
||||||
if ("id".equalsIgnoreCase(type)) {
|
public VerifyCommand() { super("verify"); }
|
||||||
try {
|
|
||||||
config.serverId = Integer.parseInt(props.getProperty(key));
|
@Override
|
||||||
} catch (NumberFormatException e) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
plugin.getLogger().warning("Ungültige Server ID für " + serverName);
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen."); return; }
|
||||||
}
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
} else if ("secret".equalsIgnoreCase(type)) {
|
if (args.length != 1) { p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>"); return; }
|
||||||
config.sharedSecret = props.getProperty(key);
|
|
||||||
}
|
// FIX #7: Servername lowercase für case-insensitiven Lookup
|
||||||
}
|
String serverName = p.getServer().getInfo().getName().toLowerCase();
|
||||||
}
|
ServerConfig config = serverConfigs.get(serverName);
|
||||||
}
|
|
||||||
}
|
if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) {
|
||||||
|
p.sendMessage(ChatColor.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt.");
|
||||||
// --- Hilfsklasse für die Daten eines Servers ---
|
p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + p.getServer().getInfo().getName());
|
||||||
private static class ServerConfig {
|
p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin.");
|
||||||
int serverId = 0;
|
return;
|
||||||
String sharedSecret = "";
|
}
|
||||||
}
|
|
||||||
|
String token = args[0].trim();
|
||||||
// --- Die Command Klasse ---
|
String playerName = p.getName();
|
||||||
private class VerifyCommand extends Command {
|
|
||||||
|
HttpURLConnection conn = null;
|
||||||
public VerifyCommand() {
|
try {
|
||||||
super("verify");
|
Charset utf8 = Charset.forName("UTF-8");
|
||||||
}
|
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
|
||||||
|
String payload = "{\"player\":\"" + escapeJson(playerName)
|
||||||
@Override
|
+ "\",\"token\":\"" + escapeJson(token)
|
||||||
public void execute(CommandSender sender, String[] args) {
|
+ "\",\"server_id\":" + config.serverId
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
+ ",\"signature\":\"" + signature + "\"}";
|
||||||
sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen.");
|
|
||||||
return;
|
URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
|
||||||
}
|
conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
conn.setReadTimeout(7000);
|
||||||
if (args.length != 1) {
|
conn.setDoOutput(true);
|
||||||
p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>");
|
conn.setRequestMethod("POST");
|
||||||
return;
|
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
||||||
}
|
try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); }
|
||||||
|
|
||||||
// --- WICHTIG: Servernamen ermitteln ---
|
int code = conn.getResponseCode();
|
||||||
String serverName = p.getServer().getInfo().getName();
|
String resp = code >= 200 && code < 300
|
||||||
|
? streamToString(conn.getInputStream(), utf8)
|
||||||
// Konfiguration für diesen Server laden
|
: streamToString(conn.getErrorStream(), utf8);
|
||||||
ServerConfig config = serverConfigs.get(serverName);
|
|
||||||
|
if (resp != null && !resp.isEmpty() && resp.trim().startsWith("{")) {
|
||||||
// Check ob Konfig existiert
|
boolean isSuccess = resp.contains("\"success\":true");
|
||||||
if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) {
|
String message = "Ein unbekannter Fehler ist aufgetreten.";
|
||||||
p.sendMessage(ChatColor.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt.");
|
int keyIndex = resp.indexOf("\"message\":\"");
|
||||||
p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + serverName);
|
if (keyIndex != -1) {
|
||||||
p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin.");
|
int startIndex = keyIndex + 11;
|
||||||
return;
|
int endIndex = resp.indexOf("\"", startIndex);
|
||||||
}
|
if (endIndex != -1) message = resp.substring(startIndex, endIndex);
|
||||||
|
}
|
||||||
String token = args[0].trim();
|
if (isSuccess) {
|
||||||
String playerName = p.getName();
|
p.sendMessage(ChatColor.GREEN + "✓ " + message);
|
||||||
|
p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!");
|
||||||
HttpURLConnection conn = null;
|
} else {
|
||||||
try {
|
p.sendMessage(ChatColor.RED + "✗ " + message);
|
||||||
Charset utf8 = Charset.forName("UTF-8");
|
}
|
||||||
// Wir signieren Name + Token mit dem SERVER-SPECIFISCHEN Secret
|
} else {
|
||||||
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
|
p.sendMessage(ChatColor.RED + "✗ Fehler beim Verbinden mit der Webseite (Code: " + code + ")");
|
||||||
|
}
|
||||||
// Payload aufbauen mit der SERVER-SPECIFISCHEN ID
|
} catch (Exception ex) {
|
||||||
String payload = "{\"player\":\"" + escapeJson(playerName) + "\",\"token\":\"" + escapeJson(token) + "\",\"server_id\":" + config.serverId + ",\"signature\":\"" + signature + "\"}";
|
p.sendMessage(ChatColor.RED + "✗ Ein interner Fehler ist aufgetreten.");
|
||||||
|
ProxyServer.getInstance().getLogger().warning("Verify error: " + ex.getMessage());
|
||||||
URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
|
ex.printStackTrace();
|
||||||
conn = (HttpURLConnection) url.openConnection();
|
} finally {
|
||||||
conn.setConnectTimeout(5000);
|
if (conn != null) conn.disconnect();
|
||||||
conn.setReadTimeout(7000);
|
}
|
||||||
conn.setDoOutput(true);
|
}
|
||||||
conn.setRequestMethod("POST");
|
}
|
||||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
|
||||||
|
private static String hmacSHA256(String data, String key, Charset charset) throws Exception {
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
os.write(payload.getBytes(utf8));
|
mac.init(new SecretKeySpec(key.getBytes(charset), "HmacSHA256"));
|
||||||
}
|
byte[] raw = mac.doFinal(data.getBytes(charset));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
int code = conn.getResponseCode();
|
for (byte b : raw) sb.append(String.format("%02x", b));
|
||||||
String resp;
|
return sb.toString();
|
||||||
|
}
|
||||||
if (code >= 200 && code < 300) {
|
|
||||||
resp = streamToString(conn.getInputStream(), utf8);
|
private static String streamToString(InputStream in, Charset charset) throws IOException {
|
||||||
} else {
|
if (in == null) return "";
|
||||||
resp = streamToString(conn.getErrorStream(), utf8);
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
|
||||||
}
|
StringBuilder sb = new StringBuilder(); String line;
|
||||||
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
// Antwort verarbeiten
|
return sb.toString();
|
||||||
if (resp != null && !resp.isEmpty() && resp.trim().startsWith("{")) {
|
}
|
||||||
boolean isSuccess = resp.contains("\"success\":true");
|
}
|
||||||
String message = "Ein unbekannter Fehler ist aufgetreten.";
|
|
||||||
|
private static String escapeJson(String s) {
|
||||||
int keyIndex = resp.indexOf("\"message\":\"");
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||||
if (keyIndex != -1) {
|
}
|
||||||
int startIndex = keyIndex + 11;
|
}
|
||||||
int endIndex = resp.indexOf("\"", startIndex);
|
|
||||||
if (endIndex != -1) {
|
|
||||||
message = resp.substring(startIndex, endIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSuccess) {
|
|
||||||
p.sendMessage(ChatColor.GREEN + "✓ " + message);
|
|
||||||
p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!");
|
|
||||||
} else {
|
|
||||||
p.sendMessage(ChatColor.RED + "✗ " + message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.sendMessage(ChatColor.RED + "✗ Fehler beim Verbinden mit der Webseite (Code: " + code + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
|
||||||
p.sendMessage(ChatColor.RED + "✗ Ein interner Fehler ist aufgetreten.");
|
|
||||||
ProxyServer.getInstance().getLogger().warning("Verify error: " + ex.getMessage());
|
|
||||||
ex.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (conn != null) {
|
|
||||||
conn.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Helper Methoden ---
|
|
||||||
private static String hmacSHA256(String data, String key, Charset charset) throws Exception {
|
|
||||||
Mac mac = Mac.getInstance("HmacSHA256");
|
|
||||||
mac.init(new SecretKeySpec(key.getBytes(charset), "HmacSHA256"));
|
|
||||||
byte[] raw = mac.doFinal(data.getBytes(charset));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (byte b : raw) sb.append(String.format("%02x", b));
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String streamToString(InputStream in, Charset charset) throws IOException {
|
|
||||||
if (in == null) return "";
|
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) sb.append(line);
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String escapeJson(String s) {
|
|
||||||
return s.replace("\\", "\\\\").replace("\"","\\\"").replace("\n","\\n").replace("\r","\\r");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package net.viper.status.ratelimit;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemeinsames Rate-Limit-Framework fuer mehrere Module.
|
||||||
|
*/
|
||||||
|
public final class GlobalRateLimitFramework {
|
||||||
|
|
||||||
|
private static final GlobalRateLimitFramework INSTANCE = new GlobalRateLimitFramework();
|
||||||
|
|
||||||
|
private final Map<String, Bucket> buckets = new ConcurrentHashMap<String, Bucket>();
|
||||||
|
|
||||||
|
private GlobalRateLimitFramework() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GlobalRateLimitFramework getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result check(String scope, String actorId, Rule rule) {
|
||||||
|
return check(scope, actorId, rule, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result check(String scope, String actorId, Rule rule, long now) {
|
||||||
|
if (rule == null || !rule.enabled || scope == null || scope.isEmpty() || actorId == null || actorId.isEmpty()) {
|
||||||
|
return Result.allowed(0, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = scope + ":" + actorId;
|
||||||
|
Bucket bucket = buckets.computeIfAbsent(key, k -> new Bucket());
|
||||||
|
|
||||||
|
synchronized (bucket) {
|
||||||
|
if (bucket.blockedUntil > now) {
|
||||||
|
return Result.blocked(Math.max(0L, bucket.blockedUntil - now), bucket.hits.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
long minTs = now - Math.max(1L, rule.windowMs);
|
||||||
|
while (!bucket.hits.isEmpty() && bucket.hits.peekFirst() < minTs) {
|
||||||
|
bucket.hits.pollFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.hits.addLast(now);
|
||||||
|
|
||||||
|
if (bucket.hits.size() > Math.max(1, rule.maxActions)) {
|
||||||
|
long blockMs = Math.max(1L, rule.blockMs);
|
||||||
|
bucket.blockedUntil = now + blockMs;
|
||||||
|
return Result.blocked(blockMs, bucket.hits.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.allowed(bucket.hits.size(), 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearActor(String actorId) {
|
||||||
|
if (actorId == null || actorId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String key : buckets.keySet()) {
|
||||||
|
if (key.endsWith(":" + actorId)) {
|
||||||
|
buckets.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Rule {
|
||||||
|
public final boolean enabled;
|
||||||
|
public final long windowMs;
|
||||||
|
public final int maxActions;
|
||||||
|
public final long blockMs;
|
||||||
|
|
||||||
|
public Rule(boolean enabled, long windowMs, int maxActions, long blockMs) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
this.windowMs = Math.max(1L, windowMs);
|
||||||
|
this.maxActions = Math.max(1, maxActions);
|
||||||
|
this.blockMs = Math.max(1L, blockMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Result {
|
||||||
|
private final boolean blocked;
|
||||||
|
private final int currentHits;
|
||||||
|
private final long remainingBlockMs;
|
||||||
|
|
||||||
|
private Result(boolean blocked, int currentHits, long remainingBlockMs) {
|
||||||
|
this.blocked = blocked;
|
||||||
|
this.currentHits = Math.max(0, currentHits);
|
||||||
|
this.remainingBlockMs = Math.max(0L, remainingBlockMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result blocked(long remainingBlockMs, int currentHits) {
|
||||||
|
return new Result(true, currentHits, remainingBlockMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result allowed(int currentHits, long remainingBlockMs) {
|
||||||
|
return new Result(false, currentHits, remainingBlockMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlocked() {
|
||||||
|
return blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentHits() {
|
||||||
|
return currentHits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRemainingBlockMs() {
|
||||||
|
return remainingBlockMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Bucket {
|
||||||
|
final Deque<Long> hits = new ArrayDeque<Long>();
|
||||||
|
long blockedUntil = 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
180
StatusAPI/src/main/java/net/viper/status/stats/PlayerStats.java
Normal file
180
StatusAPI/src/main/java/net/viper/status/stats/PlayerStats.java
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package net.viper.status.stats;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PlayerStats {
|
||||||
|
public final UUID uuid;
|
||||||
|
public String name;
|
||||||
|
public long firstSeen;
|
||||||
|
public long lastSeen;
|
||||||
|
public long totalPlaytime;
|
||||||
|
public long currentSessionStart;
|
||||||
|
public int joins;
|
||||||
|
|
||||||
|
// Combat
|
||||||
|
public int kills;
|
||||||
|
public int deaths;
|
||||||
|
|
||||||
|
// Economy
|
||||||
|
public double balance;
|
||||||
|
public double totalEarned;
|
||||||
|
public double totalSpent;
|
||||||
|
public int transactionsCount;
|
||||||
|
|
||||||
|
// Punishments
|
||||||
|
public int bansCount;
|
||||||
|
public int mutesCount;
|
||||||
|
public int warnsCount;
|
||||||
|
public long lastPunishmentAt; // Unix-Timestamp (Sek.), 0 = nie
|
||||||
|
public String lastPunishmentType; // "ban", "mute", "warn", "kick", "" = nie
|
||||||
|
public int punishmentScore;
|
||||||
|
|
||||||
|
public PlayerStats(UUID uuid, String name) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.name = name;
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
this.firstSeen = now;
|
||||||
|
this.lastSeen = now;
|
||||||
|
this.totalPlaytime = 0;
|
||||||
|
this.currentSessionStart = 0;
|
||||||
|
this.joins = 0;
|
||||||
|
this.kills = 0;
|
||||||
|
this.deaths = 0;
|
||||||
|
this.balance = 0.0;
|
||||||
|
this.totalEarned = 0.0;
|
||||||
|
this.totalSpent = 0.0;
|
||||||
|
this.transactionsCount = 0;
|
||||||
|
this.bansCount = 0;
|
||||||
|
this.mutesCount = 0;
|
||||||
|
this.warnsCount = 0;
|
||||||
|
this.lastPunishmentAt = 0;
|
||||||
|
this.lastPunishmentType = "";
|
||||||
|
this.punishmentScore = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onJoin() {
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
if (this.currentSessionStart == 0) this.currentSessionStart = now;
|
||||||
|
this.lastSeen = now;
|
||||||
|
this.joins++;
|
||||||
|
if (this.firstSeen == 0) this.firstSeen = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onQuit() {
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
if (this.currentSessionStart > 0) {
|
||||||
|
long session = now - this.currentSessionStart;
|
||||||
|
if (session > 0) this.totalPlaytime += session;
|
||||||
|
this.currentSessionStart = 0;
|
||||||
|
}
|
||||||
|
this.lastSeen = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized long getPlaytimeWithCurrentSession() {
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
if (this.currentSessionStart > 0) return totalPlaytime + (now - currentSessionStart);
|
||||||
|
return totalPlaytime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format: uuid|name|firstSeen|lastSeen|totalPlaytime|currentSessionStart|joins|
|
||||||
|
* kills|deaths|
|
||||||
|
* balance|totalEarned|totalSpent|transactionsCount|
|
||||||
|
* bansCount|mutesCount|warnsCount|lastPunishmentAt|lastPunishmentType|punishmentScore
|
||||||
|
*
|
||||||
|
* HINWEIS: kills/deaths wurden in Version 1.17.1 als Felder 7 und 8 eingefügt.
|
||||||
|
* fromLine() ist rückwärtskompatibel (alte Dateien ohne kills/deaths werden 0 gesetzt).
|
||||||
|
*/
|
||||||
|
public synchronized String toLine() {
|
||||||
|
String safeType = (lastPunishmentType == null ? "" : lastPunishmentType).replace("|", "_");
|
||||||
|
return uuid + "|" + name.replace("|", "_")
|
||||||
|
+ "|" + firstSeen
|
||||||
|
+ "|" + lastSeen
|
||||||
|
+ "|" + totalPlaytime
|
||||||
|
+ "|" + currentSessionStart
|
||||||
|
+ "|" + joins
|
||||||
|
+ "|" + kills
|
||||||
|
+ "|" + deaths
|
||||||
|
+ "|" + balance
|
||||||
|
+ "|" + totalEarned
|
||||||
|
+ "|" + totalSpent
|
||||||
|
+ "|" + transactionsCount
|
||||||
|
+ "|" + bansCount
|
||||||
|
+ "|" + mutesCount
|
||||||
|
+ "|" + warnsCount
|
||||||
|
+ "|" + lastPunishmentAt
|
||||||
|
+ "|" + safeType
|
||||||
|
+ "|" + punishmentScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerStats fromLine(String line) {
|
||||||
|
String[] parts = line.split("\\|", -1);
|
||||||
|
if (parts.length < 7) return null;
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(parts[0]);
|
||||||
|
String name = parts[1];
|
||||||
|
PlayerStats ps = new PlayerStats(uuid, name);
|
||||||
|
ps.firstSeen = Long.parseLong(parts[2]);
|
||||||
|
ps.lastSeen = Long.parseLong(parts[3]);
|
||||||
|
ps.totalPlaytime = Long.parseLong(parts[4]);
|
||||||
|
ps.currentSessionStart = Long.parseLong(parts[5]);
|
||||||
|
ps.joins = Integer.parseInt(parts[6]);
|
||||||
|
|
||||||
|
// Erkennung ob altes Format (ohne kills/deaths, Economy ab Index 7)
|
||||||
|
// oder neues Format (kills/deaths ab Index 7, Economy ab Index 9).
|
||||||
|
// Altes Format hat 17 Felder (Index 0-16), neues hat 19 (Index 0-18).
|
||||||
|
// Heuristik: Wenn parts[7] ein gültiger Integer ist UND parts[9] wie eine
|
||||||
|
// Gleitkommazahl aussieht → neues Format. Sonst altes Format.
|
||||||
|
boolean newFormat = false;
|
||||||
|
if (parts.length >= 19) {
|
||||||
|
try {
|
||||||
|
Integer.parseInt(parts[7]); // kills
|
||||||
|
Integer.parseInt(parts[8]); // deaths
|
||||||
|
Double.parseDouble(parts[9]); // balance (Gleitkomma)
|
||||||
|
newFormat = true;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newFormat) {
|
||||||
|
// Neues Format: kills ab [7], deaths ab [8], economy ab [9]
|
||||||
|
try { ps.kills = Integer.parseInt(parts[7]); } catch (Exception ignored) {}
|
||||||
|
try { ps.deaths = Integer.parseInt(parts[8]); } catch (Exception ignored) {}
|
||||||
|
if (parts.length >= 13) {
|
||||||
|
try { ps.balance = Double.parseDouble(parts[9]); } catch (Exception ignored) {}
|
||||||
|
try { ps.totalEarned = Double.parseDouble(parts[10]); } catch (Exception ignored) {}
|
||||||
|
try { ps.totalSpent = Double.parseDouble(parts[11]); } catch (Exception ignored) {}
|
||||||
|
try { ps.transactionsCount = Integer.parseInt(parts[12]); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
if (parts.length >= 19) {
|
||||||
|
try { ps.bansCount = Integer.parseInt(parts[13]); } catch (Exception ignored) {}
|
||||||
|
try { ps.mutesCount = Integer.parseInt(parts[14]); } catch (Exception ignored) {}
|
||||||
|
try { ps.warnsCount = Integer.parseInt(parts[15]); } catch (Exception ignored) {}
|
||||||
|
try { ps.lastPunishmentAt = Long.parseLong(parts[16]); } catch (Exception ignored) {}
|
||||||
|
ps.lastPunishmentType = parts[17];
|
||||||
|
try { ps.punishmentScore = Integer.parseInt(parts[18]); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Altes Format (kompatibel): kills/deaths = 0, Economy ab [7]
|
||||||
|
ps.kills = 0;
|
||||||
|
ps.deaths = 0;
|
||||||
|
if (parts.length >= 11) {
|
||||||
|
try { ps.balance = Double.parseDouble(parts[7]); } catch (Exception ignored) {}
|
||||||
|
try { ps.totalEarned = Double.parseDouble(parts[8]); } catch (Exception ignored) {}
|
||||||
|
try { ps.totalSpent = Double.parseDouble(parts[9]); } catch (Exception ignored) {}
|
||||||
|
try { ps.transactionsCount = Integer.parseInt(parts[10]); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
if (parts.length >= 17) {
|
||||||
|
try { ps.bansCount = Integer.parseInt(parts[11]); } catch (Exception ignored) {}
|
||||||
|
try { ps.mutesCount = Integer.parseInt(parts[12]); } catch (Exception ignored) {}
|
||||||
|
try { ps.warnsCount = Integer.parseInt(parts[13]); } catch (Exception ignored) {}
|
||||||
|
try { ps.lastPunishmentAt = Long.parseLong(parts[14]); } catch (Exception ignored) {}
|
||||||
|
ps.lastPunishmentType = parts[15];
|
||||||
|
try { ps.punishmentScore = Integer.parseInt(parts[16]); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ps;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
package net.viper.status.stats;
|
package net.viper.status.stats;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public class StatsManager {
|
public class StatsManager {
|
||||||
private final ConcurrentHashMap<UUID, PlayerStats> map = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<UUID, PlayerStats> map = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public PlayerStats get(UUID uuid, String name) {
|
public PlayerStats get(UUID uuid, String name) {
|
||||||
return map.compute(uuid, (k, v) -> {
|
return map.compute(uuid, (k, v) -> {
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
return new PlayerStats(uuid, name != null ? name : "");
|
return new PlayerStats(uuid, name != null ? name : "");
|
||||||
} else {
|
} else {
|
||||||
if (name != null && !name.isEmpty()) v.name = name;
|
if (name != null && !name.isEmpty()) v.name = name;
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerStats getIfPresent(UUID uuid) {
|
public PlayerStats getIfPresent(UUID uuid) {
|
||||||
return map.get(uuid);
|
return map.get(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Iterable<PlayerStats> all() {
|
public Iterable<PlayerStats> all() {
|
||||||
return map.values();
|
return map.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void put(PlayerStats ps) {
|
public void put(PlayerStats ps) {
|
||||||
map.put(ps.uuid, ps);
|
map.put(ps.uuid, ps);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void remove(UUID uuid) {
|
public void remove(UUID uuid) {
|
||||||
map.remove(uuid);
|
map.remove(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
145
StatusAPI/src/main/java/net/viper/status/stats/StatsModule.java
Normal file
145
StatusAPI/src/main/java/net/viper/status/stats/StatsModule.java
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package net.viper.status.stats;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||||
|
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StatsModule: Tracking von Spielerdaten (Playtime, Joins, Kills, Deaths).
|
||||||
|
*
|
||||||
|
* Fixes:
|
||||||
|
* - BUG-1: Crash-Recovery für currentSessionStart (verhindert falsche Spielzeit nach Absturz)
|
||||||
|
* - BUG-2: kills / deaths werden jetzt getrackt und per POST /stats/update aktualisiert
|
||||||
|
*/
|
||||||
|
public class StatsModule implements Module, Listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximale Sessionlänge nach einem Crash noch gutschreiben (24 Stunden).
|
||||||
|
* Längere Differenzen sind unrealistisch → werden ignoriert, currentSessionStart = 0 gesetzt.
|
||||||
|
*/
|
||||||
|
private static final long MAX_SESSION_SECONDS = 86_400L;
|
||||||
|
|
||||||
|
private StatsManager manager;
|
||||||
|
private StatsStorage storage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "StatsModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
manager = new StatsManager();
|
||||||
|
storage = new StatsStorage(plugin.getDataFolder());
|
||||||
|
|
||||||
|
try {
|
||||||
|
storage.load(manager);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// FIX BUG-1: Crash-Recovery – offene Sessions bereinigen.
|
||||||
|
//
|
||||||
|
// Bei normalem Shutdown setzt onDisable() currentSessionStart = 0 und speichert.
|
||||||
|
// Bei einem Crash (kill -9, OOM, etc.) passiert das nicht. Beim nächsten Start
|
||||||
|
// sind alle Spieler offline, aber currentSessionStart enthält noch den alten
|
||||||
|
// Timestamp. getPlaytimeWithCurrentSession() würde dann fälschlicherweise
|
||||||
|
// (now - alter_crash_timestamp) zur Spielzeit addieren → massiv falscher Wert.
|
||||||
|
//
|
||||||
|
// Fix: Nach dem Laden jeden Eintrag prüfen. Falls currentSessionStart > 0:
|
||||||
|
// - Plausible Differenz (≤ MAX_SESSION_SECONDS) → als echte Zeit gutschreiben
|
||||||
|
// - Unplausibel (> MAX_SESSION_SECONDS) → verwerfen, nur zurücksetzen
|
||||||
|
// - In beiden Fällen: currentSessionStart = 0 setzen
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
int recovered = 0;
|
||||||
|
for (PlayerStats ps : manager.all()) {
|
||||||
|
synchronized (ps) {
|
||||||
|
if (ps.currentSessionStart > 0) {
|
||||||
|
long delta = now - ps.currentSessionStart;
|
||||||
|
if (delta > 0 && delta <= MAX_SESSION_SECONDS) {
|
||||||
|
ps.totalPlaytime += delta;
|
||||||
|
recovered++;
|
||||||
|
} else if (delta > MAX_SESSION_SECONDS) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[StatsModule] Unplausibler currentSessionStart für " + ps.name
|
||||||
|
+ " (delta=" + delta + "s > " + MAX_SESSION_SECONDS + "s). "
|
||||||
|
+ "Session wird ohne Gutschrift zurückgesetzt."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ps.currentSessionStart = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (recovered > 0) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[StatsModule] Crash-Recovery: " + recovered + " offene Session(en) bereinigt und gespeichert."
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
storage.save(manager);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("Fehler beim Speichern nach Crash-Recovery: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
||||||
|
|
||||||
|
// Auto-Save alle 5 Minuten
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
|
try {
|
||||||
|
storage.save(manager);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}, 5, 5, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
if (manager != null && storage != null) {
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
for (PlayerStats ps : manager.all()) {
|
||||||
|
synchronized (ps) {
|
||||||
|
if (ps.currentSessionStart > 0) {
|
||||||
|
long delta = now - ps.currentSessionStart;
|
||||||
|
if (delta > 0) ps.totalPlaytime += delta;
|
||||||
|
ps.currentSessionStart = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
storage.save(manager);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatsManager getManager() {
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Events ---
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onJoin(PostLoginEvent e) {
|
||||||
|
try {
|
||||||
|
manager.get(e.getPlayer().getUniqueId(), e.getPlayer().getName()).onJoin();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onQuit(PlayerDisconnectEvent e) {
|
||||||
|
try {
|
||||||
|
PlayerStats ps = manager.getIfPresent(e.getPlayer().getUniqueId());
|
||||||
|
if (ps != null) ps.onQuit();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package net.viper.status.stats;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix #9: save() und load() sind jetzt synchronized um Race Conditions
|
||||||
|
* zwischen Auto-Save-Task und Shutdown-Aufruf zu verhindern.
|
||||||
|
*/
|
||||||
|
public class StatsStorage {
|
||||||
|
private final File file;
|
||||||
|
private final Object fileLock = new Object();
|
||||||
|
|
||||||
|
public StatsStorage(File pluginFolder) {
|
||||||
|
if (!pluginFolder.exists()) pluginFolder.mkdirs();
|
||||||
|
this.file = new File(pluginFolder, "stats.dat");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(StatsManager manager) {
|
||||||
|
synchronized (fileLock) {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||||
|
for (PlayerStats ps : manager.all()) {
|
||||||
|
bw.write(ps.toLine());
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
bw.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load(StatsManager manager) {
|
||||||
|
if (!file.exists()) return;
|
||||||
|
synchronized (fileLock) {
|
||||||
|
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
PlayerStats ps = PlayerStats.fromLine(line);
|
||||||
|
if (ps != null) manager.put(ps);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
376
StatusAPI/src/main/resources/chat.yml
Normal file
376
StatusAPI/src/main/resources/chat.yml
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
# ============================================================
|
||||||
|
# StatusAPI - ChatModule Konfiguration
|
||||||
|
# Kompatibel mit Java & Bedrock (Geyser) | BungeeCord Secure Chat
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Standard-Kanal beim Einloggen
|
||||||
|
default-channel: "global"
|
||||||
|
|
||||||
|
server-colors:
|
||||||
|
default: "&7" # Fallback für unbekannte Server
|
||||||
|
lobby:
|
||||||
|
color: "&a"
|
||||||
|
display: "Lobby" # Anzeigename (optional, sonst wird der echte Servername verwendet)
|
||||||
|
survival:
|
||||||
|
color: "&#E8A020"
|
||||||
|
display: "Survival"
|
||||||
|
skyblock:
|
||||||
|
color: "&b"
|
||||||
|
display: "SkyBlock"
|
||||||
|
citybuild:
|
||||||
|
color: "&#A020E8"
|
||||||
|
display: "CityBuild"
|
||||||
|
minigames:
|
||||||
|
color: "&e"
|
||||||
|
display: "MiniGames"
|
||||||
|
|
||||||
|
chatlog:
|
||||||
|
enabled: true
|
||||||
|
retention-days: 7 # 7 oder 14
|
||||||
|
|
||||||
|
reports:
|
||||||
|
enabled: true
|
||||||
|
webhook-enabled: true
|
||||||
|
confirm-message: "&aDein Report &8({id}) &awurde eingereicht. Danke!"
|
||||||
|
close-permission: "chat.admin.bypass"
|
||||||
|
view-permission: "chat.admin.bypass"
|
||||||
|
# Leer = jeder Spieler darf reporten, sonst Permission eintragen (z.B. "chat.report")
|
||||||
|
report-permission: ""
|
||||||
|
cooldown: 60
|
||||||
|
# Discord Webhook für Report-Benachrichtigungen (leer = deaktiviert)
|
||||||
|
discord-webhook: ""
|
||||||
|
# Telegram Chat-ID für Report-Benachrichtigungen (leer = deaktiviert)
|
||||||
|
telegram-chat-id: ""
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# KANÄLE
|
||||||
|
# Jeder Kanal hat eigene Permissions, Format und Brücken.
|
||||||
|
# format-Platzhalter:
|
||||||
|
# {server} - Servername
|
||||||
|
# {prefix} - LuckPerms Prefix
|
||||||
|
# {player} - Spielername
|
||||||
|
# {suffix} - LuckPerms Suffix
|
||||||
|
# {message} - Nachricht
|
||||||
|
# {channel} - Kanalname
|
||||||
|
# ============================================================
|
||||||
|
channels:
|
||||||
|
global:
|
||||||
|
name: "Global"
|
||||||
|
symbol: "G"
|
||||||
|
permission: ""
|
||||||
|
color: "&a"
|
||||||
|
format: "&8[&a{server}&8] {prefix}&r{player}&8: &f{message}"
|
||||||
|
discord-webhook: ""
|
||||||
|
discord-channel-id: ""
|
||||||
|
telegram-chat-id: ""
|
||||||
|
# Themen-ID für Telegram-Gruppen mit Themen (0 = kein Thema / normale Gruppe)
|
||||||
|
telegram-thread-id: 0
|
||||||
|
|
||||||
|
local:
|
||||||
|
name: "Local"
|
||||||
|
symbol: "L"
|
||||||
|
permission: "chat.channel.local"
|
||||||
|
color: "&e"
|
||||||
|
local-only: true
|
||||||
|
format: "&8[&e{server}&8] {prefix}&r{player}&8: &f{message}"
|
||||||
|
discord-webhook: ""
|
||||||
|
discord-channel-id: ""
|
||||||
|
telegram-chat-id: ""
|
||||||
|
telegram-thread-id: 0
|
||||||
|
|
||||||
|
trade:
|
||||||
|
name: "Trade"
|
||||||
|
symbol: "T"
|
||||||
|
permission: "chat.channel.trade"
|
||||||
|
color: "&6"
|
||||||
|
format: "&8[&6TRADE&8] &8[&7{server}&8] {prefix}&r{player}&8: &f{message}"
|
||||||
|
discord-webhook: ""
|
||||||
|
discord-channel-id: ""
|
||||||
|
telegram-chat-id: ""
|
||||||
|
telegram-thread-id: 0
|
||||||
|
|
||||||
|
staff:
|
||||||
|
name: "Staff"
|
||||||
|
symbol: "S"
|
||||||
|
permission: "chat.channel.staff"
|
||||||
|
color: "&c"
|
||||||
|
format: "&8[&cSTAFF&8] &8[&7{server}&8] {prefix}&r{player}&8: &f{message}"
|
||||||
|
discord-webhook: ""
|
||||||
|
discord-channel-id: ""
|
||||||
|
telegram-chat-id: ""
|
||||||
|
telegram-thread-id: 0
|
||||||
|
use-admin-bridge: true
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# HELPOP
|
||||||
|
# ============================================================
|
||||||
|
helpop:
|
||||||
|
# Format der HelpOp-Nachricht
|
||||||
|
format: "&8[&eHELPOP&8] &f{player}&8@&7{server}&8: &e{message}"
|
||||||
|
# Wer bekommt HelpOp zu sehen
|
||||||
|
receive-permission: "chat.helpop.receive"
|
||||||
|
# Cooldown in Sekunden
|
||||||
|
cooldown: 30
|
||||||
|
# Bestätigungsnachricht an den Spieler
|
||||||
|
confirm-message: "&aHilferuf wurde an das Team gesendet!"
|
||||||
|
# Discord / Telegram auch für HelpOp
|
||||||
|
discord-webhook: ""
|
||||||
|
telegram-chat-id: ""
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BROADCAST
|
||||||
|
# ============================================================
|
||||||
|
broadcast:
|
||||||
|
format: "&c&l[&6&lBroadcast&c&l] &r&e{message}"
|
||||||
|
permission: "chat.broadcast"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PRIVATE NACHRICHTEN
|
||||||
|
# ============================================================
|
||||||
|
private-messages:
|
||||||
|
enabled: true
|
||||||
|
format-sender: "&8[&7Du &8→ &b{player}&8] &f{message}"
|
||||||
|
format-receiver: "&8[&b{player} &8→ &7Dir&8] &f{message}"
|
||||||
|
# Social Spy: Admins können alle PMs sehen
|
||||||
|
format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}"
|
||||||
|
social-spy-permission: "chat.socialspy"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# JOIN / LEAVE NACHRICHTEN
|
||||||
|
# Platzhalter:
|
||||||
|
# {player} - Spielername
|
||||||
|
# {prefix} - LuckPerms Prefix
|
||||||
|
# {suffix} - LuckPerms Suffix
|
||||||
|
# {server} - Zuletzt bekannter Server (bei Leave) oder "Netzwerk"
|
||||||
|
# ============================================================
|
||||||
|
join-leave:
|
||||||
|
enabled: true
|
||||||
|
# Normale Join/Leave-Nachrichten (für alle sichtbar)
|
||||||
|
join-format: "&8[&a+&8] {prefix}&a{player}&r &7hat das Netzwerk betreten."
|
||||||
|
leave-format: "&8[&c-&8] {prefix}&c{player}&r &7hat das Netzwerk verlassen."
|
||||||
|
# Vanish: Unsichtbare Spieler erzeugen keine normalen Join/Leave-Meldungen.
|
||||||
|
# Ist vanish-show-to-admins true, sehen Admins mit bypass-permission eine
|
||||||
|
# abweichende, dezente Benachrichtigung.
|
||||||
|
vanish-show-to-admins: true
|
||||||
|
vanish-join-format: "&8[&7+&8] &8{player} &7hat das Netzwerk betreten. &8(Vanish)"
|
||||||
|
vanish-leave-format: "&8[&7-&8] &8{player} &7hat das Netzwerk verlassen. &8(Vanish)"
|
||||||
|
# Brücken-Weitergabe (leer = deaktiviert)
|
||||||
|
discord-webhook: ""
|
||||||
|
telegram-chat-id: ""
|
||||||
|
telegram-thread-id: 0
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# GLOBALES RATE-LIMIT-FRAMEWORK
|
||||||
|
# Zentraler Schutz für Chat/PM/Command-Flood.
|
||||||
|
# ============================================================
|
||||||
|
rate-limit:
|
||||||
|
chat:
|
||||||
|
enabled: true
|
||||||
|
window-ms: 2500
|
||||||
|
max-actions: 3
|
||||||
|
block-ms: 6000
|
||||||
|
message: "&cBitte nicht so schnell schreiben!"
|
||||||
|
|
||||||
|
private-messages:
|
||||||
|
enabled: true
|
||||||
|
window-ms: 5000
|
||||||
|
max-actions: 4
|
||||||
|
block-ms: 10000
|
||||||
|
message: "&cDu sendest zu viele private Nachrichten. Bitte warte kurz."
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MUTE
|
||||||
|
# ============================================================
|
||||||
|
mute:
|
||||||
|
# Standard-Mute-Dauer in Minuten (0 = permanent)
|
||||||
|
default-duration-minutes: 60
|
||||||
|
# Nachricht an gemuteten Spieler
|
||||||
|
muted-message: "&cDu bist aktuell stummgeschaltet. Noch: &f{time}"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# EMOJI
|
||||||
|
# Spieler schreiben :smile: -> wird zu \uD83D\uDE0A konvertiert
|
||||||
|
# Bedrock-Spieler erhalten Fallback-Text wenn kein Unicode
|
||||||
|
# ============================================================
|
||||||
|
emoji:
|
||||||
|
enabled: true
|
||||||
|
# Ob Bedrock-Spieler (via Geyser) auch Emojis erhalten
|
||||||
|
bedrock-support: true
|
||||||
|
mappings:
|
||||||
|
":smile:": "\uD83D\uDE0A"
|
||||||
|
":laugh:": "\uD83D\uDE04"
|
||||||
|
":sad:": "\uD83D\uDE22"
|
||||||
|
":cry:": "\uD83D\uDE2D"
|
||||||
|
":angry:": "\uD83D\uDE20"
|
||||||
|
":heart:": "\u2764\uFE0F"
|
||||||
|
":fire:": "\uD83D\uDD25"
|
||||||
|
":star:": "\u2B50"
|
||||||
|
":check:": "\u2705"
|
||||||
|
":x:": "\u274C"
|
||||||
|
":warning:": "\u26A0\uFE0F"
|
||||||
|
":thumbsup:": "\uD83D\uDC4D"
|
||||||
|
":thumbsdown:": "\uD83D\uDC4E"
|
||||||
|
":wave:": "\uD83D\uDC4B"
|
||||||
|
":clap:": "\uD83D\uDC4F"
|
||||||
|
":sword:": "\u2694\uFE0F"
|
||||||
|
":shield:": "\uD83D\uDEE1\uFE0F"
|
||||||
|
":diamond:": "\uD83D\uDC8E"
|
||||||
|
":crown:": "\uD83D\uDC51"
|
||||||
|
":skull:": "\uD83D\uDC80"
|
||||||
|
":sun:": "\u2600\uFE0F"
|
||||||
|
":moon:": "\uD83C\uDF19"
|
||||||
|
":tree:": "\uD83C\uDF33"
|
||||||
|
":house:": "\uD83C\uDFE0"
|
||||||
|
":money:": "\uD83D\uDCB0"
|
||||||
|
":rocket:": "\uD83D\uDE80"
|
||||||
|
":rainbow:": "\uD83C\uDF08"
|
||||||
|
":ghost:": "\uD83D\uDC7B"
|
||||||
|
":gift:": "\uD83C\uDF81"
|
||||||
|
":cake:": "\uD83C\uDF82"
|
||||||
|
":chicken:": "\uD83D\uDC14"
|
||||||
|
":pig:": "\uD83D\uDC37"
|
||||||
|
":creeper:": "\uD83D\uDCA3"
|
||||||
|
":gg:": "\uD83C\uDFAE"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DISCORD INTEGRATION
|
||||||
|
# ============================================================
|
||||||
|
discord:
|
||||||
|
enabled: false
|
||||||
|
# Bot-Token für bidirektionale Kommunikation
|
||||||
|
bot-token: "YOUR_BOT_TOKEN_HERE"
|
||||||
|
# Server (Guild) ID
|
||||||
|
guild-id: "YOUR_GUILD_ID"
|
||||||
|
# Polling-Intervall in Sekunden (Discord → Minecraft)
|
||||||
|
poll-interval: 3
|
||||||
|
# Format für Discord → Minecraft Nachrichten
|
||||||
|
from-discord-format: "&9[&bDiscord&9] &b{user}&8: &f{message}"
|
||||||
|
# Extra Admin-Kanal (für Staff-Kanal und HelpOp)
|
||||||
|
admin-channel-id: ""
|
||||||
|
# Standard-Embed-Farbe (Hex ohne #)
|
||||||
|
embed-color: "5865F2"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TELEGRAM INTEGRATION
|
||||||
|
# ============================================================
|
||||||
|
telegram:
|
||||||
|
enabled: false
|
||||||
|
# Bot-Token von @BotFather
|
||||||
|
bot-token: "YOUR_TELEGRAM_BOT_TOKEN"
|
||||||
|
# Polling-Intervall in Sekunden
|
||||||
|
poll-interval: 3
|
||||||
|
# Format für Telegram → Minecraft Nachrichten
|
||||||
|
from-telegram-format: "&3[&bTelegram&3] &b{user}&8: &f{message}"
|
||||||
|
# Extra Admin-Chat-ID (für Staff-Kanal und HelpOp)
|
||||||
|
admin-chat-id: ""
|
||||||
|
# Themen-Gruppe: Topic-ID für den Chat-Kanal (0 = kein Topic / normale Gruppe)
|
||||||
|
# Die message_thread_id findest du indem du eine Nachricht im Topic weiterleitest
|
||||||
|
# und dir die forwarded_from_message_id anschaust, oder via Bot-API getUpdates.
|
||||||
|
chat-topic-id: 0
|
||||||
|
# Topic-ID für den Admin-Kanal (0 = kein Topic)
|
||||||
|
admin-topic-id: 0
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ACCOUNT-VERKNÜPFUNG (Discord & Telegram)
|
||||||
|
# Spieler können ihre Minecraft-Accounts mit Discord/Telegram
|
||||||
|
# verknüpfen damit ihr Name im Chat angezeigt wird.
|
||||||
|
# ============================================================
|
||||||
|
account-linking:
|
||||||
|
enabled: true
|
||||||
|
# Token läuft nach X Minuten ab
|
||||||
|
token-expire-minutes: 10
|
||||||
|
# Nachricht die der Spieler nach /linkdiscord bekommt
|
||||||
|
discord-link-message: "&aSchreibe den folgenden Code als Nachricht an unseren Discord-Bot:\n&f&l{token}\n&7Der Code läuft in &f10 Minuten &7ab."
|
||||||
|
# Nachricht die der Spieler nach /linktelegram bekommt
|
||||||
|
telegram-link-message: "&aSchreibe den folgenden Code als Nachricht an unseren Telegram-Bot:\n&f&l{token}\n&7Der Code läuft in &f10 Minuten &7ab."
|
||||||
|
# Bestätigung nach erfolgreicher Verknüpfung (im Spiel)
|
||||||
|
success-discord: "&aDiscord-Account erfolgreich verknüpft! &8(&7{discord}&8)"
|
||||||
|
success-telegram: "&aTelegram-Account erfolgreich verknüpft! &8(&7{telegram}&8)"
|
||||||
|
# Bestätigung die der Bot in Discord/Telegram schickt
|
||||||
|
bot-success-discord: "✅ Dein Minecraft-Account **{player}** wurde erfolgreich verknüpft!"
|
||||||
|
bot-success-telegram: "✅ Dein Minecraft-Account <b>{player}</b> wurde erfolgreich verknüpft!"
|
||||||
|
# Format wenn verknüpfter Nutzer in Discord/Telegram schreibt
|
||||||
|
# {player} = Minecraft-Name, {user} = Discord/Telegram-Name, {message} = Nachricht
|
||||||
|
linked-discord-format: "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}"
|
||||||
|
linked-telegram-format: "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}"
|
||||||
|
# Themen-ID für den Admin-Chat (0 = kein Thema)
|
||||||
|
admin-thread-id: 0
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ADMIN BYPASS
|
||||||
|
# Spieler mit dieser Permission können nicht geblockt werden
|
||||||
|
# und sind von Mutes ausgenommen
|
||||||
|
# ============================================================
|
||||||
|
admin:
|
||||||
|
bypass-permission: "chat.admin.bypass"
|
||||||
|
# Admins erhalten Benachrichtigung bei Mutes/Blocks
|
||||||
|
notify-permission: "chat.admin.notify"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CHAT-FILTER & ANTI-SPAM
|
||||||
|
# ============================================================
|
||||||
|
chat-filter:
|
||||||
|
anti-spam:
|
||||||
|
enabled: true
|
||||||
|
cooldown-ms: 1500
|
||||||
|
max-messages: 3
|
||||||
|
message: "&cBitte nicht so schnell schreiben!"
|
||||||
|
duplicate-check:
|
||||||
|
enabled: true
|
||||||
|
message: "&cBitte keine identischen Nachrichten senden."
|
||||||
|
blacklist:
|
||||||
|
enabled: true
|
||||||
|
words:
|
||||||
|
- "beispielwort1"
|
||||||
|
- "beispielwort2"
|
||||||
|
caps-filter:
|
||||||
|
enabled: true
|
||||||
|
min-length: 6
|
||||||
|
max-percent: 70
|
||||||
|
|
||||||
|
anti-ad:
|
||||||
|
enabled: true
|
||||||
|
message: "&cWerbung ist in diesem Chat nicht erlaubt!"
|
||||||
|
# Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
|
||||||
|
# Vergleich ist case-insensitiv und prüft ob der Substring im Match enthalten ist
|
||||||
|
whitelist:
|
||||||
|
- "viper-network.de"
|
||||||
|
- "m-viper.de"
|
||||||
|
- "https://www.spigotmc.org"
|
||||||
|
# TLDs die als Werbung gewertet werden.
|
||||||
|
# Leer = alle Domain-Treffer blockieren (nicht empfohlen, hohe False-Positive-Rate)
|
||||||
|
blocked-tlds:
|
||||||
|
- "net"
|
||||||
|
- "com"
|
||||||
|
- "de"
|
||||||
|
- "org"
|
||||||
|
- "gg"
|
||||||
|
- "io"
|
||||||
|
- "eu"
|
||||||
|
- "tv"
|
||||||
|
- "xyz"
|
||||||
|
- "info"
|
||||||
|
- "me"
|
||||||
|
- "cc"
|
||||||
|
- "co"
|
||||||
|
- "app"
|
||||||
|
- "online"
|
||||||
|
- "site"
|
||||||
|
- "fun"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MENTIONS (@Spielername)
|
||||||
|
# ============================================================
|
||||||
|
mentions:
|
||||||
|
enabled: true
|
||||||
|
highlight-color: "&e&l"
|
||||||
|
sound: "ENTITY_EXPERIENCE_ORB_PICKUP"
|
||||||
|
allow-toggle: true
|
||||||
|
notify-prefix: "&e&l[Mention] &r"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CHAT-HISTORY
|
||||||
|
# ============================================================
|
||||||
|
chat-history:
|
||||||
|
max-lines: 50
|
||||||
|
default-lines: 10
|
||||||
16
StatusAPI/src/main/resources/filter.yml
Normal file
16
StatusAPI/src/main/resources/filter.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# ============================================================
|
||||||
|
# StatusAPI - ChatModule Wort-Blacklist
|
||||||
|
# Wörter werden case-insensitiv und als Teilwort geprüft.
|
||||||
|
# Erkannte Wörter werden durch **** ersetzt.
|
||||||
|
#
|
||||||
|
# Diese Datei wird bei /chatreload automatisch neu eingelesen.
|
||||||
|
# Wörter die hier stehen ÜBERSCHREIBEN NICHT die Einträge in
|
||||||
|
# chat.yml → beide Listen werden zusammengeführt.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
words:
|
||||||
|
- beispielwort1
|
||||||
|
- beispielwort2
|
||||||
|
# Hier eigene Wörter eintragen, eines pro Zeile:
|
||||||
|
# - schimpfwort
|
||||||
|
# - spam
|
||||||
18
StatusAPI/src/main/resources/messages.txt
Normal file
18
StatusAPI/src/main/resources/messages.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
§8[§2Viper-Netzwerk§8] §7Der Server läuft 24/7 – also keine Hektik beim Spielen :)
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Dies ist ein privater Server – hier zählt der Zusammenhalt.
|
||||||
|
§8[§dTipp§8] §7Wenn du denkst, du bist sicher… schau nochmal nach. Creeper machen keine Geräusche beim Tippen.
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Wähle einen Server, leg los – der Rest ergibt sich. Oder explodiert.
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Mehr Server. Mehr Blöcke. Mehr Unfälle. Willkommen!
|
||||||
|
§8[§dTipp§8] §7Halte eine Spitzhacke mit Glück bereit. Man weiß nie, wann das nächste Erz kommt.
|
||||||
|
§8[§dTipp§8] §7Mit §e/home§7 kannst du dich jederzeit nach Hause teleportieren.
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Das wichtigste Plugin? Du selbst. Spiel fair, sei kreativ!
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Redstone ist keine Magie – aber fast.
|
||||||
|
§8[§dTipp§8] §7Schilde sind cool. Besonders wenn Skelette zielen.
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Wenn du in Lava fällst, bist du nicht der Erste. Nur der Nächste.
|
||||||
|
§8[§dTipp§8] §7Villager sind nicht dumm – nur sehr… eigen.
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Bau groß, bau sicher – oder bau eine Treppe zur Nachbarschaftsklage.
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Gras wächst. Spieler auch. Gib jedem eine Chance!
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Ein Creeper ist keine Begrüßung. Es sei denn, du willst es spannend machen.
|
||||||
|
§8[§dTipp§8] §7Ein voller Magen ist halbe Miete. Farmen lohnt sich!
|
||||||
|
§8[§2Viper-Netzwerk§8] §7Wir haben keine Probleme – nur Redstone-Schaltungen mit Charakter.
|
||||||
|
§8[§dTipp§8] §7Markiere dein Grundstück mit §e/p claim§7, bevor es jemand anderes tut!
|
||||||
102
StatusAPI/src/main/resources/network-guard.properties
Normal file
102
StatusAPI/src/main/resources/network-guard.properties
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# ===========================
|
||||||
|
# NETWORK INFO MODUL
|
||||||
|
# ===========================
|
||||||
|
networkinfo.enabled=true
|
||||||
|
networkinfo.command.enabled=true
|
||||||
|
# Aus Datenschutzgruenden standardmaessig aus. Wenn true, erscheinen alle Spielernamen im JSON.
|
||||||
|
networkinfo.include_player_names=false
|
||||||
|
|
||||||
|
# Discord Webhook fuer Status-, Warn- und Attack-Meldungen
|
||||||
|
networkinfo.webhook.enabled=true
|
||||||
|
networkinfo.webhook.url=
|
||||||
|
networkinfo.webhook.username=StatusAPI
|
||||||
|
networkinfo.webhook.thumbnail_url=
|
||||||
|
networkinfo.webhook.notify_start_stop=true
|
||||||
|
# compact = kurze Texte | detailed = strukturierte Embeds mit Feldern
|
||||||
|
networkinfo.webhook.embed_mode=detailed
|
||||||
|
networkinfo.webhook.check_seconds=30
|
||||||
|
|
||||||
|
# Alert-Schwellwerte
|
||||||
|
networkinfo.alert.memory_percent=90
|
||||||
|
networkinfo.alert.player_percent=95
|
||||||
|
networkinfo.alert.cooldown_seconds=300
|
||||||
|
# Proxy-TPS Alert (20.0 = perfekt, Werte < 20 zeigen Main-Thread-Lag am Proxy)
|
||||||
|
networkinfo.alert.tps_enabled=true
|
||||||
|
networkinfo.alert.tps_threshold=18.0
|
||||||
|
|
||||||
|
# Attack Meldungen (Detected/Stopped)
|
||||||
|
networkinfo.attack.enabled=true
|
||||||
|
# Nutzt automatisch networkinfo.webhook.url
|
||||||
|
networkinfo.attack.source=Viper-Network
|
||||||
|
# API-Key fuer POST /network/attack
|
||||||
|
networkinfo.attack.api_key=2jN8xQ4mL9vK3sT7pR1yW6dH5cF0bZ
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# ANTIBOT / ATTACK GUARD
|
||||||
|
# ===========================
|
||||||
|
antibot.enabled=true
|
||||||
|
|
||||||
|
# Profile: strict | high-traffic
|
||||||
|
# strict: agressiver Schutz, schnelleres Blocken, VPN-Check standardmaessig aktiv
|
||||||
|
# high-traffic: toleranter fuer grosse Netzwerke mit Lastspitzen
|
||||||
|
antibot.profile=high-traffic
|
||||||
|
|
||||||
|
# Presets (Referenz):
|
||||||
|
# strict -> max_cps=120, start_cps=220, stop_cps=120, ip/min=18, block_seconds=900, vpn_check.enabled=true
|
||||||
|
# high-traffic -> max_cps=180, start_cps=300, stop_cps=170, ip/min=24, block_seconds=600, vpn_check.enabled=false
|
||||||
|
# Hinweis: Werte unten ueberschreiben das Profil bei Bedarf.
|
||||||
|
|
||||||
|
# Globaler Traffic
|
||||||
|
antibot.max_cps=180
|
||||||
|
antibot.attack.start_cps=300
|
||||||
|
antibot.attack.stop_cps=170
|
||||||
|
antibot.attack.stop_grace_seconds=25
|
||||||
|
|
||||||
|
# Pro-IP Limiter
|
||||||
|
antibot.ip.max_connections_per_minute=24
|
||||||
|
antibot.ip.block_seconds=600
|
||||||
|
antibot.kick_message=Zu viele Verbindungen von deiner IP. Bitte warte kurz.
|
||||||
|
|
||||||
|
# Optionaler VPN/Proxy/Hosting Check (ip-api)
|
||||||
|
antibot.vpn_check.enabled=false
|
||||||
|
antibot.vpn_check.block_proxy=true
|
||||||
|
antibot.vpn_check.block_hosting=true
|
||||||
|
antibot.vpn_check.cache_minutes=30
|
||||||
|
antibot.vpn_check.timeout_ms=2500
|
||||||
|
|
||||||
|
# Sicherheitslog fuer Angreifer/VPN/Proxy-Events (mit Name/UUID falls verfuegbar)
|
||||||
|
antibot.security_log.enabled=true
|
||||||
|
antibot.security_log.file=antibot-security.log
|
||||||
|
|
||||||
|
# Lernmodus: Muster mitschreiben, Score bilden und erst ab Schwellwert blockieren.
|
||||||
|
antibot.learning.enabled=true
|
||||||
|
antibot.learning.score_threshold=100
|
||||||
|
antibot.learning.decay_per_second=2
|
||||||
|
antibot.learning.state_window_seconds=120
|
||||||
|
|
||||||
|
# Punktelogik pro Muster
|
||||||
|
antibot.learning.rapid.window_ms=1500
|
||||||
|
antibot.learning.rapid.points=12
|
||||||
|
antibot.learning.ip_rate_exceeded.points=30
|
||||||
|
antibot.learning.vpn_proxy.points=40
|
||||||
|
antibot.learning.vpn_hosting.points=30
|
||||||
|
antibot.learning.attack_mode.points=12
|
||||||
|
antibot.learning.high_cps.points=10
|
||||||
|
antibot.learning.recent_events.limit=30
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# BACKEND JOIN GUARD SYNC (optional)
|
||||||
|
# ===========================
|
||||||
|
# Diese Werte koennen von BackendJoinGuard im StatusAPI-Sync-Modus abgeholt werden.
|
||||||
|
# Standalone bleibt weiterhin moeglich.
|
||||||
|
backendguard.enforcement_enabled=true
|
||||||
|
backendguard.log_blocked_attempts=true
|
||||||
|
backendguard.kick_message=&cBitte verbinde dich nur über den Proxy-Server.
|
||||||
|
# Wichtig: Hier nur echte Proxy-IP(s) eintragen.
|
||||||
|
backendguard.allowed_proxy_ips=127.0.0.1,::1,10.0.0.10
|
||||||
|
# Optional: internes Proxy-Netz als CIDR
|
||||||
|
backendguard.allowed_proxy_cidrs=10.0.0.0/24
|
||||||
|
|
||||||
|
# Optionaler API-Key fuer GET /network/backendguard/config
|
||||||
|
# Leer = kein API-Key erforderlich (nur im internen Netzwerk empfohlen)
|
||||||
|
backendguard.sync.api_key=bgSync_7Rk9pQ2nLm5xV8cH4tW1yZ6
|
||||||
295
StatusAPI/src/main/resources/plugin.yml
Normal file
295
StatusAPI/src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
name: StatusAPI
|
||||||
|
main: net.viper.status.StatusAPI
|
||||||
|
version: 4.1.1
|
||||||
|
author: M_Viper
|
||||||
|
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
|
||||||
|
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
|
||||||
|
|
||||||
|
softdepend:
|
||||||
|
- LuckPerms
|
||||||
|
- Geyser-BungeeCord
|
||||||
|
|
||||||
|
commands:
|
||||||
|
# ── ScoreboardModule ──────────────────────────────────────
|
||||||
|
scoreboard:
|
||||||
|
description: Scoreboard ein-/ausblenden oder zwischen Player/Admin wechseln
|
||||||
|
usage: /scoreboard [hide|show|player|admin]
|
||||||
|
aliases: [sb, togglesb]
|
||||||
|
|
||||||
|
# ── StatusAPI Admin ───────────────────────────────────────
|
||||||
|
statusapi:
|
||||||
|
description: StatusAPI verwalten (Reload, Info)
|
||||||
|
usage: /statusapi reload
|
||||||
|
aliases: [sapi]
|
||||||
|
permission: statusapi.admin
|
||||||
|
|
||||||
|
# /pay und /ecoadmin werden von NexEco (Spigot) verwaltet
|
||||||
|
|
||||||
|
# ── VanishModule ──────────────────────────────────────────
|
||||||
|
vanish:
|
||||||
|
description: Vanish ein-/ausschalten
|
||||||
|
usage: /vanish [Spieler]
|
||||||
|
aliases: [v]
|
||||||
|
|
||||||
|
vanishlist:
|
||||||
|
description: Alle unsichtbaren Spieler anzeigen
|
||||||
|
usage: /vanishlist
|
||||||
|
aliases: [vlist]
|
||||||
|
|
||||||
|
# ── Verify Modul ──────────────────────────────────────────
|
||||||
|
verify:
|
||||||
|
description: Verifiziere dich mit einem Token
|
||||||
|
usage: /verify <token>
|
||||||
|
|
||||||
|
# ── ForumBridge Modul ─────────────────────────────────────
|
||||||
|
forumlink:
|
||||||
|
description: Verknüpfe deinen Minecraft-Account mit dem Forum
|
||||||
|
usage: /forumlink <token>
|
||||||
|
aliases: [fl]
|
||||||
|
|
||||||
|
forum:
|
||||||
|
description: Zeigt ausstehende Forum-Benachrichtigungen an
|
||||||
|
usage: /forum
|
||||||
|
|
||||||
|
# ── NetworkInfo Modul ─────────────────────────────────────
|
||||||
|
netinfo:
|
||||||
|
description: Zeigt erweiterte Proxy- und Systeminfos an
|
||||||
|
usage: /netinfo
|
||||||
|
|
||||||
|
antibot:
|
||||||
|
description: Zeigt AntiBot-Status und Verwaltung
|
||||||
|
usage: /antibot <status|clearblocks|unblock|profile|reload>
|
||||||
|
|
||||||
|
# ── AutoMessage Modul ─────────────────────────────────────
|
||||||
|
automessage:
|
||||||
|
description: AutoMessage Verwaltung
|
||||||
|
usage: /automessage reload
|
||||||
|
|
||||||
|
# ── ChatModule – Kanal ────────────────────────────────────
|
||||||
|
channel:
|
||||||
|
description: Kanal wechseln oder Kanalliste anzeigen
|
||||||
|
usage: /channel [kanalname]
|
||||||
|
aliases: [ch, kanal]
|
||||||
|
|
||||||
|
# ── ChatModule – HelpOp ───────────────────────────────────
|
||||||
|
helpop:
|
||||||
|
description: Sende eine Hilfeanfrage an das Team
|
||||||
|
usage: /helpop <Nachricht>
|
||||||
|
|
||||||
|
# ── ChatModule – Privat-Nachrichten ───────────────────────
|
||||||
|
msg:
|
||||||
|
description: Sende eine private Nachricht
|
||||||
|
usage: /msg <Spieler> <Nachricht>
|
||||||
|
aliases: [tell, w, whisper]
|
||||||
|
|
||||||
|
r:
|
||||||
|
description: Antworte auf die letzte private Nachricht
|
||||||
|
usage: /r <Nachricht>
|
||||||
|
aliases: [reply, antwort]
|
||||||
|
|
||||||
|
# ── ChatModule – Blockieren ───────────────────────────────
|
||||||
|
ignore:
|
||||||
|
description: Spieler ignorieren
|
||||||
|
usage: /ignore <Spieler>
|
||||||
|
aliases: [block]
|
||||||
|
|
||||||
|
unignore:
|
||||||
|
description: Spieler nicht mehr ignorieren
|
||||||
|
usage: /unignore <Spieler>
|
||||||
|
aliases: [unblock]
|
||||||
|
|
||||||
|
# ── ChatModule – Mute (Admin) ─────────────────────────────
|
||||||
|
chatmute:
|
||||||
|
description: Spieler im Chat stumm schalten
|
||||||
|
usage: /chatmute <Spieler> [Minuten]
|
||||||
|
aliases: [gmute]
|
||||||
|
|
||||||
|
chatunmute:
|
||||||
|
description: Chat-Stummschaltung aufheben
|
||||||
|
usage: /chatunmute <Spieler>
|
||||||
|
aliases: [gunmute]
|
||||||
|
|
||||||
|
# ── ChatModule – Selbst-Mute ──────────────────────────────
|
||||||
|
chataus:
|
||||||
|
description: Eigenen Chat-Empfang ein-/ausschalten
|
||||||
|
usage: /chataus
|
||||||
|
aliases: [togglechat, chaton, chatoff]
|
||||||
|
|
||||||
|
# ── ChatModule – Broadcast ────────────────────────────────
|
||||||
|
broadcast:
|
||||||
|
description: Nachricht an alle Spieler senden
|
||||||
|
usage: /broadcast <Nachricht>
|
||||||
|
aliases: [bc, alert]
|
||||||
|
|
||||||
|
# ── ChatModule – Emoji ────────────────────────────────────
|
||||||
|
emoji:
|
||||||
|
description: Liste aller verfügbaren Emojis
|
||||||
|
usage: /emoji
|
||||||
|
aliases: [emojis]
|
||||||
|
|
||||||
|
# ── ChatModule – Social Spy ───────────────────────────────
|
||||||
|
socialspy:
|
||||||
|
description: Private Nachrichten mitlesen (Admin)
|
||||||
|
usage: /socialspy
|
||||||
|
aliases: [spy]
|
||||||
|
|
||||||
|
# ── ChatModule – Reload ───────────────────────────────────
|
||||||
|
chatreload:
|
||||||
|
description: Chat-Konfiguration neu laden
|
||||||
|
usage: /chatreload
|
||||||
|
|
||||||
|
# ── ChatModule – Admin-Info ───────────────────────────────
|
||||||
|
chatinfo:
|
||||||
|
description: Chat-Informationen ueber einen Spieler anzeigen (Admin)
|
||||||
|
usage: /chatinfo <Spieler>
|
||||||
|
|
||||||
|
# ── ChatModule – Chat-History ─────────────────────────────
|
||||||
|
chathist:
|
||||||
|
description: Chat-History aus dem Logfile anzeigen (Admin)
|
||||||
|
usage: /chathist [Spieler] [Anzahl]
|
||||||
|
|
||||||
|
# ── ChatModule – Mentions ─────────────────────────────────
|
||||||
|
mentions:
|
||||||
|
description: Mention-Benachrichtigungen ein-/ausschalten
|
||||||
|
usage: /mentions
|
||||||
|
aliases: [mention]
|
||||||
|
|
||||||
|
# ── ChatModule – Plugin-Bypass ────────────────────────────
|
||||||
|
chatbypass:
|
||||||
|
description: ChatModule fuer naechste Eingabe ueberspringen (fuer Plugin-Dialoge wie CMI)
|
||||||
|
usage: /chatbypass
|
||||||
|
aliases: [cbp]
|
||||||
|
|
||||||
|
# ── ChatModule – Account-Verknuepfung ─────────────────────
|
||||||
|
# FIX #4: Command-Namen stimmen jetzt mit der Code-Registrierung überein.
|
||||||
|
# Im ChatModule wird "discordlink" mit Alias "dlink" registriert,
|
||||||
|
# und "telegramlink" mit Alias "tlink".
|
||||||
|
discordlink:
|
||||||
|
description: Minecraft-Account mit Discord verknuepfen
|
||||||
|
usage: /discordlink
|
||||||
|
aliases: [dlink]
|
||||||
|
|
||||||
|
telegramlink:
|
||||||
|
description: Minecraft-Account mit Telegram verknuepfen
|
||||||
|
usage: /telegramlink
|
||||||
|
aliases: [tlink]
|
||||||
|
|
||||||
|
unlink:
|
||||||
|
description: Account-Verknuepfung aufheben
|
||||||
|
usage: /unlink <discord|telegram|all>
|
||||||
|
|
||||||
|
# ── ChatModule – Report ───────────────────────────────────
|
||||||
|
report:
|
||||||
|
description: Spieler melden
|
||||||
|
usage: /report <Spieler> <Grund>
|
||||||
|
|
||||||
|
reports:
|
||||||
|
description: Offene Reports anzeigen (Admin)
|
||||||
|
usage: /reports [all]
|
||||||
|
|
||||||
|
reportclose:
|
||||||
|
description: Report schliessen (Admin)
|
||||||
|
usage: /reportclose <ID>
|
||||||
|
|
||||||
|
# ── ServerSwitcherModule ──────────────────────────────────
|
||||||
|
go:
|
||||||
|
description: Schneller Serverwechsel ueber Chat-Menue oder direkt
|
||||||
|
usage: /go [servername]
|
||||||
|
aliases: [wechsel, switch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# ── StatusAPI Core ────────────────────────────────────────
|
||||||
|
statusapi.admin:
|
||||||
|
description: Zugang zu StatusAPI-Administrationsbefehlen (reload etc.)
|
||||||
|
default: op
|
||||||
|
|
||||||
|
statusapi.update.notify:
|
||||||
|
description: Erlaubt Update-Benachrichtigungen
|
||||||
|
default: op
|
||||||
|
|
||||||
|
statusapi.netinfo:
|
||||||
|
description: Zugriff auf /netinfo
|
||||||
|
default: op
|
||||||
|
|
||||||
|
statusapi.antibot:
|
||||||
|
description: Zugriff auf /antibot
|
||||||
|
default: op
|
||||||
|
|
||||||
|
statusapi.automessage:
|
||||||
|
description: Zugriff auf /automessage reload
|
||||||
|
default: op
|
||||||
|
|
||||||
|
# ── ChatModule – Kanaele ──────────────────────────────────
|
||||||
|
chat.channel.local:
|
||||||
|
description: Zugang zum Local-Kanal
|
||||||
|
default: true
|
||||||
|
|
||||||
|
chat.channel.trade:
|
||||||
|
description: Zugang zum Trade-Kanal
|
||||||
|
default: true
|
||||||
|
|
||||||
|
chat.channel.staff:
|
||||||
|
description: Zugang zum Staff-Kanal
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – HelpOp ───────────────────────────────────
|
||||||
|
chat.helpop.receive:
|
||||||
|
description: HelpOp-Nachrichten empfangen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Mute ─────────────────────────────────────
|
||||||
|
chat.mute:
|
||||||
|
description: Spieler muten / unmuten
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Broadcast ────────────────────────────────
|
||||||
|
chat.broadcast:
|
||||||
|
description: Broadcast-Nachrichten senden
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Social Spy ───────────────────────────────
|
||||||
|
chat.socialspy:
|
||||||
|
description: Private Nachrichten mitlesen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Admin ────────────────────────────────────
|
||||||
|
chat.admin.bypass:
|
||||||
|
description: Admin-Bypass - Kann nicht geblockt/gemutet werden
|
||||||
|
default: op
|
||||||
|
|
||||||
|
chat.admin.notify:
|
||||||
|
description: Benachrichtigungen ueber Mutes und Blocks erhalten
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Report ───────────────────────────────────
|
||||||
|
chat.report:
|
||||||
|
description: Spieler reporten (/report)
|
||||||
|
default: true
|
||||||
|
|
||||||
|
# ── ChatModule – Farben ───────────────────────────────────
|
||||||
|
chat.color:
|
||||||
|
description: Farbcodes (&a, &b, ...) im Chat nutzen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
chat.color.format:
|
||||||
|
description: Formatierungen (&l, &o, &n, ...) im Chat nutzen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Filter ───────────────────────────────────
|
||||||
|
chat.filter.bypass:
|
||||||
|
description: Chat-Filter (Anti-Spam, Caps, Blacklist) umgehen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── CommandBlocker ────────────────────────────────────────
|
||||||
|
commandblocker.bypass:
|
||||||
|
description: Command-Blocker umgehen
|
||||||
|
default: op
|
||||||
|
|
||||||
|
commandblocker.admin:
|
||||||
|
description: CommandBlocker verwalten (/cb)
|
||||||
|
default: op
|
||||||
|
|
||||||
|
# ── ServerSwitcherModule ──────────────────────────────────
|
||||||
|
serverswitcher.use:
|
||||||
|
description: Zugriff auf /go (Schneller Serverwechsel)
|
||||||
|
default: false
|
||||||
140
StatusAPI/src/main/resources/scoreboard.properties
Normal file
140
StatusAPI/src/main/resources/scoreboard.properties
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# ScoreboardModule Konfiguration
|
||||||
|
# Platzhalter Spieler: %player% %rank% %money% %server% %compass% %health% %hearts% %date%
|
||||||
|
# %ping% %online% %maxplayers% %time% %playtime% %news%
|
||||||
|
# %x% %y% %z% %world% %gamemode% %exp% %food% %foodsym% %speed%
|
||||||
|
# Platzhalter Admin: %tps% %ram% %proxymem% %uptime% %servers%
|
||||||
|
# Ticket (Spieler): %ticket_my_open%
|
||||||
|
# Ticket (Supporter): %ticket_open%
|
||||||
|
# Ticket (Admin): %ticket_open% %ticket_claimed% %ticket_rating_good% %ticket_rating_bad% %ticket_rating_pct%
|
||||||
|
# Gradient: %gradient:FARBE1:FARBE2:TEXT% (beliebig viele Farb-Stopps)
|
||||||
|
# Sonstiges: %line%
|
||||||
|
# Farben: &-Codes und Hex &#FF6600
|
||||||
|
|
||||||
|
scoreboard.enabled=true
|
||||||
|
# Update-Intervall in Millisekunden - MINIMUM 250! (500 = 0.5s empfohlen)
|
||||||
|
scoreboard.update_interval=500
|
||||||
|
scoreboard.title=&lViper Network
|
||||||
|
scoreboard.admin_title=&l[Admin] Panel
|
||||||
|
scoreboard.supporter_title=&l[Support] Panel
|
||||||
|
|
||||||
|
# Laufschrift – leer lassen zum Deaktivieren
|
||||||
|
scoreboard.ticker.text=
|
||||||
|
scoreboard.ticker.width=26
|
||||||
|
scoreboard.ticker.speed=1
|
||||||
|
|
||||||
|
scoreboard.rainbow.enabled=true
|
||||||
|
# wave = fließende Farbwelle | chars = Regenbogen pro Buchstabe | line = eine Farbe
|
||||||
|
scoreboard.rainbow.mode=wave
|
||||||
|
# Wellengeschwindigkeit: 1=sehr langsam, 10=normal, 50=schnell, 100=sehr schnell
|
||||||
|
scoreboard.rainbow.speed=10
|
||||||
|
# Farben: Hex (#RRGGBB oder &#RRGGBB) oder Minecraft-Codes (&0-&f) – kommagetrennt
|
||||||
|
# Leer lassen = voller HSB-Regenbogen
|
||||||
|
scoreboard.rainbow.colors=&f,&b
|
||||||
|
|
||||||
|
scoreboard.admin_permission=statusapi.scoreboard.admin
|
||||||
|
scoreboard.supporter_permission=statusapi.scoreboard.supporter
|
||||||
|
|
||||||
|
scoreboard.time_format=HH:mm
|
||||||
|
scoreboard.date_format=dd.MM.yyyy
|
||||||
|
scoreboard.timezone=Europe/Berlin
|
||||||
|
scoreboard.money_format=#,##0.00
|
||||||
|
scoreboard.money_decimal_separator=,
|
||||||
|
|
||||||
|
# ===================================================
|
||||||
|
# SEPARATOR – wird als %line% Placeholder genutzt
|
||||||
|
# Wähle einen Stil oder erstelle deinen eigenen:
|
||||||
|
#
|
||||||
|
# scoreboard.separator=&8&m-------------------- (Standard)
|
||||||
|
# scoreboard.separator=&8&m==================== (Doppelt)
|
||||||
|
# scoreboard.separator=&8&m~~~~~~~~~~~~~~~~~~~~ (Wellig)
|
||||||
|
# scoreboard.separator=&8&m.................... (Punkte)
|
||||||
|
# scoreboard.separator=&8&m──────────────────── (Dünn)
|
||||||
|
# scoreboard.separator=&8&m════════════════════ (Dick)
|
||||||
|
# scoreboard.separator=&8◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇◆◇ (Diamanten)
|
||||||
|
# scoreboard.separator=%gradient:&8:&7:────────────────────% (Gradient)
|
||||||
|
# scoreboard.separator=%gradient:#FF0000:#0000FF:────────────────────% (Farbig)
|
||||||
|
# scoreboard.separator= (Leer/unsichtbar)
|
||||||
|
# ===================================================
|
||||||
|
scoreboard.separator=&8&m-----------------------
|
||||||
|
|
||||||
|
# ===================================================
|
||||||
|
# NEWS-TICKER – erscheint als %news% Placeholder
|
||||||
|
# Leer lassen zum Deaktivieren
|
||||||
|
# ===================================================
|
||||||
|
scoreboard.news.text=&eWillkommen auf Viper Network!
|
||||||
|
scoreboard.news.prefix=&8[&6News&8] &r
|
||||||
|
scoreboard.news.width=26
|
||||||
|
# Geschwindigkeit: 1=langsam, 2=normal, 3=schnell
|
||||||
|
scoreboard.news.speed=1
|
||||||
|
|
||||||
|
# Sekunden pro Rotation (0 = kein Wechsel)
|
||||||
|
scoreboard.rotation_interval=4
|
||||||
|
|
||||||
|
# ===================================================
|
||||||
|
# ZEILEN – max 15 sichtbar
|
||||||
|
# Rotation pro Zeile:
|
||||||
|
# scoreboard.lines.N = Variante 1 (immer sichtbar / nur Variante)
|
||||||
|
# scoreboard.lines.N.2 = Variante 2 (wechselt alle rotation_interval Sekunden)
|
||||||
|
# scoreboard.lines.N.3 = Variante 3 usw.
|
||||||
|
# Gradient: %gradient:FARBE1:FARBE2:TEXT%
|
||||||
|
# ===================================================
|
||||||
|
scoreboard.lines.1=%line%
|
||||||
|
scoreboard.lines.2=%gradient:&b:&f:&b:&l> Player Info:%
|
||||||
|
scoreboard.lines.3=&7%rank% &f%player%
|
||||||
|
scoreboard.lines.4=
|
||||||
|
scoreboard.lines.5=&7Spielzeit: &f%playtime%
|
||||||
|
scoreboard.lines.5.2=&7Leben: &c%health%
|
||||||
|
scoreboard.lines.5.3=&7Hunger: B4513%foodsym%
|
||||||
|
scoreboard.lines.6=
|
||||||
|
scoreboard.lines.7=%gradient:&b:&f:&b:&l> Money:%
|
||||||
|
scoreboard.lines.8=&a$%money%
|
||||||
|
scoreboard.lines.9=
|
||||||
|
scoreboard.lines.10=%gradient:&b:&f:&b:&l> Server Info:%
|
||||||
|
scoreboard.lines.11=&f%server%
|
||||||
|
scoreboard.lines.11.2=&7Ping: &f%ping%ms &8| &7Online: &f%online%
|
||||||
|
scoreboard.lines.12=
|
||||||
|
scoreboard.lines.13=%news%
|
||||||
|
scoreboard.lines.14=%line%
|
||||||
|
scoreboard.lines.15=&7%compass%
|
||||||
|
|
||||||
|
# ===================================================
|
||||||
|
# SUPPORTER-ZEILEN
|
||||||
|
# ===================================================
|
||||||
|
scoreboard.supporter_lines.1=%line%
|
||||||
|
scoreboard.supporter_lines.2=%gradient:&b:&f:&b:&l> Player Info:%
|
||||||
|
scoreboard.supporter_lines.3=&7%rank% &f%player%
|
||||||
|
scoreboard.supporter_lines.4=&7Ping: &f%ping%ms &8| &7%server%
|
||||||
|
scoreboard.supporter_lines.5=
|
||||||
|
scoreboard.supporter_lines.6=%gradient:&b:&f:&b:&l> Ticket:%
|
||||||
|
scoreboard.supporter_lines.7=&7Offen: &c%ticket_open%
|
||||||
|
scoreboard.supporter_lines.8=&7In Bearbeitung: &e%ticket_claimed%
|
||||||
|
scoreboard.supporter_lines.9=
|
||||||
|
scoreboard.supporter_lines.10=%gradient:&b:&f:&b:&l> Server Info:%
|
||||||
|
scoreboard.supporter_lines.11=&7Online: &f%online% &8/ &7%maxplayers%
|
||||||
|
scoreboard.supporter_lines.12=
|
||||||
|
scoreboard.supporter_lines.13=
|
||||||
|
scoreboard.supporter_lines.14=%line%
|
||||||
|
scoreboard.supporter_lines.15=&7%compass%
|
||||||
|
|
||||||
|
# ===================================================
|
||||||
|
# ADMIN-ZEILEN
|
||||||
|
# ===================================================
|
||||||
|
scoreboard.admin_lines.1=%line%
|
||||||
|
scoreboard.admin_lines.2=%gradient:&b:&f:&b:&l> Player Info:%
|
||||||
|
scoreboard.admin_lines.3=&7%rank% &f%player%
|
||||||
|
scoreboard.admin_lines.4=&7Gamemode: &f%gamemode%
|
||||||
|
scoreboard.admin_lines.5=&7Leben: &c%health%
|
||||||
|
scoreboard.admin_lines.5.2=&7Hunger: B4513%foodsym%
|
||||||
|
scoreboard.admin_lines.6=
|
||||||
|
scoreboard.admin_lines.7=%gradient:&b:&f:&b:&l> Server Info:%
|
||||||
|
scoreboard.admin_lines.8=&f%server% &8| &7RAM: &e%ram%
|
||||||
|
scoreboard.admin_lines.8.2=&7Proxy: &f%uptime%
|
||||||
|
scoreboard.admin_lines.9=&7TPS: &a%tps%
|
||||||
|
scoreboard.admin_lines.10=
|
||||||
|
scoreboard.admin_lines.11=%gradient:&b:&f:&b:&l> Ticket:%
|
||||||
|
scoreboard.admin_lines.12=&7Tickets Offen: &c%ticket_open%
|
||||||
|
scoreboard.admin_lines.12.2=&7Tickets In Bearbeitung: &e%ticket_claimed%
|
||||||
|
scoreboard.admin_lines.13=&7Spieler: %online% &8/ &7%maxplayers%
|
||||||
|
scoreboard.admin_lines.14=%line%
|
||||||
|
scoreboard.admin_lines.15=&7%compass%
|
||||||
|
scoreboard.admin_lines.15.2=&7Pos: X:&f%x% &7Y:&f%y% &7Z:&f%z%
|
||||||
95
StatusAPI/src/main/resources/verify.properties
Normal file
95
StatusAPI/src/main/resources/verify.properties
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# _____ __ __ ___ ____ ____
|
||||||
|
# / ___// /_____ _/ /___ _______/ | / __ \/ _/
|
||||||
|
# \__ \/ __/ __ `/ __/ / / / ___/ /| | / /_/ // /
|
||||||
|
# ___/ / /_/ /_/ / /_/ /_/ (__ ) ___ |/ ____// /
|
||||||
|
# /____/\__/\__,_/\__/\__,_/____/_/ |_/_/ /___/
|
||||||
|
|
||||||
|
|
||||||
|
broadcast.enabled=true
|
||||||
|
broadcast.prefix=[Broadcast]
|
||||||
|
broadcast.prefix-color=&c
|
||||||
|
broadcast.message-color=&f
|
||||||
|
broadcast.format=%prefixColored% %messageColored%
|
||||||
|
# broadcast.format kann angepasst werden; nutze Platzhalter: %name%, %prefix%, %prefixColored%, %message%, %messageColored%, %type%
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# StatusAPI Einstellungen
|
||||||
|
# ===========================
|
||||||
|
statusapi.port=9191
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# WORDPRESS / VERIFY EINSTELLUNGEN
|
||||||
|
# ===========================
|
||||||
|
wp_verify_url=https://example.com
|
||||||
|
|
||||||
|
# Gemeinsames API-Secret (muss identisch sein mit mc_bridge_api_secret in den WP Forum-Einstellungen)
|
||||||
|
forum.api_secret=HIER_EIN_SICHERES_PASSWORT_SETZEN
|
||||||
|
|
||||||
|
# Verzögerung in Sekunden bevor Login-Benachrichtigungen zugestellt werden
|
||||||
|
# (damit der Spieler den Server-Wechsel abgeschlossen hat)
|
||||||
|
forum.login_delay_seconds=3
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# COMMAND BLOCKER
|
||||||
|
# ===========================
|
||||||
|
commandblocker.enabled=true
|
||||||
|
commandblocker.bypass.permission=commandblocker.bypass
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# SERVER KONFIGURATION
|
||||||
|
# ===========================
|
||||||
|
# Hier legst du für jeden Server alles fest:
|
||||||
|
# 1. Den Anzeigenamen für den Chat (z.B. &bLobby)
|
||||||
|
# 2. Die Server ID für WordPress (z.B. id=1)
|
||||||
|
# 3. Das Secret für WordPress (z.B. secret=...)
|
||||||
|
|
||||||
|
# Server 1: Lobby
|
||||||
|
server.Lobby=&bLobby
|
||||||
|
server.Lobby.id=64
|
||||||
|
server.Lobby.secret=GeheimesWortFuerLobby789
|
||||||
|
|
||||||
|
# Server 1: Citybuild
|
||||||
|
server.citybuild=&bCitybuild
|
||||||
|
server.citybuild.id=67
|
||||||
|
server.citybuild.secret=GeheimesWortFuerCitybuild789
|
||||||
|
|
||||||
|
# Server 2: Survival
|
||||||
|
server.survival=&aSurvival
|
||||||
|
server.survival.id=68
|
||||||
|
server.survival.secret=GeheimesWortFuerSurvival789
|
||||||
|
|
||||||
|
# Server 3: SkyBlock
|
||||||
|
server.skyblock=&dSkyBlock
|
||||||
|
server.skyblock.id=3
|
||||||
|
server.skyblock.secret=GeheimesWortFuerSkyBlock789
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# AUTOMESSAGE
|
||||||
|
# ===========================
|
||||||
|
# Aktiviert den automatischen Nachrichten-Rundruf
|
||||||
|
automessage.enabled=true
|
||||||
|
|
||||||
|
# Zeitintervall in Sekunden (Standard: 300 = 5 Minuten)
|
||||||
|
automessage.interval=300
|
||||||
|
|
||||||
|
# Optional: Ein Prefix, das VOR jede Nachricht aus der Datei gesetzt wird.
|
||||||
|
# Wenn du das Prefix bereits IN der messages.txt hast, lass dieses Feld einfach leer.
|
||||||
|
automessage.prefix=
|
||||||
|
|
||||||
|
# Der Name der Datei, in der die Nachrichten stehen (liegt im Plugin-Ordner)
|
||||||
|
automessage.file=messages.txt
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# ECONOMY (Serverübergreifendes Geld)
|
||||||
|
# ===========================
|
||||||
|
# Alle Server (SurvivalPlus + StatusAPI/BungeeCord) müssen dieselbe Datenbank nutzen.
|
||||||
|
# Die Tabelle bc_accounts wird automatisch erstellt.
|
||||||
|
economy.mysql.host=localhost
|
||||||
|
economy.mysql.port=3306
|
||||||
|
economy.mysql.database=survivalplus
|
||||||
|
economy.mysql.username=root
|
||||||
|
economy.mysql.password=
|
||||||
|
economy.start-balance=500.0
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Willkommensnachrichten, die zufällig gesendet werden, wenn ein Spieler joint.
|
# Willkommensnachrichten, die zufällig gesendet werden, wenn ein Spieler joint.
|
||||||
# %player% wird durch den Spielernamen ersetzt.
|
# %player% wird durch den Spielernamen ersetzt.
|
||||||
welcome-messages:
|
welcome-messages:
|
||||||
- "&aWillkommen, %player%! Viel Spaß auf unserem Server!"
|
- "&aWillkommen, %player%! Viel Spaß auf unserem Server!"
|
||||||
- "&aHey %player%, schön dich hier zu sehen! Los geht's!"
|
- "&aHey %player%, schön dich hier zu sehen! Los geht's!"
|
||||||
- "&a%player%, dein Abenteuer beginnt jetzt! Viel Spaß!"
|
- "&a%player%, dein Abenteuer beginnt jetzt! Viel Spaß!"
|
||||||
- "&aWillkommen an Bord, %player%! Entdecke den Server!"
|
- "&aWillkommen an Bord, %player%! Entdecke den Server!"
|
||||||
- "&a%player%, herzlich willkommen! Lass uns loslegen!"
|
- "&a%player%, herzlich willkommen! Lass uns loslegen!"
|
||||||
118
StatusAPIBridge/pom.xml
Normal file
118
StatusAPIBridge/pom.xml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>net.viper</groupId>
|
||||||
|
<artifactId>StatusAPIBridge</artifactId>
|
||||||
|
<version>1.0.2</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<!-- Niedrigste gemeinsame Basis: Java 17 (läuft auf 1.21.1 und 26.1.2) -->
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||||
|
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<!-- Standard-API-Version (wird durch Profile überschrieben) -->
|
||||||
|
<spigot.version>1.21.1-R0.1-SNAPSHOT</spigot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spigot-repo</id>
|
||||||
|
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>vault-repo</id>
|
||||||
|
<url>https://nexus.hc.to/content/repositories/pub_releases/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>placeholderapi</id>
|
||||||
|
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spigot API – Version wird durch Profil gesteuert -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot-api</artifactId>
|
||||||
|
<version>${spigot.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- Vault -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.milkbowl.vault</groupId>
|
||||||
|
<artifactId>VaultAPI</artifactId>
|
||||||
|
<version>1.7</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- PlaceholderAPI (optional – per Reflection genutzt) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.clip</groupId>
|
||||||
|
<artifactId>placeholderapi</artifactId>
|
||||||
|
<version>2.11.6</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════
|
||||||
|
Maven-Profile für Multi-Version-Build
|
||||||
|
mvn package → mc-1.21.1 (Standard)
|
||||||
|
mvn package -P mc-26.1.2 → mc-26.1.2
|
||||||
|
══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<profiles>
|
||||||
|
|
||||||
|
<!-- Profil 1: Minecraft 1.21.1 (Standard) -->
|
||||||
|
<profile>
|
||||||
|
<id>mc-1.21.1</id>
|
||||||
|
<activation>
|
||||||
|
<activeByDefault>true</activeByDefault>
|
||||||
|
</activation>
|
||||||
|
<properties>
|
||||||
|
<spigot.version>1.21.1-R0.1-SNAPSHOT</spigot.version>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
|
||||||
|
<!-- Profil 2: Minecraft 26.1.2 -->
|
||||||
|
<profile>
|
||||||
|
<id>mc-26.1.2</id>
|
||||||
|
<properties>
|
||||||
|
<spigot.version>1.21.1-R0.1-SNAPSHOT</spigot.version>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
|
||||||
|
</profiles>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>StatusAPIBridge</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals><goal>shade</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
package net.viper.statusapibridge;
|
||||||
|
|
||||||
|
import net.milkbowl.vault.economy.Economy;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.entity.EntityRegainHealthEvent;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerMoveEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||||
|
|
||||||
|
private Economy economy;
|
||||||
|
private String statusApiUrl;
|
||||||
|
private int pushDelayTicks;
|
||||||
|
private int liveSyncIntervalTicks;
|
||||||
|
private int scoreboardSyncIntervalTicks;
|
||||||
|
|
||||||
|
private final Map<UUID, Double> lastPushedBalance = new ConcurrentHashMap<>();
|
||||||
|
private final Map<UUID, Double> lastPushedHealth = new ConcurrentHashMap<>();
|
||||||
|
private final Map<UUID, String> lastPushedCompass = new ConcurrentHashMap<>();
|
||||||
|
private final Map<UUID, String> lastPushedWorld = new ConcurrentHashMap<>();
|
||||||
|
private final Map<UUID, String> lastPushedData = new ConcurrentHashMap<>();
|
||||||
|
private final Map<UUID, String> lastPushedStats = new ConcurrentHashMap<>();
|
||||||
|
private String lastPushedTicketGlobal = "";
|
||||||
|
|
||||||
|
// ── PlaceholderAPI ────────────────────────────────────────────────────────
|
||||||
|
private final Set<String> papiTokens = new java.util.LinkedHashSet<>();
|
||||||
|
private final Map<UUID, String> lastPapiValues = new ConcurrentHashMap<>();
|
||||||
|
private boolean papiEnabled = false;
|
||||||
|
|
||||||
|
// ── Versions-Detection ────────────────────────────────────────────────────
|
||||||
|
// true = 1.21.x-Modus (Spigot/Paper)
|
||||||
|
// false = 26.1.x-Modus (neuere Server-Version, kein NMS-Fallback)
|
||||||
|
private boolean isLegacyMode = true;
|
||||||
|
|
||||||
|
private final ExecutorService httpExecutor = Executors.newSingleThreadExecutor(r -> {
|
||||||
|
Thread t = new Thread(r, "StatusAPIBridge-HTTP");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
saveDefaultConfig();
|
||||||
|
detectMinecraftVersion();
|
||||||
|
statusApiUrl = getConfig().getString("statusapi-url", "http://127.0.0.1:9191").trim();
|
||||||
|
pushDelayTicks = getConfig().getInt("push-delay-ticks", 40);
|
||||||
|
liveSyncIntervalTicks = Math.max(20, getConfig().getInt("live-sync-interval-ticks", 20));
|
||||||
|
scoreboardSyncIntervalTicks = Math.max(20, getConfig().getInt("scoreboard-sync-interval-ticks", 20));
|
||||||
|
|
||||||
|
if (!setupEconomy()) {
|
||||||
|
getLogger().warning("Vault/Economy nicht gefunden – Economy-Push deaktiviert.");
|
||||||
|
} else {
|
||||||
|
getLogger().info("Vault Economy gefunden: " + economy.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
Bukkit.getPluginManager().registerEvents(this, this);
|
||||||
|
Bukkit.getScheduler().runTaskTimer(this, this::pushChangedBalancesForOnlinePlayers,
|
||||||
|
liveSyncIntervalTicks, liveSyncIntervalTicks);
|
||||||
|
Bukkit.getScheduler().runTaskTimer(this, this::pushScoreboardData,
|
||||||
|
scoreboardSyncIntervalTicks, scoreboardSyncIntervalTicks);
|
||||||
|
|
||||||
|
// TicketSystem-Daten alle 5 Sekunden pushen (100 Ticks)
|
||||||
|
Bukkit.getScheduler().runTaskTimerAsynchronously(this, this::pushTicketData, 100L, 100L);
|
||||||
|
|
||||||
|
// PlaceholderAPI-Integration
|
||||||
|
papiEnabled = getServer().getPluginManager().getPlugin("PlaceholderAPI") != null;
|
||||||
|
if (papiEnabled) {
|
||||||
|
// Tokens alle 30s von StatusAPI holen, nur bei Änderung loggen
|
||||||
|
Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> {
|
||||||
|
Set<String> before = new java.util.LinkedHashSet<>(papiTokens);
|
||||||
|
boolean fetched = fetchPapiTokensFromStatusAPI();
|
||||||
|
if (fetched && !papiTokens.equals(before)) {
|
||||||
|
if (papiTokens.isEmpty()) {
|
||||||
|
getLogger().info("[PAPI] Keine Placeholder in der StatusAPI-Config gefunden.");
|
||||||
|
} else {
|
||||||
|
getLogger().info("[PAPI] " + papiTokens.size() + " Placeholder erkannt: " + papiTokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 40L, 600L); // nach 2s starten, alle 30s wiederholen
|
||||||
|
|
||||||
|
// Sync-Task läuft dauerhaft – tut nichts wenn papiTokens leer
|
||||||
|
Bukkit.getScheduler().runTaskTimer(this, this::syncPapiValues, scoreboardSyncIntervalTicks, scoreboardSyncIntervalTicks);
|
||||||
|
} else {
|
||||||
|
getLogger().info("[PAPI] PlaceholderAPI nicht gefunden – Placeholder werden nicht aufgelöst.");
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogger().info("StatusAPIBridge gestartet. Ziel: " + statusApiUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
httpExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean setupEconomy() {
|
||||||
|
if (getServer().getPluginManager().getPlugin("Vault") == null) return false;
|
||||||
|
RegisteredServiceProvider<Economy> rsp =
|
||||||
|
getServer().getServicesManager().getRegistration(Economy.class);
|
||||||
|
if (rsp == null) return false;
|
||||||
|
economy = rsp.getProvider();
|
||||||
|
return economy != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onJoin(PlayerJoinEvent e) {
|
||||||
|
Player player = e.getPlayer();
|
||||||
|
Bukkit.getScheduler().runTaskLater(this, () -> {
|
||||||
|
if (!player.isOnline()) return;
|
||||||
|
if (economy != null) pushEconomy(player);
|
||||||
|
pushPlayerScoreboardData(player);
|
||||||
|
if (papiEnabled && !papiTokens.isEmpty()) pushPapiValues(player);
|
||||||
|
}, pushDelayTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onQuit(PlayerQuitEvent e) {
|
||||||
|
Player player = e.getPlayer();
|
||||||
|
UUID id = player.getUniqueId();
|
||||||
|
if (economy != null) pushEconomyAsync(id, player.getName(), economy.getBalance(player));
|
||||||
|
lastPushedBalance.remove(id);
|
||||||
|
lastPushedHealth.remove(id);
|
||||||
|
lastPushedCompass.remove(id);
|
||||||
|
lastPushedWorld.remove(id);
|
||||||
|
lastPushedData.remove(id);
|
||||||
|
lastPushedStats.remove(id);
|
||||||
|
lastPapiValues.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onDamage(EntityDamageEvent e) {
|
||||||
|
if (!(e.getEntity() instanceof Player player)) return;
|
||||||
|
Bukkit.getScheduler().runTaskLater(this,
|
||||||
|
() -> { if (player.isOnline()) pushHealthIfChanged(player); }, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onHeal(EntityRegainHealthEvent e) {
|
||||||
|
if (!(e.getEntity() instanceof Player player)) return;
|
||||||
|
Bukkit.getScheduler().runTaskLater(this,
|
||||||
|
() -> { if (player.isOnline()) pushHealthIfChanged(player); }, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onMove(PlayerMoveEvent e) {
|
||||||
|
// getTo() kann in 1.20.5+ bei reinen Head-Rotationen null sein
|
||||||
|
if (e.getTo() == null) return;
|
||||||
|
if (e.getFrom().getYaw() == e.getTo().getYaw()) return;
|
||||||
|
pushCompassIfChanged(e.getPlayer());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Periodische Tasks ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void pushChangedBalancesForOnlinePlayers() {
|
||||||
|
if (economy == null) return;
|
||||||
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
double current = economy.getBalance(player);
|
||||||
|
Double last = lastPushedBalance.get(player.getUniqueId());
|
||||||
|
if (last == null || Math.abs(current - last) > 0.000001d)
|
||||||
|
pushEconomyAsync(player.getUniqueId(), player.getName(), current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushScoreboardData() {
|
||||||
|
double tps = getCurrentTps();
|
||||||
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
pushPlayerScoreboardData(player);
|
||||||
|
pushTpsAsync(player.getUniqueId(), tps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushPlayerScoreboardData(Player player) {
|
||||||
|
pushHealthIfChanged(player);
|
||||||
|
pushCompassIfChanged(player);
|
||||||
|
pushWorldIfChanged(player);
|
||||||
|
pushPlayerDataIfChanged(player);
|
||||||
|
pushStatsIfChanged(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Push-Methoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void pushEconomy(Player player) {
|
||||||
|
pushEconomyAsync(player.getUniqueId(), player.getName(), economy.getBalance(player));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushEconomyAsync(UUID uuid, String name, double balance) {
|
||||||
|
httpExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
sendPost(statusApiUrl + "/economy/update",
|
||||||
|
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||||
|
+ "\",\"balance\":" + balance + "}");
|
||||||
|
lastPushedBalance.put(uuid, balance);
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("Economy-Push fehlgeschlagen fuer " + name + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushHealthIfChanged(Player player) {
|
||||||
|
double health = player.getHealth();
|
||||||
|
Double last = lastPushedHealth.get(player.getUniqueId());
|
||||||
|
if (last != null && Math.abs(health - last) < 0.01) return;
|
||||||
|
lastPushedHealth.put(player.getUniqueId(), health);
|
||||||
|
UUID uuid = player.getUniqueId(); String name = player.getName();
|
||||||
|
httpExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
sendPost(statusApiUrl + "/scoreboard/health",
|
||||||
|
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||||
|
+ "\",\"health\":" + health + "}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("Health-Push fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushCompassIfChanged(Player player) {
|
||||||
|
// Rohen Yaw normalisieren auf 0..360 (0 = Süden, wie MC-Konvention)
|
||||||
|
float rawYaw = player.getLocation().getYaw();
|
||||||
|
float normYaw = ((rawYaw % 360) + 360) % 360;
|
||||||
|
String yawStr = String.format(java.util.Locale.US, "%.1f", normYaw);
|
||||||
|
|
||||||
|
// Nur senden wenn Änderung >= 0.5° – fein genug für 1-Grad-Slots
|
||||||
|
String lastStr = lastPushedCompass.get(player.getUniqueId());
|
||||||
|
if (lastStr != null) {
|
||||||
|
try {
|
||||||
|
float lastYaw = Float.parseFloat(lastStr);
|
||||||
|
float diff = Math.abs(normYaw - lastYaw);
|
||||||
|
if (diff > 180) diff = 360 - diff; // kürzester Bogenweg
|
||||||
|
if (diff < 0.5f) return;
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPushedCompass.put(player.getUniqueId(), yawStr);
|
||||||
|
UUID uuid = player.getUniqueId(); String name = player.getName();
|
||||||
|
httpExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
sendPost(statusApiUrl + "/scoreboard/compass",
|
||||||
|
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||||
|
+ "\",\"compass\":\"" + yawStr + "\"}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("Compass-Push fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushWorldIfChanged(Player player) {
|
||||||
|
String world = player.getWorld().getName();
|
||||||
|
if (world.equals(lastPushedWorld.get(player.getUniqueId()))) return;
|
||||||
|
lastPushedWorld.put(player.getUniqueId(), world);
|
||||||
|
UUID uuid = player.getUniqueId(); String name = player.getName();
|
||||||
|
httpExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
sendPost(statusApiUrl + "/player/world",
|
||||||
|
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||||
|
+ "\",\"world\":\"" + escapeName(world) + "\"}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("World-Push fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushPlayerDataIfChanged(Player player) {
|
||||||
|
int x = player.getLocation().getBlockX();
|
||||||
|
int y = player.getLocation().getBlockY();
|
||||||
|
int z = player.getLocation().getBlockZ();
|
||||||
|
String gm = player.getGameMode().name();
|
||||||
|
int exp= player.getLevel();
|
||||||
|
int fd = player.getFoodLevel();
|
||||||
|
double sp = player.getWalkSpeed();
|
||||||
|
String wld= player.getWorld().getName();
|
||||||
|
String key = x+","+y+","+z+","+gm+","+exp+","+fd+","+String.format("%.2f",sp)+","+wld;
|
||||||
|
if (key.equals(lastPushedData.get(player.getUniqueId()))) return;
|
||||||
|
lastPushedData.put(player.getUniqueId(), key);
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
String name = player.getName();
|
||||||
|
httpExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
sendPost(statusApiUrl + "/player/data",
|
||||||
|
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||||
|
+ "\",\"x\":" + x
|
||||||
|
+ ",\"y\":" + y
|
||||||
|
+ ",\"z\":" + z
|
||||||
|
+ ",\"gamemode\":\"" + gm + "\""
|
||||||
|
+ ",\"exp\":" + exp
|
||||||
|
+ ",\"food\":" + fd
|
||||||
|
+ ",\"speed\":" + String.format(java.util.Locale.US, "%.4f", sp)
|
||||||
|
+ ",\"world\":\"" + escapeName(wld) + "\"}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("PlayerData-Push fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushStatsIfChanged(Player player) {
|
||||||
|
int kills = player.getStatistic(org.bukkit.Statistic.PLAYER_KILLS);
|
||||||
|
int deaths = player.getStatistic(org.bukkit.Statistic.DEATHS);
|
||||||
|
// Playtime in Ticks aus Minecraft-Statistik → umrechnen in Sekunden
|
||||||
|
long playtimeTicks = player.getStatistic(org.bukkit.Statistic.PLAY_ONE_MINUTE); // tatsächlich in Ticks
|
||||||
|
long playtimeSecs = playtimeTicks / 20;
|
||||||
|
|
||||||
|
String key = kills + "," + deaths + "," + playtimeSecs;
|
||||||
|
if (key.equals(lastPushedStats.get(player.getUniqueId()))) return;
|
||||||
|
lastPushedStats.put(player.getUniqueId(), key);
|
||||||
|
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
String name = player.getName();
|
||||||
|
httpExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
sendPost(statusApiUrl + "/stats/update",
|
||||||
|
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||||
|
+ "\",\"kills\":" + kills
|
||||||
|
+ ",\"deaths\":" + deaths
|
||||||
|
+ ",\"playtime\":" + playtimeSecs + "}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("Stats-Push fehlgeschlagen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TicketSystem Push ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushed TicketSystem-Daten an die StatusAPI.
|
||||||
|
* Globale Werte (offene Tickets, Bewertungen) werden einmal pro Intervall gesendet.
|
||||||
|
* Pro Spieler wird die Anzahl eigener aktiver Tickets mitgeschickt.
|
||||||
|
*
|
||||||
|
* Voraussetzung: TicketSystem muss auf demselben Bukkit-Server laufen.
|
||||||
|
*/
|
||||||
|
private void pushTicketData() {
|
||||||
|
try {
|
||||||
|
Class<?> pluginClass = Class.forName("de.ticketsystem.TicketPlugin");
|
||||||
|
Object tsPlugin = Bukkit.getPluginManager().getPlugin("TicketSystem");
|
||||||
|
if (tsPlugin == null) return;
|
||||||
|
|
||||||
|
Object db = pluginClass.getMethod("getDatabaseManager").invoke(tsPlugin);
|
||||||
|
if (db == null) return;
|
||||||
|
|
||||||
|
Class<?> dbClass = db.getClass();
|
||||||
|
Class<?> statsClass = Class.forName("de.ticketsystem.database.DatabaseManager$TicketStats");
|
||||||
|
|
||||||
|
Object stats = dbClass.getMethod("getTicketStats").invoke(db);
|
||||||
|
int totalOpen = (int) statsClass.getField("open").get(stats);
|
||||||
|
int totalClaimed = (int) statsClass.getField("closed").get(stats); // "closed" im stats-Kontext = bearbeitet
|
||||||
|
|
||||||
|
// CLAIMED direkt zählen via getTicketsByStatus
|
||||||
|
Class<?> statusEnum = Class.forName("de.ticketsystem.model.TicketStatus");
|
||||||
|
Object claimed = statusEnum.getField("CLAIMED").get(null);
|
||||||
|
// Varargs via Reflection: typisiertes Array (TicketStatus[]) erzeugen, kein Object[]
|
||||||
|
Object statusArray = java.lang.reflect.Array.newInstance(statusEnum, 1);
|
||||||
|
java.lang.reflect.Array.set(statusArray, 0, claimed);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.List<?> claimedTickets = (java.util.List<?>) dbClass
|
||||||
|
.getMethod("getTicketsByStatus", statusArray.getClass())
|
||||||
|
.invoke(db, statusArray);
|
||||||
|
int totalClaimedCount = claimedTickets == null ? 0 : claimedTickets.size();
|
||||||
|
|
||||||
|
int ratGood = (int) statsClass.getField("thumbsUp").get(stats);
|
||||||
|
int ratBad = (int) statsClass.getField("thumbsDown").get(stats);
|
||||||
|
|
||||||
|
// Globale Werte nur senden wenn geändert
|
||||||
|
String globalKey = totalOpen + "," + totalClaimedCount + "," + ratGood + "," + ratBad;
|
||||||
|
if (!globalKey.equals(lastPushedTicketGlobal)) {
|
||||||
|
lastPushedTicketGlobal = globalKey;
|
||||||
|
String globalJson = "{\"total_open\":" + totalOpen
|
||||||
|
+ ",\"total_claimed\":" + totalClaimedCount
|
||||||
|
+ ",\"rating_good\":" + ratGood
|
||||||
|
+ ",\"rating_bad\":" + ratBad + "}";
|
||||||
|
sendPost(statusApiUrl + "/ticket/update", globalJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pro Spieler: eigene aktive Tickets
|
||||||
|
for (org.bukkit.entity.Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
int myOpen = (int) dbClass
|
||||||
|
.getMethod("countOpenTicketsByPlayer", java.util.UUID.class)
|
||||||
|
.invoke(db, player.getUniqueId());
|
||||||
|
String playerJson = "{\"uuid\":\"" + player.getUniqueId() + "\",\"my_open\":" + myOpen + "}";
|
||||||
|
sendPost(statusApiUrl + "/ticket/update", playerJson);
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
// TicketSystem nicht installiert – kein Fehler loggen
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("[TicketPush] Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PlaceholderAPI ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private boolean fetchPapiTokensFromStatusAPI() {
|
||||||
|
try {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
java.net.URL url = new java.net.URI(statusApiUrl + "/papi/tokens").toURL();
|
||||||
|
java.net.HttpURLConnection c = (java.net.HttpURLConnection) url.openConnection();
|
||||||
|
c.setRequestMethod("GET");
|
||||||
|
c.setConnectTimeout(3000);
|
||||||
|
c.setReadTimeout(3000);
|
||||||
|
if (c.getResponseCode() != 200) { c.disconnect(); return false; }
|
||||||
|
java.io.InputStream is = c.getInputStream();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int ch; while ((ch = is.read()) != -1) sb.append((char) ch);
|
||||||
|
c.disconnect();
|
||||||
|
String body = sb.toString().trim();
|
||||||
|
papiTokens.clear();
|
||||||
|
if (body.startsWith("[") && body.endsWith("]")) {
|
||||||
|
String inner = body.substring(1, body.length() - 1).trim();
|
||||||
|
if (!inner.isEmpty()) {
|
||||||
|
int i = 0;
|
||||||
|
while (i < inner.length()) {
|
||||||
|
while (i < inner.length() && inner.charAt(i) != '"') i++;
|
||||||
|
if (i >= inner.length()) break;
|
||||||
|
i++;
|
||||||
|
StringBuilder token = new StringBuilder();
|
||||||
|
while (i < inner.length() && inner.charAt(i) != '"') {
|
||||||
|
char c2 = inner.charAt(i++);
|
||||||
|
if (c2 == '\\' && i < inner.length()) c2 = inner.charAt(i++);
|
||||||
|
token.append(c2);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
if (token.length() > 0) papiTokens.add(token.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncPapiValues() {
|
||||||
|
if (!papiEnabled || papiTokens.isEmpty()) return;
|
||||||
|
for (Player p : Bukkit.getOnlinePlayers()) pushPapiValues(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushPapiValues(Player p) {
|
||||||
|
try {
|
||||||
|
Class<?> papiClass = Class.forName("me.clip.placeholderapi.PlaceholderAPI");
|
||||||
|
java.lang.reflect.Method setPlaceholders = papiClass.getMethod("setPlaceholders", Player.class, String.class);
|
||||||
|
StringBuilder jsonValues = new StringBuilder();
|
||||||
|
for (String token : papiTokens) {
|
||||||
|
String resolved = (String) setPlaceholders.invoke(null, p, "%" + token + "%");
|
||||||
|
if (resolved == null) resolved = "";
|
||||||
|
if (jsonValues.length() > 0) jsonValues.append(",");
|
||||||
|
jsonValues.append("\"").append(esc(token)).append("\":\"").append(esc(resolved)).append("\"");
|
||||||
|
}
|
||||||
|
String snapshot = jsonValues.toString();
|
||||||
|
if (snapshot.equals(lastPapiValues.get(p.getUniqueId()))) return;
|
||||||
|
lastPapiValues.put(p.getUniqueId(), snapshot);
|
||||||
|
String json = "{\"uuid\":\"" + p.getUniqueId() + "\",\"placeholders\":{" + snapshot + "}}";
|
||||||
|
httpExecutor.execute(() -> {
|
||||||
|
try { sendPost(statusApiUrl + "/player/papi", json); }
|
||||||
|
catch (Exception e) { getLogger().warning("[PAPI] Push fehlgeschlagen: " + e.getMessage()); }
|
||||||
|
});
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
} catch (Exception e) { getLogger().warning("[PAPI] Fehler: " + e.getMessage()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String esc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushTpsAsync(UUID uuid, double tps) {
|
||||||
|
httpExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
sendPost(statusApiUrl + "/scoreboard/tps",
|
||||||
|
"{\"uuid\":\"" + uuid + "\",\"tps\":" + tps + "}");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erkennt beim Start die Server-Version und setzt den internen Modus.
|
||||||
|
* Sichtbar im Server-Log als [StatusAPIBridge] Versions-Modus: ...
|
||||||
|
*/
|
||||||
|
private void detectMinecraftVersion() {
|
||||||
|
String bukkitVersion = Bukkit.getBukkitVersion(); // z.B. "1.21.1-R0.1-SNAPSHOT" oder "26.1.2-R0.1-SNAPSHOT"
|
||||||
|
// Alles ab 26.x gilt als "neuer Modus" ohne NMS-Fallback
|
||||||
|
try {
|
||||||
|
String major = bukkitVersion.split("\\.")[0];
|
||||||
|
int majorVersion = Integer.parseInt(major);
|
||||||
|
isLegacyMode = majorVersion < 26;
|
||||||
|
} catch (Exception e) {
|
||||||
|
isLegacyMode = true; // Fallback: sicherer Legacy-Modus
|
||||||
|
}
|
||||||
|
getLogger().info("Versions-Modus: "
|
||||||
|
+ (isLegacyMode ? "1.21.x-Modus (NMS-Fallback aktiv)" : "26.1.x-Modus (kein NMS-Fallback)")
|
||||||
|
+ " | BukkitVersion: " + bukkitVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TPS auslesen – kompatibel mit Paper 1.21+, Spigot 1.21+, Java 17/21.
|
||||||
|
* Reihenfolge:
|
||||||
|
* 1. Paper-API: getTPS() direkt auf dem Server (sauberster Weg)
|
||||||
|
* 2. Spigot-Reflection: recentTps-Feld auf dem NMS-MinecraftServer
|
||||||
|
* 3. Fallback: 20.0
|
||||||
|
*/
|
||||||
|
private double getCurrentTps() {
|
||||||
|
// 1. Bevorzugt: Bukkit.getTPS() – funktioniert auf beiden Versionen
|
||||||
|
try {
|
||||||
|
double[] tps = (double[]) Bukkit.getServer().getClass()
|
||||||
|
.getMethod("getTPS").invoke(Bukkit.getServer());
|
||||||
|
if (tps != null && tps.length > 0) return Math.min(20.0, tps[0]);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// 2. NMS-Reflection-Fallback – nur im 1.21.x-Modus
|
||||||
|
// Auf 26.1.x schlägt recentTps fehl → wird bewusst übersprungen
|
||||||
|
if (isLegacyMode) {
|
||||||
|
try {
|
||||||
|
Object nmsServer = Bukkit.getServer().getClass()
|
||||||
|
.getMethod("getServer").invoke(Bukkit.getServer());
|
||||||
|
for (String fieldName : new String[]{"recentTps", "tps"}) {
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Field f = nmsServer.getClass().getField(fieldName);
|
||||||
|
Object val = f.get(nmsServer);
|
||||||
|
if (val instanceof double[]) {
|
||||||
|
double[] tps = (double[]) val;
|
||||||
|
if (tps.length > 0) return Math.min(20.0, tps[0]);
|
||||||
|
}
|
||||||
|
} catch (NoSuchFieldException ignored2) {}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 20.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPost(String urlStr, String json) throws Exception {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
URL url = new java.net.URI(urlStr).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
conn.setReadTimeout(3000);
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
byte[] body = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
conn.setRequestProperty("Content-Length", String.valueOf(body.length));
|
||||||
|
try (OutputStream os = conn.getOutputStream()) { os.write(body); }
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code != 200)
|
||||||
|
getLogger().warning("StatusAPI antwortete mit Code " + code + " fuer " + urlStr);
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeName(String name) {
|
||||||
|
if (name == null) return "";
|
||||||
|
return name.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
12
StatusAPIBridge/src/main/resources/config.yml
Normal file
12
StatusAPIBridge/src/main/resources/config.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# URL der BungeeCord StatusAPI (kein Slash am Ende)
|
||||||
|
statusapi-url: "http://127.0.0.1:9191"
|
||||||
|
|
||||||
|
# Wie viele Ticks nach dem Join wird die Balance gepusht? (20 Ticks = 1 Sekunde)
|
||||||
|
push-delay-ticks: 40
|
||||||
|
|
||||||
|
# Live-Sync Intervall fuer Economy-Updates waehrend der Spieler online ist (mind. 20 Ticks)
|
||||||
|
live-sync-interval-ticks: 20
|
||||||
|
|
||||||
|
# Sync-Intervall fuer Scoreboard-Daten (Health, Compass, TPS, World) in Ticks (mind. 20)
|
||||||
|
# Compass und Health werden zusaetzlich event-basiert aktualisiert.
|
||||||
|
scoreboard-sync-interval-ticks: 20
|
||||||
8
StatusAPIBridge/src/main/resources/plugin.yml
Normal file
8
StatusAPIBridge/src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
name: StatusAPIBridge
|
||||||
|
version: 1.0.2
|
||||||
|
main: net.viper.statusapibridge.StatusAPIBridge
|
||||||
|
# 1.21 als niedrigste gemeinsame Basis – wird von 1.21.1 und 26.1.2 akzeptiert
|
||||||
|
api-version: 1.21
|
||||||
|
description: Sendet Spielerdaten an die BungeeCord StatusAPI
|
||||||
|
authors: [Viper]
|
||||||
|
softdepend: [Vault, PlaceholderAPI]
|
||||||
46
backend-join-guard/pom.xml
Normal file
46
backend-join-guard/pom.xml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>net.viper.backend</groupId>
|
||||||
|
<artifactId>backend-join-guard</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>BackendJoinGuard</name>
|
||||||
|
<description>Blocks direct joins to backend servers and only allows proxy IPs.</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>papermc</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>1.20.6-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>BackendJoinGuard</finalName>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>false</filtering>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,509 @@
|
|||||||
|
package net.viper.backendguard;
|
||||||
|
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class BackendJoinGuardPlugin extends JavaPlugin implements Listener {
|
||||||
|
|
||||||
|
private boolean enforcementEnabled;
|
||||||
|
private boolean logBlockedAttempts;
|
||||||
|
private String kickMessage;
|
||||||
|
private final Set<String> allowedExactIps = new HashSet<String>();
|
||||||
|
private final List<SubnetRule> allowedCidrs = new ArrayList<SubnetRule>();
|
||||||
|
private boolean statusApiSyncEnabled;
|
||||||
|
private String statusApiBaseUrl;
|
||||||
|
private String statusApiEndpointPath;
|
||||||
|
private String statusApiApiKey;
|
||||||
|
private int statusApiIntervalSeconds;
|
||||||
|
private boolean statusApiLogSyncErrors;
|
||||||
|
private BukkitTask syncTask;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
saveDefaultConfig();
|
||||||
|
reloadGuardConfig();
|
||||||
|
getServer().getPluginManager().registerEvents(this, this);
|
||||||
|
getLogger().info("BackendJoinGuard aktiviert. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size() + ", StatusAPI-Sync=" + statusApiSyncEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
cancelSyncTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
|
||||||
|
if (!enforcementEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InetAddress address = event.getAddress();
|
||||||
|
if (address == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAllowed(address)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, colorize(kickMessage));
|
||||||
|
if (logBlockedAttempts) {
|
||||||
|
getLogger().warning("Direktjoin blockiert: player=" + event.getName() + ", ip=" + address.getHostAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!"backendguard".equalsIgnoreCase(command.getName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sender.hasPermission("backendguard.admin")) {
|
||||||
|
sender.sendMessage(colorize("&cDafuer hast du keine Rechte."));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length == 1 && "reload".equalsIgnoreCase(args[0])) {
|
||||||
|
reloadConfig();
|
||||||
|
reloadGuardConfig();
|
||||||
|
if (statusApiSyncEnabled) {
|
||||||
|
syncFromStatusApi(true);
|
||||||
|
sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. StatusAPI-Sync aktiv."));
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. Standalone-Modus aktiv."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(colorize("&e/backendguard reload"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadGuardConfig() {
|
||||||
|
FileConfiguration config = getConfig();
|
||||||
|
enforcementEnabled = config.getBoolean("enforcement-enabled", true);
|
||||||
|
logBlockedAttempts = config.getBoolean("log-blocked-attempts", true);
|
||||||
|
kickMessage = config.getString("kick-message", "&cBitte verbinde dich nur ueber den Proxy.");
|
||||||
|
|
||||||
|
allowedExactIps.clear();
|
||||||
|
allowedCidrs.clear();
|
||||||
|
|
||||||
|
for (String entry : config.getStringList("allowed-proxy-ips")) {
|
||||||
|
String normalized = normalizeIp(entry);
|
||||||
|
if (normalized != null) {
|
||||||
|
allowedExactIps.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String entry : config.getStringList("allowed-proxy-cidrs")) {
|
||||||
|
SubnetRule rule = parseSubnet(entry);
|
||||||
|
if (rule != null) {
|
||||||
|
allowedCidrs.add(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusApiSyncEnabled = config.getBoolean("statusapi-sync.enabled", false);
|
||||||
|
statusApiBaseUrl = config.getString("statusapi-sync.base-url", "http://127.0.0.1:9191");
|
||||||
|
statusApiEndpointPath = config.getString("statusapi-sync.endpoint-path", "/network/backendguard/config");
|
||||||
|
statusApiApiKey = config.getString("statusapi-sync.api-key", "");
|
||||||
|
statusApiIntervalSeconds = Math.max(10, config.getInt("statusapi-sync.interval-seconds", 60));
|
||||||
|
statusApiLogSyncErrors = config.getBoolean("statusapi-sync.log-sync-errors", true);
|
||||||
|
|
||||||
|
cancelSyncTask();
|
||||||
|
if (statusApiSyncEnabled) {
|
||||||
|
// Erst lokale Werte laden, dann zentral ueberschreiben falls Sync klappt.
|
||||||
|
syncFromStatusApi(false);
|
||||||
|
syncTask = getServer().getScheduler().runTaskTimerAsynchronously(
|
||||||
|
this,
|
||||||
|
() -> syncFromStatusApi(false),
|
||||||
|
statusApiIntervalSeconds * 20L,
|
||||||
|
statusApiIntervalSeconds * 20L
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelSyncTask() {
|
||||||
|
if (syncTask != null) {
|
||||||
|
syncTask.cancel();
|
||||||
|
syncTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncFromStatusApi(boolean manualTrigger) {
|
||||||
|
if (!statusApiSyncEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
URL url = new URL(buildSyncUrl());
|
||||||
|
conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(4000);
|
||||||
|
conn.setReadTimeout(6000);
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
if (statusApiApiKey != null && !statusApiApiKey.trim().isEmpty()) {
|
||||||
|
conn.setRequestProperty("x-api-key", statusApiApiKey.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code < 200 || code >= 300) {
|
||||||
|
if (statusApiLogSyncErrors || manualTrigger) {
|
||||||
|
getLogger().warning("StatusAPI-Sync fehlgeschlagen (HTTP " + code + "). Nutze lokale Guard-Werte.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
sb.append(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String json = sb.toString();
|
||||||
|
Boolean remoteEnforcement = extractJsonBoolean(json, "enforcement_enabled");
|
||||||
|
Boolean remoteLogBlocked = extractJsonBoolean(json, "log_blocked_attempts");
|
||||||
|
String remoteKickMessage = extractJsonString(json, "kick_message");
|
||||||
|
List<String> remoteIps = extractJsonStringArray(json, "allowed_proxy_ips");
|
||||||
|
List<String> remoteCidrs = extractJsonStringArray(json, "allowed_proxy_cidrs");
|
||||||
|
|
||||||
|
if (remoteEnforcement != null) {
|
||||||
|
enforcementEnabled = remoteEnforcement;
|
||||||
|
}
|
||||||
|
if (remoteLogBlocked != null) {
|
||||||
|
logBlockedAttempts = remoteLogBlocked;
|
||||||
|
}
|
||||||
|
if (remoteKickMessage != null && !remoteKickMessage.trim().isEmpty()) {
|
||||||
|
kickMessage = remoteKickMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteIps != null) {
|
||||||
|
allowedExactIps.clear();
|
||||||
|
for (String ip : remoteIps) {
|
||||||
|
String normalized = normalizeIp(ip);
|
||||||
|
if (normalized != null) {
|
||||||
|
allowedExactIps.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteCidrs != null) {
|
||||||
|
allowedCidrs.clear();
|
||||||
|
for (String cidr : remoteCidrs) {
|
||||||
|
SubnetRule rule = parseSubnet(cidr);
|
||||||
|
if (rule != null) {
|
||||||
|
allowedCidrs.add(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manualTrigger) {
|
||||||
|
getLogger().info("StatusAPI-Sync erfolgreich. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (statusApiLogSyncErrors || manualTrigger) {
|
||||||
|
getLogger().warning("StatusAPI-Sync Fehler: " + e.getMessage() + ". Nutze lokale Guard-Werte.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (conn != null) {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildSyncUrl() {
|
||||||
|
String base = statusApiBaseUrl == null ? "" : statusApiBaseUrl.trim();
|
||||||
|
String path = statusApiEndpointPath == null ? "" : statusApiEndpointPath.trim();
|
||||||
|
|
||||||
|
if (base.endsWith("/") && path.startsWith("/")) {
|
||||||
|
return base.substring(0, base.length() - 1) + path;
|
||||||
|
}
|
||||||
|
if (!base.endsWith("/") && !path.startsWith("/")) {
|
||||||
|
return base + "/" + path;
|
||||||
|
}
|
||||||
|
return base + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean extractJsonBoolean(String json, String key) {
|
||||||
|
String token = extractJsonToken(json, key);
|
||||||
|
if (token == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ("true".equalsIgnoreCase(token)) {
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
if ("false".equalsIgnoreCase(token)) {
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractJsonString(String json, String key) {
|
||||||
|
if (json == null || key == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String search = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(search);
|
||||||
|
if (idx < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int colon = json.indexOf(':', idx + search.length());
|
||||||
|
if (colon < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int i = colon + 1;
|
||||||
|
while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i >= json.length() || json.charAt(i) != '"') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
boolean escape = false;
|
||||||
|
while (i < json.length()) {
|
||||||
|
char ch = json.charAt(i++);
|
||||||
|
if (escape) {
|
||||||
|
if (ch == 'n') {
|
||||||
|
sb.append('\n');
|
||||||
|
} else if (ch == 'r') {
|
||||||
|
sb.append('\r');
|
||||||
|
} else {
|
||||||
|
sb.append(ch);
|
||||||
|
}
|
||||||
|
escape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '\\') {
|
||||||
|
escape = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '"') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sb.append(ch);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractJsonToken(String json, String key) {
|
||||||
|
if (json == null || key == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String search = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(search);
|
||||||
|
if (idx < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int colon = json.indexOf(':', idx + search.length());
|
||||||
|
if (colon < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int i = colon + 1;
|
||||||
|
while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i >= json.length()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (i < json.length()) {
|
||||||
|
char ch = json.charAt(i);
|
||||||
|
if (ch == ',' || ch == '}' || ch == ']' || Character.isWhitespace(ch)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sb.append(ch);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> extractJsonStringArray(String json, String key) {
|
||||||
|
if (json == null || key == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String search = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(search);
|
||||||
|
if (idx < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int colon = json.indexOf(':', idx + search.length());
|
||||||
|
if (colon < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int start = json.indexOf('[', colon + 1);
|
||||||
|
if (start < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int depth = 0;
|
||||||
|
int end = -1;
|
||||||
|
for (int i = start; i < json.length(); i++) {
|
||||||
|
char ch = json.charAt(i);
|
||||||
|
if (ch == '[') {
|
||||||
|
depth++;
|
||||||
|
} else if (ch == ']') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end < 0 || end <= start) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String raw = json.substring(start + 1, end);
|
||||||
|
List<String> out = new ArrayList<String>();
|
||||||
|
boolean inString = false;
|
||||||
|
boolean escape = false;
|
||||||
|
StringBuilder current = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < raw.length(); i++) {
|
||||||
|
char ch = raw.charAt(i);
|
||||||
|
if (!inString) {
|
||||||
|
if (ch == '"') {
|
||||||
|
inString = true;
|
||||||
|
current.setLength(0);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (escape) {
|
||||||
|
if (ch == 'n') {
|
||||||
|
current.append('\n');
|
||||||
|
} else if (ch == 'r') {
|
||||||
|
current.append('\r');
|
||||||
|
} else {
|
||||||
|
current.append(ch);
|
||||||
|
}
|
||||||
|
escape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '\\') {
|
||||||
|
escape = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '"') {
|
||||||
|
inString = false;
|
||||||
|
out.add(current.toString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.append(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAllowed(InetAddress address) {
|
||||||
|
String normalized = normalizeIp(address.getHostAddress());
|
||||||
|
if (normalized != null && allowedExactIps.contains(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (SubnetRule rule : allowedCidrs) {
|
||||||
|
if (rule.matches(address)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeIp(String raw) {
|
||||||
|
if (raw == null || raw.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return InetAddress.getByName(raw.trim()).getHostAddress().toLowerCase(Locale.ROOT);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
getLogger().warning("Ungueltige IP in Config ignoriert: " + raw);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubnetRule parseSubnet(String raw) {
|
||||||
|
if (raw == null || raw.trim().isEmpty() || !raw.contains("/")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = raw.trim().split("/", 2);
|
||||||
|
try {
|
||||||
|
InetAddress network = InetAddress.getByName(parts[0]);
|
||||||
|
int prefix = Integer.parseInt(parts[1]);
|
||||||
|
return new SubnetRule(network, prefix);
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("Ungueltige CIDR in Config ignoriert: " + raw);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String colorize(String text) {
|
||||||
|
return ChatColor.translateAlternateColorCodes('&', text == null ? "" : text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SubnetRule {
|
||||||
|
private final byte[] networkBytes;
|
||||||
|
private final int prefixLength;
|
||||||
|
|
||||||
|
private SubnetRule(InetAddress network, int prefixLength) {
|
||||||
|
this.networkBytes = network.getAddress();
|
||||||
|
this.prefixLength = prefixLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matches(InetAddress address) {
|
||||||
|
byte[] candidate = address.getAddress();
|
||||||
|
if (candidate.length != networkBytes.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fullBytes = prefixLength / 8;
|
||||||
|
int remainderBits = prefixLength % 8;
|
||||||
|
|
||||||
|
for (int i = 0; i < fullBytes; i++) {
|
||||||
|
if (candidate[i] != networkBytes[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainderBits == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mask = (-1) << (8 - remainderBits);
|
||||||
|
return (candidate[fullBytes] & mask) == (networkBytes[fullBytes] & mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
backend-join-guard/src/main/resources/config.yml
Normal file
29
backend-join-guard/src/main/resources/config.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# BackendJoinGuard
|
||||||
|
# Dieses Plugin kommt auf JEDEN Unterserver (Paper/Spigot), nicht auf den Proxy.
|
||||||
|
# Nur Verbindungen von diesen Proxy-IPs oder Netzbereichen duerfen joinen.
|
||||||
|
|
||||||
|
enforcement-enabled: true
|
||||||
|
log-blocked-attempts: true
|
||||||
|
kick-message: "&cBitte verbinde dich nur ueber den Proxy-Server. Direkte Unterserver-Joins sind blockiert."
|
||||||
|
|
||||||
|
# Exakte Proxy-IPs
|
||||||
|
allowed-proxy-ips:
|
||||||
|
- "127.0.0.1"
|
||||||
|
- "::1"
|
||||||
|
|
||||||
|
# Optional ganze Netze im CIDR-Format
|
||||||
|
allowed-proxy-cidrs: []
|
||||||
|
# Beispiel:
|
||||||
|
# allowed-proxy-cidrs:
|
||||||
|
# - "10.0.0.0/24"
|
||||||
|
# - "192.168.178.10/32"
|
||||||
|
|
||||||
|
# Optional: zentrale Steuerung ueber StatusAPI
|
||||||
|
# Standard bleibt Standalone (enabled=false), damit BackendJoinGuard auch ohne StatusAPI nutzbar ist.
|
||||||
|
statusapi-sync:
|
||||||
|
enabled: true
|
||||||
|
base-url: "http://127.0.0.1:9191"
|
||||||
|
endpoint-path: "/network/backendguard/config"
|
||||||
|
api-key: "bgSync_7Rk9pQ2nLm5xV8cH4tW1yZ6"
|
||||||
|
interval-seconds: 60
|
||||||
|
log-sync-errors: true
|
||||||
14
backend-join-guard/src/main/resources/plugin.yml
Normal file
14
backend-join-guard/src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: BackendJoinGuard
|
||||||
|
main: net.viper.backendguard.BackendJoinGuardPlugin
|
||||||
|
version: 1.0.0
|
||||||
|
author: M_Viper
|
||||||
|
description: Blockiert direkte Joins auf Backendserver und erlaubt nur Proxy-IPs.
|
||||||
|
api-version: '1.20'
|
||||||
|
commands:
|
||||||
|
backendguard:
|
||||||
|
description: BackendJoinGuard neu laden
|
||||||
|
usage: /backendguard reload
|
||||||
|
permissions:
|
||||||
|
backendguard.admin:
|
||||||
|
description: Darf BackendJoinGuard verwalten
|
||||||
|
default: op
|
||||||
52
pom.xml
52
pom.xml
@@ -1,52 +0,0 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
|
||||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<groupId>net.viper.bungee</groupId>
|
|
||||||
<artifactId>StatusAPI</artifactId>
|
|
||||||
<version>4.0.3</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<name>StatusAPI</name>
|
|
||||||
<description>BungeeCord Status API Plugin</description>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<maven.compiler.source>8</maven.compiler.source>
|
|
||||||
<maven.compiler.target>8</maven.compiler.target>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<!-- BungeeCord API -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.md-5</groupId>
|
|
||||||
<artifactId>bungeecord-api</artifactId>
|
|
||||||
<version>1.20</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- LuckPerms API (Optional) -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.luckperms</groupId>
|
|
||||||
<artifactId>api</artifactId>
|
|
||||||
<version>5.4</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<finalName>StatusAPI</finalName>
|
|
||||||
|
|
||||||
<resources>
|
|
||||||
<resource>
|
|
||||||
<directory>src/main/resources</directory>
|
|
||||||
<filtering>false</filtering>
|
|
||||||
</resource>
|
|
||||||
</resources>
|
|
||||||
</build>
|
|
||||||
|
|
||||||
</project>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package net.viper.status;
|
|
||||||
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class FileDownloader {
|
|
||||||
private final Plugin plugin;
|
|
||||||
|
|
||||||
public FileDownloader(Plugin plugin) { this.plugin = plugin; }
|
|
||||||
|
|
||||||
public void downloadFile(String urlString, File destination, Runnable onSuccess) {
|
|
||||||
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
|
||||||
BufferedInputStream bufferedInputStream = null;
|
|
||||||
FileOutputStream fileOutputStream = null;
|
|
||||||
try {
|
|
||||||
URL url = new URL(urlString);
|
|
||||||
bufferedInputStream = new BufferedInputStream(url.openStream());
|
|
||||||
fileOutputStream = new FileOutputStream(destination);
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int count;
|
|
||||||
while ((count = bufferedInputStream.read(buffer, 0, 1024)) != -1) {
|
|
||||||
fileOutputStream.write(buffer, 0, count);
|
|
||||||
}
|
|
||||||
fileOutputStream.close();
|
|
||||||
bufferedInputStream.close();
|
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, onSuccess, 1, java.util.concurrent.TimeUnit.MILLISECONDS);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
plugin.getLogger().warning("Download fehlgeschlagen: " + e.getMessage());
|
|
||||||
if (destination.exists()) destination.delete();
|
|
||||||
} finally {
|
|
||||||
if (fileOutputStream != null) try { fileOutputStream.close(); } catch (IOException ignored) {}
|
|
||||||
if (bufferedInputStream != null) try { bufferedInputStream.close(); } catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
package net.viper.status;
|
|
||||||
|
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
|
||||||
import net.md_5.bungee.api.config.ListenerInfo;
|
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
|
||||||
import net.viper.status.module.ModuleManager;
|
|
||||||
import net.viper.status.stats.PlayerStats;
|
|
||||||
import net.viper.status.stats.StatsModule;
|
|
||||||
import net.viper.status.modules.verify.VerifyModule;
|
|
||||||
import net.viper.status.modules.globalchat.GlobalChatModule;
|
|
||||||
import net.viper.status.modules.navigation.NavigationModule;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class StatusAPI extends Plugin implements Runnable {
|
|
||||||
|
|
||||||
private Thread thread;
|
|
||||||
private int port = 9191;
|
|
||||||
|
|
||||||
// Das neue Modul-System
|
|
||||||
private ModuleManager moduleManager;
|
|
||||||
|
|
||||||
// Alte Komponenten (UpdateChecker/FileDownloader bleiben hier, da sie Core-Updates steuern)
|
|
||||||
private UpdateChecker updateChecker;
|
|
||||||
private FileDownloader fileDownloader;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEnable() {
|
|
||||||
getLogger().info("StatusAPI Core wird initialisiert...");
|
|
||||||
|
|
||||||
// 1. Ordner sicherstellen
|
|
||||||
if (!getDataFolder().exists()) {
|
|
||||||
getDataFolder().mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Modul-System starten
|
|
||||||
moduleManager = new ModuleManager();
|
|
||||||
|
|
||||||
// 3. MODULE REGISTRIEREN (Hier erweiterst du die API in Zukunft!)
|
|
||||||
// Um ein neues Feature hinzuzufügen, erstelle eine Klasse, die 'Module' implementiert
|
|
||||||
// und füge hier eine Zeile hinzu: moduleManager.registerModule(new MeinNeuesModul());
|
|
||||||
|
|
||||||
moduleManager.registerModule(new StatsModule()); // Statistik System laden
|
|
||||||
|
|
||||||
moduleManager.registerModule(new VerifyModule()); // Verify Modul
|
|
||||||
|
|
||||||
moduleManager.registerModule(new GlobalChatModule()); // GlobalChat
|
|
||||||
|
|
||||||
moduleManager.registerModule(new NavigationModule()); //Server Switcher
|
|
||||||
|
|
||||||
// 4. Alle Module aktivieren
|
|
||||||
moduleManager.enableAll(this);
|
|
||||||
|
|
||||||
// 5. WebServer Thread starten
|
|
||||||
getLogger().info("Starte Web-Server auf Port " + port + "...");
|
|
||||||
thread = new Thread(this, "StatusAPI-HTTP-Server");
|
|
||||||
thread.start();
|
|
||||||
|
|
||||||
// 6. Update System Initialisieren
|
|
||||||
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
|
||||||
updateChecker = new UpdateChecker(this, currentVersion, 6);
|
|
||||||
fileDownloader = new FileDownloader(this);
|
|
||||||
|
|
||||||
File pluginFile = getFile();
|
|
||||||
File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
|
|
||||||
|
|
||||||
// Backup Cleanup
|
|
||||||
if (backupFile.exists()) {
|
|
||||||
ProxyServer.getInstance().getScheduler().schedule(this, () -> {
|
|
||||||
if (backupFile.exists()) backupFile.delete();
|
|
||||||
}, 1, TimeUnit.MINUTES);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sofortiger Check
|
|
||||||
checkAndMaybeUpdate();
|
|
||||||
// Regelmäßiger Check
|
|
||||||
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisable() {
|
|
||||||
getLogger().info("Stoppe Module...");
|
|
||||||
if (moduleManager != null) {
|
|
||||||
moduleManager.disableAll(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogger().info("Stoppe Web-Server...");
|
|
||||||
if (thread != null) {
|
|
||||||
thread.interrupt();
|
|
||||||
try { thread.join(1000); } catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Update Logik (unverändert) ---
|
|
||||||
private void checkAndMaybeUpdate() {
|
|
||||||
try {
|
|
||||||
updateChecker.checkNow();
|
|
||||||
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
|
||||||
|
|
||||||
if (updateChecker.isUpdateAvailable(currentVersion)) {
|
|
||||||
String newVersion = updateChecker.getLatestVersion();
|
|
||||||
String url = updateChecker.getLatestUrl();
|
|
||||||
getLogger().warning("----------------------------------------");
|
|
||||||
getLogger().warning("Neue Version verfügbar: " + newVersion);
|
|
||||||
getLogger().warning("Starte automatisches Update...");
|
|
||||||
getLogger().warning("----------------------------------------");
|
|
||||||
|
|
||||||
File pluginFile = getFile();
|
|
||||||
File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new");
|
|
||||||
|
|
||||||
fileDownloader.downloadFile(url, newFile, () -> triggerUpdateScript(pluginFile, newFile));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void triggerUpdateScript(File currentFile, File newFile) {
|
|
||||||
try {
|
|
||||||
File pluginsFolder = currentFile.getParentFile();
|
|
||||||
File rootFolder = pluginsFolder.getParentFile();
|
|
||||||
File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat");
|
|
||||||
|
|
||||||
String batContent = "@echo off\n" +
|
|
||||||
"echo Bitte warten, der Server fährt herunter...\n" +
|
|
||||||
"timeout /t 5 /nobreak >nul\n" +
|
|
||||||
"cd /d \"" + pluginsFolder.getAbsolutePath().replace("\\", "/") + "\"\n" +
|
|
||||||
"echo Fuehre Datei-Tausch durch...\n" +
|
|
||||||
"if exist StatusAPI.jar.bak del StatusAPI.jar.bak\n" +
|
|
||||||
"if exist StatusAPI.jar (\n" +
|
|
||||||
" ren StatusAPI.jar StatusAPI.jar.bak\n" +
|
|
||||||
")\n" +
|
|
||||||
"if exist StatusAPI.new.jar (\n" +
|
|
||||||
" ren StatusAPI.new.jar StatusAPI.jar\n" +
|
|
||||||
" echo Update erfolgreich!\n" +
|
|
||||||
") else (\n" +
|
|
||||||
" echo FEHLER: StatusAPI.new.jar nicht gefunden!\n" +
|
|
||||||
" pause\n" +
|
|
||||||
")\n" +
|
|
||||||
"del \"%~f0\"";
|
|
||||||
|
|
||||||
try (PrintWriter out = new PrintWriter(batFile)) { out.println(batContent); }
|
|
||||||
|
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
|
||||||
p.disconnect("§cServer fährt für ein Update neu herunter. Bitte etwas warten.");
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogger().info("Starte Update-Skript...");
|
|
||||||
Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\"");
|
|
||||||
ProxyServer.getInstance().stop();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- WebServer & JSON ---
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try (ServerSocket serverSocket = new ServerSocket(port)) {
|
|
||||||
serverSocket.setSoTimeout(1000);
|
|
||||||
while (!Thread.interrupted()) {
|
|
||||||
try {
|
|
||||||
Socket clientSocket = serverSocket.accept();
|
|
||||||
handleConnection(clientSocket);
|
|
||||||
} catch (java.net.SocketTimeoutException e) {}
|
|
||||||
catch (IOException e) {
|
|
||||||
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleConnection(Socket clientSocket) {
|
|
||||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
|
|
||||||
OutputStream out = clientSocket.getOutputStream()) {
|
|
||||||
|
|
||||||
String inputLine = in.readLine();
|
|
||||||
if (inputLine != null && inputLine.startsWith("GET")) {
|
|
||||||
Map<String, Object> data = new LinkedHashMap<>();
|
|
||||||
data.put("online", true);
|
|
||||||
|
|
||||||
// Version & Info
|
|
||||||
String versionRaw = ProxyServer.getInstance().getVersion();
|
|
||||||
String versionClean = (versionRaw != null && versionRaw.contains(":")) ? versionRaw.split(":")[2].trim() : versionRaw;
|
|
||||||
data.put("version", versionClean);
|
|
||||||
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
|
|
||||||
|
|
||||||
String motd = "BungeeCord";
|
|
||||||
try {
|
|
||||||
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
|
|
||||||
if (it.hasNext()) motd = it.next().getMotd();
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
data.put("motd", motd);
|
|
||||||
|
|
||||||
// StatsModul holen (Service Locator)
|
|
||||||
StatsModule statsModule = (StatsModule) moduleManager.getModule("StatsModule");
|
|
||||||
|
|
||||||
boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
|
|
||||||
List<Map<String, Object>> playersList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
|
||||||
Map<String, Object> playerInfo = new LinkedHashMap<>();
|
|
||||||
playerInfo.put("name", p.getName());
|
|
||||||
try { playerInfo.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {}
|
|
||||||
|
|
||||||
String prefix = "";
|
|
||||||
if (luckPermsEnabled) {
|
|
||||||
try {
|
|
||||||
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
|
|
||||||
Object luckPermsApi = providerClass.getMethod("get").invoke(null);
|
|
||||||
Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi);
|
|
||||||
Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId());
|
|
||||||
if (user != null) {
|
|
||||||
Class<?> queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions");
|
|
||||||
Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null);
|
|
||||||
Object cachedData = user.getClass().getMethod("getCachedData").invoke(user);
|
|
||||||
Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions);
|
|
||||||
Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData);
|
|
||||||
if (result != null) prefix = (String) result;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
playerInfo.put("prefix", prefix);
|
|
||||||
|
|
||||||
// Stats Integration via Modul
|
|
||||||
if (statsModule != null) {
|
|
||||||
PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId());
|
|
||||||
if (ps != null) {
|
|
||||||
playerInfo.put("playtime", ps.getPlaytimeWithCurrentSession());
|
|
||||||
playerInfo.put("joins", ps.joins);
|
|
||||||
playerInfo.put("first_seen", ps.firstSeen);
|
|
||||||
playerInfo.put("last_seen", ps.lastSeen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playersList.add(playerInfo);
|
|
||||||
}
|
|
||||||
data.put("players", playersList);
|
|
||||||
|
|
||||||
String json = buildJsonString(data);
|
|
||||||
byte[] jsonBytes = json.getBytes("UTF-8");
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
response.append("HTTP/1.1 200 OK\r\n");
|
|
||||||
response.append("Content-Type: application/json; charset=UTF-8\r\n");
|
|
||||||
response.append("Access-Control-Allow-Origin: *\r\n");
|
|
||||||
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
|
|
||||||
response.append("Connection: close\r\n\r\n");
|
|
||||||
out.write(response.toString().getBytes("UTF-8"));
|
|
||||||
out.write(jsonBytes);
|
|
||||||
out.flush();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildJsonString(Map<String, Object> data) {
|
|
||||||
StringBuilder sb = new StringBuilder("{");
|
|
||||||
boolean first = true;
|
|
||||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
|
||||||
if (!first) sb.append(",");
|
|
||||||
first = false;
|
|
||||||
sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue()));
|
|
||||||
}
|
|
||||||
sb.append("}");
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String valueToString(Object value) {
|
|
||||||
if (value == null) return "null";
|
|
||||||
else if (value instanceof Boolean) return value.toString();
|
|
||||||
else if (value instanceof Number) return value.toString();
|
|
||||||
else if (value instanceof Map) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> m = (Map<String, Object>) value;
|
|
||||||
return buildJsonString(m);
|
|
||||||
} else if (value instanceof List) {
|
|
||||||
StringBuilder sb = new StringBuilder("[");
|
|
||||||
List<?> list = (List<?>) value;
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
if (i > 0) sb.append(",");
|
|
||||||
Object item = list.get(i);
|
|
||||||
if (item instanceof Map) sb.append(buildJsonString((Map<String, Object>) item));
|
|
||||||
else if (item instanceof Number) sb.append(item.toString());
|
|
||||||
else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
|
|
||||||
}
|
|
||||||
sb.append("]");
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
else return "\"" + escapeJson(String.valueOf(value)) + "\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String escapeJson(String s) {
|
|
||||||
if (s == null) return "";
|
|
||||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,855 +0,0 @@
|
|||||||
package net.viper.status.modules.globalchat;
|
|
||||||
|
|
||||||
import net.md_5.bungee.api.ChatColor;
|
|
||||||
import net.md_5.bungee.api.CommandSender;
|
|
||||||
import net.md_5.bungee.api.chat.ComponentBuilder;
|
|
||||||
import net.md_5.bungee.api.chat.HoverEvent;
|
|
||||||
import net.md_5.bungee.api.chat.TextComponent;
|
|
||||||
import net.md_5.bungee.api.chat.HoverEvent.Action;
|
|
||||||
import net.md_5.bungee.api.config.ServerInfo;
|
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
|
||||||
import net.md_5.bungee.api.event.ChatEvent;
|
|
||||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
|
||||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
|
||||||
import net.md_5.bungee.api.event.ServerConnectEvent;
|
|
||||||
import net.md_5.bungee.api.event.ServerSwitchEvent;
|
|
||||||
import net.md_5.bungee.api.plugin.Command;
|
|
||||||
import net.md_5.bungee.api.plugin.Listener;
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
|
||||||
import net.md_5.bungee.chat.ComponentSerializer;
|
|
||||||
import net.md_5.bungee.event.EventHandler;
|
|
||||||
import net.viper.status.module.Module;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.lang.Class;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GlobalChatModule - Integriert Global Chat, Filter, Logs und Support in die StatusAPI.
|
|
||||||
* Nutzt die zentrale verify.properties für Chat- und Server-Einstellungen.
|
|
||||||
* Ermöglicht manuelle Ränge über UUID-Overrides.
|
|
||||||
* Nutzt REFLECTION für LuckPerms (Optional).
|
|
||||||
*/
|
|
||||||
public class GlobalChatModule implements Module, Listener {
|
|
||||||
|
|
||||||
private static final String CHANNEL_CONTROL = "global:control";
|
|
||||||
private static final String CHANNEL_CHAT = "global:chat";
|
|
||||||
|
|
||||||
private Plugin plugin;
|
|
||||||
|
|
||||||
private List<String> badWords = new ArrayList<>();
|
|
||||||
private File logFolder;
|
|
||||||
private boolean chatMuted = false;
|
|
||||||
private boolean isChatEnabled = true;
|
|
||||||
|
|
||||||
private final Map<UUID, Boolean> playerIsOp = new ConcurrentHashMap<>();
|
|
||||||
private final Map<UUID, UUID> lastSupportContact = new ConcurrentHashMap<>();
|
|
||||||
private final Set<UUID> suppressJoinQuit = ConcurrentHashMap.newKeySet();
|
|
||||||
private final Set<UUID> chatLockPlayers = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
private List<String> welcomeMessages = new ArrayList<>();
|
|
||||||
private Map<String, String> serverDisplayNames = new HashMap<>();
|
|
||||||
|
|
||||||
// Map für gruppen-spezifische Formate aus der verify.properties
|
|
||||||
private Map<String, String> groupFormats = new HashMap<>();
|
|
||||||
|
|
||||||
// NEU: Map für manuelle Ränge aus der verify.properties (Override)
|
|
||||||
private final Map<UUID, String> manualRanks = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "GlobalChatModule";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEnable(Plugin plugin) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
|
|
||||||
loadConfig();
|
|
||||||
|
|
||||||
if (!isChatEnabled) {
|
|
||||||
plugin.getLogger().info("§eGlobalChat ist in der verify.properties DEAKTIVERT.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channels registrieren
|
|
||||||
try {
|
|
||||||
plugin.getProxy().registerChannel(CHANNEL_CONTROL);
|
|
||||||
plugin.getProxy().registerChannel(CHANNEL_CHAT);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
plugin.getLogger().warning("Konnte Channels nicht registrieren.");
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
|
||||||
loadFilter();
|
|
||||||
loadWelcomeMessages();
|
|
||||||
|
|
||||||
logFolder = new File(plugin.getDataFolder(), "logs");
|
|
||||||
if (!logFolder.exists()) logFolder.mkdirs();
|
|
||||||
cleanupOldLogs();
|
|
||||||
|
|
||||||
// Befehle registrieren
|
|
||||||
plugin.getProxy().getPluginManager().registerCommand(plugin, new ReloadCommand());
|
|
||||||
plugin.getProxy().getPluginManager().registerCommand(plugin, new MuteCommand());
|
|
||||||
plugin.getProxy().getPluginManager().registerCommand(plugin, new SupportCommand());
|
|
||||||
plugin.getProxy().getPluginManager().registerCommand(plugin, new ReplyCommand());
|
|
||||||
plugin.getProxy().getPluginManager().registerCommand(plugin, new InfoCommand());
|
|
||||||
plugin.getProxy().getPluginManager().registerCommand(plugin, new ChatToggleCommand());
|
|
||||||
|
|
||||||
// NEU: ClearChat Befehl registrieren
|
|
||||||
plugin.getProxy().getPluginManager().registerCommand(plugin, new ClearChatCommand());
|
|
||||||
|
|
||||||
plugin.getLogger().info("§aGlobalChatModule aktiviert (Mit Manuellen Rang-Overrides)!");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisable(Plugin plugin) {
|
|
||||||
plugin.getLogger().info("§cGlobalChatModule deaktiviert!");
|
|
||||||
try {
|
|
||||||
plugin.getProxy().unregisterChannel(CHANNEL_CONTROL);
|
|
||||||
plugin.getProxy().unregisterChannel(CHANNEL_CHAT);
|
|
||||||
} catch (Throwable ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// Konfiguration laden
|
|
||||||
// ===========================
|
|
||||||
private void loadConfig() {
|
|
||||||
String fileName = "verify.properties";
|
|
||||||
File file = new File(plugin.getDataFolder(), fileName);
|
|
||||||
|
|
||||||
if (!file.exists()) {
|
|
||||||
plugin.getDataFolder().mkdirs();
|
|
||||||
try (InputStream in = plugin.getResourceAsStream(fileName);
|
|
||||||
OutputStream out = new FileOutputStream(file)) {
|
|
||||||
if (in == null) {
|
|
||||||
plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR gefunden.");
|
|
||||||
file.createNewFile();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int length;
|
|
||||||
while ((length = in.read(buffer)) > 0) {
|
|
||||||
out.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt.");
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().severe("Fehler beim Erstellen der Standard-Konfiguration: " + e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Properties props = new Properties();
|
|
||||||
try (InputStream in = new FileInputStream(file)) {
|
|
||||||
props.load(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
isChatEnabled = Boolean.parseBoolean(props.getProperty("chat.enabled", "true"));
|
|
||||||
|
|
||||||
serverDisplayNames.clear();
|
|
||||||
groupFormats.clear();
|
|
||||||
manualRanks.clear(); // NEU
|
|
||||||
|
|
||||||
for (String key : props.stringPropertyNames()) {
|
|
||||||
// 1. Manuelle Overrides laden (Höchste Priorität!)
|
|
||||||
if (key.startsWith("override.")) {
|
|
||||||
String uuidStr = key.substring("override.".length());
|
|
||||||
try {
|
|
||||||
UUID uuid = UUID.fromString(uuidStr);
|
|
||||||
String groupName = props.getProperty(key);
|
|
||||||
manualRanks.put(uuid, groupName);
|
|
||||||
plugin.getLogger().info("Manueller Override geladen: " + uuidStr + " -> " + groupName);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
plugin.getLogger().warning("Ungültige UUID in override: " + uuidStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 2. Server Aliase
|
|
||||||
else if (key.startsWith("server.")) {
|
|
||||||
String[] parts = key.split("\\.");
|
|
||||||
if (parts.length == 2) {
|
|
||||||
String serverName = parts[1];
|
|
||||||
String displayName = props.getProperty(key);
|
|
||||||
serverDisplayNames.put(serverName, displayName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 3. Group Formate
|
|
||||||
else if (key.startsWith("groupformat.")) {
|
|
||||||
String groupName = key.substring("groupformat.".length());
|
|
||||||
String format = props.getProperty(key);
|
|
||||||
groupFormats.put(groupName.toLowerCase(), format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
plugin.getLogger().info("§eGeladene Server-Displaynames: " + serverDisplayNames.size() + " (Chat aktiv: " + isChatEnabled + ")");
|
|
||||||
plugin.getLogger().info("§eGeladene Chat-Formate: " + groupFormats.size());
|
|
||||||
plugin.getLogger().info("§eGeladene Manuelle Overrides: " + manualRanks.size());
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDisplayName(String serverName) {
|
|
||||||
String displayName = serverDisplayNames.getOrDefault(serverName, serverName);
|
|
||||||
return ChatColor.translateAlternateColorCodes('&', displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// Willkommensnachrichten
|
|
||||||
// ===========================
|
|
||||||
private void loadWelcomeMessages() {
|
|
||||||
String fileName = "welcome.yml";
|
|
||||||
File file = new File(plugin.getDataFolder(), fileName);
|
|
||||||
|
|
||||||
if (!file.exists()) {
|
|
||||||
plugin.getDataFolder().mkdirs();
|
|
||||||
try (InputStream in = plugin.getResourceAsStream(fileName);
|
|
||||||
OutputStream out = new FileOutputStream(file)) {
|
|
||||||
if (in == null) {
|
|
||||||
plugin.getLogger().warning("Standard 'welcome.yml' nicht in JAR gefunden.");
|
|
||||||
file.createNewFile();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int length;
|
|
||||||
while ((length = in.read(buffer)) > 0) {
|
|
||||||
out.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
plugin.getLogger().info("Standard welcome.yml erstellt.");
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<String> lines = Files.readAllLines(file.toPath());
|
|
||||||
welcomeMessages.clear();
|
|
||||||
for (String line : lines) {
|
|
||||||
line = line.trim();
|
|
||||||
if (line.startsWith("-")) welcomeMessages.add(line.substring(1).trim());
|
|
||||||
}
|
|
||||||
plugin.getLogger().info("§eGeladene Welcome-Nachrichten: " + welcomeMessages.size());
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendRandomWelcomeMessage(ProxiedPlayer player) {
|
|
||||||
if (welcomeMessages.isEmpty()) return;
|
|
||||||
Random rand = new Random();
|
|
||||||
String message = welcomeMessages.get(rand.nextInt(welcomeMessages.size()));
|
|
||||||
message = message.replace("%player%", player.getName());
|
|
||||||
message = ChatColor.translateAlternateColorCodes('&', message);
|
|
||||||
player.sendMessage(new TextComponent(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// Global Broadcast Helper
|
|
||||||
// ===========================
|
|
||||||
private void broadcastGlobal(TextComponent component) {
|
|
||||||
if (!isChatEnabled) return;
|
|
||||||
|
|
||||||
// Korrektur: statt PluginChannel-Relay senden wir System/Plugin-Nachrichten an alle Spieler.
|
|
||||||
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
|
||||||
p.sendMessage(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// Chatfilter & Global-Chat (RELAY MODE)
|
|
||||||
// ===========================
|
|
||||||
@EventHandler
|
|
||||||
public void onChat(ChatEvent e) {
|
|
||||||
if (!(e.getSender() instanceof ProxiedPlayer)) return;
|
|
||||||
if (e.isCommand()) return;
|
|
||||||
|
|
||||||
ProxiedPlayer player = (ProxiedPlayer) e.getSender();
|
|
||||||
|
|
||||||
// CHAT LOCK / TOGGLE CHECK
|
|
||||||
if (chatLockPlayers.contains(player.getUniqueId())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalMsg = e.getMessage();
|
|
||||||
|
|
||||||
if (suppressJoinQuit.contains(player.getUniqueId()) &&
|
|
||||||
(originalMsg.contains("joined§ Game") || originalMsg.contains("left§ Game"))) {
|
|
||||||
plugin.getLogger().info("Unterdrücke Join-/Quit-Nachricht für " + player.getName());
|
|
||||||
e.setCancelled(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chatMuted && !player.hasPermission("globalchat.bypass")) {
|
|
||||||
player.sendMessage(new TextComponent("§cDer globale Chat ist derzeit deaktiviert!"));
|
|
||||||
e.setCancelled(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String censoredMsg = originalMsg;
|
|
||||||
for (String bad : badWords) {
|
|
||||||
if (bad == null || bad.trim().isEmpty()) continue;
|
|
||||||
censoredMsg = censoredMsg.replaceAll("(?i)" + Pattern.quote(bad), repeat("*", bad.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
e.setCancelled(true);
|
|
||||||
|
|
||||||
String serverName = player.getServer().getInfo().getName();
|
|
||||||
String serverDisplay = getDisplayName(serverName);
|
|
||||||
|
|
||||||
String[] ps = getPrefixSuffix(player);
|
|
||||||
String prefix = ps[0];
|
|
||||||
String playerColor = ps[2];
|
|
||||||
String chatColor = ps[3];
|
|
||||||
|
|
||||||
String displayTag = "";
|
|
||||||
if (!prefix.isEmpty()) displayTag = prefix;
|
|
||||||
else if (!ps[1].isEmpty()) displayTag = ps[1];
|
|
||||||
|
|
||||||
if (!displayTag.isEmpty() && !displayTag.endsWith(" ")) displayTag = displayTag + " ";
|
|
||||||
|
|
||||||
StringBuilder out = new StringBuilder();
|
|
||||||
out.append("§7[").append(serverDisplay).append("§r§7] ");
|
|
||||||
if (!displayTag.isEmpty()) out.append(displayTag);
|
|
||||||
out.append(playerColor).append(player.getName());
|
|
||||||
out.append("§f: ").append(chatColor).append(censoredMsg);
|
|
||||||
|
|
||||||
String chatOut = out.toString();
|
|
||||||
|
|
||||||
TextComponent chatComponent = new TextComponent(chatOut);
|
|
||||||
broadcastGlobal(chatComponent);
|
|
||||||
|
|
||||||
String logEntry = "[" + serverName + "] " +
|
|
||||||
(displayTag.isEmpty() ? "" : stripColor(displayTag) + " ") +
|
|
||||||
player.getName() +
|
|
||||||
": " + originalMsg;
|
|
||||||
logMessage(logEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// Global Join & Quit Events
|
|
||||||
// ===========================
|
|
||||||
@EventHandler
|
|
||||||
public void onPostLogin(PostLoginEvent e) {
|
|
||||||
if (!isChatEnabled) return;
|
|
||||||
|
|
||||||
ProxiedPlayer player = e.getPlayer();
|
|
||||||
|
|
||||||
// Willkommensnachricht senden
|
|
||||||
sendRandomWelcomeMessage(player);
|
|
||||||
|
|
||||||
// Formatierung aus verify.properties nutzen
|
|
||||||
String[] ps = getPrefixSuffix(player);
|
|
||||||
String prefix = ps[0];
|
|
||||||
String playerColor = ps[2]; // Namefarbe aus Properties
|
|
||||||
|
|
||||||
String displayTag = "";
|
|
||||||
if (!prefix.isEmpty()) displayTag = prefix;
|
|
||||||
if (!displayTag.isEmpty() && !displayTag.endsWith(" ")) displayTag = displayTag + " ";
|
|
||||||
|
|
||||||
// Nachricht: "Spieler xy hat den Server betreten"
|
|
||||||
// Der Text "hat den Server betreten" bleibt Grün (Standard), Name und Prefix kommen aus der Config
|
|
||||||
TextComponent joinMsg = new TextComponent(displayTag + playerColor + player.getName() + " §a§lhat den Server betreten.");
|
|
||||||
|
|
||||||
broadcastGlobal(joinMsg);
|
|
||||||
logMessage("[JOIN] " + player.getName() + " hat den Server betreten.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onPlayerDisconnect(PlayerDisconnectEvent e) {
|
|
||||||
if (!isChatEnabled) return;
|
|
||||||
|
|
||||||
ProxiedPlayer player = e.getPlayer();
|
|
||||||
|
|
||||||
// Formatierung aus verify.properties nutzen
|
|
||||||
String[] ps = getPrefixSuffix(player);
|
|
||||||
String prefix = ps[0];
|
|
||||||
String playerColor = ps[2]; // Namefarbe aus Properties
|
|
||||||
|
|
||||||
String displayTag = "";
|
|
||||||
if (!prefix.isEmpty()) displayTag = prefix;
|
|
||||||
if (!displayTag.isEmpty() && !displayTag.endsWith(" ")) displayTag = displayTag + " ";
|
|
||||||
|
|
||||||
// Nachricht: "Spieler xy hat den Server verlassen"
|
|
||||||
// Der Text "hat den Server verlassen" bleibt Rot (Standard), Name und Prefix kommen aus der Config
|
|
||||||
TextComponent quitMsg = new TextComponent(displayTag + playerColor + player.getName() + " §c§lhat den Server verlassen.");
|
|
||||||
|
|
||||||
broadcastGlobal(quitMsg);
|
|
||||||
logMessage("[QUIT] " + player.getName() + " hat den Server verlassen.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// Server Connect & Switch
|
|
||||||
// ===========================
|
|
||||||
@EventHandler
|
|
||||||
public void onServerConnect(ServerConnectEvent e) {
|
|
||||||
if (e.isCancelled()) return;
|
|
||||||
|
|
||||||
ProxiedPlayer player = e.getPlayer();
|
|
||||||
ServerInfo target = e.getTarget();
|
|
||||||
ServerInfo from = player.getServer() != null ? player.getServer().getInfo() : null;
|
|
||||||
|
|
||||||
if (from == null || from.equals(target)) return;
|
|
||||||
|
|
||||||
suppressJoinQuit.add(player.getUniqueId());
|
|
||||||
plugin.getLogger().info("Markiert " + player.getName() + " für Join-/Quit-Unterdrückung");
|
|
||||||
|
|
||||||
try {
|
|
||||||
sendSuppressJoinQuit(from, player.getUniqueId());
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
plugin.getLogger().warning("Fehler beim Senden der Quit-Unterdrückung: " + ex.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
sendSuppressJoinQuit(target, player.getUniqueId());
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
plugin.getLogger().warning("Fehler beim Senden der Join-Unterdrückung: " + ex.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
|
||||||
suppressJoinQuit.remove(player.getUniqueId());
|
|
||||||
plugin.getLogger().info("Entfernte Unterdrückung für " + player.getName());
|
|
||||||
}, 2, java.util.concurrent.TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onServerSwitch(ServerSwitchEvent e) {
|
|
||||||
ProxiedPlayer player = e.getPlayer();
|
|
||||||
ServerInfo from = e.getFrom();
|
|
||||||
ServerInfo to = player.getServer() != null ? player.getServer().getInfo() : null;
|
|
||||||
|
|
||||||
if (to == null || from == null) return;
|
|
||||||
if (from.getName().equalsIgnoreCase(to.getName())) return;
|
|
||||||
|
|
||||||
String fromName = from.getName();
|
|
||||||
String fromDisplay = getDisplayName(fromName);
|
|
||||||
String toName = to.getName();
|
|
||||||
String toDisplay = getDisplayName(toName);
|
|
||||||
|
|
||||||
String[] ps = getPrefixSuffix(player);
|
|
||||||
String prefix = ps[0];
|
|
||||||
String playerColor = ps[2]; // Namefarbe aus Properties für den Switch
|
|
||||||
|
|
||||||
String displayTag = "";
|
|
||||||
if (!prefix.isEmpty()) displayTag = prefix;
|
|
||||||
// Suffix wird hier ignoriert, da die || Syntax vorausgesetzt wird, aber es schadet nicht
|
|
||||||
else if (!ps[1].isEmpty()) displayTag = ps[1];
|
|
||||||
|
|
||||||
if (!displayTag.isEmpty() && !displayTag.endsWith(" ")) displayTag = displayTag + " ";
|
|
||||||
|
|
||||||
StringBuilder msg = new StringBuilder();
|
|
||||||
msg.append("§7[").append(toDisplay).append("§r§7] ");
|
|
||||||
if (!displayTag.isEmpty()) msg.append(displayTag);
|
|
||||||
// Hier wird die playerColor aus der verify.properties angewendet
|
|
||||||
msg.append(playerColor).append(player.getName());
|
|
||||||
msg.append(" §7hat den Server gewechselt: §e")
|
|
||||||
.append(fromDisplay).append(" §7→ §e").append(toDisplay).append("§7.");
|
|
||||||
|
|
||||||
String finalMsg = msg.toString();
|
|
||||||
|
|
||||||
TextComponent switchComponent = new TextComponent(finalMsg);
|
|
||||||
broadcastGlobal(switchComponent);
|
|
||||||
|
|
||||||
String logEntry = "[" + toName + "] " +
|
|
||||||
(displayTag.isEmpty() ? "" : stripColor(displayTag) + " ") +
|
|
||||||
player.getName() + " hat den Server gewechselt: " + fromName + " -> " + toName + ".";
|
|
||||||
logMessage(logEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendSuppressJoinQuit(ServerInfo server, UUID playerId) {
|
|
||||||
if (server == null) return;
|
|
||||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
DataOutputStream out = new DataOutputStream(baos)) {
|
|
||||||
out.writeUTF("suppress");
|
|
||||||
out.writeUTF(playerId.toString());
|
|
||||||
server.sendData(CHANNEL_CONTROL, baos.toByteArray());
|
|
||||||
} catch (IOException ex) {
|
|
||||||
plugin.getLogger().warning("Fehler beim Senden der suppress-Nachricht: " + ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String repeat(String str, int count) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (int i = 0; i < count; i++) sb.append(str);
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// Prefix/Suffix (Logic: Manual > LP > Perm)
|
|
||||||
// ===========================
|
|
||||||
private String[] getPrefixSuffix(ProxiedPlayer player) {
|
|
||||||
String prefix = "";
|
|
||||||
String suffix = "";
|
|
||||||
String playerColor = "§f";
|
|
||||||
String chatColor = "§f";
|
|
||||||
String groupName = "Spieler";
|
|
||||||
|
|
||||||
// 0. NEU: HÖCHSTE PRIO - Manueller Override aus verify.properties
|
|
||||||
if (manualRanks.containsKey(player.getUniqueId())) {
|
|
||||||
groupName = manualRanks.get(player.getUniqueId());
|
|
||||||
// plugin.getLogger().info("Nutze manuellen Override für " + player.getName() + ": " + groupName);
|
|
||||||
}
|
|
||||||
// 1. LuckPerms via Reflection (Bungee)
|
|
||||||
else {
|
|
||||||
String lpGroup = getLuckPermsGroup(player);
|
|
||||||
if (lpGroup != null && !lpGroup.isEmpty() && !lpGroup.equalsIgnoreCase("default")) {
|
|
||||||
groupName = lpGroup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. FALLBACK: Permission-Check
|
|
||||||
if (groupName == null || groupName.equalsIgnoreCase("default") || groupName.equalsIgnoreCase("Spieler")) {
|
|
||||||
if (player.hasPermission("group.owner")) groupName = "Owner";
|
|
||||||
else if (player.hasPermission("group.admin")) groupName = "Admin";
|
|
||||||
else if (player.hasPermission("group.developer")) groupName = "Developer";
|
|
||||||
else if (player.hasPermission("group.premium")) groupName = "Premium";
|
|
||||||
else groupName = "Spieler";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Farben aus verify.properties anwenden
|
|
||||||
if (groupName != null && groupFormats.containsKey(groupName.toLowerCase())) {
|
|
||||||
String rawFormat = groupFormats.get(groupName.toLowerCase());
|
|
||||||
|
|
||||||
if (rawFormat.contains(" || ")) {
|
|
||||||
String[] parts = rawFormat.split(" \\|\\| ");
|
|
||||||
prefix = ChatColor.translateAlternateColorCodes('&', parts[0]);
|
|
||||||
playerColor = ChatColor.translateAlternateColorCodes('&', parts[1]);
|
|
||||||
chatColor = ChatColor.translateAlternateColorCodes('&', parts[2]);
|
|
||||||
suffix = "";
|
|
||||||
} else if (rawFormat.contains(": ")) {
|
|
||||||
String[] parts = rawFormat.split(": ", 2);
|
|
||||||
prefix = ChatColor.translateAlternateColorCodes('&', parts[0]);
|
|
||||||
chatColor = ChatColor.translateAlternateColorCodes('&', parts[1]);
|
|
||||||
playerColor = "§f";
|
|
||||||
suffix = "";
|
|
||||||
} else {
|
|
||||||
prefix = ChatColor.translateAlternateColorCodes('&', rawFormat);
|
|
||||||
suffix = "";
|
|
||||||
playerColor = "§f";
|
|
||||||
chatColor = "§f";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new String[]{prefix, suffix, playerColor, chatColor};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. LuckPerms Meta Fallback (Nur falls kein Config-Format da war)
|
|
||||||
try {
|
|
||||||
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
|
|
||||||
Method getMethod = providerClass.getMethod("get");
|
|
||||||
Object api = getMethod.invoke(null);
|
|
||||||
if (api != null) {
|
|
||||||
Method getUserManagerMethod = api.getClass().getMethod("getUserManager");
|
|
||||||
Object userManager = getUserManagerMethod.invoke(api);
|
|
||||||
Method loadUserMethod = userManager.getClass().getMethod("loadUser", UUID.class);
|
|
||||||
Object userFuture = loadUserMethod.invoke(userManager, player.getUniqueId());
|
|
||||||
Method joinMethod = userFuture.getClass().getMethod("join");
|
|
||||||
Object user = joinMethod.invoke(userFuture);
|
|
||||||
if (user != null) {
|
|
||||||
Class<?> metaClass = Class.forName("net.luckperms.api.cacheddata.CachedMetaData");
|
|
||||||
Method getMetaDataMethod = user.getClass().getMethod("getCachedData");
|
|
||||||
Object meta = getMetaDataMethod.invoke(user);
|
|
||||||
if (meta != null) {
|
|
||||||
Method getPrefixMethod = metaClass.getMethod("getPrefix");
|
|
||||||
Method getSuffixMethod = metaClass.getMethod("getSuffix");
|
|
||||||
Object p = getPrefixMethod.invoke(meta);
|
|
||||||
Object s = getSuffixMethod.invoke(meta);
|
|
||||||
if (p != null) prefix = ChatColor.translateAlternateColorCodes('&', p.toString());
|
|
||||||
if (s != null) suffix = ChatColor.translateAlternateColorCodes('&', s.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {}
|
|
||||||
|
|
||||||
if (prefix == null) prefix = "";
|
|
||||||
if (suffix == null) suffix = "";
|
|
||||||
|
|
||||||
return new String[]{prefix, suffix, playerColor, chatColor};
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPER: LuckPerms Group via Reflection
|
|
||||||
private String getLuckPermsGroup(ProxiedPlayer player) {
|
|
||||||
try {
|
|
||||||
if (plugin.getProxy().getPluginManager().getPlugin("LuckPerms") == null) return null;
|
|
||||||
|
|
||||||
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
|
|
||||||
Method getMethod = providerClass.getMethod("get");
|
|
||||||
Object api = getMethod.invoke(null);
|
|
||||||
|
|
||||||
if (api != null) {
|
|
||||||
Method getUserManagerMethod = api.getClass().getMethod("getUserManager");
|
|
||||||
Object userManager = getUserManagerMethod.invoke(api);
|
|
||||||
Method loadUserMethod = userManager.getClass().getMethod("loadUser", UUID.class);
|
|
||||||
Object userFuture = loadUserMethod.invoke(userManager, player.getUniqueId());
|
|
||||||
Method joinMethod = userFuture.getClass().getMethod("join");
|
|
||||||
Object user = joinMethod.invoke(userFuture);
|
|
||||||
if (user != null) {
|
|
||||||
Method getPrimaryGroupMethod = user.getClass().getMethod("getPrimaryGroup");
|
|
||||||
return (String) getPrimaryGroupMethod.invoke(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
return null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().warning("Fehler beim Auslesen von LuckPerms (Reflection): " + e.getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String stripColor(String s) {
|
|
||||||
if (s == null) return "";
|
|
||||||
return ChatColor.stripColor(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadFilter() {
|
|
||||||
String fileName = "filter.yml";
|
|
||||||
File file = new File(plugin.getDataFolder(), fileName);
|
|
||||||
|
|
||||||
if (!file.exists()) {
|
|
||||||
plugin.getDataFolder().mkdirs();
|
|
||||||
try (InputStream in = plugin.getResourceAsStream(fileName);
|
|
||||||
OutputStream out = new FileOutputStream(file)) {
|
|
||||||
if (in == null) {
|
|
||||||
plugin.getLogger().warning("Standard 'filter.yml' nicht in JAR gefunden.");
|
|
||||||
file.createNewFile();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int length;
|
|
||||||
while ((length = in.read(buffer)) > 0) {
|
|
||||||
out.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
plugin.getLogger().info("Standard filter.yml erstellt.");
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<String> lines = Files.readAllLines(file.toPath());
|
|
||||||
badWords.clear();
|
|
||||||
for (String line : lines) {
|
|
||||||
line = line.trim();
|
|
||||||
if (line.startsWith("-")) badWords.add(line.substring(1).trim());
|
|
||||||
}
|
|
||||||
plugin.getLogger().info("§eGeladene Filter-Wörter: " + badWords.size());
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanupOldLogs() {
|
|
||||||
File[] files = logFolder.listFiles();
|
|
||||||
if (files == null) return;
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
long sevenDays = 1000L * 60 * 60 * 24 * 7;
|
|
||||||
|
|
||||||
for (File f : files) {
|
|
||||||
if (now - f.lastModified() > sevenDays) {
|
|
||||||
f.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void logMessage(String message) {
|
|
||||||
String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
|
||||||
File logFile = new File(logFolder, date + ".log");
|
|
||||||
|
|
||||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(logFile, true))) {
|
|
||||||
String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
|
|
||||||
bw.write("[" + time + "] " + message);
|
|
||||||
bw.newLine();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isStaff(ProxiedPlayer p) {
|
|
||||||
if (p == null) return false;
|
|
||||||
Boolean reportedOp = playerIsOp.get(p.getUniqueId());
|
|
||||||
if (reportedOp != null && reportedOp) return true;
|
|
||||||
|
|
||||||
if (p.hasPermission("team")) return true;
|
|
||||||
if (p.hasPermission("bungeecord.admin")) return true;
|
|
||||||
if (p.hasPermission("globalchat.op")) return true;
|
|
||||||
if (p.hasPermission("*")) return true;
|
|
||||||
|
|
||||||
if (p.hasPermission("bungeecord.command.alert")) return true;
|
|
||||||
if (p.hasPermission("bungeecord.command.reload")) return true;
|
|
||||||
if (p.hasPermission("bungeecord.command.kick")) return true;
|
|
||||||
if (p.hasPermission("bungeecord.command.send")) return true;
|
|
||||||
if (p.hasPermission("bungeecord.command.perms")) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Überladene Methode für CommandSender (für Console/Check)
|
|
||||||
private boolean isStaff(CommandSender sender) {
|
|
||||||
if (sender instanceof ProxiedPlayer) {
|
|
||||||
return isStaff((ProxiedPlayer) sender);
|
|
||||||
}
|
|
||||||
return sender.hasPermission("globalchat.clear"); // Console hat meist Rechte, aber sicher ist sicher
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// Commands (Inner Classes)
|
|
||||||
// ===========================
|
|
||||||
public class ReloadCommand extends Command {
|
|
||||||
public ReloadCommand() { super("globalreload", "globalchat.reload"); }
|
|
||||||
@Override
|
|
||||||
public void execute(CommandSender sender, String[] args) {
|
|
||||||
loadFilter();
|
|
||||||
loadConfig();
|
|
||||||
sender.sendMessage(new TextComponent("§aFilter und Chat-Konfiguration wurden neu geladen!"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MuteCommand extends Command {
|
|
||||||
public MuteCommand() { super("globalmute", "globalchat.mute"); }
|
|
||||||
@Override
|
|
||||||
public void execute(CommandSender sender, String[] args) {
|
|
||||||
chatMuted = !chatMuted;
|
|
||||||
String status = chatMuted ? "§caktiviert" : "§aaufgehoben";
|
|
||||||
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
|
||||||
p.sendMessage(new TextComponent("§7[GlobalChat] §eDer globale Chat Mute wurde " + status + "§e!"));
|
|
||||||
}
|
|
||||||
plugin.getLogger().info("GlobalMute wurde " + (chatMuted ? "aktiviert" : "deaktiviert") + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SupportCommand extends Command {
|
|
||||||
public SupportCommand() { super("support"); }
|
|
||||||
@Override
|
|
||||||
public void execute(CommandSender sender, String[] args) {
|
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
|
||||||
sender.sendMessage(new TextComponent("§cNur Spieler können Support-Nachrichten senden."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProxiedPlayer player = (ProxiedPlayer) sender;
|
|
||||||
if (args.length == 0) {
|
|
||||||
player.sendMessage(new TextComponent("§cBitte eine Nachricht angeben: /support <Nachricht>"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String msg = String.join(" ", args);
|
|
||||||
String serverRaw = player.getServer().getInfo().getName();
|
|
||||||
String serverDisplay = getDisplayName(serverRaw);
|
|
||||||
|
|
||||||
TextComponent supportMsg = new TextComponent("§7[Support] §b" + player.getName() + " §7vom Server §e" + serverDisplay + " §7: §f" + msg);
|
|
||||||
supportMsg.setHoverEvent(new HoverEvent(Action.SHOW_TEXT, new ComponentBuilder("Klicke, um /reply " + player.getName() + " zu schreiben").create()));
|
|
||||||
supportMsg.setClickEvent(new net.md_5.bungee.api.chat.ClickEvent(net.md_5.bungee.api.chat.ClickEvent.Action.SUGGEST_COMMAND, "/reply " + player.getName() + " "));
|
|
||||||
|
|
||||||
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
|
||||||
if (isStaff(p)) {
|
|
||||||
p.sendMessage(supportMsg);
|
|
||||||
lastSupportContact.put(p.getUniqueId(), player.getUniqueId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
player.sendMessage(new TextComponent("§aDeine Support-Nachricht wurde gesendet."));
|
|
||||||
logMessage("[Support][" + serverRaw + "] " + player.getName() + ": " + msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ReplyCommand extends Command {
|
|
||||||
public ReplyCommand() { super("reply"); }
|
|
||||||
@Override
|
|
||||||
public void execute(CommandSender sender, String[] args) {
|
|
||||||
if (!(sender instanceof ProxiedPlayer)) return;
|
|
||||||
|
|
||||||
ProxiedPlayer staff = (ProxiedPlayer) sender;
|
|
||||||
UUID targetId = lastSupportContact.get(staff.getUniqueId());
|
|
||||||
if (targetId == null) {
|
|
||||||
staff.sendMessage(new TextComponent("§cKein Spieler zum Antworten gefunden."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.length == 0) {
|
|
||||||
staff.sendMessage(new TextComponent("§cBitte eine Nachricht angeben."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProxiedPlayer target = plugin.getProxy().getPlayer(targetId);
|
|
||||||
if (target == null) {
|
|
||||||
staff.sendMessage(new TextComponent("§cSpieler ist nicht online."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String msg = String.join(" ", args);
|
|
||||||
target.sendMessage(new TextComponent("§7[Reply von §b" + staff.getName() + "§7]: §f" + msg));
|
|
||||||
staff.sendMessage(new TextComponent("§aDeine Nachricht wurde an §b" + target.getName() + "§a gesendet."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InfoCommand extends Command {
|
|
||||||
public InfoCommand() { super("info"); }
|
|
||||||
@Override
|
|
||||||
public void execute(CommandSender sender, String[] args) {
|
|
||||||
sender.sendMessage(new TextComponent("§8§m------------------------------"));
|
|
||||||
sender.sendMessage(new TextComponent("§6§lGlobalChat Info"));
|
|
||||||
sender.sendMessage(new TextComponent("§ePlugin-Name: §bStatusAPI (GlobalChat Module)"));
|
|
||||||
sender.sendMessage(new TextComponent("§eVersion: §b2.0 (Relay)"));
|
|
||||||
sender.sendMessage(new TextComponent("§eErsteller: §bM_Viper"));
|
|
||||||
sender.sendMessage(new TextComponent("§8§m------------------------------"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ChatToggleCommand extends Command {
|
|
||||||
public ChatToggleCommand() { super("togglechat"); }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(CommandSender sender, String[] args) {
|
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
|
||||||
sender.sendMessage(new TextComponent("§cNur Spieler können den Chat-Modus ändern."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProxiedPlayer player = (ProxiedPlayer) sender;
|
|
||||||
UUID uuid = player.getUniqueId();
|
|
||||||
|
|
||||||
if (chatLockPlayers.contains(uuid)) {
|
|
||||||
chatLockPlayers.remove(uuid);
|
|
||||||
player.sendMessage(new TextComponent("§aGlobalChat aktiviert."));
|
|
||||||
player.sendMessage(new TextComponent("§7Deine Nachrichten werden nun wieder global gesendet."));
|
|
||||||
} else {
|
|
||||||
chatLockPlayers.add(uuid);
|
|
||||||
player.sendMessage(new TextComponent("§cGlobalChat deaktiviert."));
|
|
||||||
player.sendMessage(new TextComponent("§7Deine Nachrichten gehen nur noch an den Server (z.B. für Lands oder Quests)."));
|
|
||||||
player.sendMessage(new TextComponent("§7Nutze erneut /togglechat, um den globalen Chat zu aktivieren."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ClearChatCommand extends Command {
|
|
||||||
public ClearChatCommand() {
|
|
||||||
super("clearchat", "globalchat.clear", "cc");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(CommandSender sender, String[] args) {
|
|
||||||
// Prüfen, ob der Sender OP / Team ist
|
|
||||||
// Wir nutzen hier die überladene isStaff Methode für CommandSender
|
|
||||||
if (!isStaff(sender)) {
|
|
||||||
sender.sendMessage(new TextComponent("§cKeine Berechtigung!"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 100 leere Zeilen senden zum Leeren
|
|
||||||
TextComponent emptyLine = new TextComponent(" ");
|
|
||||||
for (int i = 0; i < 100; i++) {
|
|
||||||
broadcastGlobal(emptyLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bestätigungsnachricht
|
|
||||||
TextComponent clearMsg = new TextComponent("§7[GlobalChat] §cDer Chat wurde von " + sender.getName() + " geleert.");
|
|
||||||
broadcastGlobal(clearMsg);
|
|
||||||
|
|
||||||
sender.sendMessage(new TextComponent("§aChat wurde geleert."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
package net.viper.status.modules.navigation;
|
|
||||||
|
|
||||||
import net.md_5.bungee.api.ChatColor;
|
|
||||||
import net.md_5.bungee.api.CommandSender;
|
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
|
||||||
import net.md_5.bungee.api.config.ServerInfo;
|
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
|
||||||
import net.md_5.bungee.api.plugin.Command;
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
|
||||||
import net.viper.status.module.Module;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NavigationModule - Erstellt automatisch Server-Switch Befehle basierend auf der verify.properties.
|
|
||||||
*/
|
|
||||||
public class NavigationModule implements Module {
|
|
||||||
|
|
||||||
private Plugin plugin;
|
|
||||||
private boolean isEnabled = true;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "NavigationModule";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEnable(Plugin plugin) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
loadConfig(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisable(Plugin plugin) {
|
|
||||||
// Befehle müssen nicht manuell entfernt werden (handled by Bungee)
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadConfig(Plugin plugin) {
|
|
||||||
String fileName = "verify.properties";
|
|
||||||
File file = new File(plugin.getDataFolder(), fileName);
|
|
||||||
Properties props = new Properties();
|
|
||||||
|
|
||||||
// 1. Datei kopieren falls nicht vorhanden
|
|
||||||
if (!file.exists()) {
|
|
||||||
// Wir müssen hier nichts erstellen, da GlobalChat oder Verify das schon tun
|
|
||||||
// Wir versuchen einfach nur zu laden
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Laden
|
|
||||||
try (InputStream in = new FileInputStream(file)) {
|
|
||||||
props.load(in);
|
|
||||||
} catch (IOException e) {
|
|
||||||
plugin.getLogger().warning("Konnte " + fileName + " nicht für Navigation laden.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Aktivierung prüfen
|
|
||||||
isEnabled = Boolean.parseBoolean(props.getProperty("navigation.enabled", "true"));
|
|
||||||
|
|
||||||
if (!isEnabled) {
|
|
||||||
plugin.getLogger().info("§eNavigation ist DEAKTIVIERT.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Befehle generieren
|
|
||||||
int registered = 0;
|
|
||||||
for (String key : props.stringPropertyNames()) {
|
|
||||||
if (key.startsWith("server.")) {
|
|
||||||
// Struktur: server.<ServerName>=<DisplayName>
|
|
||||||
// Wir ignorieren Unterpunkte wie .id oder .secret (teilen wir am Punkt)
|
|
||||||
String[] parts = key.split("\\.");
|
|
||||||
|
|
||||||
if (parts.length == 2) {
|
|
||||||
String targetServer = parts[1]; // Z.B. "bungee-server-1" oder "lobby"
|
|
||||||
String displayName = props.getProperty(key); // Z.B. "&aSurvival"
|
|
||||||
|
|
||||||
// Befehlsnamen generieren: Farbcodes entfernen und Leerzeichen entfernen
|
|
||||||
// "&aSurvival" -> "Survival" -> "/survival"
|
|
||||||
String alias = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', displayName))
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(" ", "");
|
|
||||||
|
|
||||||
if (alias.isEmpty()) continue;
|
|
||||||
|
|
||||||
// Command registrieren
|
|
||||||
try {
|
|
||||||
plugin.getProxy().getPluginManager().registerCommand(plugin, new ServerSwitchCommand(targetServer, displayName, alias));
|
|
||||||
registered++;
|
|
||||||
plugin.getLogger().info("§aBefehl registered: /" + alias + " -> " + targetServer);
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().warning("Konnte Befehl für " + targetServer + " nicht registrieren: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registered > 0) {
|
|
||||||
plugin.getLogger().info("§aNavigation aktiviert! " + registered + " Server-Befehle erstellt.");
|
|
||||||
} else {
|
|
||||||
plugin.getLogger().warning("§cKeine Server für Navigation gefunden (Überprüfe verify.properties).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interne Klasse für den Switch-Befehl.
|
|
||||||
* Ein Objekt davon wird für jeden Server erstellt.
|
|
||||||
*/
|
|
||||||
private class ServerSwitchCommand extends Command {
|
|
||||||
|
|
||||||
private final String targetServer; // Der technische Name (z.B. bungee-server-1)
|
|
||||||
private final String displayName; // Der hübsche Name (z.B. &aSurvival)
|
|
||||||
|
|
||||||
public ServerSwitchCommand(String targetServer, String displayName, String alias) {
|
|
||||||
super(alias); // Befehl name
|
|
||||||
this.targetServer = targetServer;
|
|
||||||
this.displayName = displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(CommandSender sender, String[] args) {
|
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
|
||||||
sender.sendMessage(ChatColor.RED + "Nur Spieler können den Server wechseln.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
|
||||||
|
|
||||||
// Check ob Spieler schon auf dem Server ist
|
|
||||||
if (p.getServer() != null && p.getServer().getInfo().getName().equalsIgnoreCase(targetServer)) {
|
|
||||||
String name = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', displayName));
|
|
||||||
p.sendMessage(ChatColor.YELLOW + "Du bist bereits auf " + name + ChatColor.YELLOW + "!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server Info holen
|
|
||||||
ServerInfo target = plugin.getProxy().getServerInfo(targetServer);
|
|
||||||
|
|
||||||
if (target == null) {
|
|
||||||
p.sendMessage(ChatColor.RED + "Der Server '" + targetServer + "' wurde nicht gefunden.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connecten
|
|
||||||
p.sendMessage(ChatColor.GRAY + "Verbinde dich mit " + displayName + ChatColor.GRAY + "...");
|
|
||||||
p.connect(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package net.viper.status.stats;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class PlayerStats {
|
|
||||||
public final UUID uuid;
|
|
||||||
public String name;
|
|
||||||
public long firstSeen;
|
|
||||||
public long lastSeen;
|
|
||||||
public long totalPlaytime;
|
|
||||||
public long currentSessionStart;
|
|
||||||
public int joins;
|
|
||||||
|
|
||||||
public PlayerStats(UUID uuid, String name) {
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.name = name;
|
|
||||||
long now = System.currentTimeMillis() / 1000L;
|
|
||||||
this.firstSeen = now;
|
|
||||||
this.lastSeen = now;
|
|
||||||
this.totalPlaytime = 0;
|
|
||||||
this.currentSessionStart = 0;
|
|
||||||
this.joins = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void onJoin() {
|
|
||||||
long now = System.currentTimeMillis() / 1000L;
|
|
||||||
if (this.currentSessionStart == 0) this.currentSessionStart = now;
|
|
||||||
this.lastSeen = now;
|
|
||||||
this.joins++;
|
|
||||||
if (this.firstSeen == 0) this.firstSeen = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void onQuit() {
|
|
||||||
long now = System.currentTimeMillis() / 1000L;
|
|
||||||
if (this.currentSessionStart > 0) {
|
|
||||||
long session = now - this.currentSessionStart;
|
|
||||||
if (session > 0) this.totalPlaytime += session;
|
|
||||||
this.currentSessionStart = 0;
|
|
||||||
}
|
|
||||||
this.lastSeen = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized long getPlaytimeWithCurrentSession() {
|
|
||||||
long now = System.currentTimeMillis() / 1000L;
|
|
||||||
if (this.currentSessionStart > 0) return totalPlaytime + (now - currentSessionStart);
|
|
||||||
return totalPlaytime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized String toLine() {
|
|
||||||
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PlayerStats fromLine(String line) {
|
|
||||||
String[] parts = line.split("\\|", -1);
|
|
||||||
if (parts.length < 7) return null;
|
|
||||||
try {
|
|
||||||
UUID uuid = UUID.fromString(parts[0]);
|
|
||||||
String name = parts[1];
|
|
||||||
PlayerStats ps = new PlayerStats(uuid, name);
|
|
||||||
ps.firstSeen = Long.parseLong(parts[2]);
|
|
||||||
ps.lastSeen = Long.parseLong(parts[3]);
|
|
||||||
ps.totalPlaytime = Long.parseLong(parts[4]);
|
|
||||||
ps.currentSessionStart = Long.parseLong(parts[5]);
|
|
||||||
ps.joins = Integer.parseInt(parts[6]);
|
|
||||||
return ps;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package net.viper.status.stats;
|
|
||||||
|
|
||||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
|
||||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
|
||||||
import net.md_5.bungee.api.plugin.Listener;
|
|
||||||
import net.md_5.bungee.event.EventHandler;
|
|
||||||
import net.viper.status.module.Module;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* StatsModule: Kümmert sich eigenständig um das Tracking der Spielerdaten.
|
|
||||||
* Implementiert Module (für das Lifecycle) und Listener (für die Events).
|
|
||||||
*/
|
|
||||||
public class StatsModule implements Module, Listener {
|
|
||||||
|
|
||||||
private StatsManager manager;
|
|
||||||
private StatsStorage storage;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "StatsModule";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEnable(Plugin plugin) {
|
|
||||||
// Initialisierung
|
|
||||||
manager = new StatsManager();
|
|
||||||
storage = new StatsStorage(plugin.getDataFolder());
|
|
||||||
|
|
||||||
// Laden
|
|
||||||
try {
|
|
||||||
storage.load(manager);
|
|
||||||
plugin.getLogger().info("Player-Stats wurden erfolgreich geladen.");
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event Listener registrieren
|
|
||||||
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
|
||||||
|
|
||||||
// Auto-Save Task (alle 5 Minuten)
|
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
|
||||||
try {
|
|
||||||
storage.save(manager);
|
|
||||||
plugin.getLogger().info("Auto-Save: Player-Stats gespeichert.");
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}, 5, 5, TimeUnit.MINUTES);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisable(Plugin plugin) {
|
|
||||||
if (manager != null && storage != null) {
|
|
||||||
// Laufende Sessions beenden vor dem Speichern
|
|
||||||
long now = System.currentTimeMillis() / 1000L;
|
|
||||||
for (PlayerStats ps : manager.all()) {
|
|
||||||
synchronized (ps) {
|
|
||||||
if (ps.currentSessionStart > 0) {
|
|
||||||
long delta = now - ps.currentSessionStart;
|
|
||||||
if (delta > 0) {
|
|
||||||
ps.totalPlaytime += delta;
|
|
||||||
}
|
|
||||||
ps.currentSessionStart = 0; // Session beenden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
storage.save(manager);
|
|
||||||
plugin.getLogger().info("Player-Stats beim Shutdown gespeichert.");
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Öffentlicher Zugriff für den WebServer
|
|
||||||
public StatsManager getManager() {
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Events ---
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onJoin(PostLoginEvent e) {
|
|
||||||
try {
|
|
||||||
manager.get(e.getPlayer().getUniqueId(), e.getPlayer().getName()).onJoin();
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onQuit(PlayerDisconnectEvent e) {
|
|
||||||
try {
|
|
||||||
PlayerStats ps = manager.getIfPresent(e.getPlayer().getUniqueId());
|
|
||||||
if (ps != null) ps.onQuit();
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package net.viper.status.stats;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
|
|
||||||
public class StatsStorage {
|
|
||||||
private final File file;
|
|
||||||
|
|
||||||
public StatsStorage(File pluginFolder) {
|
|
||||||
if (!pluginFolder.exists()) pluginFolder.mkdirs();
|
|
||||||
this.file = new File(pluginFolder, "stats.dat");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save(StatsManager manager) {
|
|
||||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
|
||||||
for (PlayerStats ps : manager.all()) {
|
|
||||||
bw.write(ps.toLine());
|
|
||||||
bw.newLine();
|
|
||||||
}
|
|
||||||
bw.flush();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void load(StatsManager manager) {
|
|
||||||
if (!file.exists()) return;
|
|
||||||
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
|
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) {
|
|
||||||
PlayerStats ps = PlayerStats.fromLine(line);
|
|
||||||
if (ps != null) manager.put(ps);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Liste von Begriffen, die im Chat zensiert werden sollen.
|
|
||||||
badwords:
|
|
||||||
- arsch
|
|
||||||
- hurensohn
|
|
||||||
- scheiße
|
|
||||||
- idiot
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
name: StatusAPI
|
|
||||||
main: net.viper.status.StatusAPI
|
|
||||||
version: 4.0.3
|
|
||||||
author: M_Viper
|
|
||||||
description: StatusAPI für BungeeCord inkl. Update-Checker und Modul-System
|
|
||||||
|
|
||||||
softdepend:
|
|
||||||
- LuckPerms
|
|
||||||
|
|
||||||
commands:
|
|
||||||
# Verify Modul Befehle
|
|
||||||
verify:
|
|
||||||
description: Verifiziere dich mit einem Token
|
|
||||||
usage: /verify <token>
|
|
||||||
|
|
||||||
# GlobalChat Modul Befehle
|
|
||||||
globalreload:
|
|
||||||
description: Lädt den Chat-Filter neu
|
|
||||||
usage: /globalreload
|
|
||||||
permission: globalchat.reload
|
|
||||||
|
|
||||||
globalmute:
|
|
||||||
description: Schaltet den globalen Chat an/aus
|
|
||||||
usage: /globalmute
|
|
||||||
permission: globalchat.mute
|
|
||||||
|
|
||||||
support:
|
|
||||||
description: Sendet eine Support-Nachricht an das Team
|
|
||||||
usage: /support <Nachricht>
|
|
||||||
|
|
||||||
reply:
|
|
||||||
description: Antwortet auf eine Support-Nachricht
|
|
||||||
usage: /reply <Nachricht>
|
|
||||||
|
|
||||||
info:
|
|
||||||
description: Zeigt Plugin-Informationen an
|
|
||||||
usage: /info
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
# StatusAPI Core Permissions
|
|
||||||
statusapi.update.notify:
|
|
||||||
description: 'Erlaubt Update-Benachrichtigungen'
|
|
||||||
default: op
|
|
||||||
|
|
||||||
# GlobalChat Permissions
|
|
||||||
globalchat.reload:
|
|
||||||
description: Erlaubt das Neuladen des Chat-Filters
|
|
||||||
default: op
|
|
||||||
|
|
||||||
globalchat.mute:
|
|
||||||
description: Erlaubt das Aktivieren/Deaktivieren des globalen Mutes
|
|
||||||
default: op
|
|
||||||
|
|
||||||
globalchat.bypass:
|
|
||||||
description: Umgeht den globalen Mute
|
|
||||||
default: op
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# ===========================
|
|
||||||
# GLOBALCHAT AKTIVIERUNG
|
|
||||||
# ===========================
|
|
||||||
chat.enabled=false
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# NAVIGATION / SERVER SWITCHER
|
|
||||||
# ===========================
|
|
||||||
# Hier kannst du das interne Navigationssystem aktivieren/deaktivieren.
|
|
||||||
# Wenn aktiviert, erstellt das Plugin automatisch Befehle basierend auf den Servernamen (z.B. /lobby, /survival).
|
|
||||||
navigation.enabled=false
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# WORDPRESS / VERIFY EINSTELLUNGEN
|
|
||||||
# ===========================
|
|
||||||
wp_verify_url=https://deine-wp-domain.tld
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# SERVER KONFIGURATION
|
|
||||||
# ===========================
|
|
||||||
# Hier legst du für jeden Server alles fest:
|
|
||||||
# 1. Den Anzeigenamen für den Chat (z.B. &bLobby)
|
|
||||||
# 2. Die Server ID für WordPress (z.B. id=1)
|
|
||||||
# 3. Das Secret für WordPress (z.B. secret=...)
|
|
||||||
|
|
||||||
# Server 1: Lobby
|
|
||||||
server.lobby=&bLobby
|
|
||||||
server.lobby.id=1
|
|
||||||
server.lobby.secret=GeheimesWortFuerLobby123
|
|
||||||
|
|
||||||
# Server 2: Survival
|
|
||||||
server.survival=&aSurvival
|
|
||||||
server.survival.id=2
|
|
||||||
server.survival.secret=GeheimesWortFuerSurvival456
|
|
||||||
|
|
||||||
# Server 3: SkyBlock
|
|
||||||
server.skyblock=&dSkyBlock
|
|
||||||
server.skyblock.id=3
|
|
||||||
server.skyblock.secret=GeheimesWortFuerSkyBlock789
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# Manuelle Ränge (Overrides)
|
|
||||||
# ===========================
|
|
||||||
# Syntax: override.<Spieler-UUID> = <Gruppenname>
|
|
||||||
# WICHTIG: Die Gruppe (z.B. Owner) muss unten bei groupformat definiert sein!
|
|
||||||
|
|
||||||
# Beispiel: Deinen UUID hier einfügen
|
|
||||||
override.uuid-hier-einfügen = Owner
|
|
||||||
override.uuid-hier-einfügen = Admin
|
|
||||||
override.uuid-hier-einfügen = Developer
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# Chat-Formate für Gruppen
|
|
||||||
# ===========================
|
|
||||||
|
|
||||||
# Der Name hinter dem Punkt (z.B. Owner) muss exakt mit der LuckPerms Gruppe übereinstimmen.
|
|
||||||
# Nutze & für Farbcodes.
|
|
||||||
|
|
||||||
# Ränge mit neuer Syntax: Rank || Spielerfarbe || Chatfarbe
|
|
||||||
# Beispiel: Rot (Rang) || Blau (Name) || Lila (Chat)
|
|
||||||
groupformat.Owner=&c[Owner] || &b || &d
|
|
||||||
groupformat.Admin=&4[Admin] || &9 || &c
|
|
||||||
groupformat.Developer=&b[Dev] || &3 || &a
|
|
||||||
groupformat.Premium=&6[Premium] || &e || &7
|
|
||||||
groupformat.Spieler=&f[Spieler] || &7 || &8
|
|
||||||
Reference in New Issue
Block a user