a few changes

- fixes nullrefs on notifications
This commit is contained in:
Lapis
2026-04-10 18:39:35 -04:00
parent 5bfae5d3c1
commit bb5b49b6fe
10 changed files with 291 additions and 98 deletions
+1
View File
@@ -50,3 +50,4 @@ public class NotificationsHub : Hub
return Context.GetHttpContext()?.RequestServices.GetService(typeof(T)) as T; return Context.GetHttpContext()?.RequestServices.GetService(typeof(T)) as T;
} }
} }
+1 -1
View File
@@ -7,7 +7,7 @@
"CDN": "https://cdn.lapis.codes", "CDN": "https://cdn.lapis.codes",
"Commerce": "https://commerce.lapis.codes", "Commerce": "https://commerce.lapis.codes",
"Matchmaking": "https://match.lapis.codes", "Matchmaking": "https://match.lapis.codes",
"Storage": "https://storage.lapis.codes", "Storage": "https://cdn.lapis.codes",
"Chat": "https://chatrr.lapis.codes", "Chat": "https://chatrr.lapis.codes",
"Leaderboard": "https://leaderboard.lapis.codes", "Leaderboard": "https://leaderboard.lapis.codes",
"Accounts": "https://accounts.lapis.codes", "Accounts": "https://accounts.lapis.codes",
+65 -41
View File
@@ -21,6 +21,8 @@ public class APIController
var storefrontService = app.Services.GetRequiredService<StorefrontFillService>(); var storefrontService = app.Services.GetRequiredService<StorefrontFillService>();
var notificationService = app.Services.GetRequiredService<NotificationService>(); var notificationService = app.Services.GetRequiredService<NotificationService>();
MapImageEndpoints(app);
app.MapGet("/api/config/v1/amplitude", () => Results.Ok(new app.MapGet("/api/config/v1/amplitude", () => Results.Ok(new
{ {
AmplitudeKey = "a", AmplitudeKey = "a",
@@ -1371,11 +1373,11 @@ public class APIController
var receivedGift = new ReceivedGift var receivedGift = new ReceivedGift
{ {
ReceiverAccountId = receiverAccountId, ReceiverAccountId = receiverAccountId,
FromPlayerId = senderPlayerId, FromPlayerId = 1,
Message = buyRequest.Gift?.Message ?? "A gift for you <3", Message = buyRequest.Gift?.Message ?? "A gift for you <3",
ConsumableItemDesc = giftDrop.ConsumableItemDesc ?? string.Empty, ConsumableItemDesc = giftDrop.ConsumableItemDesc ?? string.Empty,
AvatarItemDesc = giftDrop.AvatarItemDesc ?? string.Empty, AvatarItemDesc = giftDrop.AvatarItemDesc ?? string.Empty,
FriendlyName = giftDrop.FriendlyName ?? string.Empty, // FriendlyName = giftDrop.FriendlyName ?? string.Empty,
AvatarItemType = giftDrop.AvatarItemType, AvatarItemType = giftDrop.AvatarItemType,
EquipmentPrefabName = giftDrop.EquipmentPrefabName ?? string.Empty, EquipmentPrefabName = giftDrop.EquipmentPrefabName ?? string.Empty,
EquipmentModificationGuid = giftDrop.EquipmentModificationGuid ?? string.Empty, EquipmentModificationGuid = giftDrop.EquipmentModificationGuid ?? string.Empty,
@@ -1442,10 +1444,10 @@ public class APIController
toAccountId: id, toAccountId: id,
data: new Dictionary<string, object> data: new Dictionary<string, object>
{ {
{ "ItemId", buyRequest.PurchasableItemId }, { "BalanceAddType", 1400 },
{ "Price", price.Price }, { "Delta", -price.Price },
{ "CurrencyType", buyRequest.CurrencyType }, { "Balance", tokenBalance.Balance },
{ "NewBalance", tokenBalance.Balance } { "CurrencyType", buyRequest.CurrencyType }
} }
); );
await notificationService.SendNotificationToPlayer(id, purchaseNotification); await notificationService.SendNotificationToPlayer(id, purchaseNotification);
@@ -1570,7 +1572,7 @@ public class APIController
var receivedGift = new ReceivedGift var receivedGift = new ReceivedGift
{ {
ReceiverAccountId = id, ReceiverAccountId = id,
FromPlayerId = null, FromPlayerId = 1,
Message = message, Message = message,
ConsumableItemDesc = consumableItemDesc, ConsumableItemDesc = consumableItemDesc,
AvatarItemDesc = avatarItemDesc, AvatarItemDesc = avatarItemDesc,
@@ -1597,7 +1599,7 @@ public class APIController
var giftData = new var giftData = new
{ {
Id = receivedGift.Id, Id = receivedGift.Id,
FromPlayerId = (object?)null, FromPlayerId = 1,
ConsumableItemDesc = consumableItemDesc, ConsumableItemDesc = consumableItemDesc,
AvatarItemDesc = avatarItemDesc, AvatarItemDesc = avatarItemDesc,
FriendlyName = friendlyName, FriendlyName = friendlyName,
@@ -1746,39 +1748,7 @@ public class APIController
await db.SaveChangesAsync(); await db.SaveChangesAsync();
try return Results.Ok(RecNetResult.Ok());
{
if (!string.IsNullOrEmpty(receivedGift.ConsumableItemDesc))
{
var consumableNotification = notificationService.CreateNotification(
PushNotificationId.ConsumableMappingAdded,
id: giftId,
toAccountId: id,
data: new Dictionary<string, object>
{
{ "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 });
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -1855,4 +1825,58 @@ public class APIController
return ids; 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}");
}
});
}
} }
@@ -453,5 +453,61 @@ public class AccountsController
return Results.Json(new { success = true }); 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());
});
} }
} }
@@ -13,5 +13,50 @@ public class CDNController
var json = File.ReadAllText("JSON/loadingscreentipdata.json"); var json = File.ReadAllText("JSON/loadingscreentipdata.json");
return Results.Content(json, "application/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}");
}
});
} }
} }
@@ -8,9 +8,54 @@ public class ImageController
public void MapEndpoints(WebApplication app) public void MapEndpoints(WebApplication app)
{ {
var imagesDir = "Images";
if (!Directory.Exists(imagesDir))
{
Directory.CreateDirectory(imagesDir);
}
app.MapGet("/{imageId}", (string imageId, HttpContext context) => 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)) if (!Directory.Exists(imagesDir))
{ {
@@ -41,5 +86,47 @@ public class ImageController
context.Response.ContentType = contentType; context.Response.ContentType = contentType;
return File.OpenRead(filePath).CopyToAsync(context.Response.Body); 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}");
}
});
} }
} }
@@ -96,12 +96,10 @@ public class MatchmakingController
RoomInstance? instanceToUse; RoomInstance? instanceToUse;
string photonRoomId; 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); var existingInstance = await db.RoomInstances.FirstOrDefaultAsync(r => r.OwnerAccountId == id && r.roomId == roomData.RoomId);
if (existingInstance != null) if (existingInstance != null)
{ {
// Player already has an instance in this room, update it
photonRoomId = existingInstance.photonRoomId ?? Guid.NewGuid().ToString(); photonRoomId = existingInstance.photonRoomId ?? Guid.NewGuid().ToString();
existingInstance.roomInstanceId = existingInstance.Id; existingInstance.roomInstanceId = existingInstance.Id;
existingInstance.roomId = roomData.RoomId; existingInstance.roomId = roomData.RoomId;
@@ -156,7 +154,7 @@ public class MatchmakingController
name = roomData.Name, name = roomData.Name,
maxCapacity = maxCapacity, maxCapacity = maxCapacity,
isFull = false, 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, isInProgress = false,
EncryptVoiceChat = roomData.EncryptVoiceChat EncryptVoiceChat = roomData.EncryptVoiceChat
}; };
@@ -1,4 +1,3 @@
using CannedNet.Hubs;
using CannedNet.Services.Infrastructure; using CannedNet.Services.Infrastructure;
namespace CannedNet.Services.Controllers; namespace CannedNet.Services.Controllers;
@@ -9,16 +8,11 @@ public class NSController
public void MapEndpoints(WebApplication app) 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("/", () => app.MapGet("/", () =>
{ {
var json = File.ReadAllText("JSON/endpoints.json"); var json = File.ReadAllText("JSON/endpoints.json");
return Results.Content(json, "application/json"); return Results.Content(json, "application/json");
}); });
app.MapGet("/photon", () =>
{
var json = File.ReadAllText("JSON/photonsettings.json");
return Results.Content(json, "application/json");
});
} }
} }
+4 -2
View File
@@ -28,11 +28,11 @@ public class JwtTokenService
var claims = new List<Claim> var claims = new List<Claim>
{ {
new Claim("iss", "https://rr.lapis.codes"), new Claim("iss", "https://api.lapis.codes"),
new Claim("nbf", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()), new Claim("nbf", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
new Claim("iat", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()), new Claim("iat", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
new Claim("exp", ((DateTimeOffset)exp).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("amr", "cached_login"),
new Claim("client_id", "recroom"), new Claim("client_id", "recroom"),
new Claim("sub", accountId), new Claim("sub", accountId),
@@ -43,6 +43,8 @@ public class JwtTokenService
new Claim("rn.ver", "20210129"), new Claim("rn.ver", "20210129"),
new Claim("rn.plat", "0"), new Claim("rn.plat", "0"),
new Claim("role", "gameClient"), new Claim("role", "gameClient"),
new Claim("role", "developer"),
new Claim("role", "moderator"),
new Claim("jti", Guid.NewGuid().ToString("N").Substring(0, 32).ToUpper()) new Claim("jti", Guid.NewGuid().ToString("N").Substring(0, 32).ToUpper())
}; };
+29 -43
View File
@@ -205,55 +205,41 @@ public class NotificationService
private static async Task SendToConnection(IHubContext<NotificationsHub> hubContext, string connectionId, NotificationMessage notification) private static async Task SendToConnection(IHubContext<NotificationsHub> hubContext, string connectionId, NotificationMessage notification)
{ {
var notificationData = BuildNotificationObject(notification);
try try
{ {
var msgData = new Dictionary<string, object>();
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); 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) 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<string, object?> BuildNotificationObject(NotificationMessage notification)
{
var dict = new Dictionary<string, object?>
{
{ "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<string, object>();
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;
} }
} }