21 Commits
3.6.0 ... main

Author SHA1 Message Date
e03ebffb64 README.md aktualisiert 2026-01-08 15:25:50 +00:00
056a98b6a9 StatusAPI/pom.xml aktualisiert 2026-01-03 11:32:09 +00:00
045eb29b18 StatusAPI/src/main/resources/plugin.yml aktualisiert 2026-01-03 11:31:57 +00:00
059a0d23f6 Dateien nach "StatusAPI/src/main/java/net/viper/status" hochladen 2026-01-03 11:31:32 +00:00
b2e1338597 StatusAPI/src/main/java/net/viper/status/UpdateListener.java gelöscht 2026-01-03 11:31:03 +00:00
0ec7710840 StatusAPI/src/main/java/net/viper/status/UpdateChecker.java aktualisiert 2026-01-03 11:30:53 +00:00
3a7534c4eb StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-03 11:30:38 +00:00
3dc4ad4bb4 Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-01-03 08:49:02 +00:00
954bc4d622 Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-01-02 21:07:18 +00:00
187abfcbf5 Dateien nach "StatusAPI/src/main/java/net/viper/status" hochladen 2026-01-02 21:05:30 +00:00
5adb7f5752 StatusAPI/src/main/resources/plugin.yml aktualisiert 2026-01-02 21:04:58 +00:00
81165484b8 StatusAPI/pom.xml aktualisiert 2026-01-02 21:04:32 +00:00
280b0647a0 StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-02 21:04:10 +00:00
88d22d8d08 StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-02 21:02:57 +00:00
522551fc76 BungeeCord-Chrome/background.js aktualisiert 2026-01-01 22:28:56 +00:00
3be6b0b9a4 BungeeCord-Chrome/popup.js aktualisiert 2026-01-01 22:28:35 +00:00
f0e197ee8a Minecraft-BungeeCord-Status/minecraft-bungeecord-status.php aktualisiert 2026-01-01 21:49:48 +00:00
519bb5161e StatusAPI/src/main/java/net/viper/status/StatusAPI.java aktualisiert 2026-01-01 21:49:10 +00:00
395a26f023 Dateien nach "BungeeCord-Chrome/icons" hochladen 2026-01-01 13:31:35 +00:00
cc1b6115ce Dateien nach "BungeeCord-Chrome" hochladen 2026-01-01 13:31:09 +00:00
6915b8d807 README.md aktualisiert 2026-01-01 02:09:24 +00:00
15 changed files with 2465 additions and 887 deletions

View File

