From 98d5b1ea1eda29e95b0d526bd1749aa78f362cc3 Mon Sep 17 00:00:00 2001 From: BillyCool Date: Wed, 18 Mar 2026 20:50:20 +1100 Subject: [PATCH] Add logging interceptor and implement basic user transfer --- src/Data/UserDataSeeder.cs | 52 ++++++++++++++ src/Data/UserDataStore.cs | 16 +++++ src/Extensions/ServiceExtensions.cs | 1 + src/Interceptors/LoggingInterceptor.cs | 97 ++++++++++++++++++++++++++ src/Program.cs | 4 +- src/Services/UserService.cs | 13 +++- src/appsettings.Development.json | 3 +- 7 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 src/Data/UserDataSeeder.cs create mode 100644 src/Interceptors/LoggingInterceptor.cs diff --git a/src/Data/UserDataSeeder.cs b/src/Data/UserDataSeeder.cs new file mode 100644 index 0000000..0ae7495 --- /dev/null +++ b/src/Data/UserDataSeeder.cs @@ -0,0 +1,52 @@ +using MariesWonderland.Configuration; +using Microsoft.Extensions.Options; +using System.Text.Json; + +namespace MariesWonderland.Data; + +public class UserDataSeeder(IOptions options) +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private static readonly Type DbType = typeof(DarkUserMemoryDatabase); + + /// + /// Reads all Entity*Table.json files from the configured UserDataPath and populates + /// a new DarkUserMemoryDatabase. Returns an empty database if no files are found. + /// + 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; + } +} diff --git a/src/Data/UserDataStore.cs b/src/Data/UserDataStore.cs index d58cd71..72dbbe5 100644 --- a/src/Data/UserDataStore.cs +++ b/src/Data/UserDataStore.cs @@ -56,6 +56,22 @@ public class UserDataStore return db; } + /// + /// 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. + /// + 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) => _users.TryGetValue(userId, out db!); diff --git a/src/Extensions/ServiceExtensions.cs b/src/Extensions/ServiceExtensions.cs index cb29657..d4dff0f 100644 --- a/src/Extensions/ServiceExtensions.cs +++ b/src/Extensions/ServiceExtensions.cs @@ -21,6 +21,7 @@ public static class ServiceExtensions var masterDb = MasterDataLoader.Load(masterDataPath); services.AddSingleton(masterDb); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Interceptors/LoggingInterceptor.cs b/src/Interceptors/LoggingInterceptor.cs new file mode 100644 index 0000000..116ad73 --- /dev/null +++ b/src/Interceptors/LoggingInterceptor.cs @@ -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 logger) : Interceptor +{ + private static readonly List ExcludedPropertyNames = [ + nameof(AuthUserResponse.DiffUserData), + nameof(TableNameList.TableName), + nameof(UserDataGetResponse.UserDataJson) + ]; + + public override async Task UnaryServerHandler( + TRequest request, + ServerCallContext context, + UnaryServerMethod 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. + } +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index 94e60aa..b156552 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,6 +1,7 @@ using MariesWonderland.Configuration; using MariesWonderland.Data; using MariesWonderland.Extensions; +using MariesWonderland.Interceptors; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Options; @@ -23,7 +24,8 @@ public static class Program }); }); - builder.Services.AddGrpc(); + builder.Services.AddSingleton(); + builder.Services.AddGrpc(options => options.Interceptors.Add()); builder.Services.AddServerOptions(builder.Configuration); builder.Services.AddDataStores(builder.Configuration); diff --git a/src/Services/UserService.cs b/src/Services/UserService.cs index 1d753f5..b3163a9 100644 --- a/src/Services/UserService.cs +++ b/src/Services/UserService.cs @@ -8,9 +8,10 @@ using MariesWonderland.Proto.User; 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 UserDataSeeder _seeder = seeder; public override Task GetAndroidArgs(GetAndroidArgsRequest request, ServerCallContext context) { @@ -183,7 +184,15 @@ public class UserService(UserDataStore store) : MariesWonderland.Proto.User.User public override Task 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 TransferUserByApple(TransferUserByAppleRequest request, ServerCallContext context) diff --git a/src/appsettings.Development.json b/src/appsettings.Development.json index 100819a..ac6d852 100644 --- a/src/appsettings.Development.json +++ b/src/appsettings.Development.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "MariesWonderland": "Debug" } }, "Server": {