Add logging interceptor and implement basic user transfer

This commit is contained in:
BillyCool
2026-03-18 20:50:20 +11:00
parent f68d410d9f
commit 98d5b1ea1e
7 changed files with 182 additions and 4 deletions

View File

@@ -0,0 +1,52 @@
using MariesWonderland.Configuration;
using Microsoft.Extensions.Options;
using System.Text.Json;
namespace MariesWonderland.Data;
public class UserDataSeeder(IOptions<ServerOptions> options)
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private static readonly Type DbType = typeof(DarkUserMemoryDatabase);
/// <summary>
/// Reads all Entity*Table.json files from the configured UserDataPath and populates
/// a new DarkUserMemoryDatabase. Returns an empty database if no files are found.
/// </summary>
public DarkUserMemoryDatabase LoadFromFiles()
{
DarkUserMemoryDatabase db = new();
string rawPath = options.Value.Data.UserDataPath;
if (string.IsNullOrEmpty(rawPath))
return db;
string dataPath = Path.IsPathRooted(rawPath)
? rawPath
: Path.Combine(AppContext.BaseDirectory, rawPath);
if (!Directory.Exists(dataPath))
return db;
foreach (string file in Directory.EnumerateFiles(dataPath, "Entity*Table.json"))
{
// "EntityIUserCostumeTable.json" → "EntityIUserCostume"
string fileName = Path.GetFileName(file);
string propName = fileName[..^"Table.json".Length];
var prop = DbType.GetProperty(propName);
if (prop == null) continue;
string json = File.ReadAllText(file);
var list = JsonSerializer.Deserialize(json, prop.PropertyType, JsonOptions);
if (list != null)
prop.SetValue(db, list);
}
return db;
}
}

View File

@@ -56,6 +56,22 @@ public class UserDataStore
return db; return db;
} }
/// <summary>
/// Registers a pre-seeded database under a UUID. If the UUID is already mapped,
/// returns the existing userId without overwriting. Otherwise stores the seeded
/// database and maps the UUID to the userId found inside it.
/// </summary>
public long SeedUserFromDatabase(string uuid, DarkUserMemoryDatabase seededDb)
{
if (_uuidToUserId.TryGetValue(uuid, out var existingId))
return existingId;
long userId = seededDb.EntityIUser.FirstOrDefault()?.UserId ?? GenerateUserId();
_uuidToUserId[uuid] = userId;
_users[userId] = seededDb;
return userId;
}
public bool TryGet(long userId, out DarkUserMemoryDatabase db) public bool TryGet(long userId, out DarkUserMemoryDatabase db)
=> _users.TryGetValue(userId, out db!); => _users.TryGetValue(userId, out db!);

View File

@@ -21,6 +21,7 @@ public static class ServiceExtensions
var masterDb = MasterDataLoader.Load(masterDataPath); var masterDb = MasterDataLoader.Load(masterDataPath);
services.AddSingleton(masterDb); services.AddSingleton(masterDb);
services.AddSingleton<UserDataStore>(); services.AddSingleton<UserDataStore>();
services.AddSingleton<UserDataSeeder>();
return services; return services;
} }

View File