@@ -0,0 +1,187 @@
// background.js
const POLL_INTERVAL_SEC = 2;
// Alarm erstellen
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('periodicRefresh', { periodInMinutes: POLL_INTERVAL_SEC / 60 });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodicRefresh') {
refreshAllServers().catch(console.error);
}
});
// Message Listener
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.cmd === 'refreshNow') {
refreshAllServers()
.then(statuses => sendResponse({ ok: true, statuses }))
.catch(err => sendResponse({ ok: false, error: String(err) }));
return true;
}
});
async function refreshAllServers() {
const obj = await chrome.storage.local.get('servers');
const servers = obj.servers || [];
if (servers.length === 0) {
await chrome.storage.local.set({ serverStatuses: {} });
chrome.action.setBadgeText({ text: '' });
return {};
}
const results = {};
let totalOnlinePlayers = 0;
const tasks = servers.map(async (srv) => {
try {
const data = await fetchServerStatus(srv, 2000);
results[srv.id] = { ok: true, server: srv, fetched: Date.now(), data };
if (data) {
if (Array.isArray(data.players)) totalOnlinePlayers += data.players.length;
else if (typeof data.players === 'number') totalOnlinePlayers += data.players;
}
} catch (e) {
results[srv.id] = { ok: false, server: srv, fetched: Date.now(), error: String(e) };
}
});
await Promise.all(tasks);
await chrome.storage.local.set({ serverStatuses: results });
const badgeText = totalOnlinePlayers > 0 ? String(totalOnlinePlayers) : '';
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' });
return results;
}
async function fetchServerStatus(server, timeoutMs = 2000) {
let serverObj = typeof server === 'string' ? { id: 'legacy', url: server } : server;
let fetchUrl = serverObj.url || '';
// 1. Direkte URL-Abfrage
if (fetchUrl) {
if (!/^https?:\/\//i.test(fetchUrl)) fetchUrl = 'http://' + fetchUrl;
try {
const startTime = performance.now();
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
const resp = await fetch(fetchUrl, { method: 'GET', signal: controller.signal });
clearTimeout(id);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (resp.ok) {
let text = await resp.text();
const lastBrace = text.lastIndexOf('}');
if (lastBrace !== -1) text = text.substring(0, lastBrace + 1);
text = text.trim();
try {
const parsed = JSON.parse(text);
if (parsed && (typeof parsed.online !== 'undefined' || Array.isArray(parsed.players) || parsed.version)) {
parsed.ping = latency; // Ping hinzufügen
return parsed;
}
} catch (e) {
// Parsing fehlgeschlagen → Fallback-Objekt mit Ping
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency // bei Timeout kein sinnvoller Ping
};
}
}
// HTTP nicht ok → Offline mit gemessener Latenz (falls unter Timeout)
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
// Timeout oder Netzwerkfehler
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// 2. WordPress AJAX-Abfrage
const wpSite = serverObj.wpSite ? String(serverObj.wpSite).replace(/\/$/, '') : null;
const wpServerId = serverObj.wpServerId ? String(serverObj.wpServerId) : null;
if (wpSite && wpServerId) {
try {
const ajaxUrl = wpSite + '/wp-admin/admin-ajax.php';
const body = 'action=mcss_fetch&server_id=' + encodeURIComponent(wpServerId);
const startTime = performance.now();
const controller2 = new AbortController();
const id2 = setTimeout(() => controller2.abort(), timeoutMs);
const resp2 = await fetch(ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
signal: controller2.signal
});
clearTimeout(id2);
const endTime = performance.now();
const latency = Math.round(endTime - startTime);
if (!resp2.ok) throw new Error('WP AJAX HTTP ' + resp2.status);
const json = await resp2.json();
if (json && (typeof json.online !== 'undefined' || Array.isArray(json.players) || json.version)) {
json.ping = latency; // Ping hinzufügen
return json;
}
// JSON nicht im erwarteten Format
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: latency > timeoutMs ? null : latency
};
} catch (e) {
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}
}
// Keine gültige Konfiguration
return {
online: false,
players: [],
max_players: 0,
version: 'unknown',
motd: '',
ping: null
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -0,0 +1,25 @@
{
"manifest_version": 3,
"name": "Minecraft Bungee Status",
"description": "Zeigt den Live-Status mehrerer Bungee/Proxy-Server an. Fallback auf WordPress AJAX möglich.",
"version": "1.1.0",
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background.js"
},
"permissions": [
"storage",
"alarms"
],
"host_permissions": [
"http://*/*",
"https://*/*"
]
}

220
BungeeCord-Chrome/popup.css Normal file
View File

@@ -0,0 +1,220 @@
:root {
--bg: #111827;
--panel: #0b1220;
--text: #e6eef8;
--muted: #9aa7b3;
--accent: #3b82f6;
--danger: #ef4444;
--radius: 8px;
--online: #22c55e;
--offline: #ef4444;
}
* { box-sizing:border-box; }
html,body { margin:0; padding:0; font-family:Inter, Arial, sans-serif; background:var(--bg); color:var(--text);}
.root { width:520px; max-width:520px; padding:12px; }
header { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
header h1 { font-size:16px; margin:0; }
header .actions button { background:transparent; border:1px solid var(--muted); color:var(--text); padding:4px 8px; border-radius:6px; cursor:pointer; }
.main {
display:flex;
gap:12px;
position: relative;
}
.server-list {
width:45%;
background:var(--panel);
padding:8px;
border-radius:var(--radius);
flex-shrink: 0;
}
.detail {
flex:1;
background:var(--panel);
padding:8px;
border-radius:var(--radius);
min-height:220px;
transition: all 0.3s ease;
}
.detail.full-width {
width: 100%;
flex: 1 1 100%;
}
.add-form { display:flex; flex-direction:column; gap:6px; margin-bottom:8px; }
.add-form input { padding:6px 8px; border-radius:6px; border:1px solid #21303b; background:#071019; color:var(--text); }
.add-form button { padding:6px 8px; border-radius:6px; border:none; background:var(--accent); color:white; cursor:pointer; }
#serversContainer { list-style:none; margin:0; padding:0; max-height:260px; overflow:auto; }
.server-item {
display:flex;
align-items:center;
justify-content:space-between;
padding:6px;
margin-bottom:6px;
border-radius:6px;
cursor:pointer;
background: rgba(255,255,255,0.02);
transition: background 0.2s ease;
}
.server-item:hover { background: rgba(255,255,255,0.05); }
.server-item .meta { display:flex; flex-direction:column; }
.server-item .meta .name { font-weight:600; }
.server-item .meta .url { font-size:12px; color:var(--muted); }
.server-item .status-bubble {
font-size:12px;
padding:3px 6px;
border-radius:999px;
color:#fff;
transition: background-color 0.3s ease;
}
.placeholder { color:var(--muted); padding:12px; }
.hidden { display:none; }
/* --- Detail Header (Zeile 1) --- */
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.server-identity h2 {
margin: 0;
font-size: 18px;
line-height: 1.2;
}
.server-url {
display: block;
font-size: 12px;
color: var(--muted);
margin-top: 2px;
}
.status-wrapper {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
/* --- Pulsierender Punkt Animation --- */
.pulsing-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--offline); /* Standard Offline Rot */
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
animation: pulse-red 2s infinite;
}
.pulsing-dot.online {
background-color: var(--online); /* Online Grün */
animation: pulse-green 2s infinite;
}
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
/* --- Detail Stats (Zeile 2) --- */
.detail-stats {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255,255,255,0.05);
padding: 8px;
border-radius: var(--radius);
margin-bottom: 16px;
font-size: 13px;
}
/* ---------------------------------- */
.playerList {
list-style:none;
padding-left:16px;
max-height:140px;
overflow:auto;
margin:6px 0;
display:flex;
flex-wrap:wrap;
gap:8px;
}
.playerList li {
display:flex;
align-items:center;
gap:4px;
font-size:13px;
color:var(--text);
}
.player-avatar {
width:24px;
height:24px;
border-radius:4px;
transition: transform 0.2s ease;
}
.player-avatar:hover { transform: scale(1.1); }
.detailButtons {
display:flex;
gap:8px;
margin-top:8px;
transition: opacity 0.3s ease;
}
.detailButtons button {
padding:6px 8px;
border-radius:6px;
border:none;
cursor:pointer;
transition: opacity 0.2s ease, transform 0.1s ease;
}
.detailButtons button:hover { opacity: 0.9; transform: translateY(-1px); }
.detailButtons button:active { transform: translateY(0); }
#btnEdit { background: #f59e0b; color:#000; }
#btnDelete { background: var(--danger); color:#fff; }
#serversContainer::-webkit-scrollbar,
.playerList::-webkit-scrollbar {
width: 6px;
}
#serversContainer::-webkit-scrollbar-track,
.playerList::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
border-radius: 3px;
}
#serversContainer::-webkit-scrollbar-thumb,
.playerList::-webkit-scrollbar-thumb {
background: var(--muted);
border-radius: 3px;
}
#serversContainer::-webkit-scrollbar-thumb:hover,
.playerList::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bungee Status</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="root">
<header>
<h1>Bungee Status</h1>
<div class="actions">
<button id="btnRefresh" title="Jetzt aktualisieren"></button>
<button id="btnToggleSettings"></button>
</div>
</header>
<section class="main">
<aside class="server-list">
<div class="add-form" id="settingsForm">
<input id="inputName" placeholder="Name (z. B. Lobby)" />
<input id="inputUrl" placeholder="URL (127.0.0.1:9191)" />
<input id="inputWpSite" placeholder="WP Site" />
<input id="inputWpServerId" placeholder="WP Server ID" />
<button id="btnAddServer">Hinzufügen</button>
</div>
<ul id="serversContainer"></ul>
</aside>
<div class="detail">
<div id="noSelection" class="placeholder">Wähle einen Server aus der linken Liste.</div>
<div id="detailContent" class="detailContent hidden">
<!-- NEUES LAYOUT ZEILE 1: Name, URL, Status (mit Pulsierendem Punkt) -->
<div class="detail-header">
<div class="server-identity">
<h2 id="detailName"></h2>
<span id="detailUrlText" class="server-url"></span>
</div>
<div class="status-wrapper">
<div id="detailPulse" class="pulsing-dot"></div>
<span id="detailStatus"></span>
</div>
</div>
<!-- NEUES LAYOUT ZEILE 2: Spieler, Version, Ping -->
<div class="detail-stats">
<div><strong>Spieler:</strong> <span id="detailPlayers"></span></div>
<div><strong>Version:</strong> <span id="detailVersion"></span></div>
<div><strong>Ping:</strong> <span id="detailPing">-</span></div>
</div>
<h3>Online Spieler</h3>
<ul id="detailPlayerList" class="playerList"></ul>
<div class="detailButtons">
<button id="btnEdit">Bearbeiten</button>
<button id="btnDelete">Löschen</button>
</div>
</div>
</div>
</section>
</div>
<script src="popup.js"></script>
</body>
</html>

