608 Commits
4.0.1 ... 4.1.3

Author SHA1 Message Date
388eb2be66 README.md aktualisiert 2026-05-30 05:43:13 +00:00
dd183203cd README.md aktualisiert 2026-05-30 05:42:58 +00:00
Git Manager GUI
41d0d80811 Upload folder via GUI - src 2026-05-26 16:52:40 +02:00
45e1e3cbc0 README.md aktualisiert 2026-05-26 12:46:36 +00:00
Git Manager GUI
8e9d7bec21 Upload folder via GUI - src 2026-05-26 14:33:22 +02:00
Git Manager GUI
bb940110bd Upload via Git Manager GUI 2026-05-26 14:33:17 +02:00
Git Manager GUI
23c2525872 Upload folder via GUI - src 2026-05-24 21:44:15 +02:00
c102fb0aa5 Delete src/main/resources/welcome.yml via Git Manager GUI 2026-05-24 19:43:56 +00:00
6333ca22e8 Delete src/main/resources/scoreboard.properties via Git Manager GUI 2026-05-24 19:43:55 +00:00
c5ad9d6255 Delete src/main/resources/network-guard.properties via Git Manager GUI 2026-05-24 19:43:55 +00:00
0fa92d0cbf Delete src/main/resources/chat.yml via Git Manager GUI 2026-05-24 19:43:54 +00:00
d9ff16fe76 Delete src/main/java/net/viper/status/stats/StatsModule.java via Git Manager GUI 2026-05-24 19:43:54 +00:00
096609dba9 Delete src/main/java/net/viper/status/stats/PlayerStats.java via Git Manager GUI 2026-05-24 19:43:54 +00:00
b502485e51 Delete src/main/java/net/viper/status/modules/verify/VerifyModule.java via Git Manager GUI 2026-05-24 19:43:53 +00:00
25389f2238 Delete src/main/java/net/viper/status/modules/tablist/TablistModule.java via Git Manager GUI 2026-05-24 19:43:53 +00:00
8720ba41bb Delete src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java via Git Manager GUI 2026-05-24 19:43:52 +00:00
31510f7cd2 Delete src/main/java/net/viper/status/modules/network/MultiAccountGuard.java via Git Manager GUI 2026-05-24 19:43:52 +00:00
23080355e7 Delete src/main/java/net/viper/status/modules/forum/ForumNotification.java via Git Manager GUI 2026-05-24 19:43:51 +00:00
f47a5a0729 Delete src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java via Git Manager GUI 2026-05-24 19:43:51 +00:00
6e49d3b226 Delete src/main/java/net/viper/status/modules/economy/EconomyModule.java via Git Manager GUI 2026-05-24 19:43:51 +00:00
1b83067c3d Delete src/main/java/net/viper/status/modules/economy/EconomyDatabase.java via Git Manager GUI 2026-05-24 19:43:50 +00:00
74faabfdad Delete src/main/java/net/viper/status/modules/economy/EcoAdminCommand.java via Git Manager GUI 2026-05-24 19:43:50 +00:00
c405376a33 Delete src/main/java/net/viper/status/modules/customcommands/CustomCommandModule.java via Git Manager GUI 2026-05-24 19:43:49 +00:00
254d872139 Delete src/main/java/net/viper/status/modules/chat/bridge/TelegramBridge.java via Git Manager GUI 2026-05-24 19:43:49 +00:00
2110c339e7 Delete src/main/java/net/viper/status/modules/chat/VanishProvider.java via Git Manager GUI 2026-05-24 19:43:48 +00:00
c77411c596 Delete src/main/java/net/viper/status/modules/chat/PrivateMsgManager.java via Git Manager GUI 2026-05-24 19:43:48 +00:00
f002b2623d Delete src/main/java/net/viper/status/modules/chat/EmojiParser.java via Git Manager GUI 2026-05-24 19:43:47 +00:00
9460493b27 Delete src/main/java/net/viper/status/modules/chat/ChatLogger.java via Git Manager GUI 2026-05-24 19:43:47 +00:00
de43b18081 Delete src/main/java/net/viper/status/modules/chat/ChatConfig.java via Git Manager GUI 2026-05-24 19:43:47 +00:00
3e072d86a6 Delete src/main/java/net/viper/status/modules/chat/BlockManager.java via Git Manager GUI 2026-05-24 19:43:46 +00:00
7eb9f45de1 Delete src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java via Git Manager GUI 2026-05-24 19:43:46 +00:00
38b04ec890 Delete src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java via Git Manager GUI 2026-05-24 19:43:45 +00:00
f117a949e4 Delete src/main/java/net/viper/status/module/Module.java via Git Manager GUI 2026-05-24 19:43:45 +00:00
5be641bf78 Delete src/main/java/net/viper/status/PlayerLoginLogger.java via Git Manager GUI 2026-05-24 19:43:44 +00:00
7f58d34320 Delete src/main/resources/verify.properties via Git Manager GUI 2026-05-24 19:43:40 +00:00
f06472edfb Delete src/main/resources/plugin.yml via Git Manager GUI 2026-05-24 19:43:40 +00:00
6dc1809ae5 Delete src/main/resources/messages.txt via Git Manager GUI 2026-05-24 19:43:39 +00:00
69e896313c Delete src/main/resources/filter.yml via Git Manager GUI 2026-05-24 19:43:39 +00:00
aeed9bdb0f Delete src/main/java/net/viper/status/stats/StatsStorage.java via Git Manager GUI 2026-05-24 19:43:39 +00:00
e2bb80ccc7 Delete src/main/java/net/viper/status/stats/StatsManager.java via Git Manager GUI 2026-05-24 19:43:38 +00:00
df58149d7e Delete src/main/java/net/viper/status/ratelimit/GlobalRateLimitFramework.java via Git Manager GUI 2026-05-24 19:43:38 +00:00
e3301e70c2 Delete src/main/java/net/viper/status/modules/vanish/VanishModule.java via Git Manager GUI 2026-05-24 19:43:37 +00:00
dc30cbd8e1 Delete src/main/java/net/viper/status/modules/serverswitcher/ServerSwitcherModule.java via Git Manager GUI 2026-05-24 19:43:37 +00:00
af80800a41 Delete src/main/java/net/viper/status/modules/network/NetworkInfoModule.java via Git Manager GUI 2026-05-24 19:43:36 +00:00
1b04892356 Delete src/main/java/net/viper/status/modules/help/HelpModule.java via Git Manager GUI 2026-05-24 19:43:36 +00:00
4cc661ae0e Delete src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java via Git Manager GUI 2026-05-24 19:43:35 +00:00
2632cff7e2 Delete src/main/java/net/viper/status/modules/economy/PayCommand.java via Git Manager GUI 2026-05-24 19:43:35 +00:00
7f134c9a08 Delete src/main/java/net/viper/status/modules/economy/EconomyManager.java via Git Manager GUI 2026-05-24 19:43:34 +00:00
0049fde0fb Delete src/main/java/net/viper/status/modules/economy/EconomyListener.java via Git Manager GUI 2026-05-24 19:43:34 +00:00
0c9eadc1ce Delete src/main/java/net/viper/status/modules/customcommands/ForwardSender.java via Git Manager GUI 2026-05-24 19:43:33 +00:00
a135efa047 Delete src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java via Git Manager GUI 2026-05-24 19:43:33 +00:00
8b6d72ddab Delete src/main/java/net/viper/status/modules/chat/bridge/DiscordBridge.java via Git Manager GUI 2026-05-24 19:43:32 +00:00
9725a23f7f Delete src/main/java/net/viper/status/modules/chat/ReportManager.java via Git Manager GUI 2026-05-24 19:43:32 +00:00
a7b1e211af Delete src/main/java/net/viper/status/modules/chat/MuteManager.java via Git Manager GUI 2026-05-24 19:43:31 +00:00
51f10cb00f Delete src/main/java/net/viper/status/modules/chat/ChatModule.java via Git Manager GUI 2026-05-24 19:43:31 +00:00
c45d05fb17 Delete src/main/java/net/viper/status/modules/chat/ChatFilter.java via Git Manager GUI 2026-05-24 19:43:30 +00:00
d04fea7b5c Delete src/main/java/net/viper/status/modules/chat/ChatChannel.java via Git Manager GUI 2026-05-24 19:43:30 +00:00
cad5564773 Delete src/main/java/net/viper/status/modules/chat/AccountLinkManager.java via Git Manager GUI 2026-05-24 19:43:30 +00:00
c33eb9531a Delete src/main/java/net/viper/status/modules/antibot/AntiBotModule.java via Git Manager GUI 2026-05-24 19:43:29 +00:00
55fd069246 Delete src/main/java/net/viper/status/module/ModuleManager.java via Git Manager GUI 2026-05-24 19:43:29 +00:00
379b54fd9f Delete src/main/java/net/viper/status/UpdateChecker.java via Git Manager GUI 2026-05-24 19:43:28 +00:00
8770d2382d Delete src/main/java/net/viper/status/StatusAPI.java via Git Manager GUI 2026-05-24 19:43:28 +00:00
304b8c859f Delete pom.xml via Git Manager GUI 2026-05-24 19:43:22 +00:00
acd73cb4f9 Delete lib/BungeeCord.jar via Git Manager GUI 2026-05-24 19:43:17 +00:00
Git Manager GUI
68e81242a1 Upload folder via GUI - lib 2026-05-24 21:43:09 +02:00
Git Manager GUI
4a522b5a5a Upload folder via GUI - src 2026-05-24 21:43:03 +02:00
Git Manager GUI
5ef38326bd Upload via Git Manager GUI 2026-05-24 21:42:58 +02:00
Git Manager GUI
da8242aba4 Upload folder via GUI - src 2026-05-22 19:27:22 +02:00
Git Manager GUI
7c51370293 Upload folder via GUI - src 2026-05-22 19:26:19 +02:00
63d65313b2 Delete pom.xml via Git Manager GUI 2026-05-22 17:26:00 +00:00
51c4a1cbde Delete lib/BungeeCord.jar via Git Manager GUI 2026-05-22 17:25:49 +00:00
862d59eca8 Delete src/main/resources/welcome.yml via Git Manager GUI 2026-05-22 17:25:42 +00:00
eb91cc5657 Delete src/main/resources/scoreboard.properties via Git Manager GUI 2026-05-22 17:25:42 +00:00
9f9f2abc6d Delete src/main/resources/network-guard.properties via Git Manager GUI 2026-05-22 17:25:41 +00:00
7ba4cd9161 Delete src/main/resources/filter.yml via Git Manager GUI 2026-05-22 17:25:41 +00:00
4db99e9dea Delete src/main/java/net/viper/status/stats/StatsStorage.java via Git Manager GUI 2026-05-22 17:25:40 +00:00
fc2c4bed5f Delete src/main/java/net/viper/status/stats/StatsManager.java via Git Manager GUI 2026-05-22 17:25:40 +00:00
4e1f290652 Delete src/main/java/net/viper/status/ratelimit/GlobalRateLimitFramework.java via Git Manager GUI 2026-05-22 17:25:39 +00:00
80eac74d00 Delete src/main/java/net/viper/status/modules/vanish/VanishModule.java via Git Manager GUI 2026-05-22 17:25:39 +00:00
43b9701334 Delete src/main/java/net/viper/status/modules/serverswitcher/ServerSwitcherModule.java via Git Manager GUI 2026-05-22 17:25:39 +00:00
5fd8578f05 Delete src/main/java/net/viper/status/modules/network/NetworkInfoModule.java via Git Manager GUI 2026-05-22 17:25:38 +00:00
673ee29552 Delete src/main/java/net/viper/status/modules/help/HelpModule.java via Git Manager GUI 2026-05-22 17:25:38 +00:00
8b95dd1c04 Delete src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java via Git Manager GUI 2026-05-22 17:25:37 +00:00
ab07c8e2c3 Delete src/main/java/net/viper/status/modules/economy/PayCommand.java via Git Manager GUI 2026-05-22 17:25:37 +00:00
dc5afe9198 Delete src/main/java/net/viper/status/modules/economy/EconomyManager.java via Git Manager GUI 2026-05-22 17:25:37 +00:00
241aa93747 Delete src/main/java/net/viper/status/modules/economy/EconomyDatabase.java via Git Manager GUI 2026-05-22 17:25:36 +00:00
5105998966 Delete src/main/java/net/viper/status/modules/customcommands/CustomCommandModule.java via Git Manager GUI 2026-05-22 17:25:36 +00:00
1663901c2e Delete src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java via Git Manager GUI 2026-05-22 17:25:35 +00:00
951bacfc2a Delete src/main/java/net/viper/status/modules/chat/bridge/DiscordBridge.java via Git Manager GUI 2026-05-22 17:25:35 +00:00
2cabae6cf0 Delete src/main/java/net/viper/status/modules/chat/ReportManager.java via Git Manager GUI 2026-05-22 17:25:34 +00:00
0110e27430 Delete src/main/java/net/viper/status/modules/chat/MuteManager.java via Git Manager GUI 2026-05-22 17:25:34 +00:00
0c6ae9ba5f Delete src/main/java/net/viper/status/modules/chat/ChatModule.java via Git Manager GUI 2026-05-22 17:25:33 +00:00
ef9023adae Delete src/main/java/net/viper/status/modules/chat/ChatFilter.java via Git Manager GUI 2026-05-22 17:25:33 +00:00
e1cb881d70 Delete src/main/java/net/viper/status/modules/chat/ChatChannel.java via Git Manager GUI 2026-05-22 17:25:32 +00:00
9e223d6fab Delete src/main/java/net/viper/status/modules/chat/AccountLinkManager.java via Git Manager GUI 2026-05-22 17:25:32 +00:00
1876646f12 Delete src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java via Git Manager GUI 2026-05-22 17:25:32 +00:00
75bd8d6b7d Delete src/main/java/net/viper/status/module/ModuleManager.java via Git Manager GUI 2026-05-22 17:25:31 +00:00
6701e9511c Delete src/main/java/net/viper/status/UpdateChecker.java via Git Manager GUI 2026-05-22 17:25:31 +00:00
c99fbb1e98 Delete src/main/resources/verify.properties via Git Manager GUI 2026-05-22 17:25:27 +00:00
28488306c1 Delete src/main/resources/plugin.yml via Git Manager GUI 2026-05-22 17:25:26 +00:00
7805396501 Delete src/main/resources/messages.txt via Git Manager GUI 2026-05-22 17:25:26 +00:00
b8ce54967b Delete src/main/resources/chat.yml via Git Manager GUI 2026-05-22 17:25:25 +00:00
6fd1b7e8c4 Delete src/main/java/net/viper/status/stats/StatsModule.java via Git Manager GUI 2026-05-22 17:25:25 +00:00
0d8591213a Delete src/main/java/net/viper/status/stats/PlayerStats.java via Git Manager GUI 2026-05-22 17:25:24 +00:00
95a6abca4a Delete src/main/java/net/viper/status/modules/verify/VerifyModule.java via Git Manager GUI 2026-05-22 17:25:24 +00:00
0e450969ec Delete src/main/java/net/viper/status/modules/tablist/TablistModule.java via Git Manager GUI 2026-05-22 17:25:23 +00:00
981a6e8c18 Delete src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java via Git Manager GUI 2026-05-22 17:25:23 +00:00
6e23f6c471 Delete src/main/java/net/viper/status/modules/network/MultiAccountGuard.java via Git Manager GUI 2026-05-22 17:25:23 +00:00
6805b33c70 Delete src/main/java/net/viper/status/modules/forum/ForumNotification.java via Git Manager GUI 2026-05-22 17:25:22 +00:00
51b9561b9d Delete src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java via Git Manager GUI 2026-05-22 17:25:22 +00:00
d1833786c7 Delete src/main/java/net/viper/status/modules/economy/EconomyModule.java via Git Manager GUI 2026-05-22 17:25:21 +00:00
a40eda3d41 Delete src/main/java/net/viper/status/modules/economy/EconomyListener.java via Git Manager GUI 2026-05-22 17:25:21 +00:00
a0e1a765fd Delete src/main/java/net/viper/status/modules/economy/EcoAdminCommand.java via Git Manager GUI 2026-05-22 17:25:20 +00:00
0657a3efce Delete src/main/java/net/viper/status/modules/customcommands/ForwardSender.java via Git Manager GUI 2026-05-22 17:25:20 +00:00
8146f8ba45 Delete src/main/java/net/viper/status/modules/chat/bridge/TelegramBridge.java via Git Manager GUI 2026-05-22 17:25:20 +00:00
f74452db97 Delete src/main/java/net/viper/status/modules/chat/VanishProvider.java via Git Manager GUI 2026-05-22 17:25:19 +00:00
f6ec07e779 Delete src/main/java/net/viper/status/modules/chat/PrivateMsgManager.java via Git Manager GUI 2026-05-22 17:25:19 +00:00
f9ce4ce528 Delete src/main/java/net/viper/status/modules/chat/EmojiParser.java via Git Manager GUI 2026-05-22 17:25:18 +00:00
267b0e9ad1 Delete src/main/java/net/viper/status/modules/chat/ChatLogger.java via Git Manager GUI 2026-05-22 17:25:18 +00:00
590747a6fb Delete src/main/java/net/viper/status/modules/chat/ChatConfig.java via Git Manager GUI 2026-05-22 17:25:17 +00:00
c4a0b62c6c Delete src/main/java/net/viper/status/modules/chat/BlockManager.java via Git Manager GUI 2026-05-22 17:25:17 +00:00
40fa5c0e93 Delete src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java via Git Manager GUI 2026-05-22 17:25:16 +00:00
82185c4376 Delete src/main/java/net/viper/status/modules/antibot/AntiBotModule.java via Git Manager GUI 2026-05-22 17:25:16 +00:00
783c15b82f Delete src/main/java/net/viper/status/module/Module.java via Git Manager GUI 2026-05-22 17:25:15 +00:00
f3c103b9e3 Delete src/main/java/net/viper/status/StatusAPI.java via Git Manager GUI 2026-05-22 17:25:15 +00:00
Git Manager GUI
7876c7e68f Upload folder via GUI - src 2026-05-22 19:25:06 +02:00
Git Manager GUI
6a63ed815f Upload via Git Manager GUI 2026-05-22 19:25:00 +02:00
Git Manager GUI
3769efb283 Upload folder via GUI - lib 2026-05-22 19:24:54 +02:00
Git Manager GUI
6aa9ff2125 Upload folder via GUI - src 2026-05-22 11:15:30 +02:00
Git Manager GUI
4bf580ae2c Upload folder via GUI - src 2026-05-21 23:28:44 +02:00
Git Manager GUI
6a925246df Upload via Git Manager GUI 2026-05-21 23:28:35 +02:00
7fefde51fd README.md aktualisiert 2026-05-21 21:18:31 +00:00
9b3346c99d README.md aktualisiert 2026-05-21 16:51:10 +00:00
Git Manager GUI
5e9e05f509 Upload folder via GUI - src 2026-05-21 14:25:49 +02:00
Git Manager GUI
c052f01feb Upload via Git Manager GUI 2026-05-21 10:10:11 +02:00
Git Manager GUI
abcbd0bbae Upload folder via GUI - src 2026-05-21 10:10:02 +02:00
Git Manager GUI
627559356b Upload folder via GUI - StatusAPIBridge 2026-05-20 10:54:27 +02:00
Git Manager GUI
5012bcd95b Upload folder via GUI - StatusAPI 2026-05-20 10:54:20 +02:00
Git Manager GUI
de0255965a Upload folder via GUI - src 2026-05-14 22:09:22 +02:00
Git Manager GUI
88520bd34a Upload folder via GUI - src 2026-05-11 12:03:42 +02:00
Git Manager GUI
87274ffc9c Upload folder via GUI - src 2026-05-10 23:12:09 +02:00
Git Manager GUI
6694c40842 Upload folder via GUI - src 2026-05-10 23:10:34 +02:00
7a8261a6e5 README.md aktualisiert 2026-05-10 20:32:23 +00:00
63a18e224e README.md aktualisiert 2026-05-10 20:27:31 +00:00
Git Manager GUI
84853c7727 Upload folder via GUI - src 2026-05-10 15:51:13 +02:00
Git Manager GUI
70f98452a8 Upload folder via GUI - src 2026-05-10 15:50:38 +02:00
122e7735d5 README.md aktualisiert 2026-05-10 13:16:59 +00:00
Git Manager GUI
81786dc084 Upload folder via GUI - src 2026-05-10 15:07:07 +02:00
Git Manager GUI
a32f087353 Upload folder via GUI - src 2026-05-10 15:06:25 +02:00
Git Manager GUI
2c3050c87a Upload folder via GUI - src 2026-05-09 11:23:27 +02:00
Git Manager GUI
2c2dd9c248 Upload via Git Manager GUI 2026-05-09 11:23:21 +02:00
Git Manager GUI
23cbb557e5 Upload folder via GUI - src 2026-05-08 09:14:47 +02:00
Git Manager GUI
42e80980a3 Upload via Git Manager GUI 2026-05-08 09:14:43 +02:00
2c608469df README.md aktualisiert 2026-05-07 20:28:41 +00:00
Git Manager GUI
f29bb25435 Upload folder via GUI - BCEconomy_pl 2026-05-07 22:02:37 +02:00
Git Manager GUI
417947c396 Upload folder via GUI - StatusAPI 2026-05-07 21:56:09 +02:00
Git Manager GUI
3b2218e37e Upload folder via GUI - StatusAPIBridge 2026-05-07 21:55:53 +02:00
Git Manager GUI
39e86e4292 Upload folder via GUI - backend-join-guard 2026-05-07 21:55:33 +02:00
8ffb5ca510 Delete src/main/resources/welcome.yml via Git Manager GUI 2026-05-07 19:49:54 +00:00
a7a87fcfcd Delete src/main/resources/verify.properties via Git Manager GUI 2026-05-07 19:49:53 +00:00
f0df58fb1e Delete src/main/resources/plugin.yml via Git Manager GUI 2026-05-07 19:49:53 +00:00
216d6aa791 Delete src/main/resources/network-guard.properties via Git Manager GUI 2026-05-07 19:49:53 +00:00
b339ccc136 Delete src/main/resources/messages.txt via Git Manager GUI 2026-05-07 19:49:52 +00:00
618882032f Delete src/main/resources/filter.yml via Git Manager GUI 2026-05-07 19:49:52 +00:00
2bbec8d313 Delete src/main/resources/config.yml via Git Manager GUI 2026-05-07 19:49:52 +00:00
2593df4868 Delete src/main/resources/chat.yml via Git Manager GUI 2026-05-07 19:49:51 +00:00
9a139df523 Delete src/main/java/net/viper/statusapibridge/StatusAPIBridge.java via Git Manager GUI 2026-05-07 19:49:51 +00:00
bedbf19c6d Delete src/main/java/net/viper/status/stats/StatsStorage.java via Git Manager GUI 2026-05-07 19:49:50 +00:00
c7265b3d9e Delete src/main/java/net/viper/status/stats/StatsModule.java via Git Manager GUI 2026-05-07 19:49:50 +00:00
c707326b3d Delete src/main/java/net/viper/status/stats/StatsManager.java via Git Manager GUI 2026-05-07 19:49:50 +00:00
a6a4713dd7 Delete src/main/java/net/viper/status/stats/PlayerStats.java via Git Manager GUI 2026-05-07 19:49:49 +00:00
3f282ec83b Delete src/main/java/net/viper/status/ratelimit/GlobalRateLimitFramework.java via Git Manager GUI 2026-05-07 19:49:49 +00:00
431f017636 Delete src/main/java/net/viper/status/modules/verify/VerifyModule.java via Git Manager GUI 2026-05-07 19:49:48 +00:00
06af578f1b Delete src/main/java/net/viper/status/modules/vanish/VanishModule.java via Git Manager GUI 2026-05-07 19:49:48 +00:00
a4c5b88841 Delete src/main/java/net/viper/status/modules/tablist/TablistModule.java via Git Manager GUI 2026-05-07 19:49:48 +00:00
5492479774 Delete src/main/java/net/viper/status/modules/serverswitcher/ServerSwitcherModule.java via Git Manager GUI 2026-05-07 19:49:47 +00:00
1c3ced16b9 Delete src/main/java/net/viper/status/modules/network/NetworkInfoModule.java via Git Manager GUI 2026-05-07 19:49:47 +00:00
5ad81eae41 Delete src/main/java/net/viper/status/modules/forum/ForumNotification.java via Git Manager GUI 2026-05-07 19:49:47 +00:00
2fd226595f Delete src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java via Git Manager GUI 2026-05-07 19:49:46 +00:00
5a1ed997d7 Delete src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java via Git Manager GUI 2026-05-07 19:49:46 +00:00
5172242544 Delete src/main/java/net/viper/status/modules/economy/PayCommand.java via Git Manager GUI 2026-05-07 19:49:45 +00:00
a3b81a23c6 Delete src/main/java/net/viper/status/modules/economy/EconomyModule.java via Git Manager GUI 2026-05-07 19:49:45 +00:00
f099f5f739 Delete src/main/java/net/viper/status/modules/economy/EconomyManager.java via Git Manager GUI 2026-05-07 19:49:45 +00:00
b15cb38242 Delete src/main/java/net/viper/status/modules/economy/EconomyListener.java via Git Manager GUI 2026-05-07 19:49:44 +00:00
c4c8e5f5d7 Delete src/main/java/net/viper/status/modules/economy/EconomyDatabase.java via Git Manager GUI 2026-05-07 19:49:44 +00:00
38c1cd2f4f Delete src/main/java/net/viper/status/modules/economy/EcoAdminCommand.java via Git Manager GUI 2026-05-07 19:49:43 +00:00
7292bfbba3 Delete src/main/java/net/viper/status/modules/customcommands/ForwardSender.java via Git Manager GUI 2026-05-07 19:49:43 +00:00
e469490626 Delete src/main/java/net/viper/status/modules/customcommands/CustomCommandModule.java via Git Manager GUI 2026-05-07 19:49:43 +00:00
c8b89fc74d Delete src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java via Git Manager GUI 2026-05-07 19:49:42 +00:00
c38efbe7f9 Delete src/main/java/net/viper/status/modules/chat/bridge/TelegramBridge.java via Git Manager GUI 2026-05-07 19:49:42 +00:00
8a603cd4c8 Delete src/main/java/net/viper/status/modules/chat/bridge/DiscordBridge.java via Git Manager GUI 2026-05-07 19:49:41 +00:00
41af81fb0b Delete src/main/java/net/viper/status/modules/chat/VanishProvider.java via Git Manager GUI 2026-05-07 19:49:41 +00:00
da51744f6c Delete src/main/java/net/viper/status/modules/chat/ReportManager.java via Git Manager GUI 2026-05-07 19:49:41 +00:00
e945c7c214 Delete src/main/java/net/viper/status/modules/chat/PrivateMsgManager.java via Git Manager GUI 2026-05-07 19:49:40 +00:00
8b39bd6ed7 Delete src/main/java/net/viper/status/modules/chat/MuteManager.java via Git Manager GUI 2026-05-07 19:49:40 +00:00
120a4e138c Delete src/main/java/net/viper/status/modules/chat/EmojiParser.java via Git Manager GUI 2026-05-07 19:49:39 +00:00
a9b6a49ff6 Delete src/main/java/net/viper/status/modules/chat/ChatModule.java via Git Manager GUI 2026-05-07 19:49:39 +00:00
1195dfd94e Delete src/main/java/net/viper/status/modules/chat/ChatLogger.java via Git Manager GUI 2026-05-07 19:49:39 +00:00
82aa8ded53 Delete src/main/java/net/viper/status/modules/chat/ChatFilter.java via Git Manager GUI 2026-05-07 19:49:38 +00:00
c9940fb435 Delete src/main/java/net/viper/status/modules/chat/ChatConfig.java via Git Manager GUI 2026-05-07 19:49:38 +00:00
229e1ee1e4 Delete src/main/java/net/viper/status/modules/chat/ChatChannel.java via Git Manager GUI 2026-05-07 19:49:37 +00:00
6799e4f87d Delete src/main/java/net/viper/status/modules/chat/BlockManager.java via Git Manager GUI 2026-05-07 19:49:37 +00:00
1d12f92bfd Delete src/main/java/net/viper/status/modules/chat/AccountLinkManager.java via Git Manager GUI 2026-05-07 19:49:37 +00:00
a3d863ef82 Delete src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java via Git Manager GUI 2026-05-07 19:49:36 +00:00
ed5758f816 Delete src/main/java/net/viper/status/modules/antibot/AntiBotModule.java via Git Manager GUI 2026-05-07 19:49:36 +00:00
f6a1256ebe Delete src/main/java/net/viper/status/modules/antibot/.gitkeep via Git Manager GUI 2026-05-07 19:49:35 +00:00
8f5da4eab1 Delete src/main/java/net/viper/status/modules/abc/.gitkeep via Git Manager GUI 2026-05-07 19:49:35 +00:00
03033c2ff4 Delete src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java via Git Manager GUI 2026-05-07 19:49:35 +00:00
46c5fa9034 Delete src/main/java/net/viper/status/module/ModuleManager.java via Git Manager GUI 2026-05-07 19:49:34 +00:00
e66cb264d9 Delete src/main/java/net/viper/status/module/Module.java via Git Manager GUI 2026-05-07 19:49:34 +00:00
5c4f984215 Delete src/main/java/net/viper/status/UpdateChecker.java via Git Manager GUI 2026-05-07 19:49:33 +00:00
2f207b0a5f Delete src/main/java/net/viper/status/StatusAPI.java via Git Manager GUI 2026-05-07 19:49:33 +00:00
43e4fb440b Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ReportManager.java via Git Manager GUI 2026-05-07 19:49:24 +00:00
6f2cc360f5 Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/MuteManager.java via Git Manager GUI 2026-05-07 19:49:24 +00:00
93f437ac06 Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java via Git Manager GUI 2026-05-07 19:49:23 +00:00
244a54620d Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatFilter.java via Git Manager GUI 2026-05-07 19:49:23 +00:00
0cf19987b7 Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatChannel.java via Git Manager GUI 2026-05-07 19:49:22 +00:00
abbec8b1c6 Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java via Git Manager GUI 2026-05-07 19:49:22 +00:00
c0550435bf Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/.gitkeep via Git Manager GUI 2026-05-07 19:49:22 +00:00
ff1a8b7ad9 Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java via Git Manager GUI 2026-05-07 19:49:21 +00:00
fcb3fae8f6 Delete _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/UpdateChecker.java via Git Manager GUI 2026-05-07 19:49:21 +00:00
03ea5527b1 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/tablist/TablistModule.java via Git Manager GUI 2026-05-07 19:49:20 +00:00
0c261a2cbf Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/network/NetworkInfoModule.java via Git Manager GUI 2026-05-07 19:49:20 +00:00
6d64a2234d Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java via Git Manager GUI 2026-05-07 19:49:20 +00:00
5eccd10123 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/economy/PayCommand.java via Git Manager GUI 2026-05-07 19:49:19 +00:00
8a988a6af0 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/economy/EconomyManager.java via Git Manager GUI 2026-05-07 19:49:19 +00:00
8a61a54ed8 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/economy/EconomyDatabase.java via Git Manager GUI 2026-05-07 19:49:18 +00:00
b4ecea72eb Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/economy/EcoAdminCommand.java via Git Manager GUI 2026-05-07 19:49:18 +00:00
6058703c1a Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java via Git Manager GUI 2026-05-07 19:49:18 +00:00
bf0a723b79 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/bridge/TelegramBridge.java via Git Manager GUI 2026-05-07 19:49:17 +00:00
bc05772dca Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/VanishProvider.java via Git Manager GUI 2026-05-07 19:49:17 +00:00
1066333de6 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ReportManager.java via Git Manager GUI 2026-05-07 19:49:16 +00:00
b138ce421b Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/PrivateMsgManager.java via Git Manager GUI 2026-05-07 19:49:16 +00:00
a348c7d371 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/MuteManager.java via Git Manager GUI 2026-05-07 19:49:15 +00:00
c8d4bec89f Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/EmojiParser.java via Git Manager GUI 2026-05-07 19:49:15 +00:00
f702e3e4af Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java via Git Manager GUI 2026-05-07 19:49:15 +00:00
cf94fe4c21 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatLogger.java via Git Manager GUI 2026-05-07 19:39:51 +00:00
421c59d274 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatFilter.java via Git Manager GUI 2026-05-07 19:39:51 +00:00
ccff5f1a69 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatConfig.java via Git Manager GUI 2026-05-07 19:39:50 +00:00
440f384b98 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatChannel.java via Git Manager GUI 2026-05-07 19:39:50 +00:00
bd8fae80ff Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/BlockManager.java via Git Manager GUI 2026-05-07 19:39:49 +00:00
83f3c9a203 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java via Git Manager GUI 2026-05-07 19:39:49 +00:00
7d74bb1a4f Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java via Git Manager GUI 2026-05-07 19:39:48 +00:00
9b0904a3c8 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java via Git Manager GUI 2026-05-07 19:39:48 +00:00
e021a832fc Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/.gitkeep via Git Manager GUI 2026-05-07 19:39:47 +00:00
b8644552ed Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java via Git Manager GUI 2026-05-07 19:39:47 +00:00
fcae7fbc49 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/module/ModuleManager.java via Git Manager GUI 2026-05-07 19:39:46 +00:00
ca257a73a5 Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/module/Module.java via Git Manager GUI 2026-05-07 19:39:46 +00:00
3f1234c46e Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/UpdateChecker.java via Git Manager GUI 2026-05-07 19:39:46 +00:00
f81809eedd Delete _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/StatusAPI.java via Git Manager GUI 2026-05-07 19:39:45 +00:00
ddf2e23d4e Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ReportManager.java 2026-05-07 19:39:45 +00:00
534c6964bc Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/tablist/TablistModule.java 2026-05-07 19:39:44 +00:00
0c68486fea Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/MuteManager.java 2026-05-07 19:39:44 +00:00
c5cbede79c Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/network/NetworkInfoModule.java 2026-05-07 19:39:43 +00:00
cfadfc540e Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java 2026-05-07 19:39:42 +00:00
d4391382fe Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java 2026-05-07 19:39:42 +00:00
b6a7a94fc2 Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatFilter.java 2026-05-07 19:39:41 +00:00
d32a5ce10b Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/economy/PayCommand.java 2026-05-07 19:39:41 +00:00
9aa94b8dad Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatChannel.java 2026-05-07 19:39:40 +00:00
af07941725 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/economy/EconomyManager.java 2026-05-07 19:39:40 +00:00
b435f76ae7 Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java 2026-05-07 19:39:39 +00:00
e585f1546b Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/economy/EconomyDatabase.java 2026-05-07 19:39:39 +00:00
5e396b98a9 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/economy/EcoAdminCommand.java 2026-05-07 19:39:38 +00:00
8fe4ca5d8e Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/.gitkeep 2026-05-07 19:39:37 +00:00
386ddfdc0a Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java 2026-05-07 19:39:37 +00:00
b1643c91de Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java 2026-05-07 19:39:36 +00:00
351b14279f Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/bridge/TelegramBridge.java 2026-05-07 19:39:36 +00:00
50ea7f6e7f Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/UpdateChecker.java 2026-05-07 19:39:35 +00:00
307fceebcb Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/VanishProvider.java 2026-05-07 19:39:35 +00:00
de72836619 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ReportManager.java 2026-05-07 19:39:34 +00:00
3c5b7a8e3d Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/PrivateMsgManager.java 2026-05-07 19:39:33 +00:00
4cbb784d8c Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/MuteManager.java 2026-05-07 19:39:33 +00:00
a90466fcaf Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/EmojiParser.java 2026-05-07 19:39:32 +00:00
240566d744 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java 2026-05-07 19:39:32 +00:00
8bac2ee459 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatLogger.java 2026-05-07 19:39:31 +00:00
9757bf6475 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatFilter.java 2026-05-07 19:39:31 +00:00
4e2f27527e Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatConfig.java 2026-05-07 19:39:30 +00:00
d1d0876283 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatChannel.java 2026-05-07 19:39:30 +00:00
377a9c7d65 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/BlockManager.java 2026-05-07 19:39:29 +00:00
1b4dbc9e51 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java 2026-05-07 19:39:29 +00:00
3ddbb54442 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java 2026-05-07 19:39:28 +00:00
e32a7457eb Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java 2026-05-07 19:39:27 +00:00
e0ab059ae5 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/.gitkeep 2026-05-07 19:39:27 +00:00
a92999c82b Delete pom.xml via Git Manager GUI 2026-05-07 19:39:26 +00:00
b394136e1e Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java 2026-05-07 19:39:26 +00:00
75d112f851 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/module/ModuleManager.java 2026-05-07 19:39:25 +00:00
249d216591 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/module/Module.java 2026-05-07 19:39:25 +00:00
5f52d4fb61 Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/UpdateChecker.java 2026-05-07 19:39:24 +00:00
e47b9839fb Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/StatusAPI.java 2026-05-07 19:39:24 +00:00
b0c7d92e1e Delete _trash/2026-05-07T19-39-11-346Z/_trash/2026-05-07T19-38-24-449Z/lib/BungeeCord.jar via Git Manager GUI 2026-05-07 19:39:15 +00:00
8d802a7726 Delete _trash/2026-05-07T19-38-24-449Z/lib/BungeeCord.jar via Git Manager GUI 2026-05-07 19:39:14 +00:00
4cf95738bf Soft-delete copy _trash/2026-05-07T19-39-11-346Z/_trash/2026-05-07T19-38-24-449Z/lib/BungeeCord.jar 2026-05-07 19:39:14 +00:00
338c6320b6 Delete lib/BungeeCord.jar via Git Manager GUI 2026-05-07 19:38:36 +00:00
4b3ad926cb Soft-delete copy _trash/2026-05-07T19-38-24-449Z/lib/BungeeCord.jar 2026-05-07 19:38:27 +00:00
Git Manager GUI
66030fc835 Upload folder via GUI - src 2026-05-07 21:27:00 +02:00
Git Manager GUI
af2d3fb4e8 Upload via Git Manager GUI 2026-05-07 21:26:57 +02:00
Git Manager GUI
cc618320ab Upload folder via GUI - lib 2026-05-07 21:26:51 +02:00
Git Manager GUI
365ed04af4 Upload folder via GUI - src 2026-05-05 21:03:26 +02:00
Git Manager GUI
d686fee958 Upload via Git Manager GUI 2026-05-05 21:03:25 +02:00
Git Manager GUI
f746d46694 Upload folder via GUI - src 2026-05-02 09:37:52 +02:00
Git Manager GUI
8b43fe4264 Upload folder via GUI - src 2026-04-24 12:42:38 +02:00
Git Manager GUI
70d264f9bf Upload folder via GUI - src 2026-04-13 10:00:00 +02:00
Git Manager GUI
cfc9773ca6 Upload via Git Manager GUI 2026-04-13 09:59:58 +02:00
f67a2a687e README.md aktualisiert 2026-04-12 09:43:29 +00:00
beccbebb90 README.md aktualisiert 2026-04-12 09:42:37 +00:00
Git Manager GUI
1179398aed Upload folder via GUI - src 2026-04-07 19:55:07 +02:00
Git Manager GUI
fa07b66b76 Upload via Git Manager GUI 2026-04-07 19:55:06 +02:00
Git Manager GUI
4f9475cc09 Upload folder via GUI - src 2026-04-03 01:29:59 +02:00
Git Manager GUI
44951c4001 Upload folder via GUI - src 2026-04-02 20:56:43 +02:00
802d1dfb33 Edit src/main/resources/network-guard.properties via Git Manager GUI 2026-04-02 12:06:16 +00:00
fbd861a341 Edit src/main/resources/network-guard.properties via Git Manager GUI 2026-04-02 12:06:15 +00:00
599dd61b5b Edit src/main/resources/network-guard.properties via Git Manager GUI 2026-04-02 12:06:14 +00:00
Git Manager GUI
c1a99c1af1 Upload folder via GUI - src 2026-04-02 14:04:35 +02:00
a2d9dc59ff Upload via Git Manager GUI - pom.xml 2026-04-02 07:02:23 +00:00
Git Manager GUI
16a8090715 Upload folder via GUI - src 2026-04-02 09:02:12 +02:00
b756fe3402 Upload folder via GUI - welcome.yml 2026-04-02 06:53:42 +00:00
d783ab38da Upload folder via GUI - verify.properties 2026-04-02 06:53:42 +00:00
708aa2fbf0 Upload folder via GUI - plugin.yml 2026-04-02 06:53:41 +00:00
d327badb87 Upload folder via GUI - network-guard.properties 2026-04-02 06:53:41 +00:00
bb00590e1b Upload folder via GUI - messages.txt 2026-04-02 06:53:40 +00:00
732139ca94 Upload folder via GUI - filter.yml 2026-04-02 06:53:40 +00:00
658d0cfc4c Upload folder via GUI - UpdateChecker.java 2026-04-02 06:52:44 +00:00
a565e837de Upload folder via GUI - StatusAPI.java 2026-04-02 06:52:43 +00:00
d66ebeda2f Upload folder via GUI - StatsStorage.java 2026-04-02 06:52:43 +00:00
970bee2569 Upload folder via GUI - StatsModule.java 2026-04-02 06:52:42 +00:00
ac1eca1f7f Upload folder via GUI - StatsManager.java 2026-04-02 06:52:41 +00:00
b81d621ae3 Upload folder via GUI - PlayerStats.java 2026-04-02 06:52:41 +00:00
fed8ac4727 Upload folder via GUI - VerifyModule.java 2026-04-02 06:52:40 +00:00
edf0c4e70d Upload folder via GUI - NetworkInfoModule.java 2026-04-02 06:52:40 +00:00
749ec3dfc2 Upload folder via GUI - ForumNotifStorage.java 2026-04-02 06:52:39 +00:00
10289d1860 Upload folder via GUI - ForumNotification.java 2026-04-02 06:52:39 +00:00
0cdd6fa80f Upload folder via GUI - ForumBridgeModule.java 2026-04-02 06:52:38 +00:00
9d51485ad4 Upload folder via GUI - ForwardSender.java 2026-04-02 06:52:37 +00:00
cc11eee1cf Upload folder via GUI - CustomCommandModule.java 2026-04-02 06:52:37 +00:00
64cb5b370d Upload folder via GUI - CommandBlockerModule.java 2026-04-02 06:52:36 +00:00
bd73ba7338 Upload folder via GUI - ReportManager.java 2026-04-02 06:52:36 +00:00
c42ec83661 Upload folder via GUI - PrivateMsgManager.java 2026-04-02 06:52:35 +00:00
f7bd1c37f0 Upload folder via GUI - MuteManager.java 2026-04-02 06:52:35 +00:00
b8e4add286 Upload folder via GUI - EmojiParser.java 2026-04-02 06:52:34 +00:00
c0c6ac4259 Upload folder via GUI - ChatModule.java 2026-04-02 06:52:34 +00:00
64c46d545c Upload folder via GUI - ChatLogger.java 2026-04-02 06:52:33 +00:00
c252131a9e Upload folder via GUI - ChatFilter.java 2026-04-02 06:52:32 +00:00
036c211e8e Upload folder via GUI - ChatConfig.java 2026-04-02 06:52:32 +00:00
0563a8426a Upload folder via GUI - ChatChannel.java 2026-04-02 06:52:31 +00:00
9fb5fee007 Upload folder via GUI - TelegramBridge.java 2026-04-02 06:52:31 +00:00
5ad11178fb Upload folder via GUI - DiscordBridge.java 2026-04-02 06:52:30 +00:00
926b6d3b89 Upload folder via GUI - BlockManager.java 2026-04-02 06:52:30 +00:00
a55615e075 Upload folder via GUI - AccountLinkManager.java 2026-04-02 06:52:29 +00:00
1effb90d02 Upload folder via GUI - BroadcastModule.java 2026-04-02 06:52:29 +00:00
b6ca590121 Upload folder via GUI - AutoMessageModule.java 2026-04-02 06:52:28 +00:00
99bbc3c527 Upload folder via GUI - AntiBotModule.java 2026-04-02 06:52:28 +00:00
05e7b49416 Upload folder via GUI - ModuleManager.java 2026-04-02 06:52:27 +00:00
c7e5963e40 Upload folder via GUI - Module.java 2026-04-02 06:52:27 +00:00
bd81c7c87b Upload via Git Manager GUI - pom.xml 2026-04-02 06:52:26 +00:00
3ca3f79e19 Upload folder via GUI - welcome.yml 2026-04-02 06:50:28 +00:00
4ec4e8d0ce Upload folder via GUI - verify.properties 2026-04-02 06:50:27 +00:00
0816de11bf Upload folder via GUI - plugin.yml 2026-04-02 06:50:27 +00:00
369dc90288 Upload folder via GUI - network-guard.properties 2026-04-02 06:50:26 +00:00
4df09f3e4a Upload folder via GUI - messages.txt 2026-04-02 06:50:25 +00:00
fe52447560 Upload folder via GUI - filter.yml 2026-04-02 06:50:25 +00:00
1c3d3144d3 Upload folder via GUI - UpdateChecker.java 2026-04-02 06:50:20 +00:00
f375f61c25 Upload folder via GUI - StatusAPI.java 2026-04-02 06:50:19 +00:00
e6207e3be2 Upload folder via GUI - StatsStorage.java 2026-04-02 06:50:19 +00:00
0af4672c6b Upload folder via GUI - StatsModule.java 2026-04-02 06:50:18 +00:00
b3e435c6ae Upload folder via GUI - StatsManager.java 2026-04-02 06:50:18 +00:00
5543507321 Upload folder via GUI - PlayerStats.java 2026-04-02 06:50:17 +00:00
b2a73884b9 Upload folder via GUI - VerifyModule.java 2026-04-02 06:50:17 +00:00
725785a537 Upload folder via GUI - NetworkInfoModule.java 2026-04-02 06:50:16 +00:00
389c2f8de7 Upload folder via GUI - ForumNotifStorage.java 2026-04-02 06:50:16 +00:00
d60256e69d Upload folder via GUI - ForumNotification.java 2026-04-02 06:50:15 +00:00
45ef989641 Upload folder via GUI - ForumBridgeModule.java 2026-04-02 06:50:15 +00:00
ca18a8d22c Upload folder via GUI - ForwardSender.java 2026-04-02 06:50:14 +00:00
ceef407b17 Upload folder via GUI - CustomCommandModule.java 2026-04-02 06:50:14 +00:00
d0b99c9fef Upload folder via GUI - CommandBlockerModule.java 2026-04-02 06:50:13 +00:00
6d137928f7 Upload folder via GUI - ReportManager.java 2026-04-02 06:50:13 +00:00
e5c0b24c60 Upload folder via GUI - PrivateMsgManager.java 2026-04-02 06:50:12 +00:00
1f8ecfe7c2 Upload folder via GUI - MuteManager.java 2026-04-02 06:50:12 +00:00
5ed06071c0 Upload folder via GUI - EmojiParser.java 2026-04-02 06:50:11 +00:00
a6a8ee0daa Upload folder via GUI - ChatModule.java 2026-04-02 06:50:11 +00:00
953b46a967 Upload folder via GUI - ChatLogger.java 2026-04-02 06:50:10 +00:00
60b31bcfee Upload folder via GUI - ChatFilter.java 2026-04-02 06:50:10 +00:00
5286cc507e Upload folder via GUI - ChatConfig.java 2026-04-02 06:50:09 +00:00
a0377c98d1 Upload folder via GUI - ChatChannel.java 2026-04-02 06:50:09 +00:00
cb025e3101 Upload folder via GUI - TelegramBridge.java 2026-04-02 06:50:08 +00:00
1abfb9faa9 Upload folder via GUI - DiscordBridge.java 2026-04-02 06:50:07 +00:00
4d999dc8d8 Upload folder via GUI - BlockManager.java 2026-04-02 06:50:07 +00:00
5d2e63d5b9 Upload folder via GUI - AccountLinkManager.java 2026-04-02 06:50:06 +00:00
9e297befca Upload folder via GUI - BroadcastModule.java 2026-04-02 06:50:06 +00:00
bbaa259028 Upload folder via GUI - AutoMessageModule.java 2026-04-02 06:50:05 +00:00
19df402e4c Upload folder via GUI - AntiBotModule.java 2026-04-02 06:50:05 +00:00
dc3bc57d4a Upload folder via GUI - ModuleManager.java 2026-04-02 06:50:04 +00:00
14ca1905fa Upload folder via GUI - Module.java 2026-04-02 06:50:03 +00:00
ed597af090 Upload via Git Manager GUI - pom.xml 2026-04-02 06:50:03 +00:00
3c5007ef1a src/main/resources/verify.template.properties gelöscht 2026-04-02 06:48:33 +00:00
9de509438d src/main/resources/chat.yml gelöscht 2026-04-02 06:48:19 +00:00
a8845e295a Upload folder via GUI - welcome.yml 2026-04-02 06:46:05 +00:00
99156ef3b8 Upload folder via GUI - verify.properties 2026-04-02 06:46:05 +00:00
e1f95dcf32 Upload folder via GUI - plugin.yml 2026-04-02 06:46:04 +00:00
d9d082d2a4 Upload folder via GUI - network-guard.properties 2026-04-02 06:46:04 +00:00
5575fee327 Upload folder via GUI - messages.txt 2026-04-02 06:46:03 +00:00
703c6fce7b Upload folder via GUI - filter.yml 2026-04-02 06:46:03 +00:00
4c78ba60b0 Upload folder via GUI - chat.yml 2026-04-02 06:46:02 +00:00
f976c05b39 Upload folder via GUI - UpdateChecker.java 2026-04-02 06:46:02 +00:00
4b302aa41d Upload folder via GUI - StatusAPI.java 2026-04-02 06:46:01 +00:00
208ba1af8b Upload folder via GUI - StatsStorage.java 2026-04-02 06:46:01 +00:00
9e38e31277 Upload folder via GUI - StatsModule.java 2026-04-02 06:46:00 +00:00
61147622e7 Upload folder via GUI - StatsManager.java 2026-04-02 06:46:00 +00:00
0cd2e91a3a Upload folder via GUI - PlayerStats.java 2026-04-02 06:45:59 +00:00
e1ba6dd2ae Upload folder via GUI - VerifyModule.java 2026-04-02 06:45:59 +00:00
a47462e20f Upload folder via GUI - NetworkInfoModule.java 2026-04-02 06:45:58 +00:00
212e9e1fa6 Upload folder via GUI - ForumNotifStorage.java 2026-04-02 06:45:58 +00:00
dada2881e3 Upload folder via GUI - ForumNotification.java 2026-04-02 06:45:57 +00:00
fc2aa19ef9 Upload folder via GUI - ForumBridgeModule.java 2026-04-02 06:45:56 +00:00
1aa263e57b Upload folder via GUI - ForwardSender.java 2026-04-02 06:45:56 +00:00
5264d39885 Upload folder via GUI - CustomCommandModule.java 2026-04-02 06:45:55 +00:00
5b3345c0ee Upload folder via GUI - CommandBlockerModule.java 2026-04-02 06:45:55 +00:00
c8d617670d Upload folder via GUI - ReportManager.java 2026-04-02 06:45:54 +00:00
9e19cf8f99 Upload folder via GUI - PrivateMsgManager.java 2026-04-02 06:45:54 +00:00
4c32fb65c6 Upload folder via GUI - MuteManager.java 2026-04-02 06:45:53 +00:00
f9927a6230 Upload folder via GUI - EmojiParser.java 2026-04-02 06:45:53 +00:00
9cbd33debd Upload folder via GUI - ChatModule.java 2026-04-02 06:45:52 +00:00
210d5049d9 Upload folder via GUI - ChatLogger.java 2026-04-02 06:45:52 +00:00
ec831a2847 Upload folder via GUI - ChatFilter.java 2026-04-02 06:45:51 +00:00
ce976084d2 Upload folder via GUI - ChatConfig.java 2026-04-02 06:45:51 +00:00
293ba36730 Upload folder via GUI - ChatChannel.java 2026-04-02 06:45:50 +00:00
0a98003e8b Upload folder via GUI - TelegramBridge.java 2026-04-02 06:45:50 +00:00
d72c94723f Upload folder via GUI - DiscordBridge.java 2026-04-02 06:45:49 +00:00
c2b8f32d55 Upload folder via GUI - BlockManager.java 2026-04-02 06:45:49 +00:00
556e688bf6 Upload folder via GUI - AccountLinkManager.java 2026-04-02 06:45:48 +00:00
1e43bd6fe0 Upload folder via GUI - BroadcastModule.java 2026-04-02 06:45:48 +00:00
e315369908 Upload folder via GUI - AutoMessageModule.java 2026-04-02 06:45:47 +00:00
577d50748d Upload folder via GUI - AntiBotModule.java 2026-04-02 06:45:46 +00:00
0463cf3480 Upload folder via GUI - ModuleManager.java 2026-04-02 06:45:46 +00:00
ea34c3802d Upload folder via GUI - Module.java 2026-04-02 06:45:45 +00:00
e5baf0fde4 Upload folder via GUI - welcome.yml 2026-04-02 06:42:45 +00:00
3453c0a161 Upload folder via GUI - verify.properties 2026-04-02 06:42:44 +00:00
8c54fb2322 Upload folder via GUI - plugin.yml 2026-04-02 06:42:43 +00:00
001f46bd36 Upload folder via GUI - network-guard.properties 2026-04-02 06:42:43 +00:00
901a4ed5a8 Upload folder via GUI - messages.txt 2026-04-02 06:42:42 +00:00
5177025e22 Upload folder via GUI - filter.yml 2026-04-02 06:42:42 +00:00
eda4bcff2b Upload folder via GUI - StatsStorage.java 2026-04-02 06:42:41 +00:00
5619b602c2 Upload folder via GUI - StatusAPI.java 2026-04-02 06:42:41 +00:00
361d9724fd Upload folder via GUI - UpdateChecker.java 2026-04-02 06:42:40 +00:00
a3c82a91ff Upload folder via GUI - VerifyModule.java 2026-04-02 06:42:39 +00:00
de75c65a9e Upload folder via GUI - StatsManager.java 2026-04-02 06:42:39 +00:00
71b2cf2e0b Upload folder via GUI - StatsModule.java 2026-04-02 06:42:38 +00:00
90f85ee8ed Upload folder via GUI - NetworkInfoModule.java 2026-04-02 06:42:38 +00:00
6ef483a9ea Upload folder via GUI - ForumNotifStorage.java 2026-04-02 06:42:37 +00:00
bb3a3fd17f Upload folder via GUI - ForumNotification.java 2026-04-02 06:42:37 +00:00
52bbc2f2cd Upload folder via GUI - ForumBridgeModule.java 2026-04-02 06:42:36 +00:00
5750f324e2 Upload folder via GUI - CustomCommandModule.java 2026-04-02 06:42:36 +00:00
ee6833d126 Upload folder via GUI - CommandBlockerModule.java 2026-04-02 06:42:35 +00:00
9dc7b0ffe8 Upload folder via GUI - ReportManager.java 2026-04-02 06:42:35 +00:00
5c0c071212 Upload folder via GUI - PrivateMsgManager.java 2026-04-02 06:42:34 +00:00
8104487786 Upload folder via GUI - MuteManager.java 2026-04-02 06:42:33 +00:00
b695a9252d Upload folder via GUI - ChatFilter.java 2026-04-02 06:42:33 +00:00
d49f1706f9 Upload folder via GUI - ChatLogger.java 2026-04-02 06:42:32 +00:00
faafe5cb27 Upload folder via GUI - ChatModule.java 2026-04-02 06:42:32 +00:00
a662837b53 Upload folder via GUI - ChatConfig.java 2026-04-02 06:42:31 +00:00
1c9e7dbe1b Upload folder via GUI - ChatChannel.java 2026-04-02 06:42:30 +00:00
7545019ad6 Upload folder via GUI - AccountLinkManager.java 2026-04-02 06:42:30 +00:00
3c5e85ad99 Upload folder via GUI - TelegramBridge.java 2026-04-02 06:42:29 +00:00
389c6fab91 Upload folder via GUI - AntiBotModule.java 2026-04-02 06:42:29 +00:00
6bca3a0f04 Upload folder via GUI - BlockManager.java 2026-04-02 06:42:28 +00:00
74beb45654 Upload folder via GUI - Module.java 2026-04-02 06:42:28 +00:00
8b92f2404a Upload folder via GUI - BroadcastModule.java 2026-04-02 06:42:27 +00:00
31106f69e8 Upload folder via GUI - AutoMessageModule.java 2026-04-02 06:42:26 +00:00
2603332be5 Upload folder via GUI - verify.properties 2026-04-02 06:41:16 +00:00
c8f2747e3a Upload folder via GUI - welcome.yml 2026-04-02 06:41:15 +00:00
7a35491a6e Upload folder via GUI - network-guard.properties 2026-04-02 06:41:15 +00:00
c91243ce89 Upload folder via GUI - messages.txt 2026-04-02 06:41:14 +00:00
b1cc4121b6 Upload folder via GUI - filter.yml 2026-04-02 06:41:14 +00:00
9dfa461059 Upload folder via GUI - plugin.yml 2026-04-02 06:41:13 +00:00
238d4bf93a Upload folder via GUI - UpdateChecker.java 2026-04-02 06:41:13 +00:00
dcd4a3b158 Upload folder via GUI - chat.yml 2026-04-02 06:41:12 +00:00
96b986a2b5 Upload folder via GUI - StatsStorage.java 2026-04-02 06:41:12 +00:00
99350025f3 Upload folder via GUI - StatusAPI.java 2026-04-02 06:41:11 +00:00
7d4a4a5ac5 Upload folder via GUI - PlayerStats.java 2026-04-02 06:41:11 +00:00
950650644c Upload folder via GUI - VerifyModule.java 2026-04-02 06:41:10 +00:00
ed550a8a35 Upload folder via GUI - StatsModule.java 2026-04-02 06:41:10 +00:00
2084aea009 Upload folder via GUI - StatsManager.java 2026-04-02 06:41:09 +00:00
7183464de0 Upload folder via GUI - NetworkInfoModule.java 2026-04-02 06:41:08 +00:00
36b228123c Upload folder via GUI - ForumBridgeModule.java 2026-04-02 06:41:08 +00:00
7165a2e784 Upload folder via GUI - ForumNotification.java 2026-04-02 06:41:07 +00:00
0c374de76c Upload folder via GUI - CustomCommandModule.java 2026-04-02 06:41:07 +00:00
d40409cc0f Upload folder via GUI - ForwardSender.java 2026-04-02 06:41:06 +00:00
eaba1d437e Upload folder via GUI - CommandBlockerModule.java 2026-04-02 06:41:06 +00:00
405b4abaea Upload folder via GUI - ReportManager.java 2026-04-02 06:41:05 +00:00
7d95ba4e10 Upload folder via GUI - MuteManager.java 2026-04-02 06:41:05 +00:00
bd3b95f675 Upload folder via GUI - PrivateMsgManager.java 2026-04-02 06:41:04 +00:00
3a5de1d453 Upload folder via GUI - EmojiParser.java 2026-04-02 06:41:03 +00:00
71ccaf8922 Upload folder via GUI - ChatConfig.java 2026-04-02 06:41:03 +00:00
88c903e367 Upload folder via GUI - ChatLogger.java 2026-04-02 06:41:03 +00:00
b61677bbad Upload folder via GUI - ChatChannel.java 2026-04-02 06:41:02 +00:00
08e822ecd4 Upload folder via GUI - AntiBotModule.java 2026-04-02 06:41:02 +00:00
5ba6b18719 Upload folder via GUI - BlockManager.java 2026-04-02 06:41:01 +00:00
9705114192 Upload folder via GUI - ModuleManager.java 2026-04-02 06:41:00 +00:00
8338445c01 Upload folder via GUI - BroadcastModule.java 2026-04-02 06:41:00 +00:00
4904c6107a Upload folder via GUI - AutoMessageModule.java 2026-04-02 06:40:59 +00:00
6450325b45 Upload via Git Manager GUI - pom.xml 2026-04-02 06:36:39 +00:00
34f7dc38a2 Create src/main/java/net/viper/status/modules/abc 2026-04-02 06:28:16 +00:00
8e1aa67bbe Dateien nach "src/main/java/net/viper/status/modules/network" hochladen 2026-04-02 06:27:33 +00:00
4ada8fcde6 Dateien nach "src/main/java/net/viper/status/modules/antibot" hochladen 2026-04-02 06:26:59 +00:00
4a7a4c69cd Create src/main/java/net/viper/status/modules/antibot 2026-04-02 06:26:08 +00:00
6b0a239ba9 Upload via Git Manager GUI - PlayerStats.java 2026-04-02 06:25:50 +00:00
f1b35b198d Upload via Git Manager GUI - StatsStorage.java 2026-04-02 06:25:49 +00:00
3335dba9f2 Upload via Git Manager GUI - StatsModule.java 2026-04-02 06:25:49 +00:00
9513de9ae4 Upload via Git Manager GUI - StatsManager.java 2026-04-02 06:25:48 +00:00
898adcdc0f Upload via Git Manager GUI - ModuleManager.java 2026-04-02 06:25:36 +00:00
9dde84ed46 Upload via Git Manager GUI - Module.java 2026-04-02 06:25:36 +00:00
24de330d1c Upload via Git Manager GUI - UpdateChecker.java 2026-04-02 06:25:29 +00:00
9409302954 Upload via Git Manager GUI - StatusAPI.java 2026-04-02 06:25:28 +00:00
e89c75df5d Upload via Git Manager GUI - chat.yml 2026-04-02 06:25:12 +00:00
1865096c12 Upload via Git Manager GUI - welcome.yml 2026-04-02 06:25:11 +00:00
35deab81ae Upload via Git Manager GUI - verify.properties 2026-04-02 06:25:11 +00:00
04c46e1692 Upload via Git Manager GUI - plugin.yml 2026-04-02 06:25:10 +00:00
e62601fa65 Upload file network-guard.properties via GUI 2026-04-02 08:25:08 +02:00
53e967fc6b Upload via Git Manager GUI - messages.txt 2026-04-02 06:24:59 +00:00
31f971c602 Upload via Git Manager GUI - filter.yml 2026-04-02 06:24:59 +00:00
04046b93c4 Upload via Git Manager GUI - pom.xml 2026-04-02 06:24:20 +00:00
4330cedd83 README.md aktualisiert 2026-04-02 06:11:50 +00:00
7adfc8b5ab Dateien nach "src/main/java/net/viper/status/modules/chat/bridge" hochladen 2026-04-01 10:17:08 +00:00
96e5bfb3de Dateien nach "src/main/java/net/viper/status/modules/chat" hochladen 2026-04-01 10:16:48 +00:00
cc1cbfa13a Upload via Git Manager GUI - VerifyModule.java 2026-04-01 10:15:45 +00:00
d190c5a882 Upload via Git Manager GUI - ForumNotifStorage.java 2026-04-01 10:15:37 +00:00
9499d5bd86 Upload via Git Manager GUI - ForumNotification.java 2026-04-01 10:15:36 +00:00
815178c00a Upload via Git Manager GUI - ForumBridgeModule.java 2026-04-01 10:15:35 +00:00
c2f0e2d84d Upload via Git Manager GUI - CustomCommandModule.java 2026-04-01 10:15:26 +00:00
dbeae4ca40 Upload via Git Manager GUI - ForwardSender.java 2026-04-01 10:15:26 +00:00
6eca460fc9 Upload via Git Manager GUI - CommandBlockerModule.java 2026-04-01 10:15:15 +00:00
f053a8f96d Upload via Git Manager GUI - BroadcastModule.java 2026-04-01 10:15:05 +00:00
6b127573bd Upload via Git Manager GUI - AutoMessageModule.java 2026-04-01 10:14:56 +00:00
ee8b845c03 Upload via Git Manager GUI - ModuleManager.java 2026-04-01 10:14:42 +00:00
80362ef8e4 Upload via Git Manager GUI - Module.java 2026-04-01 10:14:42 +00:00
873480557f Upload via Git Manager GUI - StatsStorage.java 2026-04-01 10:14:30 +00:00
92d6b22924 Upload via Git Manager GUI - StatsModule.java 2026-04-01 10:14:30 +00:00
7e6a9f15ce Upload via Git Manager GUI - StatsManager.java 2026-04-01 10:14:29 +00:00
d41260f255 Upload via Git Manager GUI - PlayerStats.java 2026-04-01 10:14:29 +00:00
c278bb50d8 Upload via Git Manager GUI - UpdateChecker.java 2026-04-01 10:14:21 +00:00
e1d86cb8fb Upload via Git Manager GUI - StatusAPI.java 2026-04-01 10:14:21 +00:00
ebbe096127 Upload file chat.yml via GUI 2026-04-01 12:14:03 +02:00
90b6fb4f5e Upload via Git Manager GUI - welcome.yml 2026-04-01 10:13:55 +00:00
ed5b7bd392 Upload via Git Manager GUI - verify.properties 2026-04-01 10:13:55 +00:00
f141fa838c Upload via Git Manager GUI - plugin.yml 2026-04-01 10:13:54 +00:00
844c9e9fcc Upload via Git Manager GUI - messages.txt 2026-04-01 10:13:54 +00:00
e0ae27fd6c Upload via Git Manager GUI - filter.yml 2026-04-01 10:13:53 +00:00
e57168e2e8 Upload via Git Manager GUI - pom.xml 2026-04-01 10:12:04 +00:00
75ca0b53b3 Upload via Git Manager GUI - pom.xml 2026-04-01 10:11:36 +00:00
e0e0ad229d Upload via Git Manager GUI - pom.xml 2026-04-01 10:09:14 +00:00
a2948d8f96 Upload via Git Manager GUI - pom.xml 2026-04-01 10:08:30 +00:00
5bcd8cf4a1 README.md aktualisiert 2026-04-01 10:05:46 +00:00
1f7c7e0571 Upload via Git Manager GUI - pom.xml 2026-03-30 18:45:30 +00:00
39bbe8d4ad Update from Git Manager GUI 2026-03-30 20:45:29 +02:00
8c6b982c0e Delete src/main/resources/welcome.yml via Git Manager GUI 2026-03-30 18:45:12 +00:00
ed46839e66 Delete src/main/resources/verify.template.properties via Git Manager GUI 2026-03-30 18:45:12 +00:00
024832eea9 Delete src/main/resources/verify.properties via Git Manager GUI 2026-03-30 18:45:12 +00:00
5847b0b481 Delete src/main/resources/plugin.yml via Git Manager GUI 2026-03-30 18:45:11 +00:00
97342577ee Delete src/main/resources/messages.txt via Git Manager GUI 2026-03-30 18:45:11 +00:00
ecf7cf904c Delete src/main/resources/filter.yml via Git Manager GUI 2026-03-30 18:45:10 +00:00
95c924d3cf Delete src/main/resources/customcommands.yml via Git Manager GUI 2026-03-30 18:45:10 +00:00
ee521fe887 Delete src/main/java/net/viper/status/stats/StatsStorage.java via Git Manager GUI 2026-03-30 18:45:09 +00:00
9dce553f2a Delete src/main/java/net/viper/status/stats/StatsModule.java via Git Manager GUI 2026-03-30 18:45:09 +00:00
22379f7715 Delete src/main/java/net/viper/status/stats/StatsManager.java via Git Manager GUI 2026-03-30 18:45:09 +00:00
06c1b42bea Delete src/main/java/net/viper/status/stats/PlayerStats.java via Git Manager GUI 2026-03-30 18:45:08 +00:00
d33631e981 Delete src/main/java/net/viper/status/modules/verify/VerifyModule.java via Git Manager GUI 2026-03-30 18:45:08 +00:00
b9dbb4ed78 Delete src/main/java/net/viper/status/modules/navigation/NavigationModule.java via Git Manager GUI 2026-03-30 18:45:07 +00:00
bceffea826 Delete src/main/java/net/viper/status/modules/globalchat/GlobalChatModule.java via Git Manager GUI 2026-03-30 18:45:07 +00:00
2aa7cf340b Delete src/main/java/net/viper/status/modules/customcommands/ForwardSender.java via Git Manager GUI 2026-03-30 18:45:06 +00:00
22b39e66d1 Delete src/main/java/net/viper/status/modules/customcommands/CustomCommandModule.java via Git Manager GUI 2026-03-30 18:45:06 +00:00
396dbacfad Delete src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java via Git Manager GUI 2026-03-30 18:45:05 +00:00
85ea96a24c Delete src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java via Git Manager GUI 2026-03-30 18:45:05 +00:00
d47a79c120 Delete src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java via Git Manager GUI 2026-03-30 18:45:05 +00:00
1e986f99a2 Delete src/main/java/net/viper/status/module/ModuleManager.java via Git Manager GUI 2026-03-30 18:45:04 +00:00
e76fca05ff Delete src/main/java/net/viper/status/module/Module.java via Git Manager GUI 2026-03-30 18:45:04 +00:00
4d2cbc8500 Delete src/main/java/net/viper/status/UpdateChecker.java via Git Manager GUI 2026-03-30 18:45:03 +00:00
4c2f6a214c Delete src/main/java/net/viper/status/StatusAPI.java via Git Manager GUI 2026-03-30 18:45:03 +00:00
f9abb2f32a LICENSE gelöscht 2026-03-01 11:25:47 +00:00
202ae6ad34 LICENSE hinzugefügt 2026-01-22 18:46:35 +00:00
bf2ee6460c Upload pom.xml via GUI 2026-01-20 23:32:55 +00:00
5303cbcb95 Update from Git Manager GUI 2026-01-21 00:32:53 +01:00
bce695829e README.md aktualisiert 2026-01-18 16:08:28 +00:00
d3796a3ed9 README.md aktualisiert 2026-01-18 16:08:10 +00:00
eb77726f83 src/main/resources/plugin.yml aktualisiert 2026-01-18 15:47:46 +00:00
0c3f91fa16 pom.xml aktualisiert 2026-01-18 15:47:29 +00:00
8277758e90 src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-18 15:46:34 +00:00
36363f09c2 Dateien nach "src/main/java/net/viper/status/modules/commandblocker" hochladen 2026-01-18 15:46:12 +00:00
14b5a08fe2 Dateien nach "src/main/resources" hochladen 2026-01-18 15:45:22 +00:00
c549df684c src/main/resources/verify.properties aktualisiert 2026-01-18 15:44:57 +00:00
422cb9c352 Dateien nach "src/main/java/net/viper/status/modules/broadcast" hochladen 2026-01-17 18:27:34 +00:00
e60bff9c6d src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-17 18:26:24 +00:00
59944ece4e src/main/resources/verify.properties aktualisiert 2026-01-17 18:25:25 +00:00
b29f2e2db3 src/main/resources/plugin.yml aktualisiert 2026-01-17 18:24:39 +00:00
f37cff83fc pom.xml aktualisiert 2026-01-17 18:24:19 +00:00
2384178a3a src/main/resources/plugin.yml aktualisiert 2026-01-17 10:17:28 +00:00
5558b237bb pom.xml aktualisiert 2026-01-17 10:17:14 +00:00
e13342a767 src/main/java/net/viper/status/modules/globalchat/GlobalChatModule.java aktualisiert 2026-01-17 09:58:53 +00:00
2dc4f8dd38 README.md aktualisiert 2026-01-16 21:31:06 +00:00
503474a991 src/main/resources/plugin.yml aktualisiert 2026-01-16 21:18:30 +00:00
e3a0ea6682 pom.xml aktualisiert 2026-01-16 21:18:14 +00:00
b8e8f75f90 src/main/java/net/viper/status/modules/globalchat/GlobalChatModule.java aktualisiert 2026-01-16 21:17:59 +00:00
12fc2b82c4 src/main/resources/verify.properties aktualisiert 2026-01-12 16:23:52 +00:00
5706d3a446 src/main/java/net/viper/status/modules/globalchat/GlobalChatModule.java aktualisiert 2026-01-12 16:23:11 +00:00
aa5b1a2572 src/main/java/net/viper/status/modules/globalchat/GlobalChatModule.java aktualisiert 2026-01-12 12:26:23 +00:00
27ac00308a src/main/resources/verify.properties aktualisiert 2026-01-12 12:25:52 +00:00
c0ea8c0cd8 src/main/resources/plugin.yml aktualisiert 2026-01-12 12:25:37 +00:00
a5d54b8141 pom.xml aktualisiert 2026-01-12 12:25:23 +00:00
52ae8c8c5a README.md aktualisiert 2026-01-12 10:21:13 +00:00
fba27d6c6f README.md aktualisiert 2026-01-12 10:20:55 +00:00
85 changed files with 20919 additions and 2170 deletions

