Files
CatLink/Api/FutariLobby.cs
2026-01-18 17:59:01 +08:00

189 lines
6.2 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CatLink.Models;
using Microsoft.AspNetCore.Mvc;
namespace CatLink.Api
{
[ApiController]
[Route("recruit")]
public class FutariLobby : ControllerBase
{
private static readonly Dictionary<uint, RecruitRecord> _recruits = new();
private static readonly object _recruitsLock = new();
private static readonly string _logFilePath = "recruit.log";
private readonly ILogger<FutariLobby> _logger;
private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public FutariLobby(ILogger<FutariLobby> logger)
{
_logger = logger;
}
[HttpPost("start")]
public IActionResult StartRecruit([FromBody] JsonElement json)
{
try
{
var record = JsonSerializer.Deserialize<RecruitRecord>(json.GetRawText(), _jsonOptions);
if (record == null)
{
throw new ApiException(400, "Invalid request body");
}
record.Time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var key = record.RecruitInfo.MechaInfo.IpAddress;
lock (_recruitsLock)
{
var isNew = !_recruits.ContainsKey(key);
_recruits[key] = record;
if (isNew)
{
LogRecruit("START", record);
}
}
// Hash user IDs (commented out - client already sends numeric IDs)
// if (record.RecruitInfo.MechaInfo.UserIDs != null)
// {
// record.RecruitInfo.MechaInfo.UserIDs = record.RecruitInfo.MechaInfo.UserIDs
// .Select(HashUserId)
// .ToList();
// }
return Ok(new { success = true });
}
catch (ApiException ex)
{
return StatusCode(ex.Code, ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting recruit");
return StatusCode(500, "Internal server error");
}
}
[HttpPost("finish")]
public IActionResult FinishRecruit([FromBody] JsonElement json)
{
try
{
var record = JsonSerializer.Deserialize<RecruitRecord>(json.GetRawText(), _jsonOptions);
if (record == null)
{
throw new ApiException(400, "Invalid request body");
}
var key = record.RecruitInfo.MechaInfo.IpAddress;
lock (_recruitsLock)
{
if (!_recruits.ContainsKey(key))
{
throw new ApiException(404, "Recruit not found");
}
var existing = _recruits[key];
if (existing.Keychip != record.Keychip)
{
throw new ApiException(403, "Keychip mismatch");
}
_recruits.Remove(key);
LogRecruit("FINISH", existing);
}
return Ok(new { success = true });
}
catch (ApiException ex)
{
return StatusCode(ex.Code, ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error finishing recruit");
return StatusCode(500, "Internal server error");
}
}
[HttpGet("list")]
public IActionResult GetRecruitList()
{
try
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var timeout = 30000; // 30 seconds
List<RecruitRecord> records;
lock (_recruitsLock)
{
// Remove expired records
var expiredKeys = _recruits
.Where(kvp => now - kvp.Value.Time > timeout)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expiredKeys)
{
_recruits.Remove(key);
}
records = _recruits.Values.ToList();
}
// Return NDJSON format (newline-delimited JSON)
var sb = new System.Text.StringBuilder();
foreach (var record in records)
{
var json = System.Text.Json.JsonSerializer.Serialize(record, new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
});
sb.AppendLine(json);
}
return Content(sb.ToString(), "text/plain");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting recruit list");
return StatusCode(500, "Internal server error");
}
}
private static string HashUserId(long userId)
{
var hash = MD5.HashData(Encoding.UTF8.GetBytes(userId.ToString()));
return Convert.ToHexString(hash).ToLowerInvariant();
}
private void LogRecruit(string action, RecruitRecord record)
{
try
{
var line = $"[{DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm:ss}] {action} {record.RecruitInfo.MechaInfo.IpAddress} {record.Keychip} MusicID={record.RecruitInfo.MusicID}";
lock (_recruitsLock)
{
System.IO.File.AppendAllText(_logFilePath, line + Environment.NewLine);
}
_logger.LogInformation(line);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error writing to recruit log");
}
}
}
}