384
BungeeCord-Chrome/popup.js Normal file
View File

@@ -0,0 +1,384 @@
const $ = id => document.getElementById(id);
const uid = () => 'srv_' + Math.random().toString(36).slice(2,9);
const inputName = $('inputName');
const inputUrl = $('inputUrl');
const inputWpSite = $('inputWpSite');
const inputWpServerId = $('inputWpServerId');
const btnAdd = $('btnAddServer');
const serversContainer = $('serversContainer');
const serverListPanel = document.querySelector('.server-list');
const btnRefresh = $('btnRefresh');
const btnToggleSettings = $('btnToggleSettings');
const settingsForm = $('settingsForm');
const noSelection = $('noSelection');
const detailContent = $('detailContent');
const detailName = $('detailName');
const detailUrlText = $('detailUrlText');
const detailStatus = $('detailStatus');
const detailPulse = $('detailPulse');
const detailVersion = $('detailVersion');
const detailPlayers = $('detailPlayers');
const detailPing = $('detailPing');
const detailPlayerList = $('detailPlayerList');
const btnEdit = $('btnEdit');
const btnDelete = $('btnDelete');
let servers = [];
let selectedId = null;
let statuses = {};
let previousStatuses = {};
let settingsVisible = false;
document.addEventListener('DOMContentLoaded', init);
btnAdd.addEventListener('click', handleAdd);
btnRefresh.addEventListener('click', manualRefresh);
btnToggleSettings.addEventListener('click', toggleSettings);
btnEdit.addEventListener('click', handleEdit);
btnDelete.addEventListener('click', handleDelete);
async function init() {
await loadServersFromStorage();
await loadStatusesFromStorage();
await loadSettingsVisibility();
renderServerList();
if (servers.length === 1) selectedId = servers[0].id;
renderDetail(selectedId);
adjustDetailLayout();
}
// --- Settings Visibility ---
async function loadSettingsVisibility() {
const obj = await chrome.storage.local.get(['settingsVisible']);
settingsVisible = obj.settingsVisible !== undefined ? obj.settingsVisible : false;
if (!settingsVisible) settingsForm.classList.add('hidden');
else settingsForm.classList.remove('hidden');
}
async function saveSettingsVisibility() {
await chrome.storage.local.set({ settingsVisible });
}
async function toggleSettings() {
settingsVisible = !settingsVisible;
settingsForm.classList.toggle('hidden');
await saveSettingsVisibility();
adjustDetailLayout();
}
// --- Storage ---
async function loadServersFromStorage() {
const obj = await chrome.storage.local.get(['servers']);
servers = obj.servers || [];
for (const s of servers) if (!s.id) s.id = uid();
await chrome.storage.local.set({ servers });
}
async function saveServersToStorage() {
await chrome.storage.local.set({ servers });
}
async function loadStatusesFromStorage() {
const obj = await chrome.storage.local.get(['serverStatuses']);
statuses = obj.serverStatuses || {};
}
// --- Layout ---
function adjustDetailLayout() {
const settingsHidden = settingsForm.classList.contains('hidden');
detailContent.classList.toggle('full-width', settingsHidden);
serverListPanel.classList.toggle('hidden', settingsHidden);
btnEdit.style.display = settingsHidden ? 'none' : 'inline-block';
btnDelete.style.display = settingsHidden ? 'none' : 'inline-block';
}
// --- Render Server List ---
function renderServerList() {
serversContainer.innerHTML = '';
if (servers.length === 0) {
serversContainer.innerHTML = '<div class="placeholder">Noch keine Server hinzugefügt.</div>';
return;
}
for (const s of servers) {
const item = document.createElement('li');
item.className = 'server-item';
item.dataset.id = s.id;
const meta = document.createElement('div');
meta.className = 'meta';
const name = document.createElement('div');
name.className = 'name';
name.textContent = s.name || '(Kein Name)';
let listUrl = s.url || (s.wpSite ? s.wpSite + ' (WP)' : '');
listUrl = listUrl.replace(':9191', '');
const url = document.createElement('div');
url.className = 'url';
url.textContent = listUrl;
meta.append(name, url);
const statusBubble = document.createElement('div');
statusBubble.className = 'status-bubble';
statusBubble.textContent = '—';
statusBubble.style.backgroundColor = 'transparent';
item.statusBubble = statusBubble;
item.append(meta, statusBubble);
item.addEventListener('click', () => {
selectedId = s.id;
renderDetail(selectedId);
});
serversContainer.appendChild(item);
}
updateServerListStatuses(false);
}
// --- Render Detail ---
function renderDetail(id) {
if (!id) {
noSelection.classList.remove('hidden');
detailContent.classList.add('hidden');
return;
}
const srv = servers.find(x => x.id === id);
if (!srv) {
noSelection.classList.remove('hidden');
detailContent.classList.add('hidden');
return;
}
noSelection.classList.add('hidden');
detailContent.classList.remove('hidden');
detailName.textContent = srv.name || 'Unbenannter Server';
let urlToShow = srv.url || (srv.wpSite ? srv.wpSite : 'Lokal');
urlToShow = urlToShow.replace(':9191', '');
detailUrlText.textContent = urlToShow;
updateDetailForServer(srv, true);
}
// --- Update Detail ---
function updateDetailForServer(srv, force = false) {
const st = statuses[srv.id];
const prevSt = previousStatuses[srv.id];
const statusChanged = force ||
!prevSt ||
(prevSt.ok !== st?.ok) ||
(prevSt.data?.online !== st?.data?.online);
if (!st || !st.ok || !st.data) {
if (statusChanged) {
detailStatus.textContent = 'Offline';
detailPulse.classList.remove('online');
detailVersion.textContent = '-';
detailPlayers.textContent = '-';
detailPing.textContent = '-';
updatePlayerList([]);
}
} else {
const d = st.data;
if (statusChanged) {
detailStatus.textContent = d.online ? 'Online' : 'Offline';
if (d.online) {
detailPulse.classList.add('online');
} else {
detailPulse.classList.remove('online');
}
}
const newVersion = d.version || 'unknown';
if (force || detailVersion.textContent !== newVersion) {
detailVersion.textContent = newVersion;
}
const playersCount = Array.isArray(d.players) ? d.players.length : (typeof d.players === 'number' ? d.players : 0);
const maxPlayers = d.max_players;
let newPlayersText = String(playersCount);
if (maxPlayers && maxPlayers !== '-1') {
newPlayersText += ` / ${maxPlayers}`;
}
if (force || detailPlayers.textContent !== newPlayersText) {
detailPlayers.textContent = newPlayersText;
}
let pingVal = d.ping || d.latency || '-';
if (pingVal !== '-' && typeof pingVal === 'number') {
pingVal = pingVal + ' ms';
}
if (force || (pingVal !== '-' && detailPing.textContent !== pingVal)) {
detailPing.textContent = pingVal;
}
const currentPlayers = Array.isArray(d.players) ? d.players : [];
const prevPlayers = (prevSt && Array.isArray(prevSt.data?.players)) ? prevSt.data.players : [];
let playersChanged = false;
if (force) {
playersChanged = true;
} else {
playersChanged = JSON.stringify(currentPlayers) !== JSON.stringify(prevPlayers);
}
if (playersChanged) {
updatePlayerList(currentPlayers);
}
}
previousStatuses[srv.id] = st ? JSON.parse(JSON.stringify(st)) : null;
}
// --- Spielerliste ---
function updatePlayerList(players) {
detailPlayerList.innerHTML = '';
if (!players || players.length === 0) {
detailPlayerList.innerHTML = '<li class="placeholder">Keine Spieler online.</li>';
return;
}
for (const p of players) {
const li = document.createElement('li');
const name = typeof p === 'object' ? p.name || p.username || p.player || '' : String(p);
if (typeof p === 'object' && p.avatar) {
const img = document.createElement('img');
img.src = p.avatar;
img.className = 'player-avatar';
img.title = name;
li.appendChild(img);
} else {
// Falls kein Avatar vorhanden, generiere einen von mc-heads.net
const img = document.createElement('img');
img.src = `https://mc-heads.net/avatar/${encodeURIComponent(name)}/32`;
img.className = 'player-avatar';
img.title = name;
li.appendChild(img);
}
detailPlayerList.appendChild(li);
}
}
// --- Update Server List Statuses ---
function updateServerListStatuses() {
const items = serversContainer.querySelectorAll('.server-item');
items.forEach(item => {
const s = servers.find(x => x.id === item.dataset.id);
if (!s) return;
const st = statuses[s.id];
const prevSt = previousStatuses[s.id];
const statusChanged = !prevSt ||
(prevSt.ok !== st?.ok) ||
(prevSt.data?.online !== st?.data?.online);
if (!statusChanged) return;
if (!st || !st.ok || !st.data) {
item.statusBubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)';
} else if (st.data.online) {
item.statusBubble.textContent = 'Online';
item.statusBubble.style.backgroundColor = 'var(--online)';
} else {
item.statusBubble.textContent = 'Offline';
item.statusBubble.style.backgroundColor = 'var(--offline)';
}
previousStatuses[s.id] = st ? JSON.parse(JSON.stringify(st)) : null;
});
}
// --- Add / Edit / Delete ---
async function handleAdd() {
const name = inputName.value.trim();
const url = inputUrl.value.trim();
const wpSite = inputWpSite.value.trim();
const wpServerId = inputWpServerId.value.trim();
if (!url && !wpSite) {
alert('Bitte URL oder WP Site angeben');
return;
}
const s = {
id: uid(),
name: name || url || wpSite,
url: url || null,
wpSite: wpSite || null,
wpServerId: wpServerId || null
};
servers.push(s);
await saveServersToStorage();
inputName.value = '';
inputUrl.value = '';
inputWpSite.value = '';
inputWpServerId.value = '';
renderServerList();
}
async function handleEdit() {
if (!selectedId) return;
const srv = servers.find(s => s.id === selectedId);
if (!srv) return;
const newName = prompt('Name:', srv.name) || srv.name;
const newUrl = prompt('URL:', srv.url || '') || srv.url;
const newWpSite = prompt('WP Site:', srv.wpSite || '') || srv.wpSite;
const newWpServerId = prompt('WP Server ID:', srv.wpServerId || '') || srv.wpServerId;
srv.name = newName.trim();
srv.url = newUrl.trim();
srv.wpSite = newWpSite.trim();
srv.wpServerId = newWpServerId.trim();
await saveServersToStorage();
renderServerList();
renderDetail(selectedId);
}
async function handleDelete() {
if (!selectedId) return;
if (!confirm('Server wirklich löschen?')) return;
servers = servers.filter(s => s.id !== selectedId);
selectedId = null;
await saveServersToStorage();
renderServerList();
renderDetail(null);
}
async function manualRefresh() {
try {
chrome.runtime.sendMessage({ cmd: 'refreshNow' });
} catch(e) {
console.error('Refresh fehlgeschlagen:', e);
}
}
// --- Storage Listener ---
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.serverStatuses) {
statuses = changes.serverStatuses.newValue || {};
updateServerListStatuses();
if (selectedId) {
const srv = servers.find(s => s.id === selectedId);
if (srv) updateDetailForServer(srv);
}
}
});