92
BCEconomy_pl/pom.xml Normal file
View 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>

View 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;
}
}

View File

@@ -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; }
}

View File

@@ -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.");
}
}

View 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

View 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

1493
README.md

File diff suppressed because it is too large Load Diff

Binary file not shown.

107
StatusAPI/pom.xml Normal file
View 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.4</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>

View File

@@ -0,0 +1,72 @@
package net.viper.status;
import net.md_5.bungee.api.connection.ProxiedPlayer;
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 java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
/**
* PlayerLoginLogger schreibt bei jedem Join UUID, Name und IP
* in die Datei plugins/StatusAPI/player-logins.log
*/
public class PlayerLoginLogger implements Listener {
private static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
private final Plugin plugin;
private final File logFile;
private final ZoneId zoneId;
public PlayerLoginLogger(Plugin plugin, ZoneId zoneId) {
this.plugin = plugin;
this.zoneId = zoneId;
this.logFile = new File(plugin.getDataFolder(), "player-logins.log");
// Sicherstellen, dass das Plugin-Verzeichnis existiert
if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdirs();
}
}
@EventHandler
public void onPostLogin(PostLoginEvent event) {
ProxiedPlayer player = event.getPlayer();
String uuid = player.getUniqueId().toString();
String name = player.getName();
String ip = "unknown";
try {
InetSocketAddress addr = (InetSocketAddress) player.getSocketAddress();
if (addr != null && addr.getAddress() != null) {
ip = addr.getAddress().getHostAddress();
}
} catch (Exception e) {
plugin.getLogger().warning("[PlayerLoginLogger] Konnte IP nicht lesen: " + e.getMessage());
}
String timestamp = ZonedDateTime.now(zoneId).format(FMT);
String line = String.format("[%s] UUID=%s | Name=%-16s | IP=%s",
timestamp, uuid, name, ip);
plugin.getLogger().info("[LoginLog] " + line);
// In Datei schreiben (append)
try (PrintWriter pw = new PrintWriter(new FileWriter(logFile, true))) {
pw.println(line);
} catch (IOException e) {
plugin.getLogger().warning("[PlayerLoginLogger] Fehler beim Schreiben: " + e.getMessage());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,144 +1,142 @@
package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UpdateChecker {
private final Plugin plugin;
private final String currentVersion;
private final int intervalHours;
// 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 volatile String latestVersion = "";
private volatile String latestUrl = "";
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 TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
this.plugin = plugin;
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
this.intervalHours = Math.max(1, intervalHours);
}
public void checkNow() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code != 200) {
plugin.getLogger().warning("Gitea/Forgejo API nicht erreichbar (HTTP " + code + ")");
return;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) sb.append(line).append("\n");
}
String body = sb.toString();
// 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
String foundVersion = null;
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
if (tagM.find()) {
foundVersion = tagM.group(1).trim();
}
if (foundVersion == null) {
plugin.getLogger().warning("Keine Version (Tag) im Release gefunden.");
return;
}
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
foundVersion = foundVersion.substring(1);
}
String foundUrl = null;
// 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
Matcher nameMatcher = ASSET_NAME_PATTERN.matcher(body);
Matcher downloadMatcher = DOWNLOAD_PATTERN.matcher(body);
java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> urls = new java.util.ArrayList<>();
while (nameMatcher.find()) {
names.add(nameMatcher.group(1));
}
while (downloadMatcher.find()) {
urls.add(downloadMatcher.group(1));
}
int pairs = Math.min(names.size(), urls.size());
for (int i = 0; i < pairs; i++) {
String name = names.get(i).trim();
String url = urls.get(i);
if ("StatusAPI.jar".equalsIgnoreCase(name)) {
foundUrl = url;
break; // Erste (also neueste) passende JAR nehmen
}
}
if (foundUrl == null) {
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
return;
}
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
latestVersion = foundVersion;
latestUrl = foundUrl;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
}
}
public String getLatestVersion() {
return latestVersion != null ? latestVersion : "";
}
public String getLatestUrl() {
return latestUrl != null ? latestUrl : "";
}
public boolean isUpdateAvailable(String currentVer) {
String lv = getLatestVersion();
if (lv.isEmpty()) return false;
return compareVersions(lv, currentVer) > 0;
}
private int compareVersions(String a, String b) {
try {
String[] aa = a.split("\\.");
String[] bb = b.split("\\.");
int len = Math.max(aa.length, bb.length);
for (int i = 0; i < len; i++) {
int ai = i < aa.length ? Integer.parseInt(aa[i].replaceAll("\\D", "")) : 0;
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 a.compareTo(b);
}
}
package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UpdateChecker {
private final Plugin plugin;
private final String currentVersion;
private final int intervalHours;
// Neue Domain und korrekter API-Pfad f\u00fcr Releases
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/StatusAPI/releases";
private volatile String latestVersion = "";
private volatile String latestUrl = "";
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 TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
this.plugin = plugin;
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
this.intervalHours = Math.max(1, intervalHours);
}
public void checkNow() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code != 200) {
plugin.getLogger().warning("Gitea/Forgejo API nicht erreichbar (HTTP " + code + ")");
return;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) sb.append(line).append("\n");
}
String body = sb.toString();
// Neu: Da die API ein JSON-Array von Releases zur\u00fcckgibt, nehmen wir das erste (neueste) Release
// Wir suchen den ersten Block mit tag_name
String foundVersion = null;
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
if (tagM.find()) {
foundVersion = tagM.group(1).trim();
}
if (foundVersion == null) {
plugin.getLogger().warning("Keine Version (Tag) im Release gefunden.");
return;
}
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
foundVersion = foundVersion.substring(1);
}
String foundUrl = null;
// 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
Matcher nameMatcher = ASSET_NAME_PATTERN.matcher(body);
Matcher downloadMatcher = DOWNLOAD_PATTERN.matcher(body);
java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> urls = new java.util.ArrayList<>();
while (nameMatcher.find()) {
names.add(nameMatcher.group(1));
}
while (downloadMatcher.find()) {
urls.add(downloadMatcher.group(1));
}
int pairs = Math.min(names.size(), urls.size());
for (int i = 0; i < pairs; i++) {
String name = names.get(i).trim();
String url = urls.get(i);
if ("StatusAPI.jar".equalsIgnoreCase(name)) {
foundUrl = url;
break; // Erste (also neueste) passende JAR nehmen
}
}
if (foundUrl == null) {
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
return;
}
latestVersion = foundVersion;
latestUrl = foundUrl;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
}
}
public String getLatestVersion() {
return latestVersion != null ? latestVersion : "";
}
public String getLatestUrl() {
return latestUrl != null ? latestUrl : "";
}
public boolean isUpdateAvailable(String currentVer) {
String lv = getLatestVersion();
if (lv.isEmpty()) return false;
return compareVersions(lv, currentVer) > 0;
}
private int compareVersions(String a, String b) {
try {
String[] aa = a.split("\\.");
String[] bb = b.split("\\.");
int len = Math.max(aa.length, bb.length);
for (int i = 0; i < len; i++) {
int ai = i < aa.length ? Integer.parseInt(aa[i].replaceAll("\\D", "")) : 0;
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 a.compareTo(b);
}
}
}

