using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Plugins; /* * MajestatTopWeb 1.1.0 * ===================== * Web dashboard dla MajestatTopCore. * WYMAGA: MajestatTopCore 3.0.1+ * * TRYBY: "json" | "http" | "both" * ENDPOINTY: * GET / — strona glowna z rankingami * GET /player/{steamid} — profil gracza * GET /api/server — JSON info o serwerze * GET /api/tabs — JSON lista zakladek * GET /api/all — JSON wszystkie dane * GET /api/top/{tab} — JSON top dla zakladki * GET /api/top/{tab}/fame — JSON Hall of Fame * GET /api/player/{id} — JSON profil gracza */ namespace Oxide.Plugins { [Info("MajestatTopWeb", "Wo0t", "1.1.0")] [Description("Web dashboard for MajestatTopCore.")] class MajestatTopWeb : RustPlugin { [PluginReference] Plugin MajestatTopCore; private const string PluginVersion = "1.1.0"; private const string PluginUpdateUrl = "http://download.koldrix.com/rust/plugins/MajestatTopWeb/version.json"; private ConfigData _cfg; private HttpListener _listener; private Thread _thread; private bool _running = false; // ── Chat fields ────────────────────────────────────────────── private class ChatMessage { [JsonProperty("steamId")] public string SteamId; [JsonProperty("nick")] public string Nick; [JsonProperty("text")] public string Text; [JsonProperty("source")] public string Source; [JsonProperty("isAdmin")] public bool IsAdmin; [JsonProperty("timestamp")] public long Timestamp; } private class ConsoleEntry { [JsonProperty("text")] public string Text; [JsonProperty("source")] public string Source; [JsonProperty("timestamp")] public long Timestamp; } private class Session { public string SteamId; public string Nick; public bool IsAdmin; public bool HasPlayed; public DateTime Expires; } private const int MaxHistory = 200; private const int MaxConsoleLogs = 500; private List _chatHistory = new List(); private List _chatClients = new List(); private List _consoleLogs = new List(); private List _consoleClients = new List(); private Dictionary _sessions = new Dictionary(); private readonly object _chatLock = new object(); private readonly object _consoleLock = new object(); private Dictionary _rateLimitMap = new Dictionary(); private Dictionary _pendingRet = new Dictionary(); private int _updateIntervalMinutes = 60; // ── Config ─────────────────────────────────────────────────── private class ConfigData { [JsonProperty("Web Mode (json, http, both)")] public string WebMode = "both"; [JsonProperty("HTTP Port")] public int Port = 28020; [JsonProperty("API Key (empty = no auth)")] public string ApiKey = ""; [JsonProperty("JSON Export Interval (seconds)")] public int JsonExportInterval = 30; [JsonProperty("Leaderboard rows per tab")] public int LeaderboardRows = 10; [JsonProperty("Check for updates on startup")] public bool CheckUpdates = true; [JsonProperty("Wipe Schedule (e.g. Co tydzien w czwartek)")] public string WipeSchedule = ""; [JsonProperty("Server Type (e.g. Vanilla, Modded, PvE)")] public string ServerType = "Vanilla"; [JsonProperty("Table Number Format (none, K, M, KM)")] public string TableNumberFormat = "KM"; [JsonProperty("Table Number Decimal Places")] public int TableNumberDecimals = 1; [JsonProperty("Profile XP Number Format (none, K, M, KM)")] public string ProfileXpFormat = "KM"; [JsonProperty("Profile XP Decimal Places")] public int ProfileXpDecimals = 2; // ── Chat ───────────────────────────────────────────────── [JsonProperty("Chat Auth Mode (steam, played, readonly, open)")] public string ChatAuthMode = "steam"; [JsonProperty("Chat History Lines (0=none, -1=separate tab)")] public int ChatHistory = 50; [JsonProperty("Chat Console Mode (majestat, all, tabs)")] public string ChatConsoleMode = "majestat"; [JsonProperty("Chat Web prefix in game")] public string ChatWebPrefix = "[WEB]"; [JsonProperty("Chat Web prefix color (hex)")] public string ChatWebPrefixColor = "#4a90d9"; [JsonProperty("Chat Session timeout (minutes)")] public int ChatSessionTimeout = 120; [JsonProperty("Chat Max message length")] public int ChatMaxMessageLength = 200; [JsonProperty("Chat Rate limit (messages per minute)")] public int ChatRateLimit = 10; [JsonProperty("Steam OpenID Return URL (your domain, port same as HTTP Port)")] public string SteamReturnUrl = "http://twojserwer.pl:28020/auth/steam/callback"; } protected override void LoadDefaultConfig() { _cfg = new ConfigData(); SaveConfig(); } protected override void LoadConfig() { base.LoadConfig(); try { _cfg = Config.ReadObject(); } catch { LoadDefaultConfig(); } } protected override void SaveConfig() => Config.WriteObject(_cfg); // ── Init / Unload ──────────────────────────────────────────── void OnServerInitialized() { StartUpdateChecker(); if (MajestatTopCore == null) { PrintWarning("MajestatTopCore nie zaladowany!"); return; } Start(); } void OnPluginLoaded(Plugin p) { if (p?.Name == "MajestatTopCore") Start(); } void OnPluginUnloaded(Plugin p) { if (p?.Name == "MajestatTopCore") StopHttp(); } void Unload() { StopHttp(); lock (_chatLock) { foreach (var r in _chatClients) try{r.Close();}catch{} _chatClients.Clear(); } lock (_consoleLock) { foreach (var r in _consoleClients) try{r.Close();}catch{} _consoleClients.Clear(); } } // ── Chat Hooks ─────────────────────────────────────────────── void OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel) { if (channel != ConVar.Chat.ChatChannel.Global) return; bool isAdmin = player.IsAdmin || (MajestatTopCore != null && permission.UserHasPermission(player.UserIDString, "majestattopcore.admin")); AddChatMessage(new ChatMessage { SteamId = player.UserIDString, Nick = player.displayName, Text = message, Source = "game", IsAdmin = isAdmin, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }); } void OnPluginOutput(Plugin plugin, string message) { string src = plugin != null && plugin.Name.StartsWith("MajestatTop") ? "majestat" : "server"; string prefix = plugin != null ? "[" + plugin.Name + "] " : "[Server] "; AddConsoleLog(prefix + message, src); } private void AddChatMessage(ChatMessage msg) { lock (_chatLock) { _chatHistory.Add(msg); if (_chatHistory.Count > MaxHistory) _chatHistory.RemoveAt(0); PushSse(_chatClients, JsonConvert.SerializeObject(msg), _chatLock); } } private void AddConsoleLog(string text, string source) { lock (_consoleLock) { _consoleLogs.Add(new ConsoleEntry { Text = text, Source = source, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }); if (_consoleLogs.Count > MaxConsoleLogs) _consoleLogs.RemoveAt(0); PushSse(_consoleClients, JsonConvert.SerializeObject(new { text, source, timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }), _consoleLock); } } private void PushSse(List clients, string json, object lockObj) { byte[] buf = Encoding.UTF8.GetBytes("data: " + json + "\n\n"); var dead = new List(); foreach (var c in clients) try { c.OutputStream.Write(buf, 0, buf.Length); c.OutputStream.Flush(); } catch { dead.Add(c); } foreach (var d in dead) clients.Remove(d); } private void Start() { string mode = (_cfg.WebMode ?? "both").ToLower(); if (mode == "http" || mode == "both") StartHttp(); if (mode == "json" || mode == "both") { ExportJson(); timer.Every(_cfg.JsonExportInterval, ExportJson); } Puts("Uruchomiony. Tryb: " + _cfg.WebMode.ToUpper()); } // ── HTTP Server ────────────────────────────────────────────── private void StartHttp() { if (_running) return; try { _listener = new HttpListener(); _listener.Prefixes.Add("http://*:" + _cfg.Port + "/"); _listener.Start(); _running = true; _thread = new Thread(HttpLoop) { IsBackground = true }; _thread.Start(); Puts("HTTP serwer na porcie " + _cfg.Port + "."); } catch (Exception ex) { PrintError("HTTP start error: " + ex.Message); } } private void StopHttp() { _running = false; try { _listener?.Stop(); } catch { } _listener = null; } private void HttpLoop() { while (_running) { try { var ctx = _listener.GetContext(); string p = ctx.Request.Url.AbsolutePath.ToLower(); if (p == "/api/chat/stream" || p == "/api/console/stream") new Thread(() => HandleSse(ctx)) { IsBackground = true }.Start(); else ThreadPool.QueueUserWorkItem(_ => HandleRequest(ctx)); } catch { if (_running) Thread.Sleep(100); } } } private void HandleSse(HttpListenerContext ctx) { var resp = ctx.Response; bool isConsole = ctx.Request.Url.AbsolutePath.ToLower().Contains("console"); try { resp.ContentType = "text/event-stream; charset=utf-8"; resp.AddHeader("Cache-Control", "no-cache"); resp.AddHeader("Access-Control-Allow-Origin", "*"); resp.SendChunked = true; if (isConsole) { // Token może być w cookie lub query param (?token=...) var session = GetSession(ctx.Request); if (session == null) { string qt = ctx.Request.QueryString["token"] ?? ""; if (!string.IsNullOrEmpty(qt) && _sessions.TryGetValue(qt, out Session qs) && qs.Expires > DateTime.UtcNow) session = qs; } if (session == null || !session.IsAdmin) { resp.StatusCode = 403; resp.Close(); return; } lock (_consoleLock) { _consoleClients.Add(resp); } } else lock (_chatLock) { _chatClients.Add(resp); } // Trzymaj połączenie otwarte (SSE — keepalive co 15s) while (_running) { Thread.Sleep(15000); try { byte[] hb = Encoding.UTF8.GetBytes(": keep-alive\n\n"); resp.OutputStream.Write(hb, 0, hb.Length); resp.OutputStream.Flush(); } catch { break; } } } catch { } finally { if (isConsole) lock (_consoleLock) { _consoleClients.Remove(resp); } else lock (_chatLock) { _chatClients.Remove(resp); } try { resp.Close(); } catch { } } } private void HandleRequest(HttpListenerContext ctx) { var req = ctx.Request; var resp = ctx.Response; try { resp.AddHeader("Access-Control-Allow-Origin", "*"); resp.AddHeader("Access-Control-Allow-Headers", "X-Api-Key,Content-Type,X-Session-Token"); if (req.HttpMethod == "OPTIONS") { resp.StatusCode = 204; resp.Close(); return; } // POST tylko dla send bool isPost = req.HttpMethod == "POST"; if (!isPost && req.HttpMethod != "GET") { SendJson(resp, 405, new { error = "Method not allowed" }); return; } if (!string.IsNullOrEmpty(_cfg.ApiKey)) { string k = req.Headers["X-Api-Key"] ?? ""; if (k != _cfg.ApiKey) { SendJson(resp, 401, new { error = "Unauthorized" }); return; } } string path = req.Url.AbsolutePath.TrimEnd('/').ToLower(); if (path == "") path = "/"; string[] pts = path.Split('/'); // Strona glowna if (path == "/" || path == "/index" || path == "/index.html") { SendHtml(resp, BuildMainPage()); return; } // Profil gracza if (pts.Length == 3 && pts[1] == "player") { string html = BuildPlayerPage(pts[2]); if (html == null) { SendJson(resp, 404, new { error = "Player not found" }); return; } SendHtml(resp, html); return; } // Avatar gracza if (pts.Length == 3 && pts[1] == "avatar") { SendAvatar(ctx, pts[2]); return; } // ── Chat & Auth routes ──────────────────────────────── if (path == "/auth/steam") { HandleSteamAuthStart(req, resp); return; } if (path == "/auth/steam/callback") { HandleSteamCallback(req, resp); return; } if (path == "/auth/logout") { HandleLogout(req, resp); return; } if (path == "/api/auth/me") { HandleAuthMe(req, resp); return; } if (path == "/api/chat/messages") { HandleGetMessages(req, resp); return; } if (path == "/api/chat/send" && isPost){ HandleSendMessage(req, resp); return; } if (path == "/api/console/logs") { HandleGetLogs(req, resp); return; } // API if (path == "/api/server") { SendJson(resp, 200, BuildServerInfo()); return; } if (path == "/api/tabs") { SendJson(resp, 200, BuildTabs()); return; } if (path == "/api/all") { SendJson(resp, 200, BuildAll()); return; } if (pts.Length >= 4 && pts[1] == "api" && pts[2] == "top") { string tabId = pts[3]; bool fame = pts.Length >= 5 && pts[4] == "fame"; var data = BuildLeaderboard(tabId, fame); if (data == null) { SendJson(resp, 404, new { error = "Tab not found" }); return; } SendJson(resp, 200, data); return; } if (pts.Length == 4 && pts[1] == "api" && pts[2] == "player") { var data = BuildPlayerProfile(pts[3]); if (data == null) { SendJson(resp, 404, new { error = "Player not found" }); return; } SendJson(resp, 200, data); return; } SendJson(resp, 404, new { error = "Not found" }); } catch (Exception ex) { try { SendJson(resp, 500, new { error = ex.Message }); } catch { } } } private void SendJson(HttpListenerResponse resp, int code, object data) { resp.StatusCode = code; resp.ContentType = "application/json; charset=utf-8"; byte[] buf = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data, Formatting.Indented)); resp.ContentLength64 = buf.Length; resp.OutputStream.Write(buf, 0, buf.Length); resp.OutputStream.Close(); } private void SendHtml(HttpListenerResponse resp, string html) { resp.StatusCode = 200; resp.ContentType = "text/html; charset=utf-8"; byte[] buf = Encoding.UTF8.GetBytes(html); resp.ContentLength64 = buf.Length; resp.OutputStream.Write(buf, 0, buf.Length); resp.OutputStream.Close(); } // ── JSON Export ────────────────────────────────────────────── private void ExportJson() { if (MajestatTopCore == null) return; try { string json = JsonConvert.SerializeObject(BuildAll(), Formatting.Indented); string path = Path.Combine(Interface.Oxide.DataDirectory, "MajestatTopWeb.json"); File.WriteAllText(path, json, Encoding.UTF8); } catch (Exception ex) { PrintError("JSON export error: " + ex.Message); } } // ── Data Builders ──────────────────────────────────────────── private object BuildServerInfo() { var info = MajestatTopCore?.Call("API_GetServerInfo") as Dictionary; if (info == null) return new { error = "Core unavailable" }; int upSec = info.ContainsKey("uptime") ? Convert.ToInt32(info["uptime"]) : 0; info["uptimeFormatted"] = (upSec / 3600) + "h " + ((upSec % 3600) / 60) + "m"; info["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); info["timestampUtc"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); return info; } private object BuildTabs() => MajestatTopCore?.Call("API_GetTabs"); private object BuildLeaderboard(string tabId, bool fame) { var entries = MajestatTopCore?.Call("API_GetLeaderboard", tabId, fame, _cfg.LeaderboardRows) as List>; if (entries == null) return null; return new { tab = tabId, fame, count = entries.Count, updated = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), entries }; } private object BuildPlayerProfile(string steamId) { var stats = MajestatTopCore?.Call("API_GetAllStats", steamId, false) as Dictionary; var fame = MajestatTopCore?.Call("API_GetAllStats", steamId, true) as Dictionary; if (stats == null) return null; var pl = BasePlayer.FindByID(ulong.Parse(steamId)); return new { steamId, name = pl?.displayName ?? steamId, online = pl != null, stats, fameStats = fame, timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; } private object BuildAll() { var tabs = MajestatTopCore?.Call("API_GetTabs") as List>; var lb = new Dictionary(); var fm = new Dictionary(); if (tabs != null) foreach (var t in tabs) { string id = t["id"].ToString(); lb[id] = BuildLeaderboard(id, false); fm[id] = BuildLeaderboard(id, true); } return new { server = BuildServerInfo(), tabs, leaderboards = lb, fame = fm, timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; } // ── CSS Shared ─────────────────────────────────────────────── private string SharedCss() { return ":root{--bg:#0d0d0d;--bg2:#161616;--bg3:#1e1e1e;--border:#2a2a2a;--accent:#c8572a;--accent2:#e07840;--text:#d4c9b8;--text2:#8a8070;--green:#4a9e5c;--gold:#c8a84b;--blue:#4a7ec8;}\n" + "*{box-sizing:border-box;margin:0;padding:0;}\n" + "body{background:var(--bg);color:var(--text);font-family:'Segoe UI',sans-serif;font-size:14px;}\n" + "a{color:var(--accent2);text-decoration:none;}a:hover{text-decoration:underline;}\n" + "header{background:var(--bg2);border-bottom:2px solid var(--accent);padding:14px 24px;display:flex;align-items:center;gap:16px;justify-content:space-between;flex-wrap:nowrap;overflow:hidden;position:relative;}\n" + "header h1{font-size:18px;color:var(--accent2);white-space:nowrap;flex-shrink:0;}\n" + ".back{color:var(--text2);font-size:13px;}\n" + "main{padding:24px;max-width:1200px;margin:0 auto;}\n" + /* tabs */ ".tabs-bar{background:var(--bg2);border-bottom:1px solid var(--border);padding:0 24px;display:flex;gap:4px;align-items:stretch;}\n" + ".tab-btn{background:transparent;border:none;border-bottom:3px solid transparent;color:var(--text2);padding:12px 20px;cursor:pointer;font-size:13px;font-weight:bold;letter-spacing:1px;transition:color .2s,border-color .2s;}\n" + ".tab-btn:hover{color:var(--text);border-color:var(--border);}\n" + ".tab-btn.active{color:var(--accent2);border-color:var(--accent);}\n" + ".tab-content{display:none;}.tab-content.active{display:block;}\n" + ".tabs-bar-right{margin-left:auto;display:flex;align-items:center;padding:0 4px;gap:6px;font-size:12px;color:var(--text2);white-space:nowrap;}\n" + ".tabs-bar-right .ub-val{font-size:15px;font-weight:bold;color:var(--accent2);margin-left:4px;}\n" + /* section */ ".section{background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:24px;overflow:hidden;}\n" + ".section-title{background:var(--bg3);border-bottom:1px solid var(--border);padding:12px 20px;font-size:12px;font-weight:bold;color:var(--text2);text-transform:uppercase;letter-spacing:1px;}\n" + ".section-title.fame{color:var(--gold);}\n" + /* table */ ".table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}\n" + "table{width:100%;border-collapse:collapse;}\n" + "th{background:var(--bg3);color:var(--text2);font-size:11px;text-transform:uppercase;letter-spacing:1px;padding:10px 14px;text-align:center;border-bottom:1px solid var(--border);white-space:normal;line-height:1.3;}\n" + "th:first-child{text-align:center;}th:nth-child(2){text-align:left;}\n" + "th.xp{color:var(--gold);}\n" + "td{padding:9px 14px;border-bottom:1px solid var(--border);color:var(--text);text-align:center;}\n" + "td:first-child{text-align:center;}td:nth-child(2){text-align:left;}\n" + "tr:last-child td{border-bottom:none;}\n" + "tr:hover td{background:rgba(255,255,255,0.03);}\n" + ".rank{color:var(--text2);font-size:12px;}\n" + ".rank-1{color:#f0c040;font-weight:bold;}.rank-2{color:#b0b0b0;font-weight:bold;}.rank-3{color:#c07040;font-weight:bold;}\n" + ".player-name{font-weight:500;}\n" + ".val-xp{color:var(--gold);font-weight:bold;}.val-total{color:var(--accent2);font-weight:bold;}\n" + ".val-time{color:var(--blue);}.val-num{color:var(--text);}\n" + ".no-data{padding:24px;text-align:center;color:var(--text2);}\n" + /* header stats */ ".server-stats{display:flex;gap:16px;flex-wrap:wrap;}\n" + ".stat-badge{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:6px 14px;text-align:center;}\n" + ".stat-badge .val{font-size:20px;font-weight:bold;color:var(--accent2);}\n" + ".stat-badge .lbl{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:1px;}\n" + ".info-bar{background:var(--bg3);border-bottom:1px solid var(--border);padding:8px 24px;display:flex;gap:24px;flex-wrap:wrap;font-size:12px;color:var(--text2);}\n" + ".info-bar span{color:var(--text);}\n" + /* footer */ "footer{text-align:center;padding:14px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);margin-top:8px;line-height:1.8;}\n" + "footer .footer-copy{font-size:10px;color:var(--text2);opacity:0.5;margin-top:2px;}\n" + /* chat inline */ ".chat-wrap{display:flex;flex-direction:column;flex:1;overflow:hidden;background:var(--bg);}\n" + ".chat-auth-bar{background:var(--bg2);border-bottom:1px solid var(--border);padding:8px 16px;display:flex;align-items:center;gap:12px;flex-shrink:0;font-size:13px;}\n" + ".chat-auth-bar .user-nick{color:var(--text);font-weight:600;}\n" + ".chat-auth-bar .admin-badge{background:#3a1a0a;color:var(--accent2);font-size:10px;padding:2px 6px;border-radius:3px;border:1px solid var(--accent);margin-left:4px;}\n" + ".chat-auth-bar .btn-login{background:var(--accent);color:#fff;padding:5px 14px;border-radius:4px;font-size:12px;cursor:pointer;border:none;}\n" + ".chat-auth-bar .btn-login:hover{background:var(--accent2);}\n" + ".chat-auth-bar .btn-logout{color:var(--text2);font-size:12px;border:1px solid var(--border);padding:4px 12px;border-radius:4px;cursor:pointer;background:transparent;}\n" + ".chat-auth-bar .btn-logout:hover{color:var(--text);border-color:var(--text2);}\n" + ".chat-tab-nav{background:var(--bg3);border-bottom:1px solid var(--border);padding:0 12px;display:flex;gap:2px;flex-shrink:0;}\n" + ".chat-tab-btn{background:transparent;border:none;border-bottom:2px solid transparent;color:var(--text2);padding:8px 14px;cursor:pointer;font-size:12px;font-weight:bold;}\n" + ".chat-tab-btn:hover{color:var(--text);}\n" + ".chat-tab-btn.active{color:var(--accent2);border-color:var(--accent);}\n" + ".chat-section{display:none;flex:1;flex-direction:column;overflow:hidden;}\n" + ".chat-section.active{display:flex;}\n" + ".chat-messages{flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:4px;min-height:200px;max-height:480px;}\n" + ".msg{display:flex;gap:8px;align-items:baseline;padding:4px 8px;border-radius:4px;}\n" + ".msg:hover{background:rgba(255,255,255,0.03);}\n" + ".msg-time{font-size:11px;color:var(--text2);flex-shrink:0;width:48px;}\n" + ".msg-nick{font-weight:600;flex-shrink:0;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n" + ".msg-nick.game{color:#d4c9b8;}.msg-nick.web{color:var(--blue);}.msg-nick.server{color:var(--gold);}\n" + ".msg-nick.admin::after{content:' [A]';color:var(--accent2);font-size:10px;}\n" + ".msg-text{color:var(--text);word-break:break-word;}\n" + ".source-badge{font-size:10px;padding:1px 5px;border-radius:3px;flex-shrink:0;}\n" + ".badge-web{background:#1a2a3a;color:var(--blue);border:1px solid var(--blue);}\n" + ".badge-game{background:#1a1a1a;color:var(--text2);border:1px solid var(--border);}\n" + ".chat-input-bar{border-top:1px solid var(--border);padding:10px 16px;display:flex;gap:8px;flex-shrink:0;background:var(--bg2);}\n" + ".chat-input-bar input{flex:1;background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:4px;font-size:13px;}\n" + ".chat-input-bar input:focus{outline:none;border-color:var(--accent);}\n" + ".chat-input-bar button{background:var(--accent);color:#fff;border:none;padding:8px 18px;border-radius:4px;cursor:pointer;font-size:13px;font-weight:bold;flex-shrink:0;}\n" + ".chat-input-bar button:hover{background:var(--accent2);}\n" + ".chat-login-prompt{padding:12px 16px;text-align:center;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);font-size:13px;}\n" + ".chat-loading{color:var(--text2);padding:16px;text-align:center;}\n" + ".console-container{flex:1;overflow-y:auto;padding:12px 16px;font-family:'Courier New',monospace;font-size:12px;line-height:1.6;background:var(--bg);}\n" + ".log-entry{padding:2px 0;border-bottom:1px solid rgba(255,255,255,0.03);}\n" + ".log-entry.majestat{color:#7fd47f;}\n" + ".log-entry.server{color:#b0b0b0;}\n" + ".log-time{color:var(--text2);margin-right:8px;}\n" + "@media(max-width:600px){.msg-time{display:none;}.msg-nick{max-width:90px;}}\n" + /* responsive */ "@media(max-width:768px){.server-stats{gap:8px;}.stat-badge .val{font-size:15px;}th,td{padding:7px 8px;font-size:12px;}}\n"; } // ── Chat Inline (bez iframe) ────────────────────────────────── private string BuildChatInline() { string mode = (_cfg.ChatAuthMode ?? "steam").ToLower(); var sb = new StringBuilder(); sb.Append("
"); // Auth bar sb.Append("
"); sb.Append(""); if (mode != "open") sb.Append(""); sb.Append(""); sb.Append("
"); // Zakładki (Chat + Konsola dla admina) sb.Append("
"); sb.Append(""); sb.Append(""); sb.Append("
"); // Sekcja Chat sb.Append("
"); sb.Append("
Ładowanie...
"); sb.Append("
"); if (mode == "open") sb.Append("
"); else { sb.Append("
"); sb.Append(""); } sb.Append("
"); sb.Append("
"); // Sekcja Konsola (tylko admin — ukryta domyślnie, JS odkryje po weryfikacji) sb.Append("
"); sb.Append("
"); sb.Append("
"); sb.Append("
"); return sb.ToString(); } // ── Footer ─────────────────────────────────────────────────── private string BuildFooter() { string coreVer = MajestatTopCore?.Version.ToString() ?? "?"; // Czas pobierany z przeglądarki klienta przez JS return "
" + "MajestatTopWeb v" + PluginVersion + " • MajestatTopCore v" + coreVer + " • " + "
created by Wo0t © Koldrix Group 2007 –
" + "
" + ""; } // ── Main Page ──────────────────────────────────────────────── private string BuildMainPage() { var serverRaw = MajestatTopCore?.Call("API_GetServerInfo") as Dictionary; var tabsRaw = MajestatTopCore?.Call("API_GetTabs") as List>; string srvName = Get(serverRaw, "name", "Rust Server"); int players = GetInt(serverRaw, "players"); int maxPl = GetInt(serverRaw, "maxPlayers"); string map = Get(serverRaw, "map", "Unknown"); int seed = GetInt(serverRaw, "seed"); int wSize = GetInt(serverRaw, "worldSize"); int upSec = GetInt(serverRaw, "uptime"); string desc = Get(serverRaw, "description", ""); int uniquePl = GetInt(serverRaw, "uniquePlayers"); string uptime = (upSec / 3600) + "h " + ((upSec % 3600) / 60) + "m"; string mapUrl = "https://rustmaps.com/map/" + wSize + "/" + seed; var tabBtns = new StringBuilder(); var tabCont = new StringBuilder(); if (tabsRaw != null) { for (int i = 0; i < tabsRaw.Count; i++) { string id = tabsRaw[i]["id"].ToString(); string label = tabsRaw[i]["label"].ToString(); bool first = i == 0; tabBtns.Append(""); tabCont.Append("
"); var entries = MajestatTopCore?.Call("API_GetLeaderboard", id, false, _cfg.LeaderboardRows) as List>; tabCont.Append(BuildLeaderboardHtml(label, entries, false)); var fameEnt = MajestatTopCore?.Call("API_GetLeaderboard", id, true, _cfg.LeaderboardRows) as List>; tabCont.Append(BuildLeaderboardHtml(label + " — HALL OF FAME", fameEnt, true)); tabCont.Append("
"); } } // Zakładka CHAT — inline (bez iframe) tabBtns.Append(""); tabCont.Append("
"); tabCont.Append(BuildChatInline()); tabCont.Append("
"); var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("" + EscHtml(srvName) + " — Stats"); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("
"); sb.AppendLine("

" + EscHtml(srvName) + " STATS

"); sb.AppendLine("
"); sb.AppendLine("
" + players + "/" + maxPl + "
Online
"); sb.AppendLine("
" + uptime + "
Uptime
"); sb.AppendLine("
" + EscHtml(map) + "
Mapa
"); sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("
Seed: " + seed + "
"); sb.AppendLine("
Rozmiar: " + wSize + "
"); sb.AppendLine(" "); string srvType = GetServerType(); string wipeSch = GetWipeSchedule(); if (!string.IsNullOrEmpty(srvType)) sb.AppendLine("
Typ: " + EscHtml(srvType) + "
"); if (!string.IsNullOrEmpty(wipeSch)) sb.AppendLine("
Wipe: " + EscHtml(wipeSch) + "
"); if (!string.IsNullOrEmpty(desc)) { string descClean = EscHtml(desc).Replace("\\n", " ").Replace("\n", " ").Trim(); sb.AppendLine("
Opis: " + descClean + "
"); } sb.AppendLine("
Odswiezanie co " + _cfg.JsonExportInterval + "s
"); sb.AppendLine("
"); sb.AppendLine("
" + tabBtns + "
👥 Unikalni gracze:" + uniquePl + "
" + "
"); sb.AppendLine("
" + tabCont + "
"); sb.AppendLine(BuildFooter()); sb.AppendLine(""); sb.AppendLine(""); return sb.ToString(); } private string BuildLeaderboardHtml(string title, List> entries, bool fame) { var sb = new StringBuilder(); sb.Append("
" + title + "
"); if (entries == null || entries.Count == 0) { sb.Append("
Brak danych
"); return sb.ToString(); } var cols = entries[0].Keys.Where(k => k != "steamId" && k != "name" && k != "rank").ToList(); sb.Append("
"); sb.Append(""); foreach (var col in cols) { string lbl = FormatColumnLabel(col); string cls = col == "XP.Total" ? " class=\"xp\"" : ""; sb.Append("" + lbl + ""); } sb.Append(""); foreach (var e in entries) { int rank = Convert.ToInt32(e["rank"]); string rankCls = rank == 1 ? "rank-1" : rank == 2 ? "rank-2" : rank == 3 ? "rank-3" : "rank"; string name = EscHtml(e["name"].ToString()); string sid = e["steamId"].ToString(); sb.Append(""); sb.Append(""); foreach (var col in cols) { double val = e.ContainsKey(col) ? Convert.ToDouble(e[col]) : 0; string cls = col == "XP.Total" ? "val-xp" : col.EndsWith(".Total") ? "val-total" : col == "PlayTime.Seconds" ? "val-time" : "val-num"; string disp = col == "PlayTime.Seconds" ? FormatTime((long)val) : FormatNum(val); sb.Append(""); } sb.Append(""); } sb.Append("
#Gracz
" + rank + "" + name + "" + disp + "
"); return sb.ToString(); } // ── Player Profile Page ────────────────────────────────────── private string BuildPlayerPage(string steamId) { var stats = MajestatTopCore?.Call("API_GetAllStats", steamId, false) as Dictionary; var fame = MajestatTopCore?.Call("API_GetAllStats", steamId, true) as Dictionary; if (stats == null) return null; var serverRaw = MajestatTopCore?.Call("API_GetServerInfo") as Dictionary; string srvName = Get(serverRaw, "name", "Rust Server"); int players = GetInt(serverRaw, "players"); int maxPl = GetInt(serverRaw, "maxPlayers"); string map = Get(serverRaw, "map", "Unknown"); int upSec = GetInt(serverRaw, "uptime"); string uptime = (upSec / 3600) + "h " + ((upSec % 3600) / 60) + "m"; var player = BasePlayer.FindByID(ulong.Parse(steamId)); bool online = player != null; // Pobierz nick z cache Core (działa też dla offline graczy) string name = steamId; if (player != null) name = player.displayName; else { // Pobierz z API_GetAllStats — profil zawiera Name var profileData = MajestatTopCore?.Call("API_GetPlayerName", steamId) as string; if (!string.IsNullOrEmpty(profileData)) name = profileData; else if (stats != null) { // Fallback — szukaj w leaderboardzie var tabs = MajestatTopCore?.Call("API_GetTabs") as List>; if (tabs != null) { foreach (var t in tabs) { var lb = MajestatTopCore?.Call("API_GetLeaderboard", t["id"].ToString(), false, 1000) as List>; if (lb == null) continue; var entry = lb.FirstOrDefault(e => e.ContainsKey("steamId") && e["steamId"].ToString() == steamId); if (entry != null && entry.ContainsKey("name")) { name = entry["name"].ToString(); break; } } } } } string avatarUrl = "/avatar/" + steamId; string steamUrl = "https://steamcommunity.com/profiles/" + steamId; // Stale grupy — zawsze wyswietlaj wszystkie klucze nawet gdy 0 string[] pvpKeys = { "PvP.Kills","PvP.Deaths","PvE.Deaths", "Kill.Heli","Kill.Bradley","Kill.Scientist","Kill.Zombie","Kill.Animal","PlayTime.Seconds" }; string[] pvpKeysFame = { "XP.Total","PvP.Kills","PvP.Deaths","PvE.Deaths", "Kill.Heli","Kill.Bradley","Kill.Scientist","Kill.Zombie","Kill.Animal","PlayTime.Seconds" }; string[] gatherKeys = { "Gather.Total","Gather.Wood","Gather.Stone", "Gather.Metal","Gather.Sulfur","Gather.Scrap","Gather.Food" }; string[] lootKeys = { "Loot.Total","Loot.EliteCrate","Loot.MilitaryCrate", "Loot.LockedCrate","Loot.HackableCrate","Loot.Airdrop", "Loot.UnderwaterCrate","Loot.NormalCrate","Loot.Barrel" }; string[] buildKeys = { "Build.Total","Build.Walls","Build.Floors","Build.Doors", "Build.Turrets","Build.Traps","Build.Electrical","Build.Pipes" }; string profileCss = ".profile-header{display:flex;align-items:flex-end;gap:20px;padding:20px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:24px;flex-wrap:wrap;}\n" + ".profile-left{display:flex;flex-direction:column;align-items:center;gap:8px;flex-shrink:0;}\n" + ".avatar-link{display:block;text-decoration:none;}\n" + ".avatar-link:hover .avatar-wrap img,.avatar-link:hover .avatar-placeholder{border-color:var(--accent2);opacity:0.85;}\n" + ".avatar-wrap{position:relative;width:96px;height:96px;}\n" + ".avatar-wrap img{width:96px;height:96px;border-radius:8px;border:2px solid var(--accent);object-fit:cover;background:var(--bg3);display:block;transition:border-color .2s,opacity .2s;}\n" + ".avatar-placeholder{width:96px;height:96px;border-radius:8px;border:2px solid var(--accent);background:var(--bg3);display:flex;align-items:center;justify-content:center;font-size:36px;color:var(--text2);transition:border-color .2s,opacity .2s;}\n" + ".online-badge{font-size:12px;font-weight:bold;padding:3px 10px;border-radius:4px;text-align:center;white-space:nowrap;}\n" + ".online-badge.online{background:#1a3a1a;color:#4a9e5c;border:1px solid #4a9e5c;}\n" + ".online-badge.offline{background:var(--bg3);color:var(--text2);border:1px solid var(--border);}\n" + ".profile-info{flex:1;min-width:180px;display:flex;flex-direction:column;justify-content:space-between;align-self:stretch;}\n" + ".profile-name-link{text-decoration:none;}\n" + ".profile-name-link:hover h2{color:var(--text);text-decoration:underline;}\n" + ".profile-info h2{font-size:22px;color:var(--accent2);margin-bottom:6px;transition:color .2s;}\n" + ".profile-info .sub{font-size:12px;color:var(--text2);margin-top:3px;}\n" + ".profile-xp-big{font-size:34px;font-weight:bold;color:var(--gold);line-height:1;letter-spacing:-1px;padding-bottom:2px;}\n" + ".profile-xp-big .xp-lbl{font-size:13px;color:var(--text2);font-weight:normal;margin-right:6px;vertical-align:middle;letter-spacing:0;}\n" + ".profile-links{display:flex;flex-direction:column;gap:8px;align-self:flex-end;}\n" + ".btn-steam{display:block;background:#1b2838;border:1px solid #4a90d9;color:#4a90d9;padding:8px 18px;border-radius:4px;font-size:12px;text-align:center;}\n" + ".btn-steam:hover{background:#2a3f5f;}\n" + ".section-label{font-size:11px;font-weight:bold;color:var(--text2);text-transform:uppercase;letter-spacing:2px;margin-bottom:12px;padding-left:2px;}\n" + ".section-label.fame{color:var(--gold);}\n" + ".stats-row-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:28px;}\n" + "@media(max-width:900px){.stats-row-grid{grid-template-columns:repeat(2,1fr);}}\n" + "@media(max-width:500px){.stats-row-grid{grid-template-columns:1fr;}}\n" + ".stats-card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;overflow:hidden;}\n" + ".stats-card.fame{border-color:#c8a84b33;}\n" + ".stats-card-title{background:var(--bg3);border-bottom:1px solid var(--border);padding:10px 16px;font-size:11px;font-weight:bold;color:var(--text2);text-transform:uppercase;letter-spacing:1px;}\n" + ".stats-card.fame .stats-card-title{color:var(--gold);}\n" + ".stats-row{display:flex;justify-content:space-between;align-items:center;padding:7px 16px;border-bottom:1px solid var(--border);font-size:13px;}\n" + ".stats-row:last-child{border-bottom:none;}\n" + ".stats-row .key{color:var(--text2);}\n" + ".v-xp{color:#c8a84b;font-weight:bold;}.v-total{color:#e07840;font-weight:bold;}.v-time{color:#4a7ec8;}.v-num{color:var(--text);}\n"; var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("" + EscHtml(name) + " — " + EscHtml(srvName) + ""); sb.AppendLine(""); sb.AppendLine(""); // Header profilu: nazwa serwera (lewo) | PROFIL GRACZA (środek) | statsy (prawo) sb.AppendLine("
"); sb.AppendLine("

" + EscHtml(srvName) + " STATS

"); sb.AppendLine("
Profil Gracza
"); sb.AppendLine("
"); sb.AppendLine("
" + players + "/" + maxPl + "
Online
"); sb.AppendLine("
" + uptime + "
Uptime
"); sb.AppendLine("
" + EscHtml(map) + "
Mapa
"); sb.AppendLine("
"); sb.AppendLine("
"); // Info-bar z przyciskiem powrót po lewej sb.AppendLine("
"); sb.AppendLine(" ← Powrot do rankingu"); sb.AppendLine("
"); sb.AppendLine("
"); // XP gracza double xpTotal = stats != null && stats.ContainsKey("XP.Total") ? stats["XP.Total"] : 0; string xpFormatted = FormatXp(xpTotal); // Profile header sb.AppendLine("
"); // Lewa: avatar klikalny → Steam, badge pod spodem sb.AppendLine(" "); // Środek: nick klikalny → Steam na górze, XP na dole sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("

" + EscHtml(name) + "

"); sb.AppendLine("
SteamID64: " + steamId + "
"); sb.AppendLine("
Nick z ostatniej sesji na serwerze
"); sb.AppendLine("
"); sb.AppendLine("
XP" + xpFormatted + "
"); sb.AppendLine("
"); sb.AppendLine("
"); // Wiersz 1 — sezonowe sb.AppendLine("
Statystyki sezonowe
"); sb.AppendLine("
"); sb.Append(BuildStatsCard("PVP / PVE", pvpKeys, stats, false)); sb.Append(BuildStatsCard("GATHER", gatherKeys, stats, false)); sb.Append(BuildStatsCard("LOOT", lootKeys, stats, false)); sb.Append(BuildStatsCard("BUILD", buildKeys, stats, false)); sb.AppendLine("
"); // Wiersz 2 — Fame sb.AppendLine("
Hall of Fame
"); sb.AppendLine("
"); sb.Append(BuildStatsCard("FAME — PVP", pvpKeysFame, fame, true)); sb.Append(BuildStatsCard("FAME — GATHER", gatherKeys, fame, true)); sb.Append(BuildStatsCard("FAME — LOOT", lootKeys, fame, true)); sb.Append(BuildStatsCard("FAME — BUILD", buildKeys, fame, true)); sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine(BuildFooter()); sb.AppendLine(""); return sb.ToString(); } private string BuildStatsCard(string title, string[] keys, Dictionary stats, bool isFame) { var sb = new StringBuilder(); sb.Append("
"); sb.Append("
" + title + "
"); foreach (var key in keys) { double val = stats != null && stats.ContainsKey(key) ? stats[key] : 0; string label = key.Contains('.') ? key.Substring(key.IndexOf('.') + 1) : key; string cls, disp; if (key == "PlayTime.Seconds") { cls = "v-time"; disp = FormatTime((long)val); } else if (key == "XP.Total") { cls = "v-xp"; disp = FormatNum(val); } else if (key.EndsWith(".Total")) { cls = "v-total"; disp = FormatNum(val); } else { cls = "v-num"; disp = FormatNum(val); } sb.Append("
"); sb.Append("" + EscHtml(label) + ""); sb.Append("" + disp + ""); sb.Append("
"); } sb.Append("
\n"); return sb.ToString(); } // Cache URL avatarów steamId → url private Dictionary _avatarCache = new Dictionary(); // Pobierz avatar gracza z Steam i odeślij jako redirect private void SendAvatar(HttpListenerContext ctx, string steamId) { // Sprawdź cache if (_avatarCache.TryGetValue(steamId, out string cachedUrl)) { ctx.Response.StatusCode = 302; ctx.Response.AddHeader("Location", cachedUrl); ctx.Response.Close(); return; } // Próbuj pobrać URL avatara ze Steam XML API string fallback = "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg"; try { string xmlUrl = "https://steamcommunity.com/profiles/" + steamId + "?xml=1"; var wc = new System.Net.WebClient(); wc.Headers["User-Agent"] = "MajestatTopWeb/1.0"; string xml = wc.DownloadString(xmlUrl); int start = xml.IndexOf("= 0) { start += "", start); if (end > start) { string avatarUrl = xml.Substring(start, end - start).Trim(); if (!string.IsNullOrEmpty(avatarUrl)) { _avatarCache[steamId] = avatarUrl; ctx.Response.StatusCode = 302; ctx.Response.AddHeader("Location", avatarUrl); ctx.Response.Close(); return; } } } } catch { } _avatarCache[steamId] = fallback; ctx.Response.StatusCode = 302; ctx.Response.AddHeader("Location", fallback); ctx.Response.Close(); } // ── Chat & Auth Methods ────────────────────────────────────── private void HandleSteamAuthStart(HttpListenerRequest req, HttpListenerResponse resp) { // Auto-wykryj bazowy URL z nagłówków (działa za reverse proxy jak NPM) string baseUrl = GetBaseUrl(req); string ret = req.QueryString["ret"] ?? "/?tab=chat"; if (!ret.StartsWith("/")) ret = "/?tab=chat"; string token = Guid.NewGuid().ToString("N"); _pendingRet[token] = ret; string returnUrl = baseUrl + "/auth/steam/callback?state=" + token; string realm = baseUrl; string steamUrl = "https://steamcommunity.com/openid/login" + "?openid.ns=http://specs.openid.net/auth/2.0" + "&openid.mode=checkid_setup" + "&openid.return_to=" + Uri.EscapeDataString(returnUrl) + "&openid.realm=" + Uri.EscapeDataString(realm) + "&openid.identity=http://specs.openid.net/auth/2.0/identifier_select" + "&openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select"; resp.StatusCode = 302; resp.AddHeader("Location", steamUrl); resp.Close(); } // Zbuduj bazowy URL z nagłówków requestu (obsługuje reverse proxy) private string GetBaseUrl(HttpListenerRequest req) { // X-Forwarded-Proto ustawiane przez NPM/nginx string proto = req.Headers["X-Forwarded-Proto"] ?? ""; // X-Forwarded-Host lub standardowy Host string host = req.Headers["X-Forwarded-Host"] ?? req.Headers["Host"] ?? ""; // Usuń ewentualny port jeśli proto jest znane (NPM usuwa port z Host) if (!string.IsNullOrEmpty(proto) && !string.IsNullOrEmpty(host)) return proto + "://" + host; // Fallback: odczytaj z SteamReturnUrl w cfg jeśli ustawiony string cfgUrl = _cfg.SteamReturnUrl ?? ""; if (!string.IsNullOrEmpty(cfgUrl) && cfgUrl.Contains("/auth")) return cfgUrl.Substring(0, cfgUrl.IndexOf("/auth")); // Ostateczny fallback: lokalny adres serwera string scheme = req.IsSecureConnection ? "https" : "http"; return scheme + "://" + req.UserHostName; } private void HandleSteamCallback(HttpListenerRequest req, HttpListenerResponse resp) { string claimedId = req.QueryString["openid.claimed_id"] ?? ""; string steamId = ""; int idx = claimedId.LastIndexOf('/'); if (idx >= 0 && idx < claimedId.Length - 1) steamId = claimedId.Substring(idx + 1); if (string.IsNullOrEmpty(steamId) || steamId.Length < 15) { SendHtml(resp, "Błąd logowania Steam."); return; } // Pobierz ret URL z pendingRet przez state token string state = req.QueryString["state"] ?? ""; string retUrl = "/?tab=chat"; if (!string.IsNullOrEmpty(state) && _pendingRet.TryGetValue(state, out string pendingUrl)) { retUrl = pendingUrl; _pendingRet.Remove(state); } bool hasPlayed = MajestatTopCore != null && MajestatTopCore.Call("API_GetPlayerName", steamId) != null; if (_cfg.ChatAuthMode == "played" && !hasPlayed) { SendHtml(resp, ErrorPage("Dostęp tylko dla graczy którzy grali na tym serwerze.")); return; } bool isAdmin = permission.UserHasPermission(steamId, "majestattopcore.admin"); string nick = MajestatTopCore?.Call("API_GetPlayerName", steamId) as string ?? steamId; string token = Guid.NewGuid().ToString("N"); _sessions[token] = new Session { SteamId = steamId, Nick = nick, IsAdmin = isAdmin, HasPlayed = hasPlayed, Expires = DateTime.UtcNow.AddMinutes(_cfg.ChatSessionTimeout) }; resp.AddHeader("Set-Cookie", "session=" + token + "; Path=/; Max-Age=" + (_cfg.ChatSessionTimeout * 60) + "; HttpOnly; SameSite=Lax"); resp.StatusCode = 302; resp.AddHeader("Location", retUrl); resp.Close(); AddConsoleLog("[Auth] " + nick + " (" + steamId + ") zalogował się.", "majestat"); } private void HandleLogout(HttpListenerRequest req, HttpListenerResponse resp) { var session = GetSession(req); if (session != null) _sessions.Remove(GetSessionToken(req)); resp.AddHeader("Set-Cookie", "session=; Path=/; Max-Age=0"); resp.StatusCode = 302; resp.AddHeader("Location", "/"); resp.Close(); } private void HandleAuthMe(HttpListenerRequest req, HttpListenerResponse resp) { var s = GetSession(req); if (s == null) { SendJson(resp, 200, new { loggedIn = false }); return; } SendJson(resp, 200, new { loggedIn = true, steamId = s.SteamId, nick = s.Nick, isAdmin = s.IsAdmin }); } private void HandleGetMessages(HttpListenerRequest req, HttpListenerResponse resp) { int limit = _cfg.ChatHistory <= 0 ? 50 : _cfg.ChatHistory; List msgs; lock (_chatLock) { msgs = _chatHistory.TakeLast(limit).ToList(); } SendJson(resp, 200, msgs); } private void HandleSendMessage(HttpListenerRequest req, HttpListenerResponse resp) { string mode = (_cfg.ChatAuthMode ?? "steam").ToLower(); var session = GetSession(req); bool loggedIn = session != null; if (mode != "open" && mode != "readonly" && !loggedIn) { SendJson(resp, 401, new { ok = false, error = "Zaloguj się przez Steam." }); return; } if (mode == "readonly") { SendJson(resp, 403, new { ok = false, error = "Chat jest tylko do odczytu." }); return; } string body = ""; try { using (var sr = new System.IO.StreamReader(req.InputStream)) body = sr.ReadToEnd(); } catch { } Dictionary data = null; try { data = JsonConvert.DeserializeObject>(body); } catch { } string text = data != null && data.ContainsKey("text") ? data["text"]?.ToString() ?? "" : ""; string nick = loggedIn ? session.Nick : (data != null && data.ContainsKey("nick") ? data["nick"]?.ToString() ?? "Gość" : "Gość"); if (string.IsNullOrWhiteSpace(text)) { SendJson(resp, 400, new { ok = false, error = "Pusta wiadomość." }); return; } if (text.Length > _cfg.ChatMaxMessageLength) text = text.Substring(0, _cfg.ChatMaxMessageLength); // Rate limit string rateKey = loggedIn ? session.SteamId : (req.RemoteEndPoint?.Address?.ToString() ?? "anon"); long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (_rateLimitMap.TryGetValue(rateKey, out long lastMsg) && (now - lastMsg) < (60 / _cfg.ChatRateLimit)) { SendJson(resp, 429, new { ok = false, error = "Za szybko. Poczekaj chwilę." }); return; } _rateLimitMap[rateKey] = now; // Wyślij w grze string color = _cfg.ChatWebPrefixColor ?? "#4a90d9"; string prefix = _cfg.ChatWebPrefix ?? "[WEB]"; Server.Broadcast("" + prefix + " " + EscHtml(nick) + ": " + EscHtml(text)); AddChatMessage(new ChatMessage { SteamId = session?.SteamId ?? "", Nick = nick, Text = text, Source = "web", IsAdmin = session?.IsAdmin ?? false, Timestamp = now }); SendJson(resp, 200, new { ok = true }); } private void HandleGetLogs(HttpListenerRequest req, HttpListenerResponse resp) { var session = GetSession(req); if (session == null || !session.IsAdmin) { SendJson(resp, 403, new { error = "Brak uprawnień." }); return; } List logs; lock (_consoleLock) { logs = _consoleLogs.ToList(); } SendJson(resp, 200, logs); } private Session GetSession(HttpListenerRequest req) { if (req == null) return null; string token = GetSessionToken(req); if (string.IsNullOrEmpty(token)) return null; if (!_sessions.TryGetValue(token, out Session s)) return null; if (s.Expires < DateTime.UtcNow) { _sessions.Remove(token); return null; } s.Expires = DateTime.UtcNow.AddMinutes(_cfg.ChatSessionTimeout); return s; } private string GetSessionToken(HttpListenerRequest req) { // Cookie lub header string header = req.Headers["X-Session-Token"] ?? ""; if (!string.IsNullOrEmpty(header)) return header; string cookie = req.Headers["Cookie"] ?? ""; foreach (var c in cookie.Split(';')) { var parts = c.Trim().Split('='); if (parts.Length == 2 && parts[0].Trim() == "session") return parts[1].Trim(); } return ""; } private string ErrorPage(string msg) { return "

Brak dostępu

" + EscHtml(msg) + "

"; } // ── Server Tags (from server.tags ConVar) ──────────────────── private string GetServerType() { try { string tags = ConVar.Server.tags ?? ""; var parts = tags.Split(','); foreach (var tag in parts) { string t = tag.Trim().ToLower(); if (t == "pve") return "PvE"; if (t == "softcore") return "Softcore"; } foreach (var tag in parts) { string t = tag.Trim().ToLower(); if (t == "modded") return "Modded"; if (t == "vanilla") return "Vanilla"; } return !string.IsNullOrEmpty(_cfg.ServerType) ? _cfg.ServerType : ""; } catch { return !string.IsNullOrEmpty(_cfg.ServerType) ? _cfg.ServerType : ""; } } private string GetWipeSchedule() { try { string tags = ConVar.Server.tags ?? ""; var parts = tags.Split(','); foreach (var tag in parts) { string t = tag.Trim().ToLower(); switch (t) { case "weekly": return "Weekly"; case "biweekly": return "Biweekly"; case "monthly": return "Monthly"; case "daily": return "Daily"; } } return !string.IsNullOrEmpty(_cfg.WipeSchedule) ? _cfg.WipeSchedule : ""; } catch { return !string.IsNullOrEmpty(_cfg.WipeSchedule) ? _cfg.WipeSchedule : ""; } } // ── Helpers ────────────────────────────────────────────────── private string Get(Dictionary d, string key, string def) => d != null && d.ContainsKey(key) ? d[key]?.ToString() ?? def : def; private int GetInt(Dictionary d, string key) => d != null && d.ContainsKey(key) ? Convert.ToInt32(d[key]) : 0; // Podziel długą nazwę kolumny na dwie linie HTML // np. "UnderwaterCrate" → "UNDERWATER
CRATE" private string FormatColumnLabel(string col) { if (col == "PlayTime.Seconds") return "CZAS
GRY"; // Wyciągnij część po kropce string raw = col.Contains(".") ? col.Substring(col.IndexOf('.') + 1) : col; // Mapa czytelnych nazw switch (raw) { case "Total": return col == "XP.Total" ? "TOTAL
XP" : "TOTAL"; case "Kills": return "KILLS"; case "Deaths": return "DEATHS"; case "Heli": return "HELI"; case "Bradley": return "BRADLEY"; case "Scientist": return "SCIENTIST"; case "Zombie": return "ZOMBIE"; case "Animal": return "ANIMAL"; case "Wood": return "WOOD"; case "Stone": return "STONE"; case "Metal": return "METAL"; case "Sulfur": return "SULFUR"; case "Scrap": return "SCRAP"; case "Food": return "FOOD"; case "EliteCrate": return "ELITE
CRATE"; case "MilitaryCrate": return "MILITARY
CRATE"; case "LockedCrate": return "LOCKED
CRATE"; case "HackableCrate": return "HACKABLE
CRATE"; case "Airdrop": return "AIRDROP"; case "UnderwaterCrate": return "UNDERWATER
CRATE"; case "NormalCrate": return "NORMAL
CRATE"; case "Barrel": return "BARREL"; case "Walls": return "WALLS"; case "Floors": return "FLOORS"; case "Doors": return "DOORS"; case "Turrets": return "TURRETS"; case "Traps": return "TRAPS"; case "Electrical": return "ELECTRICAL"; case "Pipes": return "PIPES"; default: return raw.ToUpper(); } } private string EscHtml(string s) { if (string.IsNullOrEmpty(s)) return ""; return s.Replace("&", "&").Replace("<", "<").Replace(">", ">") .Replace("\"", """).Replace("'", "'"); } private string FormatTime(long seconds) { if (seconds <= 0) return "—"; long h = seconds / 3600; long m = (seconds % 3600) / 60; return h > 0 ? h + "h " + m.ToString("D2") + "m" : m + "m"; } private string FormatNum(double val) { string fmt = (_cfg.TableNumberFormat ?? "KM").ToUpper(); int dec = _cfg.TableNumberDecimals; string decFmt = "F" + dec; if (fmt.Contains("M") && val >= 1_000_000) return (val / 1_000_000).ToString(decFmt) + "M"; if (fmt.Contains("K") && val >= 1_000) return (val / 1_000).ToString(decFmt) + "K"; return ((long)val).ToString(); } private string FormatXp(double val) { string fmt = (_cfg.ProfileXpFormat ?? "KM").ToUpper(); int dec = _cfg.ProfileXpDecimals; string decFmt = "F" + dec; if (fmt.Contains("M") && val >= 1_000_000) return (val / 1_000_000).ToString(decFmt) + "M"; if (fmt.Contains("K") && val >= 1_000) return (val / 1_000).ToString(decFmt) + "K"; return ((long)val).ToString(); } // ── Update Checker ─────────────────────────────────────────── private void StartUpdateChecker() { try { var iv = MajestatTopCore?.Call("API_GetUpdateInterval"); _updateIntervalMinutes = iv != null ? Convert.ToInt32(iv) : 60; } catch { _updateIntervalMinutes = 60; } timer.Once(5f, CheckForUpdate); if (_updateIntervalMinutes > 0) timer.Every(_updateIntervalMinutes * 60f, CheckForUpdate); } void OnMajestatUpdateCheck() { webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); } private void CheckForUpdate() { webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); } private void OnUpdateResponse(int code, string response) { string pluginName = "MajestatTopWeb"; if (code == 0 || code >= 400 || string.IsNullOrEmpty(response)) { Puts("[Update] Nie mozna sprawdzic aktualizacji (HTTP " + code + ")."); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); return; } try { var data = JsonConvert.DeserializeObject>(response); string latest = data != null && data.ContainsKey("version") ? data["version"].Trim() : null; if (latest != null && IsNewerVersion(latest, PluginVersion)) { string url = data.ContainsKey("url") ? data["url"] : PluginUpdateUrl; PrintWarning("[Update] Dostepna nowsza wersja (" + latest + ") - Twoja (" + PluginVersion + ")"); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, latest, url); } else { Puts("[Update] Wersja aktualna (" + PluginVersion + ")."); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); } } catch (Exception ex) { Puts("[Update] Blad: " + ex.Message); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); } } private bool IsNewerVersion(string latest, string current) { try { var l = Array.ConvertAll(latest.Split('.'), int.Parse); var c = Array.ConvertAll(current.Split('.'), int.Parse); int len = Math.Max(l.Length, c.Length); for (int i = 0; i < len; i++) { int lv = i < l.Length ? l[i] : 0; int cv = i < c.Length ? c[i] : 0; if (lv > cv) return true; if (lv < cv) return false; } return false; } catch { return false; } } } }