189 lines
6.2 KiB
C#
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");
|
|
}
|
|
}
|
|
}
|
|
} |