Files
squad-rain-ops-mini/Apis/ServerApi.cs

299 lines
14 KiB
C#

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>("/chatHub");
app.MapHub<PresenceHub>("/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<PlayerProfile>()
.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<ChatLog>()
.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<ChatLog>());
}
});
// 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<ChatLog>();
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<ChatHub> 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}" });
}
});
}
}
}