View File

@@ -1,24 +1,24 @@
package net.viper.status.module;
import net.md_5.bungee.api.plugin.Plugin;
/**
* Interface für alle zukünftigen Erweiterungen.
*/
public interface Module {
/**
* Wird aufgerufen, wenn die API startet.
*/
void onEnable(Plugin plugin);
/**
* Wird aufgerufen, wenn die API stoppt.
*/
void onDisable(Plugin plugin);
/**
* Eindeutiger Name des Moduls (z.B. "StatsModule").
*/
String getName();
package net.viper.status.module;
import net.md_5.bungee.api.plugin.Plugin;
/**
* Interface f\u00fcr alle zuk\u00fcnftigen Erweiterungen.
*/
public interface Module {
/**
* Wird aufgerufen, wenn die API startet.
*/
void onEnable(Plugin plugin);
/**
* Wird aufgerufen, wenn die API stoppt.
*/
void onDisable(Plugin plugin);
/**
* Eindeutiger Name des Moduls (z.B. "StatsModule").
*/
String getName();
}

View File

@@ -1,60 +1,67 @@
package net.viper.status.module;
import net.md_5.bungee.api.plugin.Plugin;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Verwaltet alle geladenen Module.
*/
public class ModuleManager {
private final Map<String, Module> modules = new HashMap<>();
public void registerModule(Module module) {
modules.put(module.getName().toLowerCase(), module);
}
public void enableAll(Plugin plugin) {
for (Module module : modules.values()) {
try {
plugin.getLogger().info("Aktiviere Modul: " + module.getName() + "...");
module.onEnable(plugin);
} catch (Exception e) {
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
e.printStackTrace();
}
}
}
public void disableAll(Plugin plugin) {
for (Module module : modules.values()) {
try {
plugin.getLogger().info("Deaktiviere Modul: " + module.getName() + "...");
module.onDisable(plugin);
} catch (Exception e) {
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());
}
}
modules.clear();
}
/**
* Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
*/
public Module getModule(String name) {
return modules.get(name.toLowerCase());
}
@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;
}
}
package net.viper.status.module;
import net.md_5.bungee.api.plugin.Plugin;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Verwaltet alle geladenen Module.
* Verwendet LinkedHashMap um die Registrierungsreihenfolge zu erhalten,
* damit Abh\u00e4ngigkeiten (z.B. VanishModule ChatModule) korrekt aufgel\u00f6st werden.
*/
public class ModuleManager {
private final Map<String, Module> modules = new LinkedHashMap<>();
public void registerModule(Module module) {
modules.put(module.getName().toLowerCase(), module);
}
public void enableAll(Plugin plugin) {
for (Module module : modules.values()) {
try {
module.onEnable(plugin);
} catch (Exception e) {
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
e.printStackTrace();
}
}
}
public void disableAll(Plugin plugin) {
for (Module module : modules.values()) {
try {
module.onDisable(plugin);
} catch (Exception e) {
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());
}
}
modules.clear();
}
/**
* Erm\u00f6glicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
*/
public Module getModule(String name) {
return modules.get(name.toLowerCase());
}
/**
* Ersetzt ein bestehendes Modul durch eine neue Instanz (f\u00fcr Reload).
* Das alte Modul muss bereits deaktiviert worden sein.
*/
public void replaceModule(String name, Module newModule) {
modules.put(name.toLowerCase(), newModule);
}
@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;
}
}

