diff --git a/CannedNet/Hubs/NotificationsHub.cs b/CannedNet/Hubs/NotificationsHub.cs index 0851445..c7937bd 100644 --- a/CannedNet/Hubs/NotificationsHub.cs +++ b/CannedNet/Hubs/NotificationsHub.cs @@ -50,3 +50,4 @@ public class NotificationsHub : Hub return Context.GetHttpContext()?.RequestServices.GetService(typeof(T)) as T; } } + diff --git a/CannedNet/JSON/endpoints.json b/CannedNet/JSON/endpoints.json index a18a47a..a7572e2 100644 --- a/CannedNet/JSON/endpoints.json +++ b/CannedNet/JSON/endpoints.json @@ -7,7 +7,7 @@ "CDN": "https://cdn.lapis.codes", "Commerce": "https://commerce.lapis.codes", "Matchmaking": "https://match.lapis.codes", - "Storage": "https://storage.lapis.codes", + "Storage": "https://cdn.lapis.codes", "Chat": "https://chatrr.lapis.codes", "Leaderboard": "https://leaderboard.lapis.codes", "Accounts": "https://accounts.lapis.codes", diff --git a/CannedNet/Services/Controllers/APIController.cs b/CannedNet/Services/Controllers/APIController.cs index ba08210..8e7bb2d 100644 --- a/CannedNet/Services/Controllers/APIController.cs +++ b/CannedNet/Services/Controllers/APIController.cs @@ -21,6 +21,8 @@ public class APIController var storefrontService = app.Services.GetRequiredService(); var notificationService = app.Services.GetRequiredService(); + MapImageEndpoints(app); + app.MapGet("/api/config/v1/amplitude", () => Results.Ok(new { AmplitudeKey = "a", @@ -1371,11 +1373,11 @@ public class APIController var receivedGift = new ReceivedGift { ReceiverAccountId = receiverAccountId, - FromPlayerId = senderPlayerId, + FromPlayerId = 1, Message = buyRequest.Gift?.Message ?? "A gift for you <3", ConsumableItemDesc = giftDrop.ConsumableItemDesc ?? string.Empty, AvatarItemDesc = giftDrop.AvatarItemDesc ?? string.Empty, - FriendlyName = giftDrop.FriendlyName ?? string.Empty, + // FriendlyName = giftDrop.FriendlyName ?? string.Empty, AvatarItemType = giftDrop.AvatarItemType, EquipmentPrefabName = giftDrop.EquipmentPrefabName ?? string.Empty, EquipmentModificationGuid = giftDrop.EquipmentModificationGuid ?? string.Empty, @@ -1442,10 +1444,10 @@ public class APIController toAccountId: id, data: new Dictionary { - { "ItemId", buyRequest.PurchasableItemId }, - { "Price", price.Price }, - { "CurrencyType", buyRequest.CurrencyType }, - { "NewBalance", tokenBalance.Balance } + { "BalanceAddType", 1400 }, + { "Delta", -price.Price }, + { "Balance", tokenBalance.Balance }, + { "CurrencyType", buyRequest.CurrencyType } } ); await notificationService.SendNotificationToPlayer(id, purchaseNotification); @@ -1570,7 +1572,7 @@ public class APIController var receivedGift = new ReceivedGift { ReceiverAccountId = id, - FromPlayerId = null, + FromPlayerId = 1, Message = message, ConsumableItemDesc = consumableItemDesc, AvatarItemDesc = avatarItemDesc, @@ -1597,7 +1599,7 @@ public class APIController var giftData = new { Id = receivedGift.Id, - FromPlayerId = (object?)null, + FromPlayerId = 1, ConsumableItemDesc = consumableItemDesc, AvatarItemDesc = avatarItemDesc, FriendlyName = friendlyName, @@ -1746,39 +1748,7 @@ public class APIController await db.SaveChangesAsync(); - try - { - if (!string.IsNullOrEmpty(receivedGift.ConsumableItemDesc)) - { - 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 (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 }); + return Results.Ok(RecNetResult.Ok()); } catch (Exception ex) { @@ -1855,4 +1825,58 @@ public class APIController return ids; } + + private static readonly string ImagesDir; + static APIController() + { + var dir = "Images"; + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + ImagesDir = dir; + } + + private void MapImageEndpoints(WebApplication app) + { + app.MapPost("/api/images/v4/uploadsaved", async (HttpContext context) => + { + try + { + var form = await context.Request.ReadFormAsync(); + var file = form.Files.FirstOrDefault(); + + if (file == null) + { + return Results.BadRequest(new { error = "No file found in request" }); + } + + var imageId = Guid.NewGuid().ToString("N"); + var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); + + var validExtensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" }; + if (string.IsNullOrEmpty(extension) || !validExtensions.Contains(extension)) + { + extension = ".png"; + } + + var savedFileName = imageId + extension; + var filePath = Path.Combine(ImagesDir, savedFileName); + + using (var fileStream = File.Create(filePath)) + { + await file.CopyToAsync(fileStream); + } + + return Results.Ok(new + { + ImageName = savedFileName + }); + } + catch (Exception ex) + { + return Results.Problem($"Error uploading image: {ex.Message}"); + } + }); + } } diff --git a/CannedNet/Services/Controllers/AccountsController.cs b/CannedNet/Services/Controllers/AccountsController.cs index 03a49e4..70c9d9b 100644 --- a/CannedNet/Services/Controllers/AccountsController.cs +++ b/CannedNet/Services/Controllers/AccountsController.cs @@ -453,5 +453,61 @@ public class AccountsController return Results.Json(new { success = true }); }); + + app.MapPut("/account/me/profileimage", 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 newProfileImage = ""; + + 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 == "imageName") + newProfileImage = value; + } + } + } + request.Body.Position = 0; + } + catch { } + } + + var account = await db.Accounts.FindAsync(id); + if (account == null) + return Results.NotFound(); + + account.ProfileImage = newProfileImage; + await db.SaveChangesAsync(); + + return Results.Ok(RecNetResult.Ok()); + }); } } diff --git a/CannedNet/Services/Controllers/CDNController.cs b/CannedNet/Services/Controllers/CDNController.cs index 5dd6179..338834c 100644 --- a/CannedNet/Services/Controllers/CDNController.cs +++ b/CannedNet/Services/Controllers/CDNController.cs @@ -13,5 +13,50 @@ public class CDNController var json = File.ReadAllText("JSON/loadingscreentipdata.json"); return Results.Content(json, "application/json"); }); + + app.MapPost("/upload", async (HttpContext context) => + { + try + { + var form = await context.Request.ReadFormAsync(); + var file = form.Files.FirstOrDefault(); + + if (file == null) + { + return Results.BadRequest(new { error = "No file found in request" }); + } + + var imageId = Guid.NewGuid().ToString("N"); + var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); + + var validExtensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" }; + if (string.IsNullOrEmpty(extension) || !validExtensions.Contains(extension)) + { + extension = ".png"; + } + + var savedFileName = imageId + extension; + var filePath = Path.Combine("Images", savedFileName); + + if (!Directory.Exists("Images")) + { + Directory.CreateDirectory("Images"); + } + + using (var fileStream = File.Create(filePath)) + { + await file.CopyToAsync(fileStream); + } + + return Results.Ok(new + { + filename = savedFileName + }); + } + catch (Exception ex) + { + return Results.Problem($"Error uploading image: {ex.Message}"); + } + }); } } \ No newline at end of file diff --git a/CannedNet/Services/Controllers/ImageController.cs b/CannedNet/Services/Controllers/ImageController.cs index afbbea2..b0f037f 100644 --- a/CannedNet/Services/Controllers/ImageController.cs +++ b/CannedNet/Services/Controllers/ImageController.cs @@ -8,9 +8,54 @@ public class ImageController public void MapEndpoints(WebApplication app) { + var imagesDir = "Images"; + if (!Directory.Exists(imagesDir)) + { + Directory.CreateDirectory(imagesDir); + } + app.MapGet("/{imageId}", (string imageId, HttpContext context) => { - var imagesDir = "Images"; + if (!Directory.Exists(imagesDir)) + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + } + + var files = Directory.GetFiles(imagesDir, $"{imageId}.*"); + var filePath = files.FirstOrDefault(); + + if (filePath == null) + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + } + + var extension = Path.GetExtension(filePath).ToLowerInvariant(); + var contentType = extension switch + { + ".png" => "image/png", + ".jpg" or ".jpeg" => "image/jpeg", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".bmp" => "image/bmp", + _ => "application/octet-stream" + }; + + context.Response.ContentType = contentType; + return File.OpenRead(filePath).CopyToAsync(context.Response.Body); + }); + + app.MapGet("/", (HttpContext context) => + { + var imageId = context.Request.Query["imageId"].FirstOrDefault(); + var sig = context.Request.Query["sig"].FirstOrDefault(); + + if (string.IsNullOrEmpty(imageId)) + { + context.Response.StatusCode = 400; + return Task.CompletedTask; + } if (!Directory.Exists(imagesDir)) { @@ -41,5 +86,47 @@ public class ImageController context.Response.ContentType = contentType; return File.OpenRead(filePath).CopyToAsync(context.Response.Body); }); + + app.MapPost("/api/images/v4/uploadsaved", async (HttpContext context) => + { + try + { + var form = await context.Request.ReadFormAsync(); + var file = form.Files.FirstOrDefault(); + + if (file == null) + { + return Results.BadRequest(new { error = "No file found in request" }); + } + + var imageId = Guid.NewGuid().ToString("N"); + var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); + + var validExtensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" }; + if (string.IsNullOrEmpty(extension) || !validExtensions.Contains(extension)) + { + extension = ".png"; + } + + var savedFileName = imageId + extension; + var filePath = Path.Combine(imagesDir, savedFileName); + + using (var fileStream = File.Create(filePath)) + { + await file.CopyToAsync(fileStream); + } + + return Results.Ok(new + { + imageId = imageId, + imageUrl = $"/{imageId}{extension}", + success = true + }); + } + catch (Exception ex) + { + return Results.Problem($"Error uploading image: {ex.Message}"); + } + }); } } \ No newline at end of file diff --git a/CannedNet/Services/Controllers/MatchmakingController.cs b/CannedNet/Services/Controllers/MatchmakingController.cs index 548db74..7b7bfba 100644 --- a/CannedNet/Services/Controllers/MatchmakingController.cs +++ b/CannedNet/Services/Controllers/MatchmakingController.cs @@ -96,12 +96,10 @@ public class MatchmakingController RoomInstance? instanceToUse; string photonRoomId; - // Check if player already has an instance in this room var existingInstance = await db.RoomInstances.FirstOrDefaultAsync(r => r.OwnerAccountId == id && r.roomId == roomData.RoomId); if (existingInstance != null) { - // Player already has an instance in this room, update it photonRoomId = existingInstance.photonRoomId ?? Guid.NewGuid().ToString(); existingInstance.roomInstanceId = existingInstance.Id; existingInstance.roomId = roomData.RoomId; @@ -156,7 +154,7 @@ public class MatchmakingController name = roomData.Name, maxCapacity = maxCapacity, isFull = false, - isPrivate = roomData.IsDorm || roomData.RoomId == 1, // Private if it's a dorm or special room + isPrivate = roomData.IsDorm || roomData.RoomId == 1, isInProgress = false, EncryptVoiceChat = roomData.EncryptVoiceChat }; diff --git a/CannedNet/Services/Controllers/NSController.cs b/CannedNet/Services/Controllers/NSController.cs index 1a95928..1c54866 100644 --- a/CannedNet/Services/Controllers/NSController.cs +++ b/CannedNet/Services/Controllers/NSController.cs @@ -1,4 +1,3 @@ -using CannedNet.Hubs; using CannedNet.Services.Infrastructure; namespace CannedNet.Services.Controllers; @@ -9,16 +8,11 @@ public class NSController public void MapEndpoints(WebApplication app) { + // Rec Room automatically calls this and sets the endpoints that correspond to services in the JSON file. These will be moved to the main server config once I get there. app.MapGet("/", () => { var json = File.ReadAllText("JSON/endpoints.json"); return Results.Content(json, "application/json"); }); - - app.MapGet("/photon", () => - { - var json = File.ReadAllText("JSON/photonsettings.json"); - return Results.Content(json, "application/json"); - }); } } diff --git a/CannedNet/Services/JwtTokenService.cs b/CannedNet/Services/JwtTokenService.cs index 1b99b69..15bd90f 100644 --- a/CannedNet/Services/JwtTokenService.cs +++ b/CannedNet/Services/JwtTokenService.cs @@ -28,11 +28,11 @@ public class JwtTokenService var claims = new List { - new Claim("iss", "https://rr.lapis.codes"), + new Claim("iss", "https://api.lapis.codes"), new Claim("nbf", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()), new Claim("iat", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()), new Claim("exp", ((DateTimeOffset)exp).ToUnixTimeSeconds().ToString()), - new Claim("aud", "https://rr.lapis.codes/resources"), + new Claim("aud", "https://api.lapis.codes/resources"), new Claim("amr", "cached_login"), new Claim("client_id", "recroom"), new Claim("sub", accountId), @@ -43,6 +43,8 @@ public class JwtTokenService new Claim("rn.ver", "20210129"), new Claim("rn.plat", "0"), new Claim("role", "gameClient"), + new Claim("role", "developer"), + new Claim("role", "moderator"), new Claim("jti", Guid.NewGuid().ToString("N").Substring(0, 32).ToUpper()) }; diff --git a/CannedNet/Services/NotificationService.cs b/CannedNet/Services/NotificationService.cs index be6a3cf..278aaa7 100644 --- a/CannedNet/Services/NotificationService.cs +++ b/CannedNet/Services/NotificationService.cs @@ -205,55 +205,41 @@ public class NotificationService private static async Task SendToConnection(IHubContext hubContext, string connectionId, NotificationMessage notification) { - var notificationData = BuildNotificationObject(notification); try { + var msgData = new Dictionary(); + if (notification.Data != null) + { + foreach (var kvp in notification.Data) + { + if (kvp.Value == null) + continue; + + if (kvp.Value is DateTime dt) + { + msgData[kvp.Key] = dt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + } + else + { + msgData[kvp.Key] = kvp.Value; + } + } + } + + var notificationData = new + { + Id = (int?)notification.NotificationType ?? 0, + Msg = msgData + }; + var json = JsonSerializer.Serialize(notificationData); - System.Console.WriteLine($"[NotificationService] Sending notification to {connectionId}: {json}"); + Console.WriteLine($"[NotificationService] Sending notification to {connectionId}: {json}"); + + await hubContext.Clients.Client(connectionId).SendAsync("Notification", json); } catch (Exception ex) { - System.Console.WriteLine($"[NotificationService] Error serializing notification: {ex.Message}"); + Console.WriteLine($"[NotificationService] Error sending 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; } }