File diff suppressed because it is too large Load Diff

138
README.md
View File

@@ -1,2 +1,138 @@
# Minecraft-BungeeCord-Status # Minecraft BungeeCord Status
StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner Webseite an inklusive Online/Offline, Version, Ping, Spieleranzahl und Spieler-Avatare.
**StatusAPI Repository:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
---
## Features
- Zeigt Serverstatus: **Online / Offline**
- Zeigt Version und Ping
- Zeigt die Anzahl der Spieler und ihre Avatare
- Klick auf Servername kopiert die Adresse automatisch
- Spielerübersicht wird automatisch alle **2 Sekunden** aktualisiert
- Hinweise/Banner können vom Nutzer geschlossen werden
- Anpassbare Darstellung für verschiedene Layouts
---
## Wichtige Hinweise
### BungeeCord Plugin (Voraussetzung)
⚠️ **Die [StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI) MUSS auf deinem BungeeCord-Server installiert sein!**
Das Plugin **StatusAPI.jar** muss in den BungeeCord Plugin-Ordner kopiert werden, damit die API korrekt funktioniert.
```text
BungeeCord/
├─ plugins/
│ └─ StatusAPI.jar ← PFLICHT
└─ config.yml
```
**Download:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
### WordPress Integration
In WordPress muss der API-Port auf **9191** eingestellt sein, damit die Serverdaten korrekt abgerufen werden.
Stelle sicher, dass dein Server und die Webseite auf diesen Port zugreifen können.
Die StatusAPI stellt einen HTTP-Endpunkt bereit (Standard: `http://localhost:9191`), den das WordPress-Plugin abfragt.
### Shortcode für WordPress
Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein:
```html
[bungeecord_status id="Bungeecord"]
```
Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebunden werden.
---
## Installation
### Schritt 1: StatusAPI auf BungeeCord installieren
1. Lade die **[StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)** herunter
2. Kopiere die **StatusAPI.jar** in den `plugins`-Ordner deines BungeeCord-Servers
3. Starte den BungeeCord-Server neu
4. Die API läuft nun standardmäßig auf Port **9191**
### Schritt 2: WordPress-Plugin einrichten
1. Installiere das WordPress-Plugin für den BungeeCord-Status
2. Gehe zu den Plugin-Einstellungen
3. Trage die **StatusAPI URL** ein (z.B. `http://localhost:9191`)
4. Speichere die Einstellungen
### Schritt 3: Shortcode einbinden
Füge den Shortcode auf einer beliebigen WordPress-Seite ein:
```html
[bungeecord_status id="Bungeecord"]
```
---
## Beispielanzeige
**Status:** Online
**Version:** 1.8 - 1.21
**Ping:** 1 ms
**Spieler:** 1
**Avatare:**
![M_Viper Avatar](https://example.com/avatar/M_Viper.png) <!-- Platzhalter wird im Plugin dynamisch geladen -->
> Der Abstand zwischen dem Text „Spieler:" und den Avataren ist bewusst etwas größer, um die Übersichtlichkeit zu erhöhen.
---
## Nutzung
1. Shortcode an der gewünschten Stelle einfügen
2. Der Serverstatus aktualisiert sich automatisch, sodass die angezeigten Spieler und der Ping immer aktuell sind
3. Alle Daten werden live von der StatusAPI bezogen
---
## Technische Details
- **API-Port:** 9191 (Standard)
- **Update-Intervall:** 2 Sekunden (automatisch)
- **Protokoll:** HTTP/JSON
- **Datenquelle:** StatusAPI auf BungeeCord
---
## Support
Bei Problemen überprüfe bitte:
- Ob das Plugin **[StatusAPI.jar](https://git.viper.ipv64.net/M_Viper/StatusAPI)** korrekt im BungeeCord Plugin-Ordner liegt
- Ob der API-Port in WordPress korrekt auf **9191** eingestellt ist
- Ob der BungeeCord-Server läuft und die StatusAPI erreichbar ist
- Ob keine Firewall den Port 9191 blockiert
- Ob die StatusAPI gültige JSON-Daten zurückliefert
### Häufige Fehler
**"Server offline" obwohl Server läuft**
- StatusAPI ist nicht installiert oder läuft nicht
- Falscher Port in WordPress eingestellt
- Firewall blockiert Port 9191
**Keine Spieler werden angezeigt**
- StatusAPI liefert keine Spielerdaten
- API-Verbindung fehlgeschlagen
- WordPress kann die API-URL nicht erreichen
---
## Weitere Informationen
Für detaillierte Informationen zur Installation, Konfiguration und Fehlerbehebung der StatusAPI:
**[https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)**

View File

@@ -1,43 +1,52 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd"> https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>net.viper.bungee</groupId> <groupId>net.viper.bungee</groupId>
<artifactId>StatusAPI</artifactId> <artifactId>StatusAPI</artifactId>
<version>1.0</version> <version>3.6.2</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>StatusAPI</name> <name>StatusAPI</name>
<description>BungeeCord Status API Plugin</description> <description>BungeeCord Status API Plugin</description>
<properties> <properties>
<maven.compiler.source>8</maven.compiler.source> <maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target> <maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies> <dependencies>
<!-- BungeeCord API (lokal installiert) --> <!-- BungeeCord API -->
<dependency> <dependency>
<groupId>net.md-5</groupId> <groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId> <artifactId>bungeecord-api</artifactId>
<version>1.20</version> <version>1.20</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
</dependencies>
<!-- LuckPerms API (Optional) -->
<build> <dependency>
<finalName>StatusAPI</finalName> <groupId>net.luckperms</groupId>
<artifactId>api</artifactId>
<resources> <version>5.4</version>
<resource> <scope>provided</scope>
<directory>src/main/resources</directory> <optional>true</optional>
<filtering>false</filtering> </dependency>
</resource> </dependencies>
</resources>
</build> <build>
<finalName>StatusAPI</finalName>
</project>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>

View File

@@ -0,0 +1,59 @@
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;
/**
* FileDownloader: Lädt Dateien asynchron herunter (CMILib Style).
*/
public class FileDownloader {
private final Plugin plugin;
public FileDownloader(Plugin plugin) {
this.plugin = plugin;
}
/**
* Lädt eine Datei herunter.
* @param urlString Die Download URL
* @param destination Die Zieldatei
* @param onSuccess Callback, der im Hauptthread ausgeführt wird, wenn fertig.
*/
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);
}
// Schließen
fileOutputStream.close();
bufferedInputStream.close();
// Callback im Main Thread
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,146 +1,293 @@
package net.viper.status; package net.viper.status;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
import java.io.*;
import java.net.ServerSocket; import java.io.BufferedReader;
import java.net.Socket; import java.io.File;
import java.util.ArrayList; import java.io.IOException;
import java.util.HashMap; import java.io.InputStreamReader;
import java.util.List; import java.io.OutputStream;
import java.util.Map; import java.io.PrintWriter;
import java.net.ServerSocket;
public class StatusAPI extends Plugin implements Runnable { import java.net.Socket;
import java.util.*;
private Thread thread; import java.util.concurrent.TimeUnit;
private int port = 9191;
public class StatusAPI extends Plugin implements Runnable {
@Override
public void onEnable() { private Thread thread;
getLogger().info("StatusAPI wird aktiviert..."); private int port = 9191;
getLogger().info("Starte Web-Server auf Port " + port + "..."); private UpdateChecker updateChecker;
private FileDownloader fileDownloader;
thread = new Thread(this);
thread.start(); @Override
} public void onEnable() {
getLogger().info("StatusAPI wird aktiviert...");
@Override getLogger().info("Starte Web-Server auf Port " + port + "...");
public void onDisable() {
getLogger().info("Stoppe Web-Server..."); // Start HTTP server thread
if (thread != null) { thread = new Thread(this, "StatusAPI-HTTP-Server");
thread.interrupt(); thread.start();
}
} String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
@Override fileDownloader = new FileDownloader(this);
public void run() {
try (ServerSocket serverSocket = new ServerSocket(port)) { File pluginFile = getFile();
serverSocket.setSoTimeout(1000); File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
while (!Thread.interrupted()) { // --- AUTOMATISCHES BACKUP-CLEANUP ---
try { // Falls ein altes Update (.bak) im Ordner liegt, löschen wir es nach 1 Minute Startzeit.
Socket clientSocket = serverSocket.accept(); // Wenn der Server kurz crashen würde, hast du noch 60 Sekunden Zeit, ihn zu stoppen,
handleConnection(clientSocket); // damit das Backup erhalten bleibt. Läuft er stabil, wird der Platz freigegeben.
} catch (java.net.SocketTimeoutException e) { if (backupFile.exists()) {
// Loop Check ProxyServer.getInstance().getScheduler().schedule(this, () -> {
} catch (IOException e) { if (backupFile.exists()) {
// Ignorieren if (backupFile.delete()) {
} getLogger().info("Altes Backup (.bak) wurde erfolgreich gelöscht.");
} }
} catch (IOException e) { }
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port); }, 1, TimeUnit.MINUTES);
e.printStackTrace(); }
} // ---------------------------------------
}
// Sofortiger Start-Check
private void handleConnection(Socket clientSocket) { checkAndMaybeUpdate();
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { // Regelmäßiger Check (alle 6 Stunden)
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
String inputLine = in.readLine(); }
if (inputLine != null && inputLine.startsWith("GET")) { @Override
public void onDisable() {
Map<String, Object> data = new HashMap<>(); getLogger().info("Stoppe Web-Server...");
data.put("online", true); if (thread != null) {
thread.interrupt();
// --- VERSION CLEANUP START --- try { thread.join(1000); } catch (InterruptedException ignored) {}
String versionRaw = ProxyServer.getInstance().getVersion(); }
String versionClean = versionRaw; // Fallback }
// Regex um die saubere Version zu extrahieren (z.B. 1.21-R0.5-SNAPSHOT) /**
// Aus: git:BungeeCord-Bootstrap:1.21-R0.5-SNAPSHOT:36e6154:2012 * Prüft Update und startet Download falls nötig.
if (versionRaw.matches("git-BungeeCord-Bootstrap:.*")) { */
String[] parts = versionRaw.split(":"); private void checkAndMaybeUpdate() {
if(parts.length > 2) { try {
versionClean = parts[2]; updateChecker.checkNow();
} String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
}
data.put("version", versionClean); if (updateChecker.isUpdateAvailable(currentVersion)) {
// --- VERSION CLEANUP ENDE --- String newVersion = updateChecker.getLatestVersion();
String url = updateChecker.getLatestUrl();
data.put("max_players", ProxyServer.getInstance().getConfig().getPlayerLimit()); getLogger().warning("----------------------------------------");
getLogger().warning("Neue Version verfügbar: " + newVersion);
String motd = "BungeeCord"; getLogger().warning("Starte automatisches Update...");
try { getLogger().warning("----------------------------------------");
ListenerInfo listener = ProxyServer.getInstance().getConfig().getListeners().iterator().next();
if (listener != null) { // Download Starten
motd = listener.getMotd(); File pluginFile = getFile();
} File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new");
} catch (Exception e) {
// Fallback fileDownloader.downloadFile(url, newFile, () -> {
} // Callback: Wenn Download erfolgreich
data.put("motd", motd); triggerUpdateScript(pluginFile, newFile);
});
List<String> playerNames = new ArrayList<>(); }
for (net.md_5.bungee.api.connection.ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { } catch (Exception e) {
playerNames.add(p.getName()); getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
} }
data.put("players", playerNames); }
String json = buildJsonString(data); /**
* Erstellt ein externes Batch-Skript, startet es und stoppt den Server.
out.println("HTTP/1.1 200 OK"); * Das Skript führt den Datei-Tausch durch, wenn der Server weg ist.
out.println("Content-Type: application/json"); */
out.println("Access-Control-Allow-Origin: *"); private void triggerUpdateScript(File currentFile, File newFile) {
out.println("Content-Length: " + json.length()); try {
out.println("Connection: close"); File pluginsFolder = currentFile.getParentFile();
out.println(); // Wir legen das Skript neben die Haupt-JAR (root) damit es nicht im Plugin-Ordner liegt
out.println(json); File rootFolder = pluginsFolder.getParentFile();
} File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat");
} catch (Exception e) {
e.printStackTrace(); // Batch Inhalt
} // 1. Wartet 5 Sekunden (Server fährt runter)
} // 2. Geht in den Plugin Ordner
// 3. Sichert .jar zu .bak
private String buildJsonString(Map<String, Object> data) { // 4. Benennt .new zu .jar um
StringBuilder sb = new StringBuilder("{"); // 5. Löscht sich selbst
boolean first = true; String batContent = "@echo off\n" +
for (Map.Entry<String, Object> entry : data.entrySet()) { "echo Bitte warten, der Server f\"ahrt herunter...\n" +
if (!first) sb.append(","); "timeout /t 5 /nobreak >nul\n" +
first = false; "cd /d \"" + pluginsFolder.getAbsolutePath().replace("\\", "/") + "\"\n" +
sb.append("\"").append(entry.getKey()).append("\":"); "echo Fuehre Datei-Tausch durch...\n" +
"if exist StatusAPI.jar.bak del StatusAPI.jar.bak\n" +
Object value = entry.getValue(); "if exist StatusAPI.jar (\n" +
if (value instanceof List) { " ren StatusAPI.jar StatusAPI.jar.bak\n" +
sb.append("["); ")\n" +
List<?> list = (List<?>) value; "if exist StatusAPI.new.jar (\n" +
for (int i = 0; i < list.size(); i++) { " ren StatusAPI.new.jar StatusAPI.jar\n" +
if (i > 0) sb.append(","); " echo Update erfolgreich!\n" +
sb.append("\"").append(list.get(i)).append("\""); ") else (\n" +
} " echo FEHLER: StatusAPI.new.jar nicht gefunden!\n" +
sb.append("]"); " pause\n" +
} else if (value instanceof String) { ")\n" +
sb.append("\"").append(((String) value).replace("\"", "\\\"")).append("\""); "del \"%~f0\"";
} else if (value instanceof Boolean) {
sb.append(value); try (PrintWriter out = new PrintWriter(batFile)) {
} else { out.println(batContent);
sb.append("\"").append(value).append("\""); }
}
} // Spieler kicken
sb.append("}"); try {
return sb.toString(); for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
} p.disconnect("§cServer f\"ahrt f\"ur ein Update neu herunter. Bitte etwas warten.");
}
} catch (Exception ignored) {}
// Skript starten
getLogger().info("Starte Update-Skript im Hintergrund...");
try {
Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\"");
} catch (IOException e) {
getLogger().warning("Konnte Skript nicht starten. Update wird manuell ben\"otigt.");
}
// Server stoppen
ProxyServer.getInstance().stop();
} catch (Exception e) {
getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage());
}
}
@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 auf Port " + port + ": " + 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
String versionRaw = ProxyServer.getInstance().getVersion();
String versionClean = versionRaw;
if (versionRaw != null && versionRaw.contains(":")) {
String[] parts = versionRaw.split(":");
if (parts.length >= 3) versionClean = parts[2].trim();
}
data.put("version", versionClean);
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
// Motd
String motd = "BungeeCord";
try {
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
if (it.hasNext()) {
ListenerInfo listener = it.next();
if (listener != null && listener.getMotd() != null) motd = listener.getMotd();
}
} catch (Exception ignored) {}
data.put("motd", motd);
// LuckPerms (Optional)
boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
List<Map<String, String>> playersList = new ArrayList<>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
Map<String, String> playerInfo = new LinkedHashMap<>();
playerInfo.put("name", p.getName());
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);
playersList.add(playerInfo);
}
data.put("players", playersList);
// Response
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 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 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