View File

@@ -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 \u00c4nderungen an messages.txt sofort wirken ohne Neustart.
* - Neuer Befehl /automessage reload (Permission: statusapi.automessage)
* l\u00e4dt die Konfiguration neu und setzt den Z\u00e4hler zur\u00fcck.
* - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes f\u00fcr §-Codes.
*/
public class AutoMessageModule implements Module {
private int taskId = -1;
private StatusAPI api;
private final AtomicInteger currentIndex = new AtomicInteger(0);
// Konfiguration (f\u00fcr Reload zug\u00e4nglich)
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\u00fcner 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\u00fcltiges Intervall f\u00fcr 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 → \u00c4nderungen 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 \u00fcbersetzen (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;
}
}
}

View File

@@ -0,0 +1,621 @@
package net.viper.status.modules.afk;
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.Title;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.ChatEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
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.api.scheduler.ScheduledTask;
import net.md_5.bungee.event.EventHandler;
import net.viper.status.StatusAPI;
import net.viper.status.module.Module;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* AfkModule /afk Befehl + automatische AFK-Erkennung nach Inaktivit\u00e4t.
*
* Unterst\u00fctzt %gradient:FARBE1:FARBE2:...:TEXT% in allen Title-Eintr\u00e4gen,
* genau wie Scoreboard und Tablist.
*
* Config: afk.properties (wird beim ersten Start automatisch erstellt)
*
* Title-Format: "Title-Zeile|Subtitle-Zeile" (Subtitle optional)
* Beispiel mit Gradient:
* afk.title.set.1=%gradient:&b:&f:&b:&l [AFK] %|&8Bewege dich um zur\u00fcckzukehren
*/
public class AfkModule implements Module, Listener {
private static final String CONFIG_FILE = "afk.properties";
private Plugin plugin;
private ScheduledTask idleCheckTask;
private static AfkModule INSTANCE;
private java.lang.reflect.Method sendPkt;
public static final ConcurrentHashMap<UUID, Long> lastActivity = new ConcurrentHashMap<>();
// Laufender Title-Repeat-Task pro AFK-Spieler
private final ConcurrentHashMap<UUID, ScheduledTask> titleTasks = new ConcurrentHashMap<>();
// Welcher Title-Eintrag diesem Spieler gerade angezeigt wird (bleibt gleich solange AFK)
private final ConcurrentHashMap<UUID, String[]> activeTitleEntry = new ConcurrentHashMap<>();
// Config
private boolean enabled = true;
private boolean idleEnabled = true;
private int idleSeconds = 300;
private String bypassPerm = "statusapi.afk.bypass";
private int titleFadeIn = 10;
private int titleStay = 60;
private int titleFadeOut = 20;
// Titel-Paar: set-Nachricht + passende unset-Nachricht
private static class TitlePair {
final String[] set; // { title, subtitle }
final String[] unset; // { title, subtitle }
TitlePair(String[] set, String[] unset) { this.set = set; this.unset = unset; }
}
private final List<TitlePair> titlePairs = new ArrayList<>();
// Welches Paar diesem Spieler gerade zugewiesen ist (bleibt f\u00fcr die gesamte AFK-Zeit gleich)
private final ConcurrentHashMap<UUID, TitlePair> activePair = new ConcurrentHashMap<>();
private final Random random = new Random();
@Override public String getName() { return "AfkModule"; }
@Override
public void onEnable(Plugin plugin) {
this.plugin = plugin;
INSTANCE = this;
try {
Class<?> uc = Class.forName("net.md_5.bungee.UserConnection");
sendPkt = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class);
sendPkt.setAccessible(true);
} catch (Exception e) {
plugin.getLogger().severe("[AfkModule] sendPacketQueued nicht gefunden: " + e.getMessage());
}
ensureConfigExists();
loadConfig();
if (!enabled) { plugin.getLogger().info("[AfkModule] Deaktiviert."); return; }
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
ProxyServer.getInstance().getPluginManager().registerCommand(plugin,
new Command("afk") {
@Override
public void execute(CommandSender sender, String[] args) {
if (!(sender instanceof ProxiedPlayer)) {
sender.sendMessage(plain("&cNur f\u00fcr Spieler."));
return;
}
toggleAfk((ProxiedPlayer) sender);
}
});
if (idleEnabled) {
idleCheckTask = ProxyServer.getInstance().getScheduler().schedule(
plugin, this::checkIdle, 10L, 10L, TimeUnit.SECONDS);
}
plugin.getLogger().info("[AfkModule] Aktiviert. idle=" + idleEnabled
+ " idleSeconds=" + idleSeconds
+ " pairs=" + titlePairs.size());
}
@Override
public void onDisable(Plugin plugin) {
if (idleCheckTask != null) { idleCheckTask.cancel(); idleCheckTask = null; }
titleTasks.values().forEach(ScheduledTask::cancel);
titleTasks.clear();
activeTitleEntry.clear();
activePair.clear();
StatusAPI.playerAfk.clear();
lastActivity.clear();
INSTANCE = null;
}
// ── Events ───────────────────────────────────────────────────────────────
@EventHandler
public void onChat(ChatEvent e) {
if (!(e.getSender() instanceof ProxiedPlayer)) return;
ProxiedPlayer p = (ProxiedPlayer) e.getSender();
recordActivity(p.getUniqueId());
if (!e.getMessage().toLowerCase().startsWith("/afk") && isAfk(p.getUniqueId()))
setAfk(p, false);
}
@EventHandler
public void onDisconnect(PlayerDisconnectEvent e) {
UUID id = e.getPlayer().getUniqueId();
StatusAPI.playerAfk.remove(id);
lastActivity.remove(id);
stopTitleTask(id);
activePair.remove(id);
}
// ── Public API ───────────────────────────────────────────────────────────
public static void setBridgeAfk(UUID uuid, boolean afk) {
if (afk) {
StatusAPI.playerAfk.put(uuid, true);
} else {
StatusAPI.playerAfk.remove(uuid);
lastActivity.put(uuid, System.currentTimeMillis());
}
}
/**
* Wird von StatusAPI aufgerufen wenn die Bridge eine Koordinaten\u00e4nderung meldet.
* Hebt AFK sofort auf und zeigt den Unset-Title an.
*/
public static void unAfkByMovement(UUID uuid) {
AfkModule inst = INSTANCE;
if (inst == null) return;
if (!Boolean.TRUE.equals(StatusAPI.playerAfk.get(uuid))) return;
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
if (p == null) return;
inst.setAfk(p, false);
}
public static void recordActivity(UUID uuid) {
lastActivity.put(uuid, System.currentTimeMillis());
}
// ── Interne Logik ────────────────────────────────────────────────────────
private boolean isAfk(UUID uuid) {
return Boolean.TRUE.equals(StatusAPI.playerAfk.get(uuid));
}
private void toggleAfk(ProxiedPlayer p) {
setAfk(p, !isAfk(p.getUniqueId()));
}
private void setAfk(ProxiedPlayer p, boolean afk) {
UUID id = p.getUniqueId();
if (isAfk(id) == afk) return;
if (afk) {
StatusAPI.playerAfk.put(id, true);
startTitleTask(p);
} else {
StatusAPI.playerAfk.remove(id);
lastActivity.put(id, System.currentTimeMillis());
TitlePair pair = activePair.get(id);
stopTitleTask(id);
clearTitle(p);
if (pair != null) sendTitleEntry(p, pair.unset);
}
}
private void checkIdle() {
long now = System.currentTimeMillis();
long thresholdMs = idleSeconds * 1000L;
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (!p.isConnected()) continue;
UUID id = p.getUniqueId();
if (p.hasPermission(bypassPerm) || isAfk(id)) continue;
Long last = lastActivity.get(id);
if (last == null) { lastActivity.put(id, now); continue; }
if (now - last >= thresholdMs) setAfk(p, true);
}
}
/**
* W\u00e4hlt einen zuf\u00e4lligen AFK-Title und wiederholt ihn dauerhaft,
* solange der Spieler AFK ist kein Ausblenden m\u00f6glich.
*/
private void startTitleTask(ProxiedPlayer p) {
if (titlePairs.isEmpty()) return;
UUID id = p.getUniqueId();
stopTitleTask(id);
// Zuf\u00e4lliges Paar w\u00e4hlen bleibt f\u00fcr die gesamte AFK-Zeit gleich
TitlePair pair = titlePairs.get(random.nextInt(titlePairs.size()));
activePair.put(id, pair);
activeTitleEntry.put(id, pair.set);
sendTitleEntry(p, pair.set);
long intervalMs = Math.max(500, (titleStay - 20) * 50L);
ScheduledTask task = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(id);
if (online == null || !online.isConnected() || !isAfk(id)) {
stopTitleTask(id);
return;
}
String[] current = activeTitleEntry.get(id);
if (current != null) sendTitleEntry(online, current);
}, intervalMs, intervalMs, TimeUnit.MILLISECONDS);
titleTasks.put(id, task);
}
private void stopTitleTask(UUID id) {
ScheduledTask old = titleTasks.remove(id);
if (old != null) old.cancel();
activeTitleEntry.remove(id);
// activePair bleibt bis setAfk(false) es ausliest, danach:
}
/** Entfernt den Title sofort vom Bildschirm. */
/** Entfernt den Title sofort vom Bildschirm via ClearTitles-Packet. */
private void clearTitle(ProxiedPlayer p) {
try {
net.md_5.bungee.protocol.packet.ClearTitles clear = new net.md_5.bungee.protocol.packet.ClearTitles();
if (sendPkt != null) sendPkt.invoke(p, clear);
} catch (Exception ignored) {}
}
/**
* Sendet Title + Subtitle als raw Packets (wie ScoreboardModule)
* dadurch werden Hex-Farben korrekt \u00fcbertragen, ohne durch TextComponent.fromArray() zu laufen.
*/
private void sendTitleEntry(ProxiedPlayer p, String[] entry) {
String titleRaw = ChatColor.translateAlternateColorCodes('&', applyGradients(entry[0]));
String subtitleRaw = entry.length > 1 ? ChatColor.translateAlternateColorCodes('&', applyGradients(entry[1])) : "";
try {
if (sendPkt == null) throw new IllegalStateException("sendPkt not initialized");
// Times zuerst senden
net.md_5.bungee.protocol.packet.TitleTimes times = new net.md_5.bungee.protocol.packet.TitleTimes();
times.setFadeIn(titleFadeIn);
times.setStay(titleStay);
times.setFadeOut(titleFadeOut);
sendPkt.invoke(p, times);
// Title-Packet (Action = TITLE, ordinal 0)
net.md_5.bungee.protocol.packet.Title titlePkt = new net.md_5.bungee.protocol.packet.Title();
titlePkt.setAction(net.md_5.bungee.protocol.packet.Title.Action.TITLE);
titlePkt.setText(mergeComponents(buildComponents(titleRaw)));
sendPkt.invoke(p, titlePkt);
// Subtitle-Packet
net.md_5.bungee.protocol.packet.Subtitle subPkt = new net.md_5.bungee.protocol.packet.Subtitle();
subPkt.setText(mergeComponents(buildComponents(subtitleRaw)));
sendPkt.invoke(p, subPkt);
} catch (Exception e) {
// Fallback auf Title-API
try {
Title title = ProxyServer.getInstance().createTitle();
title.title(buildComponents(titleRaw));
title.subTitle(buildComponents(subtitleRaw));
title.fadeIn(titleFadeIn);
title.stay(titleStay);
title.fadeOut(titleFadeOut);
p.sendTitle(title);
} catch (Exception ignored) {}
}
}
/** Fasst BaseComponent[] in eine TextComponent zusammen (f\u00fcr Title/Subtitle setText). */
private static net.md_5.bungee.api.chat.BaseComponent mergeComponents(BaseComponent[] parts) {
TextComponent root = new TextComponent("");
for (BaseComponent bc : parts) root.addExtra(bc);
return root;
}
/** Fasst ein BaseComponent[]-Array in eine einzelne TextComponent zusammen. */
private static BaseComponent wrap(BaseComponent[] parts) {
return mergeComponents(parts);
}
// ── Gradient-Verarbeitung ─────────────────────────────────────────────────
// Identische Implementierung wie im ScoreboardModule verarbeitet
// %gradient:FARBE1:FARBE2:...:TEXT% mit beliebig vielen Farb-Stopps.
// Farben: #RRGGBB, &#RRGGBB oder &b, &f, &c usw.
// Formatcodes (&l, &o, &n, &m) im Text bleiben erhalten.
private String applyGradients(String input) {
if (input == null || !input.contains("%gradient:")) return input;
StringBuilder result = new StringBuilder();
int i = 0;
while (i < input.length()) {
int start = input.indexOf("%gradient:", i);
if (start < 0) { result.append(input.substring(i)); break; }
result.append(input, i, start);
int end = input.indexOf("%", start + 10);
if (end < 0) { result.append(input.substring(start)); break; }
String inner = input.substring(start + 10, end);
List<int[]> stops = new ArrayList<>();
int colonIdx = 0;
while (colonIdx < inner.length()) {
int nextColon = inner.indexOf(':', colonIdx);
if (nextColon < 0) break;
String candidate = inner.substring(colonIdx, nextColon);
int[] rgb = parseGradientColor(candidate);
if (rgb != null) { stops.add(rgb); colonIdx = nextColon + 1; }
else break;
}
if (stops.size() < 2) { result.append(input, start, end + 1); i = end + 1; continue; }
String text = inner.substring(colonIdx);
String plain = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', text));
boolean bold = text.contains("&l") || text.contains("\u00A7l");
boolean italic = text.contains("&o") || text.contains("\u00A7o");
boolean underline = text.contains("&n") || text.contains("\u00A7n");
boolean strike = text.contains("&m") || text.contains("\u00A7m");
String fmt = (bold ? "\u00A7l" : "") + (italic ? "\u00A7o" : "")
+ (underline ? "\u00A7n" : "") + (strike ? "\u00A7m" : "");
int visLen = 0;
for (char ch : plain.toCharArray()) if (ch != ' ') visLen++;
if (visLen == 0) visLen = 1;
int charIdx = 0;
int[] lastRgb = stops.get(0); // Farbe f\u00fcr f\u00fchrende Leerzeichen
for (char ch : plain.toCharArray()) {
if (ch == ' ') {
// Leerzeichen mit der letzten aktiven Gradient-Farbe einf\u00e4rben
result.append('\u00A7').append('x');
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
result.append(fmt).append(ch);
continue;
}
float pos = visLen <= 1 ? 0f : (float) charIdx / (visLen - 1);
lastRgb = interpolateGradient(stops, pos);
result.append('\u00A7').append('x');
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
result.append(fmt).append(ch);
charIdx++;
}
i = end + 1;
}
return result.toString();
}
private int[] parseGradientColor(String s) {
s = s.trim();
if (s.startsWith("&#")) s = s.substring(1);
if (s.startsWith("#") && s.length() == 7) {
try {
return new int[]{
Integer.parseInt(s.substring(1,3),16),
Integer.parseInt(s.substring(3,5),16),
Integer.parseInt(s.substring(5,7),16)
};
} catch (Exception ignored) {}
}
if (s.startsWith("&") && s.length() == 2) return mcColorToRgb(s.charAt(1));
return null;
}
private int[] interpolateGradient(List<int[]> stops, float pos) {
if (stops.size() == 1) return stops.get(0);
float scaled = pos * (stops.size() - 1);
int i0 = Math.min((int) scaled, stops.size() - 2);
float t = scaled - i0;
return new int[]{
(int)(stops.get(i0)[0] * (1-t) + stops.get(i0+1)[0] * t),
(int)(stops.get(i0)[1] * (1-t) + stops.get(i0+1)[1] * t),
(int)(stops.get(i0)[2] * (1-t) + stops.get(i0+1)[2] * t)
};
}
private static int[] mcColorToRgb(char code) {
switch (Character.toLowerCase(code)) {
case '0': return new int[]{ 0, 0, 0};
case '1': return new int[]{ 0, 0, 170};
case '2': return new int[]{ 0, 170, 0};
case '3': return new int[]{ 0, 170, 170};
case '4': return new int[]{170, 0, 0};
case '5': return new int[]{170, 0, 170};
case '6': return new int[]{255, 170, 0};
case '7': return new int[]{170, 170, 170};
case '8': return new int[]{ 85, 85, 85};
case '9': return new int[]{ 85, 85, 255};
case 'a': return new int[]{ 85, 255, 85};
case 'b': return new int[]{ 85, 255, 255};
case 'c': return new int[]{255, 85, 85};
case 'd': return new int[]{255, 85, 255};
case 'e': return new int[]{255, 255, 85};
case 'f': return new int[]{255, 255, 255};
default: return null;
}
}
// ── BaseComponent-Builder ─────────────────────────────────────────────────
// Wandelt einen vorverarbeiteten String (§x§R§R§G§G§B§B + §-Codes) in
// ein BaseComponent[]-Array um identisch zu ScoreboardModule.buildComponents().
private static BaseComponent[] buildComponents(String text) {
if (text == null || text.isEmpty())
return new BaseComponent[]{ new TextComponent("") };
List<BaseComponent> parts = new ArrayList<>();
net.md_5.bungee.api.ChatColor currentColor = net.md_5.bungee.api.ChatColor.WHITE;
boolean bold = false, italic = false, underline = false, strike = false, magic = false;
int i = 0;
StringBuilder buf = new StringBuilder();
while (i < text.length()) {
char c = text.charAt(i);
// §x§R§R§G§G§B§B RGB Hex (14 Zeichen)
if (c == '§' && i + 13 < text.length() && text.charAt(i+1) == 'x') {
if (buf.length() > 0) {
parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
buf.setLength(0);
}
try {
String hex = "" + text.charAt(i+3) + text.charAt(i+5)
+ text.charAt(i+7) + text.charAt(i+9)
+ text.charAt(i+11) + text.charAt(i+13);
currentColor = net.md_5.bungee.api.ChatColor.of("#" + hex);
} catch (Exception ignored) {}
i += 14;
continue;
}
// §X Farb-/Formatcodes
if (c == '§' && i + 1 < text.length()) {
char code = Character.toLowerCase(text.charAt(i+1));
if (buf.length() > 0) {
parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
buf.setLength(0);
}
switch (code) {
case 'r': currentColor = net.md_5.bungee.api.ChatColor.WHITE;
bold=false; italic=false; underline=false; strike=false; magic=false; break;
case 'l': bold=true; break;
case 'o': italic=true; break;
case 'n': underline=true; break;
case 'm': strike=true; break;
case 'k': magic=true; break;
default:
net.md_5.bungee.api.ChatColor col = net.md_5.bungee.api.ChatColor.getByChar(code);
if (col != null) { currentColor=col; bold=false; italic=false; underline=false; strike=false; magic=false; }
}
i += 2;
continue;
}
buf.append(c);
i++;
}
if (buf.length() > 0)
parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
if (parts.isEmpty())
return new BaseComponent[]{ new TextComponent("") };
return parts.toArray(new BaseComponent[0]);
}
private static BaseComponent makeComp(String text,
net.md_5.bungee.api.ChatColor color,
boolean bold, boolean italic, boolean underline, boolean strike, boolean magic) {
TextComponent tc = new TextComponent(text);
tc.setColor(color);
tc.setBold(bold);
tc.setItalic(italic);
tc.setUnderlined(underline);
tc.setStrikethrough(strike);
tc.setObfuscated(magic);
return tc;
}
// ── Config ───────────────────────────────────────────────────────────────
private void loadConfig() {
java.io.File file = new java.io.File(plugin.getDataFolder(), CONFIG_FILE);
Map<String, String> map = new LinkedHashMap<>();
if (file.exists()) {
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(
new java.io.FileInputStream(file),
java.nio.charset.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("[AfkModule] Ladefehler: " + e.getMessage());
}
}
enabled = Boolean.parseBoolean(map.getOrDefault("afk.enabled", "true"));
idleEnabled = Boolean.parseBoolean(map.getOrDefault("afk.idle_enabled", "true"));
idleSeconds = parseInt(map.getOrDefault("afk.idle_seconds", "300"), 300);
bypassPerm = map.getOrDefault("afk.permission.bypass", "statusapi.afk.bypass");
titleFadeIn = parseInt(map.getOrDefault("afk.title.fade_in", "10"), 10);
titleStay = parseInt(map.getOrDefault("afk.title.stay", "100"), 100);
titleFadeOut = parseInt(map.getOrDefault("afk.title.fade_out", "10"), 10);
titlePairs.clear();
for (int i = 1; i <= 20; i++) {
String raw = map.get("afk.title.pair." + i);
if (raw == null || raw.isEmpty()) continue;
// Format: setTitle|setSubtitle||unsetTitle|unsetSubtitle
int sep = raw.indexOf("||");
if (sep < 0) continue;
String[] set = splitTitleLine(raw.substring(0, sep));
String[] unset = splitTitleLine(raw.substring(sep + 2));
titlePairs.add(new TitlePair(set, unset));
}
if (titlePairs.isEmpty()) {
titlePairs.add(new TitlePair(
new String[]{"%gradient:&b:&f:&b:&l [AFK] %", "&8Bewege dich um zur\u00fcckzukehren"},
new String[]{"&aWillkommen zur\u00fcck!", ""}
));
}
}
private String[] splitTitleLine(String raw) {
int pipe = raw.indexOf('|');
if (pipe < 0) return new String[]{ raw, "" };
return new String[]{ raw.substring(0, pipe), raw.substring(pipe + 1) };
}
private void ensureConfigExists() {
java.io.File file = new java.io.File(plugin.getDataFolder(), CONFIG_FILE);
if (file.exists()) return;
try (java.io.OutputStreamWriter w = new java.io.OutputStreamWriter(
new java.io.FileOutputStream(file), java.nio.charset.StandardCharsets.UTF_8)) {
w.write(
"# =====================================================\n" +
"# AfkModule /afk Befehl & automatische AFK-Erkennung\n" +
"# =====================================================\n\n" +
"afk.enabled=true\n\n" +
"# Automatisch AFK setzen nach X Sekunden ohne Aktivitaet\n" +
"afk.idle_enabled=true\n" +
"afk.idle_seconds=300\n\n" +
"# Berechtigung zum Umgehen des auto-AFK\n" +
"afk.permission.bypass=statusapi.afk.bypass\n\n" +
"# ── Title-Anzeigezeiten (in Ticks, 20 Ticks = 1 Sekunde) ──\n" +
"afk.title.fade_in=10\n" +
"afk.title.stay=100\n" +
"afk.title.fade_out=10\n\n" +
"# ── Nachrichten-Paare ─────────────────────────────────────\n" +
"# Format: setTitle|setSubtitle||unsetTitle|unsetSubtitle\n" +
"# | trennt Title und Subtitle innerhalb einer Seite\n" +
"# || trennt AFK-Nachricht (links) von R\u00fcckkehr-Nachricht (rechts)\n" +
"# Gradient: %gradient:FARBE1:FARBE2:...:TEXT%\n" +
"# Farben: &b, &f, &a usw. oder #RRGGBB\n" +
"# Es wird zufaellig ein Paar gewaehlt (max. 20 Paare).\n" +
"# Die R\u00fcckkehr-Nachricht passt thematisch zur AFK-Nachricht.\n\n" +
"afk.title.pair.1=%gradient:&b:&f:&b:&l [AFK] %|&8Wahrscheinlich auf dem Klo...||%gradient:&a:&f:&a:&l Willkommen zur\u00fcck! %|&7War das Klo sauber?\n" +
"afk.title.pair.2=%gradient:&b:&f:&b:&l [AFK] %|&8Hat den Stecker gezogen||%gradient:&a:&f:&a:&l Wieder eingesteckt! %|&7Der Strom ist zur\u00fcck\n" +
"afk.title.pair.3=%gradient:&b:&f:&b:&l [AFK] %|&8Schaut seit 10min die Decke an||%gradient:&a:&f:&a:&l Na endlich! %|&7Die Decke hat dich freigegeben\n" +
"afk.title.pair.4=%gradient:&b:&f:&b:&l [AFK] %|&8Vom K\u00fchlschrank verschluckt worden||%gradient:&a:&f:&a:&l Er lebt! %|&7Der Snack war es wert, oder?\n" +
"afk.title.pair.5=%gradient:&b:&f:&b:&l [AFK] %|&8Loading... Spieler nicht gefunden||%gradient:&a:&f:&a:&l Error behoben! %|&7Spieler erfolgreich neugestartet\n" +
"afk.title.pair.6=%gradient:&b:&f:&b:&l [AFK] %|&8Vermutlich beim Snackholen||%gradient:&a:&f:&a:&l Snack erfolgreich geholt! %|&7Weiter gehts!\n" +
"afk.title.pair.7=%gradient:&b:&f:&b:&l [AFK] %|&8Eingeschlafen. Bitte nicht wecken.||%gradient:&a:&f:&a:&l Aufgewacht! %|&7Der Wecker hat funktioniert\n" +
"afk.title.pair.8=%gradient:&b:&f:&b:&l [AFK] %|&8Hat die Realit\u00e4t betreten||%gradient:&a:&f:&a:&l Zur\u00fcck aus der Realit\u00e4t! %|&7Und? War's schlimm?\n" +
"afk.title.pair.9=%gradient:&b:&f:&b:&l [AFK] %|&8Gehirn: AFK. K\u00f6rper: noch da.||%gradient:&a:&f:&a:&l Gehirn wieder eingeschaltet! %|&7Willkommen zur\u00fcck\n" +
"afk.title.pair.10=%gradient:&b:&f:&b:&l [AFK] %|&8Spricht mit echten Menschen. Seltsam.||%gradient:&a:&f:&a:&l Zur\u00fcck zu den Pixeln! %|&7Echte Menschen sind \u00fcbersch\u00e4tzt\n" +
"afk.title.pair.11=%gradient:&b:&f:&b:&l [AFK] %|&8Sucht den Einschalter f\u00fcrs Leben||%gradient:&a:&f:&a:&l Einschalter gefunden! %|&7Das Leben l\u00e4uft wieder\n" +
"afk.title.pair.12=%gradient:&b:&f:&b:&l [AFK] %|&8Mom hat gerufen. RIP.||%gradient:&a:&f:&a:&l Mom ist wieder weg. %|&7Puh. Knapp entkommen.\n" +
"afk.title.pair.13=%gradient:&b:&f:&b:&l [AFK] %|&8Error 404: Spieler nicht gefunden||%gradient:&a:&f:&a:&l Spieler wieder online! %|&7404 behoben\n" +
"afk.title.pair.14=%gradient:&b:&f:&b:&l [AFK] %|&8Tut so als w\u00e4re er besch\u00e4ftigt||%gradient:&a:&f:&a:&l Aufgeflogen! %|&7War eh nicht \u00fcberzeugend\n" +
"afk.title.pair.15=%gradient:&b:&f:&b:&l [AFK] %|&8Kaffeepause. Die wichtigste Pause.||%gradient:&a:&f:&a:&l Koffein erfolgreich zugef\u00fchrt! %|&7Jetzt wieder einsatzbereit\n"
);
} catch (Exception e) {
plugin.getLogger().warning("[AfkModule] Konnte Config nicht erstellen: " + e.getMessage());
}
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private static TextComponent plain(String text) {
return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
}
private static int parseInt(String s, int def) {
try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; }
}
}

