diff --git a/CannedNet/Hubs/NotificationsHub.cs b/CannedNet/Hubs/NotificationsHub.cs index e6d4e0c..0851445 100644 --- a/CannedNet/Hubs/NotificationsHub.cs +++ b/CannedNet/Hubs/NotificationsHub.cs @@ -28,7 +28,11 @@ public class NotificationsHub : Hub public async Task SubscribeToPlayers(SubscriptionList subscriptionList) { - await _notificationService.SubscribeToPlayers(Context.ConnectionId, subscriptionList); + var hubContext = GetService>(); + if (hubContext != null) + { + await _notificationService.SubscribeToPlayers(Context.ConnectionId, subscriptionList, hubContext); + } } public Task> GetSubscriptions() @@ -40,4 +44,9 @@ public class NotificationsHub : Hub { return _notificationService; } + + private T? GetService() where T : class + { + return Context.GetHttpContext()?.RequestServices.GetService(typeof(T)) as T; + } } diff --git a/CannedNet/Models/Economy/BuyItemRequest.cs b/CannedNet/Models/Economy/BuyItemRequest.cs index 2a6959e..0295b68 100644 --- a/CannedNet/Models/Economy/BuyItemRequest.cs +++ b/CannedNet/Models/Economy/BuyItemRequest.cs @@ -17,6 +17,21 @@ public class BuyItemRequest public int? CouponConsumablePlayerMappingId { get; set; } [JsonPropertyName("Gift")] - public object? Gift { get; set; } + public GiftRequest? Gift { get; set; } +} + +public class GiftRequest +{ + [JsonPropertyName("ToPlayerId")] + public int ToPlayerId { get; set; } + + [JsonPropertyName("Message")] + public string Message { get; set; } = string.Empty; + + [JsonPropertyName("Anonymous")] + public bool Anonymous { get; set; } + + [JsonPropertyName("GiftContext")] + public int GiftContext { get; set; } } diff --git a/CannedNet/Models/Notifications/NotificationModels.cs b/CannedNet/Models/Notifications/NotificationModels.cs index df4ae43..3b0fb7d 100644 --- a/CannedNet/Models/Notifications/NotificationModels.cs +++ b/CannedNet/Models/Notifications/NotificationModels.cs @@ -8,6 +8,7 @@ public class NotificationMessage public string? NotificationId { get; set; } [JsonPropertyName("notificationType")] + [JsonConverter(typeof(JsonStringEnumConverter))] public PushNotificationId? NotificationType { get; set; } [JsonPropertyName("id")] diff --git a/CannedNet/Services/Controllers/APIController.cs b/CannedNet/Services/Controllers/APIController.cs index 9bfaadc..ba08210 100644 --- a/CannedNet/Services/Controllers/APIController.cs +++ b/CannedNet/Services/Controllers/APIController.cs @@ -1353,15 +1353,26 @@ public class APIController db.TokenBalances.Update(tokenBalance); await db.SaveChangesAsync(); - // Store each gift in the database as a received gift with all properties + var receiverAccountId = id; + int? senderPlayerId = null; + + if (buyRequest.Gift != null) + { + receiverAccountId = buyRequest.Gift.ToPlayerId; + if (!buyRequest.Gift.Anonymous) + { + senderPlayerId = id; + } + } + var storedGifts = new List(); foreach (var giftDrop in storefrontItem.GiftDrops) { var receivedGift = new ReceivedGift { - ReceiverAccountId = id, - FromPlayerId = null, - Message = "A gift for you <3", + ReceiverAccountId = receiverAccountId, + FromPlayerId = senderPlayerId, + Message = buyRequest.Gift?.Message ?? "A gift for you <3", ConsumableItemDesc = giftDrop.ConsumableItemDesc ?? string.Empty, AvatarItemDesc = giftDrop.AvatarItemDesc ?? string.Empty, FriendlyName = giftDrop.FriendlyName ?? string.Empty, @@ -1375,7 +1386,7 @@ public class APIController Platform = -1, PlatformsToSpawnOn = -1, BalanceType = 0, - GiftContext = giftDrop.Context, + GiftContext = buyRequest.Gift?.GiftContext ?? giftDrop.Context, GiftRarity = giftDrop.Rarity, ReceivedAt = DateTime.UtcNow, IsConsumed = false @@ -1386,7 +1397,6 @@ public class APIController } await db.SaveChangesAsync(); - // Build the response with stored gift IDs var giftData = storedGifts.Select(rg => new GiftData { Id = rg.Id, @@ -1424,28 +1434,27 @@ public class APIController Platform = -1 }; - // Send notification for purchase try { - var hubContext = app.Services.GetService(); - if (hubContext != null) - { - var purchaseNotification = notificationService.CreateNotification( - PushNotificationId.StorefrontBalancePurchase, - id: storefrontItem.Id, - toAccountId: id, - data: new Dictionary - { - { "ItemId", buyRequest.PurchasableItemId }, - { "Price", price.Price }, - { "CurrencyType", buyRequest.CurrencyType }, - { "NewBalance", tokenBalance.Balance } - } - ); - await notificationService.SendNotificationToPlayer(null, hubContext, id, purchaseNotification); - } + var purchaseNotification = notificationService.CreateNotification( + PushNotificationId.StorefrontBalancePurchase, + id: storefrontItem.Id, + toAccountId: id, + data: new Dictionary + { + { "ItemId", buyRequest.PurchasableItemId }, + { "Price", price.Price }, + { "CurrencyType", buyRequest.CurrencyType }, + { "NewBalance", tokenBalance.Balance } + } + ); + await notificationService.SendNotificationToPlayer(id, purchaseNotification); } - catch { } + catch (Exception notifEx) + { + Console.WriteLine($"Error sending purchase notification: {notifEx.Message}"); + } + return Results.Ok(response); } @@ -1501,7 +1510,6 @@ public class APIController } } - // Get all earnable rewards for this context var earnableRewards = await db.EarnableRewards .Where(er => er.RewardContext == giftContext) .ToListAsync(); @@ -1515,30 +1523,25 @@ public class APIController int giftRarity = 0; string consumableItemDesc = ""; - // 60/40 roll - 60% earnable item, 40% tokens bool isEarnableItem = random.Next(0, 100) < 60; if (isEarnableItem && earnableRewards.Any()) { - // Get rewards that are avatar items (have avatarItemDesc set) var avatarRewards = earnableRewards.Where(er => !string.IsNullOrEmpty(er.AvatarItemDesc)).ToList(); if (avatarRewards.Any()) { - // Get all avatar items the player currently owns var ownedAvatarItems = await db.AvatarItems .Where(ai => ai.OwnerAccountId == id) .Select(ai => ai.AvatarItemDesc) .ToListAsync(); - // Filter to rewards the player doesn't own var unownedRewards = avatarRewards .Where(ar => !ownedAvatarItems.Contains(ar.AvatarItemDesc)) .ToList(); if (unownedRewards.Any()) { - // Randomly select one of the unowned rewards var selectedReward = unownedRewards[random.Next(unownedRewards.Count)]; avatarItemDesc = selectedReward.AvatarItemDesc; friendlyName = selectedReward.FriendlyName; @@ -1547,28 +1550,23 @@ public class APIController } else { - // Player owns all avatar items, give token box instead isEarnableItem = false; } } else { - // No avatar rewards, give token box instead isEarnableItem = false; } } - // If not earnable item or player owns all items, generate a token box if (!isEarnableItem) { - int[] tokenAmounts = { 10, 25, 50, 100, 250, 500 }; + int[] tokenAmounts = {10, 25, 50, 100, 250, 500}; currency = tokenAmounts[random.Next(tokenAmounts.Length)]; currencyType = 2; - consumableItemDesc = $"TokenBox_{currency}"; giftRarity = 20; } - // Create the received gift record var receivedGift = new ReceivedGift { ReceiverAccountId = id, @@ -1640,7 +1638,6 @@ public class APIController if (string.IsNullOrEmpty(accountId) || !int.TryParse(accountId.AsSpan(), out var id)) return Results.Unauthorized(); - // Parse form data request.EnableBuffering(); request.Body.Position = 0; using var reader = new StreamReader(request.Body); @@ -1669,7 +1666,6 @@ public class APIController return Results.BadRequest(new { success = false, error = "Invalid gift ID" }); } - // Look for the ReceivedGift var receivedGift = await db.ReceivedGifts .FirstOrDefaultAsync(rg => rg.Id == giftId && rg.ReceiverAccountId == id); @@ -1678,7 +1674,6 @@ public class APIController return Results.NotFound(new { success = false, error = "Gift not found" }); } - // Process the gift based on its properties if (!string.IsNullOrEmpty(receivedGift.ConsumableItemDesc)) { var existingConsumable = await db.ConsumableItems @@ -1719,44 +1714,69 @@ public class APIController }; db.AvatarItems.Add(avatarItem); } + + if (!string.IsNullOrEmpty(receivedGift.Currency.ToString()) && receivedGift.CurrencyType == 2) + { + var tokenBalance = await db.TokenBalances + .FirstOrDefaultAsync(tb => tb.Id == id && + tb.CurrencyType == receivedGift.CurrencyType && + tb.BalanceType == -1); + + if (tokenBalance == null) + { + tokenBalance = new TokenBalance + { + Id = id, + CurrencyType = receivedGift.CurrencyType, + BalanceType = -1, + Balance = receivedGift.Currency + }; + db.TokenBalances.Add(tokenBalance); + } + else + { + tokenBalance.Balance += receivedGift.Currency; + db.TokenBalances.Update(tokenBalance); + } + } - // Mark the gift as consumed receivedGift.IsConsumed = true; receivedGift.ConsumedAt = DateTime.UtcNow; db.ReceivedGifts.Update(receivedGift); await db.SaveChangesAsync(); - // Send notifications based on what was added try { - var hubContext = app.Services.GetService(); - if (hubContext != null) + if (!string.IsNullOrEmpty(receivedGift.ConsumableItemDesc)) { - if (!string.IsNullOrEmpty(receivedGift.ConsumableItemDesc)) - { - var consumableNotification = notificationService.CreateNotification( - PushNotificationId.ConsumableMappingAdded, - id: giftId, - toAccountId: id, - data: new Dictionary - { - { "Id", (long)giftId }, - { "ConsumableItemDesc", receivedGift.ConsumableItemDesc }, - { "PlatformMask", -1 }, - { "CreatedAt", DateTime.UtcNow }, - { "Count", 1 }, - { "InitialCount", 1 }, - { "UnlockedLevel", 0 }, - { "IsActive", false }, - { "ActiveDurationMinutes", null } - } - ); - await notificationService.SendNotificationToPlayer(null, hubContext, id, consumableNotification); - } + var consumableNotification = notificationService.CreateNotification( + PushNotificationId.ConsumableMappingAdded, + id: giftId, + toAccountId: id, + data: new Dictionary + { + { "Id", giftId }, + { "ConsumableType", "JfnVXFmilU6ysv-VbTAe3A" }, + { "PlatformMask", 4294967295 }, + { "Count", 1 }, + { "InitialCount", 1 }, + { "UnlockedLevel", 0 }, + { "CreatedAt", DateTime.UtcNow }, + { "IsActive", false }, + { "ActiveDurationMinutes", null }, + { "Category", 0 }, + { "IsPlatformLocked", false } + } + ); + await notificationService.SendNotificationToPlayer(id, consumableNotification); } } - catch { } + catch (Exception notifEx) + { + // Log the error but don't fail the gift consumption + Console.WriteLine($"Error sending notification: {notifEx.Message}"); + } return Results.Ok(new { success = true }); } diff --git a/CannedNet/Services/Controllers/AccountsController.cs b/CannedNet/Services/Controllers/AccountsController.cs index bdf5b3e..03a49e4 100644 --- a/CannedNet/Services/Controllers/AccountsController.cs +++ b/CannedNet/Services/Controllers/AccountsController.cs @@ -16,7 +16,7 @@ public class AccountsController return Results.Ok("FUCKL MYSJDIOJD"); }); - app.MapGet("/account/me", (HttpRequest request, AppDbContext db) => + app.MapGet("/account/me", async (HttpRequest request, AppDbContext db) => { var authHeader = request.Headers.Authorization.ToString(); @@ -32,17 +32,21 @@ public class AccountsController if (!int.TryParse(accountId.AsSpan(), out var id)) return Results.Unauthorized(); + var account = await db.Accounts.FindAsync(id); + if (account == null) + return Results.NotFound(); + var selfAccount = new SelfAccount { AccountId = id, - ProfileImage = "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", - IsJunior = false, - Platforms = 0, - PersonalPronouns = 0, - IdentityFlags = 0, - Username = $"Player{id}", - DisplayName = $"Player{id}", - CreatedAt = DateTime.UtcNow, + ProfileImage = account.ProfileImage ?? "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", + IsJunior = account.IsJunior, + Platforms = account.Platforms ?? 0, + PersonalPronouns = account.PersonalPronouns ?? 0, + IdentityFlags = account.IdentityFlags ?? 0, + Username = account.Username ?? $"Player{id}", + DisplayName = account.DisplayName ?? $"Player{id}", + CreatedAt = account.CreatedAt, Email = null, Phone = null, JuniorState = null, @@ -54,54 +58,62 @@ public class AccountsController return Results.Ok(selfAccount); }); - app.MapGet("/account/bulk", (HttpRequest request) => + app.MapGet("/account/bulk", async (HttpRequest request, AppDbContext db) => { var ids = request.Query["id"]; - var accounts = new List(); + var accountIds = new List(); foreach (var id in ids) { if (int.TryParse(id, out var accountId)) { - accounts.Add(new Account - { - AccountId = accountId, - ProfileImage = "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", - IsJunior = false, - Platforms = 0, - PersonalPronouns = 0, - IdentityFlags = 0, - Username = $"Player{accountId}", - DisplayName = $"Player{accountId}", - CreatedAt = DateTime.UtcNow - }); + accountIds.Add(accountId); } } - return Results.Json(accounts); + var accounts = await db.Accounts + .Where(a => accountIds.Contains(a.AccountId.Value)) + .ToListAsync(); + + var result = accounts.Select(a => new Account + { + AccountId = a.AccountId, + ProfileImage = a.ProfileImage ?? "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", + IsJunior = a.IsJunior, + Platforms = a.Platforms ?? 0, + PersonalPronouns = a.PersonalPronouns ?? 0, + IdentityFlags = a.IdentityFlags ?? 0, + Username = a.Username ?? $"Player{a.AccountId}", + DisplayName = a.DisplayName ?? $"Player{a.AccountId}", + CreatedAt = a.CreatedAt + }).ToList(); + + return Results.Json(result); }); - app.MapGet("/account/{id}", (HttpRequest request, string id) => + app.MapGet("/account/{id}", async (HttpRequest request, string id, AppDbContext db) => { - var accounts = new Account(); - - if (int.TryParse(id, out var accountId)) - { - accounts = new Account - { - AccountId = accountId, - ProfileImage = "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", - IsJunior = false, - Platforms = 0, - PersonalPronouns = 0, - IdentityFlags = 0, - Username = $"Player{accountId}", - DisplayName = $"Player{accountId}", - CreatedAt = DateTime.UtcNow - }; - } + if (!int.TryParse(id, out var accountId)) + return Results.BadRequest(); - return Results.Json(accounts); + var account = await db.Accounts.FindAsync(accountId); + if (account == null) + return Results.NotFound(); + + var result = new Account + { + AccountId = account.AccountId, + ProfileImage = account.ProfileImage ?? "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", + IsJunior = account.IsJunior, + Platforms = account.Platforms ?? 0, + PersonalPronouns = account.PersonalPronouns ?? 0, + IdentityFlags = account.IdentityFlags ?? 0, + Username = account.Username ?? $"Player{account.AccountId}", + DisplayName = account.DisplayName ?? $"Player{account.AccountId}", + CreatedAt = account.CreatedAt + }; + + return Results.Json(result); }); app.MapPost("/account/create", async (HttpRequest httpRequest, AppDbContext db) => @@ -175,7 +187,7 @@ public class AccountsController var maxRoomId = await db.Rooms.MaxAsync(r => (int?)r.RoomId) ?? 0; var maxId = await db.Rooms.MaxAsync(r => (int?)r.Id) ?? 0; var dormRoomId = maxRoomId + 1; - var dormRoom = new Room + var dormRoom = new Room { Id = maxId + 1, RoomId = dormRoomId, @@ -300,7 +312,146 @@ public class AccountsController account.DisplayName = newDisplayName; await db.SaveChangesAsync(); - return Results.Ok(); + return Results.Ok(RecNetResult.Ok()); + }); + + app.MapPut("/account/me/username", async (HttpRequest request, AppDbContext db) => + { + var authHeader = request.Headers.Authorization.ToString(); + + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return Results.Unauthorized(); + + var token = authHeader.Substring("Bearer ".Length); + var accountId = jwtService.ValidateAndGetAccountId(token); + + if (string.IsNullOrEmpty(accountId)) + return Results.Unauthorized(); + + if (!int.TryParse(accountId.AsSpan(), out var id)) + return Results.Unauthorized(); + + string newAccountName = ""; + + if (request.ContentLength.HasValue && request.ContentLength > 0) + { + try + { + request.EnableBuffering(); + using var reader = new StreamReader(request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + + if (!string.IsNullOrWhiteSpace(body)) + { + foreach (var pair in body.Split('&')) + { + var keyValue = pair.Split('='); + if (keyValue.Length == 2) + { + var key = Uri.UnescapeDataString(keyValue[0]); + var value = Uri.UnescapeDataString(keyValue[1]); + + if (key == "username") + newAccountName = value; + } + } + } + request.Body.Position = 0; + } + catch { } + } + + var account = await db.Accounts.FindAsync(id); + if (account == null) + return Results.NotFound(); + + account.Username = newAccountName; + await db.SaveChangesAsync(); + + return Results.Ok(RecNetResult.Ok()); + }); + + app.MapGet("/account/{id}/bio", async (HttpRequest request, string id, AppDbContext db) => + { + if (!int.TryParse(id, out var accountId)) + return Results.BadRequest(); + + var bio = await db.PlayerBios.FirstOrDefaultAsync(b => b.accountId == accountId); + + if (bio == null) + { + return Results.Json(new { accountId = accountId, bio = "" }); + } + + return Results.Json(new { accountId = bio.accountId, bio = bio.bio }); + }); + + app.MapPut("/account/me/bio", async (HttpRequest request, AppDbContext db) => + { + var authHeader = request.Headers.Authorization.ToString(); + + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return Results.Unauthorized(); + + var token = authHeader.Substring("Bearer ".Length); + var accountId = jwtService.ValidateAndGetAccountId(token); + + if (string.IsNullOrEmpty(accountId)) + return Results.Unauthorized(); + + if (!int.TryParse(accountId.AsSpan(), out var id)) + return Results.Unauthorized(); + + string newBio = ""; + + if (request.ContentLength.HasValue && request.ContentLength > 0) + { + try + { + request.EnableBuffering(); + using var reader = new StreamReader(request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + + if (!string.IsNullOrWhiteSpace(body)) + { + foreach (var pair in body.Split('&')) + { + var keyValue = pair.Split('='); + if (keyValue.Length == 2) + { + var key = Uri.UnescapeDataString(keyValue[0]); + var value = Uri.UnescapeDataString(keyValue[1]); + + if (key == "bio") + newBio = Uri.UnescapeDataString(value); + } + } + } + request.Body.Position = 0; + } + catch { } + } + + var bio = await db.PlayerBios.FirstOrDefaultAsync(b => b.accountId == id); + + if (bio == null) + { + bio = new PlayerBio + { + accountId = id, + bio = newBio + }; + db.PlayerBios.Add(bio); + } + else + { + bio.bio = newBio; + db.PlayerBios.Update(bio); + } + + await db.SaveChangesAsync(); + + return Results.Json(new { success = true }); }); } } diff --git a/CannedNet/Services/Controllers/MatchmakingController.cs b/CannedNet/Services/Controllers/MatchmakingController.cs index f5724e4..548db74 100644 --- a/CannedNet/Services/Controllers/MatchmakingController.cs +++ b/CannedNet/Services/Controllers/MatchmakingController.cs @@ -17,32 +17,39 @@ public class MatchmakingController app.MapPost("/player/login", () => Results.Ok()); - app.MapGet("/player", (HttpRequest request) => + app.MapGet("/player", async (HttpRequest request, AppDbContext db) => { - /* - var id = request.Query["id"]; - var accounts = new List(); - - if (int.TryParse(id, out var accountId)) + var id = request.Query["id"].FirstOrDefault(); + if (string.IsNullOrEmpty(id) || !int.TryParse(id, out var accountId)) { - accounts.Add(new Account - { - AccountId = accountId, - ProfileImage = "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", - IsJunior = false, - Platforms = 0, - PersonalPronouns = 0, - IdentityFlags = 0, - Username = $"Player{accountId}", - DisplayName = $"Player{accountId}", - CreatedAt = DateTime.UtcNow - }); + var json = File.ReadAllText("JSON/getplayer.json"); + return Results.Content(json, "application/json"); } - return Results.Json(accounts);*/ - - var json = File.ReadAllText("JSON/getplayer.json"); - return Results.Content(json, "application/json"); + var account = await db.Accounts.FindAsync(accountId); + if (account == null) + { + var json = File.ReadAllText("JSON/getplayer.json"); + return Results.Content(json, "application/json"); + } + + var result = new List + { + new Account + { + AccountId = account.AccountId, + ProfileImage = account.ProfileImage ?? "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", + IsJunior = account.IsJunior, + Platforms = account.Platforms ?? 0, + PersonalPronouns = account.PersonalPronouns ?? 0, + IdentityFlags = account.IdentityFlags ?? 0, + Username = account.Username ?? $"Player{account.AccountId}", + DisplayName = account.DisplayName ?? $"Player{account.AccountId}", + CreatedAt = account.CreatedAt + } + }; + + return Results.Json(result); }); app.MapPost("/goto/room/{room}", async (HttpRequest request, string room, AppDbContext db) => diff --git a/CannedNet/Services/Controllers/NotifyController.cs b/CannedNet/Services/Controllers/NotifyController.cs index a6c2a41..7d3fae6 100644 --- a/CannedNet/Services/Controllers/NotifyController.cs +++ b/CannedNet/Services/Controllers/NotifyController.cs @@ -1,5 +1,6 @@ using CannedNet.Hubs; using CannedNet.Services.Infrastructure; +using Microsoft.AspNetCore.SignalR; namespace CannedNet.Services.Controllers; @@ -10,5 +11,8 @@ public class NotifyController public void MapEndpoints(WebApplication app) { app.MapHub("/hub/v1"); + + var hubContext = app.Services.GetRequiredService>(); + NotificationService.SetHubContext(hubContext); } } diff --git a/CannedNet/Services/NotificationService.cs b/CannedNet/Services/NotificationService.cs index 23c881b..be6a3cf 100644 --- a/CannedNet/Services/NotificationService.cs +++ b/CannedNet/Services/NotificationService.cs @@ -1,26 +1,39 @@ using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; +using CannedNet.Hubs; using Microsoft.AspNetCore.SignalR; namespace CannedNet.Services; public class NotificationService { - private readonly ConcurrentDictionary> _connectionSubscriptions = new(); - private readonly ConcurrentDictionary> _playerConnections = new(); + private static readonly ConcurrentDictionary> _connectionSubscriptions = new(); + private static readonly ConcurrentDictionary> _playerConnections = new(); + private static readonly ConcurrentDictionary> _pendingNotifications = new(); + private static IHubContext? _hubContext; private readonly ILogger _logger; + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; public NotificationService(ILogger logger) { _logger = logger; } + public static void SetHubContext(IHubContext hubContext) + { + _hubContext = hubContext; + } + public void OnConnected(string connectionId) { + _logger.LogInformation($"Client connected: {connectionId}"); _connectionSubscriptions.TryAdd(connectionId, new HashSet()); } public void OnDisconnected(string connectionId) { + _logger.LogInformation($"Client disconnected: {connectionId}"); if (_connectionSubscriptions.TryRemove(connectionId, out var subscriptions)) { foreach (var playerId in subscriptions) @@ -37,11 +50,13 @@ public class NotificationService } } - public Task SubscribeToPlayers(string connectionId, SubscriptionList subscriptionList) + public async Task SubscribeToPlayers(string connectionId, SubscriptionList subscriptionList, IHubContext hubContext) { var oldSubscriptions = _connectionSubscriptions.GetValueOrDefault(connectionId, new HashSet()); var newPlayerIds = subscriptionList.PlayerIds ?? new List(); + _logger.LogInformation($"Connection {connectionId} subscribing to players: {string.Join(", ", newPlayerIds)}"); + _connectionSubscriptions[connectionId] = new HashSet(newPlayerIds); foreach (var playerId in newPlayerIds) @@ -54,6 +69,22 @@ public class NotificationService existing.Add(connectionId); return existing; }); + + if (_pendingNotifications.TryRemove(playerId, out var pendingQueue)) + { + _logger.LogInformation($"Found {pendingQueue.Count} pending notifications for player {playerId}. Sending to connection {connectionId}"); + while (pendingQueue.TryDequeue(out var notification)) + { + try + { + await SendToConnection(hubContext, connectionId, notification); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error sending pending notification to player {playerId}"); + } + } + } } foreach (var playerId in oldSubscriptions) @@ -63,8 +94,6 @@ public class NotificationService connections.Remove(connectionId); } } - - return Task.CompletedTask; } public HashSet? GetSubscriptions(string connectionId) @@ -90,25 +119,45 @@ public class NotificationService return Enumerable.Empty(); } - public async Task SendNotificationToPlayer(string callerConnectionId, IHubContext hubContext, int playerId, NotificationMessage notification) + public async Task SendNotificationToPlayer(int playerId, NotificationMessage notification) { + if (_hubContext == null) + { + _logger.LogWarning("No hub context available. Cannot send notification."); + return; + } + try { - var connections = GetConnectionsForPlayer(playerId); - foreach (var connectionId in connections) - { - if (connectionId == callerConnectionId) - continue; + var connections = GetConnectionsForPlayer(playerId).ToList(); + _logger.LogInformation($"Attempting to send notification to player {playerId}. Found {connections.Count} connections. NotificationType: {notification.NotificationType}"); - try + if (connections.Any()) + { + foreach (var connectionId in connections) { - await hubContext.Clients.Client(connectionId).SendAsync("Notification", notification); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error sending notification to player {playerId} on connection {connectionId}. NotificationType: {notification.NotificationType}"); + try + { + await SendToConnection(_hubContext, connectionId, notification); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error sending notification to player {playerId} on connection {connectionId}. NotificationType: {notification.NotificationType}"); + } } } + else + { + _logger.LogWarning($"No connections found for player {playerId}. Queueing notification for later delivery. NotificationType: {notification.NotificationType}"); + _pendingNotifications.AddOrUpdate( + playerId, + _ => new Queue(new[] { notification }), + (_, queue) => + { + queue.Enqueue(notification); + return queue; + }); + } } catch (Exception ex) { @@ -116,11 +165,23 @@ public class NotificationService } } - public async Task BroadcastNotification(IHubContext hubContext, NotificationMessage notification) + public async Task BroadcastNotification(NotificationMessage notification) { + if (_hubContext == null) + { + _logger.LogWarning("No hub context available. Cannot broadcast notification."); + return; + } + try { - await hubContext.Clients.All.SendAsync("Notification", notification); + foreach (var kvp in _playerConnections) + { + foreach (var connectionId in kvp.Value) + { + await SendToConnection(_hubContext, connectionId, notification); + } + } } catch (Exception ex) { @@ -141,4 +202,58 @@ public class NotificationService Data = data }; } + + private static async Task SendToConnection(IHubContext hubContext, string connectionId, NotificationMessage notification) + { + var notificationData = BuildNotificationObject(notification); + try + { + var json = JsonSerializer.Serialize(notificationData); + System.Console.WriteLine($"[NotificationService] Sending notification to {connectionId}: {json}"); + } + catch (Exception ex) + { + System.Console.WriteLine($"[NotificationService] Error serializing notification: {ex.Message}"); + } + await hubContext.Clients.Client(connectionId).SendAsync("Notification", notificationData); + } + + private static Dictionary BuildNotificationObject(NotificationMessage notification) + { + var dict = new Dictionary + { + { "notificationId", notification.NotificationId ?? "" }, + { "notificationType", (int?)notification.NotificationType ?? 0 }, + }; + + // Only add optional fields if they have values + if (notification.Id.HasValue) + dict.Add("id", notification.Id.Value); + if (notification.FromAccountId.HasValue) + dict.Add("fromAccountId", notification.FromAccountId.Value); + if (notification.ToAccountId.HasValue) + dict.Add("toAccountId", notification.ToAccountId.Value); + if (notification.CreatedAt.HasValue) + dict.Add("createdAt", notification.CreatedAt.Value.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); + + // Properly serialize the data dictionary, converting DateTime objects to ISO 8601 strings + var serializedData = new Dictionary(); + if (notification.Data != null && notification.Data.Count > 0) + { + foreach (var kvp in notification.Data) + { + if (kvp.Value is DateTime dt) + { + serializedData[kvp.Key] = dt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + } + else if (kvp.Value != null) + { + serializedData[kvp.Key] = kvp.Value; + } + } + } + + dict.Add("data", serializedData); + return dict; + } }