@@ -0,0 +1,151 @@
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;
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
private volatile String latestVersion = "";
private volatile String latestUrl = "";
// Pattern für Dateinamen im Assets-Array
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
// Pattern für Download-URL
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
// Pattern für Tag Version (WICHTIG: Wir suchen global, um das Haupt-Release zu finden)
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 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();
// 1. Die LATEST Version (Tag) finden
// Wir suchen das erste "tag_name" im gesamten JSON, das ist meistens das neueste Release.
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;
}
// Version säubern (v vorne entfernen)
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
foundVersion = foundVersion.substring(1);
}
// 2. Download URL für StatusAPI.jar finden
// Wir suchen das Asset "StatusAPI.jar"
String foundUrl = null;
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
Matcher releaseMatcher = releasePattern.matcher(body);
while (releaseMatcher.find()) {
String block = releaseMatcher.group();
java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> downloads = new java.util.ArrayList<>();
Matcher nm = ASSET_NAME_PATTERN.matcher(block);
while (nm.find()) names.add(nm.group(1));
Matcher dm = DOWNLOAD_PATTERN.matcher(block);
while (dm.find()) downloads.add(dm.group(1));
int pairs = Math.min(names.size(), downloads.size());
for (int i = 0; i < pairs; i++) {
String name = names.get(i);
String dl = downloads.get(i);
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
foundUrl = dl;
break;
}
}
if (foundUrl != null) break;
}
if (foundUrl == null) {
plugin.getLogger().warning("Keine JAR-Datei für dieses 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);
}
}
}

View File

@@ -1,4 +1,13 @@
name: StatusAPI name: StatusAPI
version: 1.0 main: net.viper.status.StatusAPI
main: net.viper.status.StatusAPI version: 3.6.2
author: M_Viper author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker
softdepend:
- LuckPerms
permissions:
statusapi.update.notify:
description: 'Erlaubt Update-Benachrichtigungen'
default: op