View File

@@ -0,0 +1,853 @@
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\u00e4ndiger 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);
}
}
/**
* Sperrt eine IP f\u00fcr die konfigurierte Block-Dauer (antibot.ip.block_seconds).
* Kann von anderen Modulen aufgerufen werden (z. B. MultiAccountGuard).
* @param ip Die zu sperrende IP-Adresse
* @param durationSeconds Sperrdauer in Sekunden (0 = antibot-Standard verwenden)
*/
public void blockIpExternal(String ip, int durationSeconds) {
long now = System.currentTimeMillis();
long duration = durationSeconds > 0 ? durationSeconds : Math.max(1, ipBlockSeconds);
blockedIpsUntil.put(ip, now + duration * 1000L);
blockedConnectionsTotal.incrementAndGet();
}
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");
}
}
}

View File

@@ -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 \u00fcbersetzt. (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 \u00fcbersetzen
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\u00e4nger=" + sent + "): " + message);
return true;
}
/**
* Baut ein BaseComponent-Array aus einem formatierten String.
* URLs (http/https) werden als anklickbare TextComponents eingebettet.
* Unterst\u00fctzt auch echte Newlines (\n) als Zeilenumbruch.
*/
private BaseComponent[] buildClickableComponents(String text) {
// Regex f\u00fcr 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;
}
}
}

View File

@@ -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\u00fcpfung 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\u00fcpfung 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\u00fcpfte Accounts
private final ConcurrentHashMap<UUID, LinkedAccount> links = new ConcurrentHashMap<>();
// Ausstehende Token: token → UUID (l\u00e4uft 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\u00fcpft
public String telegramUserId = ""; // leer = nicht verkn\u00fcpft
public String telegramUsername = ""; // @username f\u00fcr Anzeige
public String discordUsername = ""; // f\u00fcr 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\u00fcpfungs-Token f\u00fcr einen Spieler.
* Bestehende Token f\u00fcr denselben Spieler+Typ werden \u00fcberschrieben.
*
* @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\u00fcr 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\u00f6sen =====
/**
* Versucht einen Token einzul\u00f6sen (aufgerufen wenn Bot eine Nachricht empf\u00e4ngt).
*
* @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\u00fcltig/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 \u00fcbereinstimmen
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\u00fcr eine Discord-User-ID zur\u00fcck, oder null. */
public String getMinecraftNameByDiscordId(String discordUserId) {
LinkedAccount a = getByDiscordId(discordUserId);
return a != null ? a.minecraftName : null;
}
/** Gibt den Minecraft-Namen f\u00fcr eine Telegram-User-ID zur\u00fcck, oder null. */
public String getMinecraftNameByTelegramId(String telegramUserId) {
LinkedAccount a = getByTelegramId(telegramUserId);
return a != null ? a.minecraftName : null;
}
/** Pr\u00fcft ob ein Token gerade aussteht (f\u00fcr 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\u00fcpfung 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\u00fcr Bridges =====
/**
* L\u00f6st einen Telegram-Token ein.
* Wrapper f\u00fcr redeemToken mit type="telegram".
*/
public LinkedAccount redeemTelegram(String token, String telegramUserId, String telegramUsername) {
return redeemToken(token, telegramUserId, telegramUsername, "telegram");
}
/**
* L\u00f6st einen Discord-Token ein.
* Wrapper f\u00fcr redeemToken mit type="discord".
*/
public LinkedAccount redeemDiscord(String token, String discordUserId, String discordUsername) {
return redeemToken(token, discordUserId, discordUsername, "discord");
}
/**
* Gibt den Anzeigenamen f\u00fcr einen Telegram-Nutzer zur\u00fcck.
* Wenn verkn\u00fcpft: "MinecraftName (@telegram)", sonst: "@telegram"
*/
public String resolveTelegramName(String telegramUserId, String fallbackName) {
String mc = getMinecraftNameByTelegramId(telegramUserId);
return mc != null ? mc : fallbackName;
}
/**
* Gibt den Anzeigenamen f\u00fcr einen Discord-Nutzer zur\u00fcck.
* Wenn verkn\u00fcpft: 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\u00fcck abh\u00e4ngig ob Account verkn\u00fcpft.
* 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("\\\\", "\\");
}
}

View File

@@ -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\u00fcr `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\u00fcft 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\u00fcft ob eine Nachricht von `sender` an `receiver` zugestellt werden soll.
* Gibt false zur\u00fcck, 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\u00fcck, 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());
}
}
}

View File

@@ -0,0 +1,69 @@
package net.viper.status.modules.chat;
/**
* Repr\u00e4sentiert einen Chat-Kanal mit allen zugeh\u00f6rigen 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\u00fcft ob der Kanal eine Permission erfordert. */
public boolean hasPermission() {
return permission != null && !permission.isEmpty();
}
/** Gibt das formatierte Kanalprefix zur\u00fcck, 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 + "}";
}
}

View File

@@ -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\u00e4dt und verwaltet die chat.yml Konfiguration.
*
* Fix #8: Rate-Limit-Werte aus anti-spam werden nicht mehr durch nachfolgende
* Berechnungen \u00fcberschrieben. Der rate-limit.chat-Block hat jetzt Vorrang.
* Die Reihenfolge ist: erst rate-limit.chat einlesen, dann ggf. durch anti-spam
* als Fallback erg\u00e4nzen, 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\u00e4le geladen.");
}
private void parseConfig() {
defaultChannel = config.getString("default-channel", "global");
// --- Kan\u00e4le ---
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\u00fcpft!") : "&aDiscord verkn\u00fcpft!";
linkSuccessTelegram = al != null ? al.getString("success-telegram", "&aTelegram verkn\u00fcpft!") : "&aTelegram verkn\u00fcpft!";
linkBotSuccessDiscord = al != null ? al.getString("bot-success-discord", "✅ Verkn\u00fcpft: {player}") : "✅ Verkn\u00fcpft: {player}";
linkBotSuccessTelegram = al != null ? al.getString("bot-success-telegram", "✅ Verkn\u00fcpft: {player}") : "✅ Verkn\u00fcpft: {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 \u00fcberschreiben 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\u00fcltigen 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 \u00fcberschreibt die anti-spam-Fallbacks vollst\u00e4ndig
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; }
}

View File

@@ -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\u00fcfungen in processChat():
* 1. Spam-Cooldown (zu schnell geschrieben?)
* 2. Gleiche Nachricht wiederholt?
* 3. Zu viele Gro\u00dfbuchstaben?
* 4. Verbotene W\u00f6rter → 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\u00fcr Duplikat-Check)
private final Map<UUID, String> lastMessageText = new ConcurrentHashMap<>();
// Kompilierte Regex-Pattern f\u00fcr Blacklist-W\u00f6rter
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\u00e4ndert (W\u00f6rter 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 \u00fcberspringen
* @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\u00e4hle Gro\u00dfbuchstaben
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\u00e4chstes Zeichen \u00fcberspringen
if (isColor || isFormat) { i++; continue; }
// Hex: &# + 6 Zeichen \u00fcberspringen (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\u00fcft ob die Nachricht Werbung enth\u00e4lt (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\u00e4nge f\u00fcr Caps-Check
public int capsMaxPercent = 70; // Max. % Gro\u00dfbuchstaben
// 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\u00fcfen)
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"
));
}
}

View File

@@ -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\u00e4glich 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\u00e4hler f\u00fcr 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\u00fcck.
*
* @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\u00f6scht Log-Dateien, die \u00e4lter 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\u00f6scht: " + 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\u00fcckgegeben.
*
* @param playerFilter Spielername (case-insensitiv) oder null f\u00fcr alle
* @param maxLines Maximale Anzahl zur\u00fcckgegebener Zeilen
* @return Liste der Logzeilen (\u00e4lteste 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\u00fcckgeben
if (allLines.size() <= maxLines) return allLines;
return allLines.subList(allLines.size() - maxLines, allLines.size());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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\u00fctzen Unicode-Emojis ebenfalls,
* da sie als regul\u00e4re UTF-8 Zeichen in TextComponents \u00fcbertragen 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\u00e4ndert.
*
* @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\u00fcck (f\u00fcr /emoji list).
*/
public String buildEmojiList() {
if (mappings.isEmpty()) return "&cKeine Emojis konfiguriert.";
StringBuilder sb = new StringBuilder();
sb.append("&eVerf\u00fcgbare 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();
}
}