@@ -0,0 +1,97 @@
using Google.Protobuf;
using Grpc.Core;
using Grpc.Core.Interceptors;
using MariesWonderland.Proto.Data;
using MariesWonderland.Proto.User;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace MariesWonderland.Interceptors;
public class LoggingInterceptor(ILogger<LoggingInterceptor> logger) : Interceptor
{
private static readonly List<string> ExcludedPropertyNames = [
nameof(AuthUserResponse.DiffUserData),
nameof(TableNameList.TableName),
nameof(UserDataGetResponse.UserDataJson)
];
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
string methodName = context.Method;
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug("[GRPC] >> {Method} (request)", methodName);
logger.LogDebug("{Json}", Serialize(request));
}
TResponse response = await continuation(request, context);
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug("[GRPC] << {Method} (response)", methodName);
logger.LogDebug("{Json}", Serialize(response));
}
return response;
}
private static string Serialize(object obj)
{
string json = obj is IMessage message
? JsonFormatter.Default.Format(message)
: JsonSerializer.Serialize(obj);
return RemovePropertiesFromJson(json);
}
private static string RemovePropertiesFromJson(string json)
{
try
{
JsonNode? node = JsonNode.Parse(json);
if (node is null) return json;
RemoveProperties(node);
// Use compact JSON (no indentation) to match prior logging style.
return node.ToJsonString(new JsonSerializerOptions { WriteIndented = false });
}
catch
{
// If parsing fails for any reason, return the original JSON so logging still occurs.
return json;
}
}
private static void RemoveProperties(JsonNode? node)
{
if (node is JsonObject obj)
{
// Iterate over a snapshot of the keys because we'll be mutating the object.
foreach (var key in obj.Select(kvp => kvp.Key).ToList())
{
if (ExcludedPropertyNames.Contains(key, StringComparer.OrdinalIgnoreCase))
{
obj.Remove(key);
}
else
{
RemoveProperties(obj[key]);
}
}
}
else if (node is JsonArray arr)
{
foreach (var item in arr)
{
RemoveProperties(item);
}
}
// Primitives (JsonValue) do not contain nested properties; nothing to do.
}
}

View File

@@ -1,6 +1,7 @@
using MariesWonderland.Configuration; using MariesWonderland.Configuration;
using MariesWonderland.Data; using MariesWonderland.Data;
using MariesWonderland.Extensions; using MariesWonderland.Extensions;
using MariesWonderland.Interceptors;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -23,7 +24,8 @@ public static class Program
}); });
}); });
builder.Services.AddGrpc(); builder.Services.AddSingleton<LoggingInterceptor>();
builder.Services.AddGrpc(options => options.Interceptors.Add<LoggingInterceptor>());
builder.Services.AddServerOptions(builder.Configuration); builder.Services.AddServerOptions(builder.Configuration);
builder.Services.AddDataStores(builder.Configuration); builder.Services.AddDataStores(builder.Configuration);

View File

@@ -8,9 +8,10 @@ using MariesWonderland.Proto.User;
namespace MariesWonderland.Services; namespace MariesWonderland.Services;
public class UserService(UserDataStore store) : MariesWonderland.Proto.User.UserService.UserServiceBase public class UserService(UserDataStore store, UserDataSeeder seeder) : MariesWonderland.Proto.User.UserService.UserServiceBase
{ {
private readonly UserDataStore _store = store; private readonly UserDataStore _store = store;
private readonly UserDataSeeder _seeder = seeder;
public override Task<GetAndroidArgsResponse> GetAndroidArgs(GetAndroidArgsRequest request, ServerCallContext context) public override Task<GetAndroidArgsResponse> GetAndroidArgs(GetAndroidArgsRequest request, ServerCallContext context)
{ {
@@ -183,7 +184,15 @@ public class UserService(UserDataStore store) : MariesWonderland.Proto.User.User
public override Task<TransferUserResponse> TransferUser(TransferUserRequest request, ServerCallContext context) public override Task<TransferUserResponse> TransferUser(TransferUserRequest request, ServerCallContext context)
{ {
return Task.FromResult(new TransferUserResponse()); DarkUserMemoryDatabase seededDb = _seeder.LoadFromFiles();
long userId = _store.SeedUserFromDatabase(request.Uuid, seededDb);
TransferUserResponse response = new()
{
UserId = userId,
Signature = $"sig_{userId}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"
};
return Task.FromResult(response);
} }
public override Task<TransferUserByAppleResponse> TransferUserByApple(TransferUserByAppleRequest request, ServerCallContext context) public override Task<TransferUserByAppleResponse> TransferUserByApple(TransferUserByAppleRequest request, ServerCallContext context)

View File

@@ -2,7 +2,8 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning",
"MariesWonderland": "Debug"
} }
}, },
"Server": { "Server": {