using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; using SqlSugar; using RainOpsMini.Helpers; using RainOpsMini.Models; using RainOpsMini.Services; using RainOpsMini.Hubs; using RainOpsMini.Plugins; using RainOpsMini; using RainOpsMini.Models; using RainOpsMini.Helpers.RCON; using RainOpsMini.Helpers.SquadGameLog; namespace RainOpsMini.Apis { public static class ServerApi { public static void MapServerApis(this IEndpointRouteBuilder app) { // API: 注册 SignalR 集线器 app.MapHub("/chatHub"); app.MapHub("/presenceHub"); // API: 获取服务器信息 app.MapGet("/api/server-info", (RconDataCache cache, HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "server.view") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); var data = cache.GetData(); // Get game times from cache var gameTimes = cache.GetGameTimes().ToDictionary(k => k.Key, v => v.Value / 60); // Ensure all online players have game time from DB if missing in cache if (data.Players != null) { var onlineSteamIds = data.Players .Where(p => !string.IsNullOrEmpty(p.SteamId) && !gameTimes.ContainsKey(p.SteamId)) .Select(p => p.SteamId) .ToList(); if (onlineSteamIds.Any()) { try { using (var db = DbHelper.GetInstance()) { var profiles = db.Queryable() .Where(p => onlineSteamIds.Contains(p.SteamID)) .Select(p => new { p.SteamID, p.SquadPlaytimeHours }) .ToList(); foreach (var p in profiles) { if (!gameTimes.ContainsKey(p.SteamID)) { gameTimes[p.SteamID] = p.SquadPlaytimeHours; // Update cache to avoid DB hit next time cache.UpdateGameTime(p.SteamID, p.SquadPlaytimeHours * 60); } } } } catch (Exception ex) { Console.WriteLine($"Error fetching missing game times: {ex.Message}"); } } } // Try to get tickets from SquadGameLog if available int team1Tickets = 0; int team2Tickets = 0; // Logic for tickets omitted for now as not requested, but placeholder variables exist return Results.Json(new { serverInfo = data.ServerInfo, squads = data.Squads, players = data.Players, gameTimes = gameTimes, tickets = new { team1 = team1Tickets, team2 = team2Tickets }, lastUpdated = data.ServerInfo != null ? cache.LastUpdated.ToString("yyyy-MM-dd HH:mm:ss") : null, interval = cache.RefreshIntervalSeconds }); }); // API: 获取最近的聊天记录 // 从数据库中查询最近 300 条聊天日志 app.MapGet("/api/rcon/chat", (HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "chat") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); try { using (var db = DbHelper.GetInstance()) { var history = db.Queryable() .OrderByDescending(x => x.Timestamp) .Take(300) .ToList(); return Results.Json(history); } } catch (Exception ex) { // Fallback to empty list or error Console.WriteLine($"Error fetching chat history: {ex.Message}"); return Results.Json(new List()); } }); // API: 获取分页聊天记录 // 支持搜索功能,按时间倒序排列 app.MapGet("/api/rcon/chat/history", (int? page, int? pageSize, string? search, HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "chat") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); int p = page ?? 1; int ps = pageSize ?? 50; if (ps > 100) ps = 100; try { using (var db = DbHelper.GetInstance()) { var query = db.Queryable(); if (!string.IsNullOrEmpty(search)) { query = query.Where(x => x.Message.Contains(search) || x.Name.Contains(search) || x.SteamId.Contains(search)); } int total = 0; var list = query.OrderByDescending(x => x.Timestamp) .ToPageList(p, ps, ref total); return Results.Ok(new { total, page = p, pageSize = ps, items = list }); } } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // API: 发送调试聊天消息 // 仅用于测试 SignalR 推送功能 app.MapPost("/api/debug/chat", (ChatMessage msg, IHubContext hub, HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "chat") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); if (string.IsNullOrEmpty(msg.Type)) msg.Type = "ChatAll"; if (string.IsNullOrEmpty(msg.Name)) msg.Name = "DebugUser"; if (string.IsNullOrEmpty(msg.Message)) msg.Message = "Test Message"; msg.Timestamp = DateTime.Now; Program.RconCache.AddChatMessage(msg); hub.Clients.All.SendAsync("ReceiveChat", msg); return Results.Ok(msg); }); // API: 执行 RCON 命令 // 发送任意 RCON 命令到游戏服务器并返回结果 app.MapPost("/api/command", async (CommandRequest req, HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "rcon") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); if (Program.Client == null || !Program.Client.IsConnected) return Results.Json(new { error = "Not connected" }, statusCode: 503); if (string.IsNullOrWhiteSpace(req.Command)) return Results.BadRequest(); string response = await Program.Client.SendCommandAsync(req.Command); return Results.Text(response); }); // API: 设置 RCON 刷新间隔 app.MapPost("/api/rcon/interval", (IntervalRequest req, HttpContext ctx, RconDataCache cache) => { if (!ctx.User.HasClaim("Permission", "configs") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); cache.RefreshIntervalSeconds = req.Seconds; return Results.Ok(new { interval = cache.RefreshIntervalSeconds }); }); // API: 获取 RCON 刷新间隔 app.MapGet("/api/rcon/interval", (HttpContext ctx, RconDataCache cache) => { if (!ctx.User.HasClaim("Permission", "configs") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); return Results.Json(new { interval = cache.RefreshIntervalSeconds }); }); // API: 修改阵营票数 app.MapPost("/api/rcon/tickets", (TicketRequest req, HttpContext ctx, PluginManager pm) => { if (!ctx.User.HasClaim("Permission", "configs") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); try { var ticketPlugin = pm.GetPlugin("阵营票数查询") as TicketQueryPlugin; if (ticketPlugin == null || !ticketPlugin.IsEnabled) return Results.BadRequest(new { error = "Plugin disabled or not found" }); int current = ticketPlugin.SetTickets(req.TeamId, req.Tickets); return Results.Ok(new { tickets = current }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // API: 更换地图 // 发送 RCON 命令切换到指定地图层 app.MapPost("/api/map/change", async (CommandRequest req, HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "maps") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); if (string.IsNullOrWhiteSpace(req.Command)) return Results.BadRequest(); // Security check: ensure command is map related if (!req.Command.StartsWith("AdminSetNextLayer", StringComparison.OrdinalIgnoreCase) && !req.Command.StartsWith("AdminChangeLayer", StringComparison.OrdinalIgnoreCase)) { return Results.BadRequest(new { error = "Invalid map command" }); } if (Program.Client == null || !Program.Client.IsConnected) return Results.Json(new { error = "Not connected" }, statusCode: 503); string response = await Program.Client.SendCommandAsync(req.Command); return Results.Text(response); }); // 配置文件管理 API var configBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "SquadGame", "ServerConfig"); // API: 获取配置文件列表 // 列出 ServerConfig 目录下的所有文件 app.MapGet("/api/config/files", (HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "configs") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); if (!Directory.Exists(configBaseDir)) { return Results.NotFound(new { error = "Config directory not found" }); } var files = Directory.GetFiles(configBaseDir) .Select(Path.GetFileName) .Where(f => f.EndsWith(".cfg") || f.EndsWith(".txt") || f.EndsWith(".ini")) .ToList(); return Results.Ok(files); }); // API: 读取配置文件内容 // 读取指定配置文件的文本内容 app.MapGet("/api/config/read", (string filename, HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "configs") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); if (string.IsNullOrEmpty(filename) || filename.Contains("..") || filename.Contains("/") || filename.Contains("\\")) { return Results.BadRequest(new { error = "Invalid filename" }); } string path = Path.Combine(configBaseDir, filename); if (!File.Exists(path)) return Results.NotFound(); return Results.Text(File.ReadAllText(path)); }); // API: 保存配置文件 // 将内容写入指定配置文件,包含权限检查 app.MapPost("/api/config/save", (ConfigSaveRequest req, HttpContext ctx) => { if (!ctx.User.HasClaim("Permission", "configs") && !ctx.User.HasClaim("Permission", "all") && ctx.User.FindFirst("IsSuperAdmin")?.Value != "true") return Results.Unauthorized(); if (string.IsNullOrEmpty(req.Filename) || req.Filename.Contains("..") || req.Filename.Contains("/") || req.Filename.Contains("\\")) { return Results.BadRequest(new { error = "Invalid filename" }); } string path = Path.Combine(configBaseDir, req.Filename); // Security check: Only allow editing files in the config directory if (!path.StartsWith(configBaseDir)) return Results.BadRequest(new { error = "Access denied" }); try { File.WriteAllText(path, req.Content); return Results.Ok(); } catch (Exception ex) { return Results.BadRequest(new { error = $"保存配置失败: {ex.Message}" }); } }); } } }