View File

@@ -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\u00f6nnen 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\u00fcr 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\u00fcft 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\u00fcck.
* Gibt "permanent" zur\u00fcck 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());
}
}
}

View File

@@ -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\u00e4chspartner UUID (f\u00fcr /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\u00fcr 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\u00fcr /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\u00e4chspartner 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\u00e4ngers
* {player} → Gespr\u00e4chspartner aus Sicht des jeweiligen Empf\u00e4ngers:
* Beim Sender: der Empf\u00e4nger (an wen schreibt er?)
* Beim Empf\u00e4nger: 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\u00e4chspartner aus Sicht des Betrachters
.replace("{message}", message);
}
private TextComponent color(String text) {
return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
}
}

View File

@@ -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\u00dft.
*
* Online-Admins werden sofort benachrichtigt.
* Offline-Admins erhalten eine verz\u00f6gerte Benachrichtigung beim n\u00e4chsten Login
* (gesteuert von au\u00dfen 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\u00e4hler f\u00fcr 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\u00dfenden 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\u00fcr Kontext)
* @param reason Freitext-Begr\u00fcndung
* @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\u00dft einen Report.
*
* @param id Report-ID (z.B. RPT-0001, case-insensitiv)
* @param adminName Name des Admins, der den Report schlie\u00dft
* @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\u00fcck (case-insensitiv). */
public ChatReport getReport(String id) {
if (id == null) return null;
return reports.get(id.toUpperCase());
}
/** Gibt alle offenen Reports chronologisch (\u00e4lteste zuerst) zur\u00fcck. */
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\u00fcck (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\u00e4hler auf h\u00f6chste 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\u00fcche 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("\\\\", "\\");
}
}

View File

@@ -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\u00fcft via
* {@link #isVanished} bevor es Join-/Leave-Nachrichten sendet oder
* Privat-Nachrichten zul\u00e4sst.
*
* 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\u00fcr Debugging / Logs). */
public static Set<UUID> getVanishedPlayers() {
return Collections.unmodifiableSet(vanishedPlayers);
}
}

View File

@@ -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\u00fccke f\u00fcr bidirektionale Kommunikation.
*
* Fix #12: extractJsonString() behandelt Escape-Sequenzen jetzt korrekt.
* Statt Zeichenvergleich mit dem Vorg\u00e4nger-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\u00fccke 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\u00fcpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
else sendToChannel(channelId, "❌ Ung\u00fcltiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausf\u00fchren.");
}
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\u00fcr 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\u00e4nger-Char zu vergleichen (der bei '\\' + '"' versagt).
* Gibt immer einen leeren String zur\u00fcck 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\u00e4nger-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("\\\\", "\\");
}
}

View File

@@ -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\u00fccke f\u00fcr 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\u00fcnschten Gruppen/Kan\u00e4le einladen
* - Bot zu Admin machen (f\u00fcr 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\u00fcr 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\u00fccke gestartet (Poll-Intervall: " + interval + "s).");
}
public void stop() {
running = false;
}
// ===== Minecraft → Telegram =====
/**
* Sendet eine Nachricht an eine Telegram-Chat-ID.
* Unterst\u00fctzt 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\u00fctzt 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\u00f6sung: /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\u00fcpfung erfolgreich! Minecraft-Account: <b>"
+ escapeHtml(acc.minecraftName) + "</b>");
} else {
sendToTelegram(update.chatId, update.threadId,
"❌ Ung\u00fcltiger oder abgelaufener Token. Bitte /telegramlink im Spiel erneut ausf\u00fchren.");
}
}
continue; // Nicht als Chat-Nachricht weiterleiten
}
// Bot-Befehle ignorieren
if (update.text.startsWith("/")) continue;
// ── Account-Name aufl\u00f6sen ──
String displayName = (linkManager != null)
? linkManager.resolveTelegramName(update.fromId, update.fromName)
: update.fromName;
// Welchem Minecraft-Kanal geh\u00f6rt diese Telegram-Chat-ID + Thread?
final boolean isAdminChat = update.chatId.equals(config.getTelegramAdminChatId())
&& (config.getTelegramAdminTopicId() == 0
|| config.getTelegramAdminTopicId() == update.threadId);
// Pr\u00fcfen ob die Nachricht zu einem konfigurierten Kanal-Thema geh\u00f6rt
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\u00fcr Account-Link)
String fromName = "";
String text = "";
boolean isBot = false;
int threadId = 0; // message_thread_id f\u00fcr 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\u00fcft ob ein Update zu einem konfigurierten Kanal-Thema geh\u00f6rt. */
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 \u00fcbereinstimmen
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
}

View File

@@ -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\u00e4\u00dfig 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;
}
}
}

View File

@@ -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\u00fcr 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\u00f6tig.
// Wir nutzen hier das \u00fcbergebene '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);
}
}

View File

@@ -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();
}
}

View File

@@ -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 \u00fcbernimmt 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) {}
}

View File

@@ -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\u00fcr withdraw+deposit → kein Geldverlust bei Absturz
* - FOR UPDATE Lock → kein Race-Condition-Bug bei gleichzeitigen \u00dcberweisungen
*/
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\u00e4gt 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\u00e4dt den Kontostand direkt aus der DB. Gibt -1 zur\u00fcck 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\u00fcr " + 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\u00fcr " + uuid + ": " + e.getMessage());
}
}
/**
* Atomare \u00dcberweisung von → to.
* Nutzt eine SQL-Transaktion mit FOR UPDATE Lock race-condition-sicher.
* Gibt false zur\u00fcck 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\u00e4nger gutschreiben (Konto anlegen falls n\u00f6tig)
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;
}
}

View File

@@ -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\u00e4umen der playerBalances Map.
*
* Das Bef\u00fcllen der Map geschieht ausschlie\u00dflich durch die StatusAPIBridge
* (Spigot) die \u00fcber 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\u00f6tigt
}
@EventHandler
public void onLogin(PostLoginEvent event) {
// Wird von StatusAPIBridge bef\u00fcllt 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\u00f6tig
}
}

View File

@@ -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\u00dflich \u00fcber 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; }
}

View File

@@ -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\u00dflich \u00fcber NexEco (Spigot) verwaltet.
* Die StatusAPIBridge (Spigot-Plugin) liest den Kontostand \u00fcber Vault/NexEco
* und pushed ihn per HTTP an die StatusAPI → playerBalances Map.
*
* Damit gibt es nur EINE Datenquelle f\u00fcr Kontost\u00e4nde: NexEco / money_accounts.
* Das alte EconomyModule schrieb in bc_accounts das f\u00fchrte zu doppelten,
* inkonsistenten Kontost\u00e4nden.
*/
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\u00e4ndig.");
plugin.getLogger().info("[Economy] Kontost\u00e4nde kommen via StatusAPIBridge (Vault → NexEco → HTTP).");
}
@Override
public void onDisable(Plugin plugin) {}
public EconomyManager getManager() { return null; }
}

View File

@@ -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 \u00fcbernimmt /pay direkt.
* Diese Klasse existiert nur noch f\u00fcr Kompilier-Kompatibilit\u00e4t.
*/
public class PayCommand extends Command {
public PayCommand(Plugin plugin, EconomyManager manager) {
super("pay_disabled_nexeco", null);
}
@Override
public void execute(CommandSender sender, String[] args) {}
}

View File

@@ -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\u00fcck.
* Alle Aufrufer m\u00fcssen nicht mehr auf null pr\u00fcfen, 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\u00f6glich
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\u00fcck)
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 \u00f6ffnen").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\u00f6nnen 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\u00e4ltst du in deinem Forum-Profil unter §fMinecraft-Verkn\u00fcpfung§7."));
return;
}
String token = args[0].trim().toUpperCase();
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verkn\u00fcpfung ist nicht konfiguriert.")); return; }
p.sendMessage(new TextComponent("§7\u00dcberpr\u00fcfe 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\u00fcpft!"));
if (!show.isEmpty()) p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
p.sendMessage(new TextComponent("§7 Du erh\u00e4ltst 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\u00fcpft.")));
else if ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ung\u00fcltiger Token."));
else p.sendMessage(new TextComponent("§c✗ Verkn\u00fcpfung 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\u00f6nnen 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 \u00f6ffnen");
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke um das Forum zu \u00f6ffnen").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 \u00d6ffnen").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\u00fcck, 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();
}
}
}

View File

@@ -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\u00e4chsten 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\u00fcgt 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\u00fcck.
*/
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 \u00e4lter 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());
}
}
}

View File

@@ -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\u00fcr 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\u00fcr den Benachrichtigungstyp.
*/
public String getTypeLabel() {
switch (type) {
case "reply": return "Neue Antwort";
case "mention": return "Erw\u00e4hnung";
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\u00fcn
case "poll": return "§3"; // Dunkel-Aqua
case "answer": return "§2"; // Dunkel-Gr\u00fcn
default: return "§f"; // Wei\u00df
}
}
/**
* Serialisierung f\u00fcr 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;
}
}
}

View File

@@ -0,0 +1,252 @@
package net.viper.status.modules.help;
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.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
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.StatusAPI;
import net.viper.status.module.Module;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* HelpModule seitenbasierte Ingame-Hilfe.
*
* verify.properties:
* statusapi.help=vn → /vn help [seite]
* statusapi.help.permission=statusapi.admin
*/
public class HelpModule implements Module {
private String commandName = "help";
private String adminPermission = "statusapi.admin";
@Override
public String getName() { return "HelpModule"; }
@Override
public void onEnable(Plugin plugin) {
Properties props = ((StatusAPI) plugin).getVerifyProperties();
if (props != null) {
String cn = props.getProperty("statusapi.help", "help").trim();
if (!cn.isEmpty()) commandName = cn;
String ap = props.getProperty("statusapi.help.permission", "statusapi.admin").trim();
if (!ap.isEmpty()) adminPermission = ap;
}
ProxyServer.getInstance().getPluginManager().registerCommand(plugin,
new HelpCommand(commandName, adminPermission));
plugin.getLogger().info("[HelpModule] /" + commandName + " help registriert (Admin-Permission: " + adminPermission + ")");
}
@Override
public void onDisable(Plugin plugin) {}
// ─────────────────────────────────────────────────────────────────────────
private static class HelpCommand extends Command {
private final String adminPerm;
// Jede Seite ist eine Liste von Zeilen
// Seiten werden zur Laufzeit je nach Berechtigung zusammengebaut
HelpCommand(String name, String adminPerm) {
super(name, null);
this.adminPerm = adminPerm;
}
@Override
public void execute(CommandSender sender, String[] args) {
if (args.length == 0) {
send(sender, "&7Nutze &e/" + getName() + " help &7f\u00fcr eine Befehls\u00fcbersicht.");
return;
}
if (!args[0].equalsIgnoreCase("help")) {
send(sender, "&cUnbekannter Unterbefehl. Nutze &e/" + getName() + " help&c.");
return;
}
boolean isAdmin = !(sender instanceof ProxiedPlayer)
|| sender.hasPermission(adminPerm)
|| sender.hasPermission("statusapi.admin");
// Seiten aufbauen
List<List<String>> pages = buildPages(isAdmin);
int totalPages = pages.size();
int page = 1;
if (args.length >= 2) {
try {
page = Integer.parseInt(args[1].trim());
} catch (NumberFormatException e) {
send(sender, "&cUng\u00fcltige Seitenzahl. Nutze &e/" + getName() + " help <1-" + totalPages + ">&c.");
return;
}
}
if (page < 1 || page > totalPages) {
send(sender, "&cSeite &e" + page + " &cexistiert nicht. Verf\u00fcgbar: &e1&c-&e" + totalPages + "&c.");
return;
}
// Header
send(sender, "");
send(sender, "&8&m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
send(sender, " &6&lStatusAPI &8| &7Hilfe &8 &7Seite &e" + page + "&8/&e" + totalPages);
send(sender, "&8&m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
send(sender, "");
// Seiteninhalte
for (String line : pages.get(page - 1)) {
send(sender, line);
}
send(sender, "");
// Navigation
sendNavigation(sender, getName(), page, totalPages);
send(sender, "");
}
/** Baut alle Seiten zusammen. Admins bekommen zus\u00e4tzliche Seiten. */
private List<List<String>> buildPages(boolean isAdmin) {
List<List<String>> pages = new ArrayList<>();
// ── Seite 1: Allgemein & Chat ─────────────────────────────────────
List<String> p1 = new ArrayList<>();
p1.add(" &e&lAllgemein");
p1.add(" &a/verify <token> &8 &7Account verifizieren");
p1.add(" &a/forumlink &8(&7/fl&8) &8 &7Forum-Account verkn\u00fcpfen");
p1.add(" &a/forum &8 &7Forum-Benachrichtigungen");
p1.add(" &a/go [server] &8(&7/wechsel, /switch&8) &8 &7Serverwechsel");
p1.add(" &a/scoreboard &8(&7/sb&8) [hide|show] &8 &7Scoreboard umschalten");
p1.add("");
p1.add(" &e&lChat");
p1.add(" &a/msg <Spieler> <Text> &8(&7/w, /tell&8) &8 &7Private Nachricht");
p1.add(" &a/r <Text> &8(&7/reply, /antwort&8) &8 &7Auf PN antworten");
p1.add(" &a/ignore <Spieler> &8(&7/block&8) &8 &7Spieler ignorieren");
p1.add(" &a/unignore <Spieler> &8(&7/unblock&8) &8 &7Ignorierung aufheben");
p1.add(" &a/channel [kanal] &8(&7/ch, /kanal&8) &8 &7Kanal wechseln");
p1.add(" &a/chataus &8(&7/togglechat&8) &8 &7Chat-Empfang umschalten");
pages.add(p1);
// ── Seite 2: Chat (weiter) & Account-Verkn\u00fcpfungen ───────────────
List<String> p2 = new ArrayList<>();
p2.add(" &e&lChat (Fortsetzung)");
p2.add(" &a/emoji &8(&7/emojis&8) &8 &7Alle Emojis anzeigen");
p2.add(" &a/mentions &8(&7/mention&8) &8 &7Mention-Benachrichtigungen");
p2.add(" &a/helpop <Nachricht> &8 &7Team um Hilfe bitten");
p2.add(" &a/report <Spieler> <Grund> &8 &7Spieler melden");
p2.add(" &a/chatbypass &8(&7/cbp&8) &8 &7ChatModule \u00fcberspringen");
p2.add("");
p2.add(" &e&lAccount-Verkn\u00fcpfungen");
p2.add(" &a/discordlink &8(&7/dlink&8) &8 &7Discord verkn\u00fcpfen");
p2.add(" &a/telegramlink &8(&7/tlink&8) &8 &7Telegram verkn\u00fcpfen");
p2.add(" &a/unlink <discord|telegram|all> &8 &7Verkn\u00fcpfung aufheben");
pages.add(p2);
// ── Admin-Seiten nur f\u00fcr Berechtigte ──────────────────────────────
if (isAdmin) {
// ── Seite 3: StatusAPI, AntiBot, Vanish ───────────────────────
List<String> p3 = new ArrayList<>();
p3.add(" &c&lAdmin &8 &eStatusAPI & AntiBot");
p3.add(" &c/statusapi reload &8(&7/sapi reload&8) &8 &7Scoreboard & Tablist neu laden");
p3.add(" &c/netinfo &8 &7Proxy- & Systeminfos");
p3.add("");
p3.add(" &c/antibot status &8 &7AntiBot-Status anzeigen");
p3.add(" &c/antibot clearblocks &8 &7IP-Blockliste leeren");
p3.add(" &c/antibot unblock <IP> &8 &7IP entsperren");
p3.add(" &c/antibot profile &8 &7Schutzprofil wechseln");
p3.add(" &c/antibot reload &8 &7AntiBot neu laden");
p3.add("");
p3.add(" &c&lAdmin &8 &eVanish");
p3.add(" &c/vanish [Spieler] &8(&7/v&8) &8 &7Unsichtbar schalten");
p3.add(" &c/vanishlist &8(&7/vlist&8) &8 &7Unsichtbare Spieler anzeigen");
pages.add(p3);
// ── Seite 4: Chat-Admin, Reports, sonstige ────────────────────
List<String> p4 = new ArrayList<>();
p4.add(" &c&lAdmin &8 &eChat-Administration");
p4.add(" &c/broadcast <Text> &8(&7/bc, /alert&8) &8 &7Broadcast an alle");
p4.add(" &c/chatmute <Spieler> [Min.] &8(&7/gmute&8) &8 &7Spieler muten");
p4.add(" &c/chatunmute <Spieler> &8(&7/gunmute&8) &8 &7Mute aufheben");
p4.add(" &c/socialspy &8(&7/spy&8) &8 &7Private Nachrichten mitlesen");
p4.add(" &c/chatinfo <Spieler> &8 &7Chat-Info eines Spielers");
p4.add(" &c/chathist [Spieler] [n] &8 &7Chat-Verlauf anzeigen");
p4.add(" &c/chatreload &8 &7Chat-Konfiguration neu laden");
p4.add("");
p4.add(" &c&lAdmin &8 &eReports, Tools");
p4.add(" &c/reports [all] &8 &7Offene Reports anzeigen");
p4.add(" &c/reportclose <ID> &8 &7Report schlie\u00dfen");
p4.add(" &c/automessage reload &8 &7AutoMessage neu laden");
p4.add(" &c/bcmds reload &8 &7Custom-Commands neu laden");
p4.add(" &c/cb <Befehl> &8 &7Command-Blocker verwalten");
p4.add(" &c/scoreboard admin|player &8 &7Admin/Spieler-Ansicht wechseln");
pages.add(p4);
}
return pages;
}
/** Sendet eine klickbare Navigationszeile mit ◀ Seite X/Y ▶ */
private void sendNavigation(CommandSender sender, String cmd, int page, int total) {
// F\u00fcr Konsole: einfacher Text
if (!(sender instanceof ProxiedPlayer)) {
String nav = " ";
if (page > 1) nav += "&7[&e◀&7] ";
nav += "&8Seite &e" + page + "&8/&e" + total;
if (page < total) nav += " &7[&e▶&7]";
send(sender, nav);
return;
}
// F\u00fcr Spieler: klickbare Buttons
TextComponent line = new TextComponent(" ");
// ◀ zur\u00fcck
if (page > 1) {
TextComponent prev = new TextComponent(
ChatColor.translateAlternateColorCodes('&', "&7[&e◀&7] "));
prev.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
"/" + cmd + " help " + (page - 1)));
prev.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new ComponentBuilder(ChatColor.translateAlternateColorCodes('&',
"&7Seite &e" + (page - 1) + " &7anzeigen")).create()));
line.addExtra(prev);
}
// Seitenanzeige
TextComponent mid = new TextComponent(
ChatColor.translateAlternateColorCodes('&',
"&8Seite &e" + page + "&8/&e" + total));
line.addExtra(mid);
// ▶ vor
if (page < total) {
TextComponent next = new TextComponent(
ChatColor.translateAlternateColorCodes('&', " &7[&e▶&7]"));
next.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
"/" + cmd + " help " + (page + 1)));
next.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new ComponentBuilder(ChatColor.translateAlternateColorCodes('&',
"&7Seite &e" + (page + 1) + " &7anzeigen")).create()));
line.addExtra(next);
}
sender.sendMessage(line);
}
private static void send(CommandSender s, String text) {
s.sendMessage(new TextComponent(
ChatColor.translateAlternateColorCodes('&', text)));
}
}
}

View File

@@ -0,0 +1,445 @@
package net.viper.status.modules.network;
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.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.md_5.bungee.event.EventPriority;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.model.user.User;
import net.luckperms.api.node.Node;
import net.luckperms.api.node.types.PermissionNode;
import net.viper.status.StatusAPI;
import net.viper.status.module.Module;
import net.viper.status.modules.antibot.AntiBotModule;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/**
* MultiAccountGuard
*
* Features:
* - IP-Check: blockiert zweiten Account von gleicher IP
* - Bypass NUR \u00fcber LuckPerms (OP z\u00e4hlt nicht)
* - Persistentes Log in multiaccountguard.log
* - Staff-Benachrichtigung ingame (Permission: statusapi.staff.notify)
* - Tempor\u00e4rer IP-Bann nach X Versuchen (Integration mit AntiBotModule)
* - Discord-Webhook bei Konflikt
*/
public class MultiAccountGuard implements Module, Listener {
private static final String CONFIG_FILE = "network-guard.properties";
private static final String LOG_FILE = "multiaccountguard.log";
public static final String BYPASS_PERM = "statusapi.multiaccountguard.bypass";
public static final String STAFF_PERM = "statusapi.staff.notify";
private static final DateTimeFormatter LOG_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
private Plugin plugin;
private Logger log;
private File logFile;
// Config
private boolean enabled = true;
private boolean checkIp = true;
private boolean kickExisting = false;
private String kickMessage = "&cDu bist bereits mit einem anderen Account online!\n&7Bitte trenne deinen anderen Account zuerst.";
// Staff-Benachrichtigung
private boolean staffNotifyEnabled = true;
private String staffNotifyFormat = "&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip}";
// Tempor\u00e4rer IP-Bann
private boolean tempBanEnabled = true;
private int tempBanMaxAttempts = 3;
private int tempBanDurationSecs = 300;
/** IP → Anzahl Konflikte seit letztem Reset */
private final Map<String, Integer> attemptsByIp = new ConcurrentHashMap<>();
// Webhook
private boolean webhookEnabled = true;
private String webhookUrl = "";
private String webhookUsername = "StatusAPI";
private String webhookThumbnailUrl = "";
@Override public String getName() { return "MultiAccountGuard"; }
// -------------------------------------------------------------------------
// Enable / Disable
// -------------------------------------------------------------------------
@Override
public void onEnable(Plugin plugin) {
this.plugin = plugin;
this.log = plugin.getLogger();
this.logFile = new File(plugin.getDataFolder(), LOG_FILE);
loadConfig();
if (!enabled) {
log.info("[MultiAccountGuard] Deaktiviert.");
return;
}
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
log.info("[MultiAccountGuard] Aktiv | IP-Check=" + checkIp
+ " | kickExisting=" + kickExisting
+ " | staffNotify=" + staffNotifyEnabled
+ " | tempBan=" + tempBanEnabled + "(max=" + tempBanMaxAttempts + ", " + tempBanDurationSecs + "s)"
+ " | Webhook=" + (webhookEnabled && !webhookUrl.isEmpty()));
log.info("[MultiAccountGuard] Bypass NUR via LuckPerms: /lp user <Name> permission set " + BYPASS_PERM + " true");
log.info("[MultiAccountGuard] Log-Datei: " + logFile.getAbsolutePath());
}
@Override
public void onDisable(Plugin plugin) {}
// -------------------------------------------------------------------------
// Event
// -------------------------------------------------------------------------
@EventHandler(priority = EventPriority.HIGHEST)
public void onPostLogin(PostLoginEvent event) {
if (!enabled) return;
ProxiedPlayer joining = event.getPlayer();
if (hasBypass(joining)) {
log.info("[MultiAccountGuard] " + joining.getName() + " hat Bypass (LuckPerms) \u00fcbersprungen.");
return;
}
UUID joiningUuid = joining.getUniqueId();
String joiningIp = extractIp(joining.getSocketAddress());
if (joiningIp == null) {
log.warning("[MultiAccountGuard] Konnte IP von " + joining.getName() + " nicht lesen \u00fcbersprungen.");
return;
}
log.info("[MultiAccountGuard] Login-Check: " + joining.getName()
+ " | UUID=" + joiningUuid + " | IP=" + joiningIp);
// Alle anderen Spieler (sich selbst per UUID ausschlie\u00dfen)
List<ProxiedPlayer> others = new ArrayList<>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (p.getUniqueId().equals(joiningUuid)) continue;
others.add(p);
}
for (ProxiedPlayer online : others) {
if (hasBypass(online)) continue;
String onlineIp = extractIp(online.getSocketAddress());
if (onlineIp == null) continue;
if (checkIp && joiningIp.equals(onlineIp)) {
log.warning("[MultiAccountGuard] KONFLIKT: "
+ joining.getName() + " (" + joiningUuid + ")"
+ " <-> " + online.getName() + " (" + online.getUniqueId() + ")"
+ " IP=" + joiningIp);
handleConflict(joining, online, joiningIp);
return;
}
}
log.info("[MultiAccountGuard] " + joining.getName() + " kein Konflikt.");
}
// -------------------------------------------------------------------------
// Konflikt behandeln
// -------------------------------------------------------------------------
private void handleConflict(ProxiedPlayer joining, ProxiedPlayer existing, String ip) {
TextComponent msg = new TextComponent(
ChatColor.translateAlternateColorCodes('&', kickMessage));
final String blockedName, allowedName;
final UUID blockedUuid, allowedUuid;
if (kickExisting) {
existing.disconnect(msg);
blockedName = existing.getName(); blockedUuid = existing.getUniqueId();
allowedName = joining.getName(); allowedUuid = joining.getUniqueId();
log.warning("[MultiAccountGuard] Bestehender Account " + existing.getName() + " getrennt.");
} else {
joining.disconnect(msg);
blockedName = joining.getName(); blockedUuid = joining.getUniqueId();
allowedName = existing.getName(); allowedUuid = existing.getUniqueId();
log.warning("[MultiAccountGuard] Neuer Account " + joining.getName() + " blockiert.");
}
// 1. Persistentes Log
writeLog(blockedName, blockedUuid, allowedName, allowedUuid, ip);
// 2. Staff-Benachrichtigung
if (staffNotifyEnabled) {
notifyStaff(blockedName, allowedName, ip);
}
// 3. Tempor\u00e4rer IP-Bann
if (tempBanEnabled) {
int attempts = attemptsByIp.merge(ip, 1, Integer::sum);
log.info("[MultiAccountGuard] IP " + ip + " hat " + attempts + "/" + tempBanMaxAttempts + " Versuche.");
if (attempts >= tempBanMaxAttempts) {
attemptsByIp.remove(ip);
banIp(ip);
}
}
// 4. Discord-Webhook (async)
if (webhookEnabled && webhookUrl != null && !webhookUrl.isEmpty()) {
final String bn = blockedName, an = allowedName;
final UUID bu = blockedUuid, au = allowedUuid;
final int att = attemptsByIp.getOrDefault(ip, tempBanMaxAttempts);
ProxyServer.getInstance().getScheduler().runAsync(plugin,
() -> sendWebhook(bn, bu, an, au, ip, att));
}
}
// -------------------------------------------------------------------------
// 1. Persistentes Log
// -------------------------------------------------------------------------
private void writeLog(String blockedName, UUID blockedUuid,
String allowedName, UUID allowedUuid, String ip) {
try {
if (!logFile.getParentFile().exists()) logFile.getParentFile().mkdirs();
String line = String.format("[%s] KONFLIKT | Geblockt: %s (%s) | Online: %s (%s) | IP: %s%n",
LOG_FMT.format(Instant.now()),
blockedName, blockedUuid,
allowedName, allowedUuid,
ip);
Files.write(logFile.toPath(), line.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (Exception e) {
log.warning("[MultiAccountGuard] Log-Fehler: " + e.getMessage());
}
}
// -------------------------------------------------------------------------
// 2. Staff-Benachrichtigung
// -------------------------------------------------------------------------
private void notifyStaff(String blockedName, String existingName, String ip) {
String raw = staffNotifyFormat
.replace("{blocked}", blockedName)
.replace("{existing}", existingName)
.replace("{ip}", ip);
String formatted = ChatColor.translateAlternateColorCodes('&', raw);
TextComponent msg = new TextComponent(formatted);
int notified = 0;
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (p.hasPermission(STAFF_PERM)) {
p.sendMessage(msg);
notified++;
}
}
log.info("[MultiAccountGuard] Staff-Benachrichtigung gesendet an " + notified + " Spieler.");
}
// -------------------------------------------------------------------------
// 3. Tempor\u00e4rer IP-Bann via AntiBotModule
// -------------------------------------------------------------------------
private void banIp(String ip) {
try {
StatusAPI statusApi = (StatusAPI) ProxyServer.getInstance()
.getPluginManager().getPlugin("StatusAPI");
if (statusApi == null) {
log.warning("[MultiAccountGuard] StatusAPI nicht gefunden IP-Bann nicht m\u00f6glich.");
return;
}
AntiBotModule antiBot = statusApi.getModuleManager().getModule(AntiBotModule.class);
if (antiBot == null) {
log.warning("[MultiAccountGuard] AntiBotModule nicht gefunden IP-Bann nicht m\u00f6glich.");
return;
}
antiBot.blockIpExternal(ip, tempBanDurationSecs);
log.warning("[MultiAccountGuard] IP " + ip + " f\u00fcr " + tempBanDurationSecs + "s gebannt (zu viele Multi-Account-Versuche).");
// Staff \u00fcber den Bann informieren
String banMsg = ChatColor.translateAlternateColorCodes('&',
"&8[&cMAG&8] &7IP &f" + ip + " &7wurde f\u00fcr &c" + tempBanDurationSecs + "s &7gebannt &8(zu viele Versuche).");
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (p.hasPermission(STAFF_PERM)) {
p.sendMessage(new TextComponent(banMsg));
}
}
// In Log schreiben
try {
String line = String.format("[%s] IP-BANN | IP: %s | Dauer: %ds%n",
LOG_FMT.format(Instant.now()), ip, tempBanDurationSecs);
Files.write(logFile.toPath(), line.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (Exception ignored) {}
} catch (Exception e) {
log.warning("[MultiAccountGuard] IP-Bann Fehler: " + e.getMessage());
}
}
// -------------------------------------------------------------------------
// 4. Discord-Webhook
// -------------------------------------------------------------------------
private void sendWebhook(String blockedName, UUID blockedUuid,
String allowedName, UUID allowedUuid,
String ip, int attempts) {
StringBuilder fields = new StringBuilder();
appendField(fields, "\uD83D\uDEAB Geblockter Account",
blockedName + "\n`" + blockedUuid + "`", false);
appendField(fields, "\u2705 Verbundener Account",
allowedName + "\n`" + allowedUuid + "`", false);
appendField(fields, "\uD83C\uDF10 IP", "`" + ip + "`", true);
appendField(fields, "Aktion",
kickExisting ? "Alter Account getrennt" : "Neuer Account blockiert", true);
if (tempBanEnabled) {
appendField(fields, "\u26A0\uFE0F Versuche",
attempts + " / " + tempBanMaxAttempts
+ (attempts >= tempBanMaxAttempts ? " \u2192 IP gebannt!" : ""), true);
}
String body = "{\"username\":\"" + esc(webhookUsername) + "\","
+ "\"embeds\":[{"
+ "\"title\":\"\uD83D\uDD12 Multi-Account erkannt\","
+ "\"description\":\"Ein Spieler hat versucht mit einem zweiten Account beizutreten.\","
+ "\"color\":15158332,"
+ "\"fields\":[" + fields + "],"
+ "\"footer\":{\"text\":\"StatusAPI \u2022 MultiAccountGuard\"},"
+ "\"timestamp\":\"" + Instant.now() + "\""
+ (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()
? ",\"thumbnail\":{\"url\":\"" + esc(webhookThumbnailUrl) + "\"}" : "")
+ "}]}";
HttpURLConnection conn = null;
try {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
conn = (HttpURLConnection) new URL(webhookUrl).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) log.warning("[MultiAccountGuard] Webhook HTTP " + code);
else log.info("[MultiAccountGuard] Webhook gesendet (HTTP " + code + ")");
} catch (Exception e) {
log.warning("[MultiAccountGuard] Webhook-Fehler: " + e.getMessage());
} finally {
if (conn != null) conn.disconnect();
}
}
private void appendField(StringBuilder sb, String name, String value, boolean inline) {
if (sb.length() > 0) sb.append(",");
sb.append("{\"name\":\"").append(esc(name))
.append("\",\"value\":\"").append(esc(value))
.append("\",\"inline\":").append(inline).append("}");
}
private String esc(String s) {
if (s == null) return "";
return s.replace("\\","\\\\").replace("\"","\\\"")
.replace("\n","\\n").replace("\r","\\r");
}
// -------------------------------------------------------------------------
// Bypass NUR LuckPerms, kein OP-Fallback
// -------------------------------------------------------------------------
private boolean hasBypass(ProxiedPlayer player) {
try {
User user = LuckPermsProvider.get().getUserManager().getUser(player.getUniqueId());
if (user == null) return false;
for (Node node : user.getNodes()) {
if (node instanceof PermissionNode) {
PermissionNode pn = (PermissionNode) node;
if (pn.getPermission().equalsIgnoreCase(BYPASS_PERM) && pn.getValue()) {
return true;
}
}
}
return false;
} catch (Exception e) {
log.warning("[MultiAccountGuard] LuckPerms-Check fehlgeschlagen f\u00fcr " + player.getName() + ": " + e.getMessage());
return false;
}
}
// -------------------------------------------------------------------------
// Config
// -------------------------------------------------------------------------
private void loadConfig() {
File file = new File(plugin.getDataFolder(), CONFIG_FILE);
if (!file.exists()) {
log.info("[MultiAccountGuard] Config nicht gefunden Defaults werden verwendet.");
return;
}
Properties p = new Properties();
try (FileInputStream fis = new FileInputStream(file);
InputStreamReader r = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
p.load(r);
enabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.enabled", "true"));
checkIp = Boolean.parseBoolean(p.getProperty("multiaccountguard.check_ip", "true"));
kickExisting = Boolean.parseBoolean(p.getProperty("multiaccountguard.kick_existing", "false"));
kickMessage = p.getProperty("multiaccountguard.kick_message", kickMessage);
staffNotifyEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.staff_notify.enabled", "true"));
staffNotifyFormat = p.getProperty("multiaccountguard.staff_notify.format", staffNotifyFormat);
tempBanEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.tempban.enabled", "true"));
tempBanMaxAttempts = parseInt(p.getProperty("multiaccountguard.tempban.max_attempts", "3"), 3);
tempBanDurationSecs = parseInt(p.getProperty("multiaccountguard.tempban.duration_secs", "300"), 300);
webhookEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.webhook.enabled", "true"));
webhookUrl = p.getProperty("networkinfo.webhook.url", "").trim();
webhookUsername = p.getProperty("networkinfo.webhook.username", "StatusAPI").trim();
webhookThumbnailUrl = p.getProperty("networkinfo.webhook.thumbnail_url", "").trim();
} catch (Exception e) {
log.warning("[MultiAccountGuard] Config-Fehler: " + e.getMessage());
}
}
private int parseInt(String s, int fallback) {
try { return Integer.parseInt(s == null ? "" : s.trim()); }
catch (Exception ignored) { return fallback; }
}
// -------------------------------------------------------------------------
private String extractIp(SocketAddress addr) {
if (addr instanceof InetSocketAddress)
return ((InetSocketAddress) addr).getAddress().getHostAddress();
return null;
}
public boolean isEnabled() { return enabled; }
public boolean isCheckIp() { return checkIp; }
public boolean isKickExisting() { return kickExisting; }
}

View File

@@ -0,0 +1,862 @@
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\u00fcr 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: \u00d6ffentlicher 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\u00dcberwachung 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",
"\u00dcberwachung und Webhook-Alerts sind jetzt aktiv.",
0x2ECC71,
fields.toString(),
false
);
}
private boolean sendLifecycleStopNotification() {
if (isCompactEmbedMode()) {
return sendWebhookEmbed(
webhookUrl,
"🛑 NetworkInfo gestoppt",
"Die NetworkInfo-\u00dcberwachung wurde gestoppt.\nKeine weiteren Auto-Alerts bis zum n\u00e4chsten 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-\u00dcberwachung 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\u00f6hnlich 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<Map<String, Object>> playerNames = new ArrayList<Map<String, Object>>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
Map<String, Object> entry = new LinkedHashMap<String, Object>();
entry.put("name", p.getName());
try { entry.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {}
try {
if (p.getServer() != null && p.getServer().getInfo() != null) {
entry.put("server", p.getServer().getInfo().getName());
}
} catch (Exception ignored) {}
playerNames.add(entry);
}
out.put("player_names", playerNames);
}
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 \u00fcberschritten.",
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

View File

@@ -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\u00fc &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\u00fc &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\u00fcr Spieler verf\u00fcgbar."));
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\u00fcr 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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,272 @@
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\u00fcr StatusAPI (BungeeCord)
*
* Features:
* - /vanish zum Ein-/Ausschalten
* - /vanish <Spieler> f\u00fcr 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\u00e4chsten 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())) {
// Status SOFORT setzen kein Delay, damit das ChatModule (2s-Task)
// den Vanish-Status garantiert vorfindet und keine Join-Nachricht sendet.
VanishProvider.setVanished(player.getUniqueId(), true);
// Nur die Best\u00e4tigungsnachricht an den Spieler wird verz\u00f6gert,
// 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\u00e4chsten 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\u00fcr /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\u00fcr 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\u00fchrenden
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\u00dfer dem Ausf\u00fchrenden)
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();
}
/**
* \u00d6ffentliche API f\u00fcr 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));
}
}

View File

@@ -1,248 +1,196 @@
package net.viper.status.modules.verify;
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.plugin.Command;
import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.module.Module;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* VerifyModule: Multi-Server Support.
* Liest pro Server die passende ID und das Secret aus der verify.properties.
*/
public class VerifyModule implements Module {
private String wpVerifyUrl;
// Speichert für jeden Servernamen (z.B. "Lobby") die passende Konfiguration
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
@Override
public String getName() {
return "VerifyModule";
}
@Override
public void onEnable(Plugin plugin) {
loadConfig(plugin);
// Befehl registrieren
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
}
@Override
public void onDisable(Plugin plugin) {
// Befehl muss nicht manuell entfernt werden, BungeeCord übernimmt das beim Plugin-Stop
}
// --- Konfiguration Laden & Kopieren ---
private void loadConfig(Plugin plugin) {
String fileName = "verify.properties";
File configFile = new File(plugin.getDataFolder(), fileName);
Properties props = new Properties();
// 1. Datei kopieren, falls sie noch nicht existiert
if (!configFile.exists()) {
plugin.getDataFolder().mkdirs();
try (InputStream in = plugin.getResourceAsStream(fileName);
OutputStream out = new FileOutputStream(configFile)) {
if (in == null) {
plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR gefunden. Erstelle manuell.");
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 Config: " + e.getMessage());
return;
}
}
// 2. Eigentliche Config laden
try (InputStream in = new FileInputStream(configFile)) {
props.load(in);
} catch (IOException e) {
e.printStackTrace();
return;
}
// Globale URL
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
// Server-Configs parsen (z.B. server.Lobby.id)
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];
String type = parts[2];
// Eintrag in der Map erstellen oder holen
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
if ("id".equalsIgnoreCase(type)) {
try {
config.serverId = Integer.parseInt(props.getProperty(key));
} catch (NumberFormatException e) {
plugin.getLogger().warning("Ungültige Server ID für " + serverName);
}
} else if ("secret".equalsIgnoreCase(type)) {
config.sharedSecret = props.getProperty(key);
}
}
}
}
}
// --- Hilfsklasse für die Daten eines Servers ---
private static class ServerConfig {
int serverId = 0;
String sharedSecret = "";
}
// --- Die Command Klasse ---
private class VerifyCommand extends Command {
public VerifyCommand() {
super("verify");
}
@Override
public void execute(CommandSender sender, String[] args) {
if (!(sender instanceof ProxiedPlayer)) {
sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen.");
return;
}
ProxiedPlayer p = (ProxiedPlayer) sender;
if (args.length != 1) {
p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>");
return;
}
// --- WICHTIG: Servernamen ermitteln ---
String serverName = p.getServer().getInfo().getName();
// Konfiguration für diesen Server laden
ServerConfig config = serverConfigs.get(serverName);
// Check ob Konfig existiert
if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) {
p.sendMessage(ChatColor.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt.");
p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + serverName);
p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin.");
return;
}
String token = args[0].trim();
String playerName = p.getName();
HttpURLConnection conn = null;
try {
Charset utf8 = Charset.forName("UTF-8");
// Wir signieren Name + Token mit dem SERVER-SPECIFISCHEN Secret
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
// Payload aufbauen mit der SERVER-SPECIFISCHEN ID
String payload = "{\"player\":\"" + escapeJson(playerName) + "\",\"token\":\"" + escapeJson(token) + "\",\"server_id\":" + config.serverId + ",\"signature\":\"" + signature + "\"}";
URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(7000);
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
try (OutputStream os = conn.getOutputStream()) {
os.write(payload.getBytes(utf8));
}
int code = conn.getResponseCode();
String resp;
if (code >= 200 && code < 300) {
resp = streamToString(conn.getInputStream(), utf8);
} else {
resp = streamToString(conn.getErrorStream(), utf8);
}
// Antwort verarbeiten
if (resp != null && !resp.isEmpty() && resp.trim().startsWith("{")) {
boolean isSuccess = resp.contains("\"success\":true");
String message = "Ein unbekannter Fehler ist aufgetreten.";
int keyIndex = resp.indexOf("\"message\":\"");
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");
}
}
package net.viper.status.modules.verify;
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.connection.ProxiedPlayer;
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.Plugin;
import net.viper.status.module.Module;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* VerifyModule: Multi-Server Support.
*
* Fix #7: Servernamen werden jetzt case-insensitiv verglichen.
* Keys in serverConfigs werden beim Laden auf lowercase normalisiert
* und die Suche erfolgt ebenfalls lowercase.
*/
public class VerifyModule implements Module {
private String wpVerifyUrl;
// Keys sind lowercase normalisiert f\u00fcr case-insensitiven Vergleich
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
@Override
public String getName() { return "VerifyModule"; }
@Override
public void onEnable(Plugin plugin) {
loadConfig(plugin);
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
plugin.getLogger().fine("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
}
@Override
public void onDisable(Plugin plugin) {}
private void loadConfig(Plugin plugin) {
String fileName = "verify.properties";
File configFile = new File(plugin.getDataFolder(), fileName);
Properties props = new Properties();
if (!configFile.exists()) {
plugin.getDataFolder().mkdirs();
try (InputStream in = plugin.getResourceAsStream(fileName);
OutputStream out = new FileOutputStream(configFile)) {
if (in == null) { plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR."); return; }
byte[] buffer = new byte[1024]; int length;
while ((length = in.read(buffer)) > 0) out.write(buffer, 0, length);
StatusAPI.debugLog(plugin, "Konfigurationsdatei '" + fileName + "' erstellt.");
} catch (Exception e) { plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage()); return; }
}
try (InputStream in = new FileInputStream(configFile)) {
props.load(in);
} catch (IOException e) { e.printStackTrace(); return; }
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
// FIX #7: Keys beim Laden auf lowercase normalisieren
this.serverConfigs.clear();
for (String key : props.stringPropertyNames()) {
if (key.startsWith("server.")) {
String[] parts = key.split("\\.");
if (parts.length == 3) {
// Servername lowercase case-insensitiver Lookup
String serverName = parts[1].toLowerCase();
String type = parts[2];
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
if ("id".equalsIgnoreCase(type)) {
try { config.serverId = Integer.parseInt(props.getProperty(key)); }
catch (NumberFormatException e) { plugin.getLogger().warning("Ung\u00fcltige Server ID f\u00fcr " + serverName); }
} else if ("secret".equalsIgnoreCase(type)) {
config.sharedSecret = props.getProperty(key);
}
}
}
}
}
private static class ServerConfig {
int serverId = 0;
String sharedSecret = "";
}
private class VerifyCommand extends Command {
public VerifyCommand() { super("verify"); }
@Override
public void execute(CommandSender sender, String[] args) {
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(ChatColor.RED + "Nur Spieler k\u00f6nnen diesen Befehl benutzen."); return; }
ProxiedPlayer p = (ProxiedPlayer) sender;
if (args.length != 1) { p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>"); return; }
// FIX #7: Servername lowercase f\u00fcr 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.");
p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + p.getServer().getInfo().getName());
p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin.");
return;
}
String token = args[0].trim();
String playerName = p.getName();
HttpURLConnection conn = null;
try {
Charset utf8 = Charset.forName("UTF-8");
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
String payload = "{\"player\":\"" + escapeJson(playerName)
+ "\",\"token\":\"" + escapeJson(token)
+ "\",\"server_id\":" + config.serverId
+ ",\"signature\":\"" + signature + "\"}";
URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(7000);
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=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.isEmpty() && resp.trim().startsWith("{")) {
boolean isSuccess = resp.contains("\"success\":true");
String message = "Ein unbekannter Fehler ist aufgetreten.";
int keyIndex = resp.indexOf("\"message\":\"");
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();
}
}
}
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");
}
}

View File

@@ -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;
}
}

View 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\u00fcgt.
* fromLine() ist r\u00fcckw\u00e4rtskompatibel (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\u00fcltiger 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;
}
}
}

View File

@@ -1,35 +1,35 @@
package net.viper.status.stats;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class StatsManager {
private final ConcurrentHashMap<UUID, PlayerStats> map = new ConcurrentHashMap<>();
public PlayerStats get(UUID uuid, String name) {
return map.compute(uuid, (k, v) -> {
if (v == null) {
return new PlayerStats(uuid, name != null ? name : "");
} else {
if (name != null && !name.isEmpty()) v.name = name;
return v;
}
});
}
public PlayerStats getIfPresent(UUID uuid) {
return map.get(uuid);
}
public Iterable<PlayerStats> all() {
return map.values();
}
public void put(PlayerStats ps) {
map.put(ps.uuid, ps);
}
public void remove(UUID uuid) {
map.remove(uuid);
}
package net.viper.status.stats;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class StatsManager {
private final ConcurrentHashMap<UUID, PlayerStats> map = new ConcurrentHashMap<>();
public PlayerStats get(UUID uuid, String name) {
return map.compute(uuid, (k, v) -> {
if (v == null) {
return new PlayerStats(uuid, name != null ? name : "");
} else {
if (name != null && !name.isEmpty()) v.name = name;
return v;
}
});
}
public PlayerStats getIfPresent(UUID uuid) {
return map.get(uuid);
}
public Iterable<PlayerStats> all() {
return map.values();
}
public void put(PlayerStats ps) {
map.put(ps.uuid, ps);
}
public void remove(UUID uuid) {
map.remove(uuid);
}
}

View 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\u00fcr 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\u00e4nge nach einem Crash noch gutschreiben (24 Stunden).
* L\u00e4ngere 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\u00e4chsten Start
// sind alle Spieler offline, aber currentSessionStart enth\u00e4lt noch den alten
// Timestamp. getPlaytimeWithCurrentSession() w\u00fcrde dann f\u00e4lschlicherweise
// (now - alter_crash_timestamp) zur Spielzeit addieren → massiv falscher Wert.
//
// Fix: Nach dem Laden jeden Eintrag pr\u00fcfen. Falls currentSessionStart > 0:
// - Plausible Differenz (≤ MAX_SESSION_SECONDS) → als echte Zeit gutschreiben
// - Unplausibel (> MAX_SESSION_SECONDS) → verwerfen, nur zur\u00fccksetzen
// - In beiden F\u00e4llen: 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\u00fcr " + ps.name
+ " (delta=" + delta + "s > " + MAX_SESSION_SECONDS + "s). "
+ "Session wird ohne Gutschrift zur\u00fcckgesetzt."
);
}
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) {}
}
}

View File

@@ -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();
}
}
}
}

View 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

View 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

View 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!

View File

@@ -0,0 +1,140 @@
# ===========================
# 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 für Status-, Warn- und Attack-Meldungen
networkinfo.webhook.enabled=false
networkinfo.webhook.url=
networkinfo.webhook.username=
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=
# API-Key fuer POST /network/attack
networkinfo.attack.api_key=
# Zeitzone (IANA-Format, z.B. Europe/Berlin, UTC, America/New_York)
# Vollstaendige Liste: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
login.log.timezone=Europe/Berlin
# ===========================
# 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=false
backendguard.log_blocked_attempts=false
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=
# ===========================
# MULTI ACCOUNT GUARD
# ===========================
# Verhindert, dass ein Spieler mit zwei Accounts gleichzeitig online ist.
multiaccountguard.enabled=false
# IP-Check: Gleiche IP mit unterschiedlichem Namen -> blockieren
multiaccountguard.check_ip=false
# UUID-Check: Gleiche UUID mit unterschiedlichem Namen -> blockieren (Bedrock-Edge-Cases)
multiaccountguard.check_uuid=false
# true = bestehenden (alten) Account rauswerfen, neuen reinlassen
# false = neuen Account blockieren (Standard)
multiaccountguard.kick_existing=false
# Kick-Nachricht (& fuer Farbcodes, \n fuer Zeilenumbruch)
multiaccountguard.kick_message=&cDu bist bereits mit einem anderen Account online!\n&7Bitte trenne deinen anderen Account zuerst.
# Staff-Benachrichtigung bei Konflikt (Permission: statusapi.staff.notify)
multiaccountguard.staff_notify.enabled=false
multiaccountguard.staff_notify.format=&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip}
# Temporaerer IP-Bann nach X Versuchen (Integration mit AntiBotModule)
# max_attempts: Anzahl Konflikte bevor die IP gebannt wird
# duration_secs: Bann-Dauer in Sekunden
multiaccountguard.tempban.enabled=false
multiaccountguard.tempban.max_attempts=3
multiaccountguard.tempban.duration_secs=300
# Discord-Meldung bei jedem Konflikt (nutzt networkinfo.webhook.url automatisch)
multiaccountguard.webhook.enabled=false

View File

@@ -0,0 +1,324 @@
name: StatusAPI
main: net.viper.status.StatusAPI
version: 4.1.4
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:
# ── AfkModule ──────────────────────────────────────────────
afk:
description: AFK-Modus ein- oder ausschalten
usage: /afk
# ── HelpModule ────────────────────────────────────────────
help:
description: Zeigt alle verfügbaren Befehle (Admin-Befehle nur mit Berechtigung)
usage: /<command> help
# Hinweis: Der Befehlsname ist in verify.properties unter statusapi.help konfigurierbar
# Beispiel: statusapi.help=vn → /vn help
# ── 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:
# ── AfkModule ──────────────────────────────────────────────
# KEIN default Permission muss manuell vergeben werden!
# lp user <Name> permission set statusapi.afk.bypass true
statusapi.afk.bypass:
description: Automatisches AFK nach Inaktivität umgehen (nur manuell vergeben)
default: false
# ── 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
# ── MultiAccountGuard ─────────────────────────────────────
# KEIN default Permission muss manuell vergeben werden!
# lp user <Name> permission set statusapi.multiaccountguard.bypass true
statusapi.multiaccountguard.bypass:
description: Erlaubt mehrere gleichzeitige Accounts (nur manuell vergeben)
statusapi.staff.notify:
description: Empfaengt Ingame-Benachrichtigungen vom MultiAccountGuard
default: false
# ── 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

View File

@@ -0,0 +1,322 @@
name: StatusAPI
main: net.viper.status.StatusAPI
version: 4.1.4
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:
# ── AfkModule ──────────────────────────────────────────────
afk:
description: AFK-Modus ein- oder ausschalten
usage: /afk
# ── HelpModule ────────────────────────────────────────────
help:
description: Zeigt alle verfügbaren Befehle (Admin-Befehle nur mit Berechtigung)
usage: /<command> help
# Hinweis: Der Befehlsname ist in verify.properties unter statusapi.help konfigurierbar
# Beispiel: statusapi.help=vn → /vn help
# ── 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:
# ── AfkModule ──────────────────────────────────────────────
statusapi.afk.bypass:
description: Automatisches AFK nach Inaktivität umgehen
default: op
# ── 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
# ── MultiAccountGuard ─────────────────────────────────────
# KEIN default Permission muss manuell vergeben werden!
# lp user <Name> permission set statusapi.multiaccountguard.bypass true
statusapi.multiaccountguard.bypass:
description: Erlaubt mehrere gleichzeitige Accounts (nur manuell vergeben)
statusapi.staff.notify:
description: Empfaengt Ingame-Benachrichtigungen vom MultiAccountGuard
default: false
# ── 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

View File

@@ -0,0 +1,147 @@
# 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: &#8B4513%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: &#8B4513%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%
# ===================================================
# NAMETAG - Prefix ueber dem Spieler-Kopf
# ===================================================
# Zeigt den LuckPerms-Prefix ueber dem Spieler-Avatar an.
# Auf false setzen zum Deaktivieren.
nametag.enabled=true

View File

@@ -0,0 +1,113 @@
# _____ __ __ ___ ____ ____
# / ___// /_____ _/ /___ _______/ | / __ \/ _/
# \__ \/ __/ __ `/ __/ / / / ___/ /| | / /_/ // /
# ___/ / /_/ /_/ / /_/ /_/ (__ ) ___ |/ ____// /
# /____/\__/\__,_/\__/\__,_/____/_/ |_/_/ /___/
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
# ===========================
# PLAYER LOGIN LOGGER
# ===========================
# Schreibt UUID, Name und IP jedes Spielers beim Join in player-logins.log
# Standardmaessig deaktiviert - nur auf deinem Server auf true setzen
login-logger.enabled=false
# ===========================
# INGAME HILFE
# ===========================
# Befehlsname für die Ingame-Hilfe (Standard: help)
# Beispiel: statusapi.help=vn → Befehl wird /vn
statusapi.help=sapi
# Permission, die Admin-Befehle in der Hilfe sichtbar macht
# (OP und Spieler mit dieser Permission sehen die Admin-Sektion)
statusapi.help.permission=statusapi.admin
# ===========================
# 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

View File

@@ -1,8 +1,8 @@
# Willkommensnachrichten, die zufällig gesendet werden, wenn ein Spieler joint.
# %player% wird durch den Spielernamen ersetzt.
welcome-messages:
- "&aWillkommen, %player%! Viel Spaß auf unserem Server!"
- "&aHey %player%, schön dich hier zu sehen! Los geht's!"
- "&a%player%, dein Abenteuer beginnt jetzt! Viel Spaß!"
- "&aWillkommen an Bord, %player%! Entdecke den Server!"
# Willkommensnachrichten, die zufällig gesendet werden, wenn ein Spieler joint.
# %player% wird durch den Spielernamen ersetzt.
welcome-messages:
- "&aWillkommen, %player%! Viel Spaß auf unserem Server!"
- "&aHey %player%, schön dich hier zu sehen! Los geht's!"
- "&a%player%, dein Abenteuer beginnt jetzt! Viel Spaß!"
- "&aWillkommen an Bord, %player%! Entdecke den Server!"
- "&a%player%, herzlich willkommen! Lass uns loslegen!"

118
StatusAPIBridge/pom.xml Normal file
View 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>

View File

@@ -0,0 +1,681 @@
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;
// ── Nametag ───────────────────────────────────────────────────────────────
/** Scoreboard für Nametag-Teams (einmalig pro Server erstellt) */
private org.bukkit.scoreboard.Scoreboard nametagBoard = null;
/** Zuletzt gesetzter Prefix pro Spieler (Change-Detection) */
private final Map<UUID, String> lastNametagPrefix = new ConcurrentHashMap<>();
/** Feature aktivierbar via config: nametag-enabled */
private boolean nametagEnabled = true;
// ── 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();
nametagEnabled = getConfig().getBoolean("nametag-enabled", true);
if (nametagEnabled) {
// Eigenes Scoreboard für Nametag-Teams erstellen
nametagBoard = Bukkit.getScoreboardManager().getNewScoreboard();
getLogger().info("Nametag-Prefix aktiviert (LuckPerms).");
}
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);
// Nametag: LuckPerms-Prefix über dem Kopf setzen
if (nametagEnabled) applyNametag(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);
lastNametagPrefix.remove(id);
// Nametag-Team beim Quit aufräumen
if (nametagEnabled) removeNametag(player);
}
@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);
// Nametag periodisch aktualisieren (reagiert auf Rang-Änderungen)
if (nametagEnabled) applyNametag(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) {}
});
}
// ── Nametag-Methoden ──────────────────────────────────────────────────────
/**
* Setzt den LuckPerms-Prefix als Nametag über dem Spieler-Kopf.
* Nutzt die Bukkit Scoreboard Team API zuverlässig auf allen Spigot/Paper-Versionen.
* Wird bei Join und periodisch (scoreboard-sync) aufgerufen.
*/
@SuppressWarnings("deprecation")
private void applyNametag(Player player) {
if (!nametagEnabled || nametagBoard == null) return;
String prefix = getLuckPermsPrefix(player);
// Change-Detection: nicht neu setzen wenn Prefix gleich geblieben
String last = lastNametagPrefix.get(player.getUniqueId());
if (prefix.equals(last)) return;
lastNametagPrefix.put(player.getUniqueId(), prefix);
// Team-Name: "vnt_" + erste 12 Zeichen der UUID (ohne Bindestriche)
// Minecraft-Limit: 16 Zeichen für Teamnamen
String teamName = "vnt" + player.getUniqueId().toString().replace("-", "").substring(0, 13);
try {
// Bestehendes Team holen oder neu erstellen
org.bukkit.scoreboard.Team team = nametagBoard.getTeam(teamName);
if (team == null) {
team = nametagBoard.registerNewTeam(teamName);
}
// Prefix setzen (Bukkit konvertiert §-Codes automatisch)
String coloredPrefix = org.bukkit.ChatColor.translateAlternateColorCodes('&', prefix) + " ";
team.setPrefix(coloredPrefix);
team.setSuffix("");
// Spieler dem Team zuweisen
team.addEntry(player.getName());
// Scoreboard dem Spieler zuweisen
player.setScoreboard(nametagBoard);
// Alle anderen Spieler auf dasselbe Scoreboard setzen damit sie den Prefix sehen
for (Player other : Bukkit.getOnlinePlayers()) {
if (!other.equals(player) && other.getScoreboard() != nametagBoard) {
other.setScoreboard(nametagBoard);
}
}
} catch (Exception e) {
getLogger().warning("[Nametag] Fehler beim Setzen des Prefixes für " + player.getName() + ": " + e.getMessage());
}
}
/**
* Entfernt den Spieler aus seinem Nametag-Team beim Disconnect.
*/
private void removeNametag(Player player) {
if (nametagBoard == null) return;
String teamName = "vnt" + player.getUniqueId().toString().replace("-", "").substring(0, 13);
try {
org.bukkit.scoreboard.Team team = nametagBoard.getTeam(teamName);
if (team != null) {
team.removeEntry(player.getName());
// Team löschen wenn leer
if (team.getEntries().isEmpty()) team.unregister();
}
} catch (Exception ignored) {}
}
/**
* Holt den LuckPerms-Prefix eines Spielers via Reflection (keine harte Dependency).
* Gibt leeren String zurück wenn LuckPerms nicht vorhanden oder kein Prefix gesetzt.
*/
private String getLuckPermsPrefix(Player player) {
try {
Class<?> provClass = Class.forName("net.luckperms.api.LuckPermsProvider");
Object api = provClass.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) return "";
Class<?> qoClass = Class.forName("net.luckperms.api.query.QueryOptions");
Object opts = qoClass.getMethod("defaultContextualOptions").invoke(null);
Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
Object meta = cache.getClass().getMethod("getMetaData", qoClass).invoke(cache, opts);
Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
if (pfx != null && !pfx.toString().isEmpty()) return pfx.toString();
} catch (ClassNotFoundException ignored) {
// LuckPerms nicht installiert
} catch (Exception e) {
getLogger().warning("[Nametag] LuckPerms-Prefix konnte nicht gelesen werden: " + e.getMessage());
}
return "";
}
// ── 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("\"", "\\\"");
}
}

View File

@@ -0,0 +1,16 @@
# 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
# Nametag: LuckPerms-Prefix ueber dem Spieler-Kopf anzeigen
# Auf false setzen zum Deaktivieren.
nametag-enabled: true

View 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]

View 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>

View File

@@ -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);
}
}
}

View 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

View 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
View File

@@ -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.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 -->
<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>

View File

@@ -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) {}
}
});
}
}

View File

@@ -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");
}
}

View File

@@ -1,685 +0,0 @@
package net.viper.status.modules.globalchat;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.model.user.User;
import net.luckperms.api.cacheddata.CachedMetaData;
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.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; // Wichtig für JSON
import net.md_5.bungee.event.EventHandler;
import net.viper.status.module.Module;
import java.io.*;
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.
* Sendet Nachrichten via Plugin Channel an Backend-Server, um Signatur-Probleme zu vermeiden.
*/
public class GlobalChatModule implements Module, Listener {
private static final String CHANNEL_CONTROL = "global:control";
private static final String CHANNEL_CHAT = "global:chat"; // NEU: Kanal für Chat-Relay
private Plugin plugin;
private List<String> badWords = new ArrayList<>();
private File logFolder;
private boolean chatMuted = false;
private boolean isChatEnabled = true; // Status ob der Chat aktiv ist
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(); // Toggle Chat List
private List<String> welcomeMessages = new ArrayList<>();
private Map<String, String> serverDisplayNames = new HashMap<>();
@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 DEAKTIVIERT.");
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());
plugin.getLogger().info("§aGlobalChatModule aktiviert (Relay-Mode aktiv)!");
}
@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();
for (String key : props.stringPropertyNames()) {
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);
}
}
}
plugin.getLogger().info("§eGeladene Server-Displaynames: " + serverDisplayNames.size() + " (Chat aktiv: " + isChatEnabled + ")");
} 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));
}
// ===========================
// 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; // Nachricht geht direkt zum Server (z.B. für Lands/Quests)
}
// ========================================================================
String originalMsg = e.getMessage();
if (suppressJoinQuit.contains(player.getUniqueId()) &&
(originalMsg.contains("joined the Game") || originalMsg.contains("left the Game"))) {
plugin.getLogger().info("Unterdrücke Join-/Quit-Nachricht für " + player.getName());
e.setCancelled(true);
return;
}
// Globaler Mute
if (chatMuted && !player.hasPermission("globalchat.bypass")) {
player.sendMessage(new TextComponent("§cDer globale Chat ist derzeit deaktiviert!"));
e.setCancelled(true);
return;
}
// Badword-Zensur
String censoredMsg = originalMsg;
for (String bad : badWords) {
if (bad == null || bad.trim().isEmpty()) continue;
censoredMsg = censoredMsg.replaceAll("(?i)" + Pattern.quote(bad), repeat("*", bad.length()));
}
// Event canceln (Wichtig, damit nicht doppelt angezeigt wird)
e.setCancelled(true);
String serverName = player.getServer().getInfo().getName();
String serverDisplay = getDisplayName(serverName);
String[] ps = getPrefixSuffix(player);
String prefix = ps[0] == null ? "" : ps[0].trim();
String suffix = ps[1] == null ? "" : ps[1].trim();
String displayTag = "";
if (!prefix.isEmpty()) {
displayTag = prefix;
} else if (!suffix.isEmpty()) {
displayTag = suffix;
}
if (!displayTag.isEmpty() && !displayTag.endsWith(" ")) displayTag = displayTag + " ";
StringBuilder out = new StringBuilder();
out.append("§7[").append(serverDisplay).append("] ");
if (!displayTag.isEmpty()) out.append(displayTag);
out.append(player.getName());
out.append("§f: ").append(censoredMsg);
String chatOut = out.toString();
// NEU: Nachricht erstellen und an alle Server senden
TextComponent chatComponent = new TextComponent(chatOut);
String jsonMessage = ComponentSerializer.toString(chatComponent);
for (ServerInfo server : plugin.getProxy().getServers().values()) {
// Wir senden nur an Server, die Spieler haben (Performance)
if (server.getPlayers().isEmpty()) continue;
try {
server.sendData(CHANNEL_CHAT, jsonMessage.getBytes(StandardCharsets.UTF_8));
} catch (Exception ex) {
plugin.getLogger().warning("Konnte Chat-Nachricht nicht an Server " + server.getName() + " senden.");
}
}
// Loggen (lokal auf Bungee)
String logEntry = "[" + serverName + "] " +
(displayTag.isEmpty() ? "" : stripColor(displayTag) + " ") +
player.getName() +
": " + originalMsg;
logMessage(logEntry);
}
// ===========================
// 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());
plugin.getLogger().info("Sent suppress quit message for " + player.getName() + " to server " + from.getName());
} catch (Throwable ex) {
plugin.getLogger().warning("Fehler beim Senden der Quit-Unterdrückung: " + ex.getMessage());
}
try {
sendSuppressJoinQuit(target, player.getUniqueId());
plugin.getLogger().info("Sent suppress join message for " + player.getName() + " to server " + target.getName());
} 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] == null ? "" : ps[0].trim();
String suffix = ps[1] == null ? "" : ps[1].trim();
String displayTag = "";
if (!prefix.isEmpty()) displayTag = prefix;
else if (!suffix.isEmpty()) displayTag = suffix;
if (!displayTag.isEmpty() && !displayTag.endsWith(" ")) displayTag = displayTag + " ";
StringBuilder msg = new StringBuilder();
msg.append("§7[").append(toDisplay).append("] ");
if (!displayTag.isEmpty()) msg.append(displayTag);
msg.append(player.getName());
msg.append(" §7hat den Server gewechselt: §e")
.append(fromDisplay).append(" §7→ §e").append(toDisplay).append("§7.");
String finalMsg = msg.toString();
// NEU: Switch Nachricht auch über Relay senden
TextComponent switchComponent = new TextComponent(finalMsg);
String jsonMessage = ComponentSerializer.toString(switchComponent);
for (ServerInfo server : plugin.getProxy().getServers().values()) {
if (server.getPlayers().isEmpty()) continue;
try {
server.sendData(CHANNEL_CHAT, jsonMessage.getBytes(StandardCharsets.UTF_8));
} catch (Exception ex) {
plugin.getLogger().warning("Konnte Switch-Nachricht nicht senden.");
}
}
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 via LuckPerms
// ===========================
private String[] getPrefixSuffix(ProxiedPlayer player) {
String prefix = "";
String suffix = "";
try {
LuckPerms lp = LuckPermsProvider.get();
if (lp != null) {
User user = lp.getUserManager().getUser(player.getUniqueId());
if (user == null) {
try {
user = lp.getUserManager().loadUser(player.getUniqueId()).join();
} catch (Exception ignored) {
user = null;
}
}
if (user != null) {
CachedMetaData meta = user.getCachedData().getMetaData();
if (meta != null) {
String p = meta.getPrefix();
String s = meta.getSuffix();
if (p != null) prefix = p;
if (s != null) suffix = s;
}
}
}
} catch (Throwable ignored) {
}
if (prefix != null && !prefix.isEmpty()) prefix = ChatColor.translateAlternateColorCodes('&', prefix);
if (suffix != null && !suffix.isEmpty()) suffix = ChatColor.translateAlternateColorCodes('&', suffix);
if (prefix == null) prefix = "";
if (suffix == null) suffix = "";
return new String[]{prefix, suffix};
}
private String stripColor(String s) {
if (s == null) return "";
return ChatColor.stripColor(s);
}
// ===========================
// Filter einlesen
// ===========================
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();
}
}
// ===========================
// Logs aufräumen / schreiben
// ===========================
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();
}
}
// ===========================
// Staff-Check
// ===========================
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;
}
// ===========================
// 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);
// Support-Nachricht geht nur ans Team, nicht über den Global Chat Relay
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."));
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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) {}
}
}

View File

@@ -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();
}
}
}

View File

@@ -1,6 +0,0 @@
# Liste von Begriffen, die im Chat zensiert werden sollen.
badwords:
- arsch
- hurensohn
- scheiße
- idiot

View File

@@ -1,56 +0,0 @@
name: StatusAPI
main: net.viper.status.StatusAPI
version: 4.0.1
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

View File

@@ -1,39 +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