From 3315a200d71398a923b8af65743fd3b26ce0f2a3 Mon Sep 17 00:00:00 2001 From: Lapis <47952866+LapisGit@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:12:49 -0400 Subject: [PATCH] h --- CannedNet.API/Data/AppDbContext.cs | 38 -- CannedNet.API/Program.cs | 337 ------------------ CannedNet.API/endpoints.json | 26 -- CannedNet.slnx | 2 +- CannedNet/AppConfigs/appsettings.API.json | 6 + .../AppConfigs/appsettings.Accounts.json | 6 + CannedNet/AppConfigs/appsettings.Auth.json | 6 + CannedNet/AppConfigs/appsettings.Chat.json | 6 + CannedNet/AppConfigs/appsettings.Match.json | 6 + CannedNet/AppConfigs/appsettings.NS.json | 6 + CannedNet/AppConfigs/appsettings.Notify.json | 6 + .../CannedNet.csproj | 0 CannedNet/Data/AppDbContext.cs | 85 +++++ CannedNet/Hubs/NotificationsHub.cs | 42 +++ .../JSON}/configv2.json | 0 .../JSON}/eacchallenge.txt | 0 CannedNet/JSON/endpoints.json | 26 ++ .../JSON}/endpointsreference.json | 0 .../JSON}/gameconfigs.json | 0 .../JSON}/photonsettings.json | 0 .../20260331191343_InitialCreate.Designer.cs | 112 ++++++ .../20260331191343_InitialCreate.cs | 68 ++++ ...01002616_AddPlayerProgressions.Designer.cs | 140 ++++++++ .../20260401002616_AddPlayerProgressions.cs | 43 +++ ...260401123437_AddPlayerSettings.Designer.cs | 170 +++++++++ .../20260401123437_AddPlayerSettings.cs | 43 +++ ...0260401125651_AddPlayerAvatars.Designer.cs | 254 +++++++++++++ .../20260401125651_AddPlayerAvatars.cs | 72 ++++ .../Migrations/AppDbContextModelSnapshot.cs | 251 +++++++++++++ .../Models/Account.cs | 0 CannedNet/Models/AvatarItem.cs | 28 ++ .../Models/CachedAccount.cs | 0 .../Enums => CannedNet/Models}/JuniorState.cs | 0 CannedNet/Models/NotificationModels.cs | 231 ++++++++++++ CannedNet/Models/NotificationTypes.cs | 40 +++ .../Models}/PlatformType.cs | 0 CannedNet/Models/PlayerAvatar.cs | 21 ++ CannedNet/Models/PlayerProgression.cs | 24 ++ CannedNet/Models/PlayerSetting.cs | 15 + .../Models/RecNetResult.cs | 0 .../Models/SelfAccount.cs | 0 CannedNet/Models/SettingPayload.cs | 7 + CannedNet/Models/SubscriptionList.cs | 9 + CannedNet/Program.cs | 25 ++ CannedNet/Services/APIService.cs | 249 +++++++++++++ CannedNet/Services/AccountsService.cs | 151 ++++++++ CannedNet/Services/AuthService.cs | 80 +++++ CannedNet/Services/ChatService.cs | 11 + .../Services}/JwtTokenService.cs | 8 +- CannedNet/Services/MatchmakingService.cs | 35 ++ CannedNet/Services/NSService.cs | 23 ++ CannedNet/Services/NotificationService.cs | 117 ++++++ CannedNet/Services/NotifiyService.cs | 13 + CannedNet/Services/ServiceExtensions.cs | 32 ++ CannedNet/Services/ServiceRegistry.cs | 24 ++ .../appsettings.Production.json | 4 + {CannedNet.API => CannedNet}/appsettings.json | 6 +- 57 files changed, 2497 insertions(+), 407 deletions(-) delete mode 100644 CannedNet.API/Data/AppDbContext.cs delete mode 100644 CannedNet.API/Program.cs delete mode 100644 CannedNet.API/endpoints.json create mode 100644 CannedNet/AppConfigs/appsettings.API.json create mode 100644 CannedNet/AppConfigs/appsettings.Accounts.json create mode 100644 CannedNet/AppConfigs/appsettings.Auth.json create mode 100644 CannedNet/AppConfigs/appsettings.Chat.json create mode 100644 CannedNet/AppConfigs/appsettings.Match.json create mode 100644 CannedNet/AppConfigs/appsettings.NS.json create mode 100644 CannedNet/AppConfigs/appsettings.Notify.json rename CannedNet.API/CannedNet.API.csproj => CannedNet/CannedNet.csproj (100%) create mode 100644 CannedNet/Data/AppDbContext.cs create mode 100644 CannedNet/Hubs/NotificationsHub.cs rename {CannedNet.API => CannedNet/JSON}/configv2.json (100%) rename {CannedNet.API => CannedNet/JSON}/eacchallenge.txt (100%) create mode 100644 CannedNet/JSON/endpoints.json rename {CannedNet.API => CannedNet/JSON}/endpointsreference.json (100%) rename {CannedNet.API => CannedNet/JSON}/gameconfigs.json (100%) rename {CannedNet.API => CannedNet/JSON}/photonsettings.json (100%) create mode 100644 CannedNet/Migrations/20260331191343_InitialCreate.Designer.cs create mode 100644 CannedNet/Migrations/20260331191343_InitialCreate.cs create mode 100644 CannedNet/Migrations/20260401002616_AddPlayerProgressions.Designer.cs create mode 100644 CannedNet/Migrations/20260401002616_AddPlayerProgressions.cs create mode 100644 CannedNet/Migrations/20260401123437_AddPlayerSettings.Designer.cs create mode 100644 CannedNet/Migrations/20260401123437_AddPlayerSettings.cs create mode 100644 CannedNet/Migrations/20260401125651_AddPlayerAvatars.Designer.cs create mode 100644 CannedNet/Migrations/20260401125651_AddPlayerAvatars.cs create mode 100644 CannedNet/Migrations/AppDbContextModelSnapshot.cs rename {CannedNet.API => CannedNet}/Models/Account.cs (100%) create mode 100644 CannedNet/Models/AvatarItem.cs rename {CannedNet.API => CannedNet}/Models/CachedAccount.cs (100%) rename {CannedNet.API/Enums => CannedNet/Models}/JuniorState.cs (100%) create mode 100644 CannedNet/Models/NotificationModels.cs create mode 100644 CannedNet/Models/NotificationTypes.cs rename {CannedNet.API/Enums => CannedNet/Models}/PlatformType.cs (100%) create mode 100644 CannedNet/Models/PlayerAvatar.cs create mode 100644 CannedNet/Models/PlayerProgression.cs create mode 100644 CannedNet/Models/PlayerSetting.cs rename {CannedNet.API => CannedNet}/Models/RecNetResult.cs (100%) rename {CannedNet.API => CannedNet}/Models/SelfAccount.cs (100%) create mode 100644 CannedNet/Models/SettingPayload.cs create mode 100644 CannedNet/Models/SubscriptionList.cs create mode 100644 CannedNet/Program.cs create mode 100644 CannedNet/Services/APIService.cs create mode 100644 CannedNet/Services/AccountsService.cs create mode 100644 CannedNet/Services/AuthService.cs create mode 100644 CannedNet/Services/ChatService.cs rename {CannedNet.API => CannedNet/Services}/JwtTokenService.cs (94%) create mode 100644 CannedNet/Services/MatchmakingService.cs create mode 100644 CannedNet/Services/NSService.cs create mode 100644 CannedNet/Services/NotificationService.cs create mode 100644 CannedNet/Services/NotifiyService.cs create mode 100644 CannedNet/Services/ServiceExtensions.cs create mode 100644 CannedNet/Services/ServiceRegistry.cs rename {CannedNet.API => CannedNet}/appsettings.Production.json (81%) rename {CannedNet.API => CannedNet}/appsettings.json (51%) diff --git a/CannedNet.API/Data/AppDbContext.cs b/CannedNet.API/Data/AppDbContext.cs deleted file mode 100644 index 0a0cddc..0000000 --- a/CannedNet.API/Data/AppDbContext.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace CannedNet.Data; - -public class AppDbContext : DbContext -{ - public AppDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Accounts { get; set; } - public DbSet CachedLogins { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - // accounts - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.AccountId); - entity.Property(e => e.AccountId).ValueGeneratedNever(); - entity.Property(e => e.Username).IsRequired(); - entity.Property(e => e.DisplayName).IsRequired(); - entity.ToTable("accounts"); - }); - - // cachedlogins - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.Property(e => e.Id).ValueGeneratedOnAdd(); - entity.HasIndex(e => new { e.Platform, e.PlatformID }).IsUnique(); - entity.ToTable("cached_logins"); - }); - } -} - diff --git a/CannedNet.API/Program.cs b/CannedNet.API/Program.cs deleted file mode 100644 index 93a344e..0000000 --- a/CannedNet.API/Program.cs +++ /dev/null @@ -1,337 +0,0 @@ -using CannedNet; -using CannedNet.Data; -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); - -// TODO: organize this into multiple files -// TODO: cdn stuff soon so i can not just have 0 images for this lmao -// TODO: setup signalr and get websockets running, currently should focus on notifications as logging in is being blocked by that - -// needed so it won't try to auto capitalize random stuff that may be case-sensitive -builder.Services.ConfigureHttpJsonOptions(options => -{ - options.SerializerOptions.PropertyNamingPolicy = null; -}); - -// postgresql db, will probably have it configurable later on but for now just hardcode it in -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") - ?? "Host=localhost;Port=5432;Database=cannednet;Username=postgres;Password=postgres"; - -builder.Services.AddDbContext(options => - options.UseNpgsql(connectionString)); - -builder.Services.AddScoped(); - -var app = builder.Build(); - -// apply db migrations on startup so we don't have to worry about it later bc i hate doing db things manually lol -using (var scope = app.Services.CreateScope()) -{ - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); -} - -var jwtService = app.Services.GetRequiredService(); - -// request logging -app.Use(async (context, next) => -{ - var request = context.Request; - Console.WriteLine($"\n{request.Method} {request.Path}{request.QueryString}"); - - foreach (var header in request.Headers) - Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}"); - - if (request.ContentLength > 0) - { - request.EnableBuffering(); - using (var reader = new StreamReader(request.Body, leaveOpen: true)) - { - var body = await reader.ReadToEndAsync(); - Console.WriteLine($" Body: {body}"); - } - request.Body.Position = 0; - } - - await next(context); -}); - -app.MapGet("/", () => -{ - var json = File.ReadAllText("endpoints.json"); - return Results.Content(json, "application/json"); -}); - -app.MapGet("/api/config/v1/amplitude", () => -{ - var config = new - { - AmplitudeKey = "fd846cd67066d19eec62fbdd00be9258", - StatSigKey = "client-mnew4Ggjuu3Fbh86Ed3tRgWUxapp0D7PPB0i3MXUEao", - RudderStackKey = "37qVlzZyXqdmZckXJ61vQeEJp8Y", - UseRudderStack = true - }; - - return Results.Ok(config); -}); - -app.MapGet("/api/config/v2", () => -{ - var json = File.ReadAllText("configv2.json"); - return Results.Content(json, "application/json"); -}); - -app.MapGet("/api/versioncheck/v4", () => -{ - return Results.Content("{\"VersionStatus\":0}","application/json"); -}); - -app.MapGet("/api/gameconfigs/v1/all", () => -{ - var json = File.ReadAllText("gameconfigs.json"); - return Results.Content(json, "application/json"); -}); - -app.MapGet("/photon", () => -{ - var json = File.ReadAllText("photonsettings.json"); - return Results.Content(json, "application/json"); -}); - -app.MapGet("/eac/challenge", () => -{ - var file = File.ReadAllText("eacchallenge.txt"); - return Results.Content(file,"text/plain"); -}); - -app.MapGet("/api/playerReputation/v1/{id}", (string id) => -{ - // TODO: make rep actually work - return Results.Content($"{{\"AccountId\":{id},\"Noteriety\":0,\"CheerGeneral\":0,\"CheerHelpful\":0,\"CheerCreative\":0,\"CheerGreatHost\":0,\"CheerSportsman\":0,\"CheerCredit\":20,\"SelectedCheer\":null}}","application/json"); -}); - -app.MapGet("/api/players/v1/progression/{id}", (string id) => -{ - // TODO: make progression actually work - return Results.Content($"{{\"PlayerId\":{id},\"Level\":1,\"XP\":0}}","application/json"); -}); - -app.MapGet("/api/PlayerReporting/v1/moderationBlockDetails", () => -{ - // TODO: make this actually work - return Results.Content($"{{\"ReportCategory\":0,\"Duration\":0,\"GameSessionId\":0,\"IsHostKick\":false,\"Message\":\"\",\"PlayerIdReporter\":null,\"IsBan\":false}}","application/json"); -}); - -app.MapPost("/player/login", () => -{ - return Results.Ok(); -}); - -app.MapGet("/cachedlogin/forplatformid/{platform}/{id}", async (string platform, string id, AppDbContext db) => -{ - var platformType = int.Parse(platform); - var logins = await db.CachedLogins - .Where(c => c.Platform == (PlatformType)platformType && c.PlatformID == id) - .ToListAsync(); - - return Results.Json(logins.Any() ? logins : new List()); -}); - -app.MapGet("/account/me", (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) || !int.TryParse(accountId, out var id)) - return Results.Unauthorized(); - - 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, - Email = null, - Phone = null, - JuniorState = null, - Birthday = null, - ParentAccountId = null, - AvailableUsernameChanges = 1 - }; - - return Results.Ok(selfAccount); -}); - -app.MapGet("/account/bulk", (HttpRequest request) => -{ - var ids = request.Query["id"]; - var accounts = new List(); - - foreach (var id in ids) - { - if (int.TryParse(id, out var accountId)) - { - var account = new Account - { - AccountId = accountId, - ProfileImage = "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", - IsJunior = false, - Platforms = 0, - PersonalPronouns = 0, - IdentityFlags = 0, - Username = $"Player{accountId}", - DisplayName = $"Player{accountId}", - CreatedAt = DateTime.UtcNow - }; - accounts.Add(account); - } - } - - return Results.Json(accounts); -}); - -app.MapPost("/account/create", async (HttpRequest httpRequest, AppDbContext db) => -{ - int platform = 0; - string platformId = ""; - - if (httpRequest.ContentLength.HasValue && httpRequest.ContentLength > 0) - { - try - { - var contentType = httpRequest.ContentType ?? ""; - httpRequest.EnableBuffering(); - - using (var reader = new StreamReader(httpRequest.Body, leaveOpen: true)) - { - var body = await reader.ReadToEndAsync(); - - if (!string.IsNullOrWhiteSpace(body)) - { - if (contentType.Contains("application/x-www-form-urlencoded")) - { - var pairs = body.Split('&'); - foreach (var pair in pairs) - { - var keyValue = pair.Split('='); - if (keyValue.Length == 2) - { - var key = Uri.UnescapeDataString(keyValue[0]); - var value = Uri.UnescapeDataString(keyValue[1]); - - if (key == "platform" && int.TryParse(value, out var parsedPlatform)) - platform = parsedPlatform; - else if (key == "platformId") - platformId = value; - } - } - } - } - } - httpRequest.Body.Position = 0; - } - catch { } - } - - var accountId = new Random().Next(10000, 99999); - var account = new Account - { - AccountId = accountId, - ProfileImage = "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", - IsJunior = false, - Platforms = 0, - PersonalPronouns = 0, - IdentityFlags = 0, - Username = $"Player{accountId}", - DisplayName = $"Player{accountId}", - CreatedAt = DateTime.UtcNow - }; - - db.Accounts.Add(account); - - if (!string.IsNullOrEmpty(platformId)) - { - var loginRecord = new CachedLogin - { - AccountId = accountId, - Platform = (PlatformType)platform, - PlatformID = platformId, - LastLoginTime = DateTime.UtcNow, - RequirePassword = false - }; - - db.CachedLogins.Add(loginRecord); - } - - await db.SaveChangesAsync(); - - return Results.Ok(RecNetResultWithValue.Ok(account)); -}); - -app.MapPost("/connect/token", async (HttpRequest httpRequest) => -{ - string accountId = ""; - string platformId = ""; - string platform = ""; - - if (httpRequest.ContentLength.HasValue && httpRequest.ContentLength > 0) - { - try - { - httpRequest.EnableBuffering(); - using (var reader = new StreamReader(httpRequest.Body, leaveOpen: true)) - { - var body = await reader.ReadToEndAsync(); - if (!string.IsNullOrWhiteSpace(body)) - { - var pairs = body.Split('&'); - foreach (var pair in pairs) - { - var keyValue = pair.Split('='); - if (keyValue.Length == 2) - { - var key = Uri.UnescapeDataString(keyValue[0]); - var value = Uri.UnescapeDataString(keyValue[1]); - - if (key == "account_id") - accountId = value; - else if (key == "platform_id") - platformId = value; - } - } - } - } - httpRequest.Body.Position = 0; - } - catch { } - } - - var accessToken = jwtService.GenerateToken(accountId, platformId, platform); - - var response = new - { - access_token = accessToken, - expires_in = 3600, - token_type = "Bearer", - refresh_token = Guid.NewGuid().ToString("N").ToUpper() + "-1", - scope = "offline_access profile rn rn.accounts rn.accounts.gc rn.api rn.chat rn.clubs rn.commerce rn.match.read rn.match.write rn.notify rn.rooms rn.storage", - key = "8oQ+e+WQaOBPbEcakhqs3dwZZdOmmyDUmJSD9u4AHMY=" - }; - - return Results.Json(response); -}); - -app.Run(); - - diff --git a/CannedNet.API/endpoints.json b/CannedNet.API/endpoints.json deleted file mode 100644 index e5829b4..0000000 --- a/CannedNet.API/endpoints.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "Auth": "https://rr.lapis.codes", - "API": "https://rr.lapis.codes", - "WWW": "https://www.lapis.codes", - "Notifications": "https://rr.lapis.codes", - "Images": "https://rr.lapis.codes", - "CDN": "https://rr.lapis.codes", - "Commerce": "https://rr.lapis.codes", - "Matchmaking": "https://rr.lapis.codes", - "Storage": "https://rr.lapis.codes", - "Chat": "https://rr.lapis.codes", - "Leaderboard": "https://rr.lapis.codes", - "Accounts": "https://rr.lapis.codes", - "Link": "https://rr.lapis.codes", - "RoomComments": "https://rr.lapis.codes", - "Clubs": "https://rr.lapis.codes", - "Rooms": "https://rr.lapis.codes", - "PlatformNotifications": "https://rr.lapis.codes", - "Moderation": "https://rr.lapis.codes", - "DataCollection": "https://rr.lapis.codes", - "BugReporting": "https://rr.lapis.codes", - "Discovery": "https://rr.lapis.codes", - "PlayerSettings": "https://rr.lapis.codes", - "Rendering": "https://rr.lapis.codes", - "Launcher": "https://rr.lapis.codes" -} \ No newline at end of file diff --git a/CannedNet.slnx b/CannedNet.slnx index e907e66..b1d39e0 100644 --- a/CannedNet.slnx +++ b/CannedNet.slnx @@ -1,4 +1,4 @@ - + diff --git a/CannedNet/AppConfigs/appsettings.API.json b/CannedNet/AppConfigs/appsettings.API.json new file mode 100644 index 0000000..f345fb5 --- /dev/null +++ b/CannedNet/AppConfigs/appsettings.API.json @@ -0,0 +1,6 @@ +{ + "Hosting": { + "Url": "http://*:5000" + } +} + diff --git a/CannedNet/AppConfigs/appsettings.Accounts.json b/CannedNet/AppConfigs/appsettings.Accounts.json new file mode 100644 index 0000000..07865db --- /dev/null +++ b/CannedNet/AppConfigs/appsettings.Accounts.json @@ -0,0 +1,6 @@ +{ + "Hosting": { + "Url": "http://*:5005" + } +} + diff --git a/CannedNet/AppConfigs/appsettings.Auth.json b/CannedNet/AppConfigs/appsettings.Auth.json new file mode 100644 index 0000000..0a93f73 --- /dev/null +++ b/CannedNet/AppConfigs/appsettings.Auth.json @@ -0,0 +1,6 @@ +{ + "Hosting": { + "Url": "http://*:5002" + } +} + diff --git a/CannedNet/AppConfigs/appsettings.Chat.json b/CannedNet/AppConfigs/appsettings.Chat.json new file mode 100644 index 0000000..322a307 --- /dev/null +++ b/CannedNet/AppConfigs/appsettings.Chat.json @@ -0,0 +1,6 @@ +{ + "Hosting": { + "Url": "http://*:5003" + } +} + diff --git a/CannedNet/AppConfigs/appsettings.Match.json b/CannedNet/AppConfigs/appsettings.Match.json new file mode 100644 index 0000000..e264816 --- /dev/null +++ b/CannedNet/AppConfigs/appsettings.Match.json @@ -0,0 +1,6 @@ +{ + "Hosting": { + "Url": "http://*:5004" + } +} + diff --git a/CannedNet/AppConfigs/appsettings.NS.json b/CannedNet/AppConfigs/appsettings.NS.json new file mode 100644 index 0000000..771f6b9 --- /dev/null +++ b/CannedNet/AppConfigs/appsettings.NS.json @@ -0,0 +1,6 @@ +{ + "Hosting": { + "Url": "http://*:5001" + } +} + diff --git a/CannedNet/AppConfigs/appsettings.Notify.json b/CannedNet/AppConfigs/appsettings.Notify.json new file mode 100644 index 0000000..d96c059 --- /dev/null +++ b/CannedNet/AppConfigs/appsettings.Notify.json @@ -0,0 +1,6 @@ +{ + "Hosting": { + "Url": "http://*:5006" + } +} + diff --git a/CannedNet.API/CannedNet.API.csproj b/CannedNet/CannedNet.csproj similarity index 100% rename from CannedNet.API/CannedNet.API.csproj rename to CannedNet/CannedNet.csproj diff --git a/CannedNet/Data/AppDbContext.cs b/CannedNet/Data/AppDbContext.cs new file mode 100644 index 0000000..527b25a --- /dev/null +++ b/CannedNet/Data/AppDbContext.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore; + +namespace CannedNet.Data; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Accounts { get; set; } + public DbSet CachedLogins { get; set; } + public DbSet PlayerProgressions { get; set; } + public DbSet PlayerSettings { get; set; } + public DbSet AvatarItems { get; set; } + public DbSet PlayerAvatars { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // accounts + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.AccountId); + entity.Property(e => e.AccountId).ValueGeneratedNever(); + entity.Property(e => e.Username).IsRequired(); + entity.Property(e => e.DisplayName).IsRequired(); + entity.ToTable("accounts"); + }); + + // cachedlogins + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Id).ValueGeneratedOnAdd(); + entity.HasIndex(e => new { e.Platform, e.PlatformID }).IsUnique(); + entity.ToTable("cached_logins"); + }); + + // player_progressions + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Id).ValueGeneratedOnAdd(); + entity.HasIndex(e => e.PlayerId).IsUnique(); + entity.ToTable("player_progressions"); + }); + + // player_settings_kv (key-value pairs) + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Id).ValueGeneratedOnAdd(); + entity.Property(e => e.PlayerId).IsRequired(); + entity.Property(e => e.Key).IsRequired(); + entity.Property(e => e.Value).IsRequired(); + entity.HasIndex(e => new { e.PlayerId, e.Key }).IsUnique(); + entity.ToTable("player_settings"); + }); + + // avatar_items + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Id).ValueGeneratedOnAdd(); + entity.Property(e => e.OwnerAccountId).IsRequired(); + entity.Property(e => e.AvatarItemDesc).IsRequired(); + entity.Property(e => e.FriendlyName).IsRequired(); + entity.HasIndex(e => e.OwnerAccountId); + entity.ToTable("avatar_items"); + }); + + // player_avatars + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Id).ValueGeneratedOnAdd(); + entity.Property(e => e.OwnerAccountId).IsRequired(); + entity.HasIndex(e => e.OwnerAccountId).IsUnique(); + entity.ToTable("player_avatars"); + }); + } +} + diff --git a/CannedNet/Hubs/NotificationsHub.cs b/CannedNet/Hubs/NotificationsHub.cs new file mode 100644 index 0000000..b2cf90e --- /dev/null +++ b/CannedNet/Hubs/NotificationsHub.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using CannedNet.Services; +using Microsoft.AspNetCore.SignalR; + +namespace CannedNet.Hubs; + +public class NotificationsHub : Hub +{ + private static NotificationService _notificationService = new(); + + public NotificationsHub() + { + } + + public override async Task OnConnectedAsync() + { + _notificationService.OnConnected(Context.ConnectionId); + await Clients.Caller.SendAsync("OnConnect"); + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + _notificationService.OnDisconnected(Context.ConnectionId); + await base.OnDisconnectedAsync(exception); + } + + public async Task SubscribeToPlayers(SubscriptionList subscriptionList) + { + await _notificationService.SubscribeToPlayers(Context.ConnectionId, subscriptionList); + } + + public Task> GetSubscriptions() + { + return Task.FromResult(_notificationService.GetSubscribedPlayers(Context.ConnectionId).ToHashSet()); + } + + public static NotificationService GetNotificationService() + { + return _notificationService; + } +} diff --git a/CannedNet.API/configv2.json b/CannedNet/JSON/configv2.json similarity index 100% rename from CannedNet.API/configv2.json rename to CannedNet/JSON/configv2.json diff --git a/CannedNet.API/eacchallenge.txt b/CannedNet/JSON/eacchallenge.txt similarity index 100% rename from CannedNet.API/eacchallenge.txt rename to CannedNet/JSON/eacchallenge.txt diff --git a/CannedNet/JSON/endpoints.json b/CannedNet/JSON/endpoints.json new file mode 100644 index 0000000..d63680e --- /dev/null +++ b/CannedNet/JSON/endpoints.json @@ -0,0 +1,26 @@ +{ + "Auth": "https://auth.lapis.codes", + "API": "https://api.lapis.codes", + "WWW": "https://www.lapis.codes", + "Notifications": "https://notify.lapis.codes", + "Images": "https://img.lapis.codes", + "CDN": "https://cdn.lapis.codes", + "Commerce": "https://commerce.lapis.codes", + "Matchmaking": "https://match.lapis.codes", + "Storage": "https://storage.lapis.codes", + "Chat": "https://chatrr.lapis.codes", + "Leaderboard": "https://rr.lapis.codes", + "Accounts": "http://localhost:5005", + "Link": "https://link.lapis.codes", + "RoomComments": "https://roomcomments.lapis.codes", + "Clubs": "https://clubs.lapis.codes", + "Rooms": "https://rooms.lapis.codes", + "PlatformNotifications": "https://api.lapis.codes", + "Moderation": "https://api.lapis.codes", + "DataCollection": "https://api.lapis.codes", + "BugReporting": "https://api.lapis.codes", + "Discovery": "https://api.lapis.codes", + "PlayerSettings": "https://api.lapis.codes", + "Rendering": "https://api.lapis.codes", + "Launcher": "https://rr.lapis.codes" +} \ No newline at end of file diff --git a/CannedNet.API/endpointsreference.json b/CannedNet/JSON/endpointsreference.json similarity index 100% rename from CannedNet.API/endpointsreference.json rename to CannedNet/JSON/endpointsreference.json diff --git a/CannedNet.API/gameconfigs.json b/CannedNet/JSON/gameconfigs.json similarity index 100% rename from CannedNet.API/gameconfigs.json rename to CannedNet/JSON/gameconfigs.json diff --git a/CannedNet.API/photonsettings.json b/CannedNet/JSON/photonsettings.json similarity index 100% rename from CannedNet.API/photonsettings.json rename to CannedNet/JSON/photonsettings.json diff --git a/CannedNet/Migrations/20260331191343_InitialCreate.Designer.cs b/CannedNet/Migrations/20260331191343_InitialCreate.Designer.cs new file mode 100644 index 0000000..360d579 --- /dev/null +++ b/CannedNet/Migrations/20260331191343_InitialCreate.Designer.cs @@ -0,0 +1,112 @@ +// +using System; +using CannedNet.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260331191343_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CannedNet.Account", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "createdAt"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "displayName"); + + b.Property("IdentityFlags") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "identityFlags"); + + b.Property("IsJunior") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "isJunior"); + + b.Property("PersonalPronouns") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "personalPronouns"); + + b.Property("Platforms") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platforms"); + + b.Property("ProfileImage") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profileImage"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "username"); + + b.HasKey("AccountId"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("CannedNet.CachedLogin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("LastLoginTime") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "lastLoginTime"); + + b.Property("Platform") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platform"); + + b.Property("PlatformID") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "platformId"); + + b.Property("RequirePassword") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "requirePassword"); + + b.HasKey("Id"); + + b.HasIndex("Platform", "PlatformID") + .IsUnique(); + + b.ToTable("cached_logins", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CannedNet/Migrations/20260331191343_InitialCreate.cs b/CannedNet/Migrations/20260331191343_InitialCreate.cs new file mode 100644 index 0000000..eeb8d06 --- /dev/null +++ b/CannedNet/Migrations/20260331191343_InitialCreate.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "accounts", + columns: table => new + { + AccountId = table.Column(type: "integer", nullable: false), + ProfileImage = table.Column(type: "text", nullable: true), + IsJunior = table.Column(type: "boolean", nullable: false), + Platforms = table.Column(type: "integer", nullable: true), + PersonalPronouns = table.Column(type: "integer", nullable: true), + IdentityFlags = table.Column(type: "integer", nullable: true), + Username = table.Column(type: "text", nullable: false), + DisplayName = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_accounts", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "cached_logins", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AccountId = table.Column(type: "integer", nullable: false), + Platform = table.Column(type: "integer", nullable: false), + PlatformID = table.Column(type: "text", nullable: false), + LastLoginTime = table.Column(type: "timestamp with time zone", nullable: false), + RequirePassword = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cached_logins", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_cached_logins_Platform_PlatformID", + table: "cached_logins", + columns: new[] { "Platform", "PlatformID" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "accounts"); + + migrationBuilder.DropTable( + name: "cached_logins"); + } + } +} diff --git a/CannedNet/Migrations/20260401002616_AddPlayerProgressions.Designer.cs b/CannedNet/Migrations/20260401002616_AddPlayerProgressions.Designer.cs new file mode 100644 index 0000000..1b565c8 --- /dev/null +++ b/CannedNet/Migrations/20260401002616_AddPlayerProgressions.Designer.cs @@ -0,0 +1,140 @@ +// +using System; +using CannedNet.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260401002616_AddPlayerProgressions")] + partial class AddPlayerProgressions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CannedNet.Account", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "createdAt"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "displayName"); + + b.Property("IdentityFlags") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "identityFlags"); + + b.Property("IsJunior") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "isJunior"); + + b.Property("PersonalPronouns") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "personalPronouns"); + + b.Property("Platforms") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platforms"); + + b.Property("ProfileImage") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profileImage"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "username"); + + b.HasKey("AccountId"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("CannedNet.CachedLogin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("LastLoginTime") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "lastLoginTime"); + + b.Property("Platform") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platform"); + + b.Property("PlatformID") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "platformId"); + + b.Property("RequirePassword") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "requirePassword"); + + b.HasKey("Id"); + + b.HasIndex("Platform", "PlatformID") + .IsUnique(); + + b.ToTable("cached_logins", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerProgression", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "level"); + + b.Property("PlayerId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "playerId"); + + b.Property("Xp") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "xp"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("player_progressions", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CannedNet/Migrations/20260401002616_AddPlayerProgressions.cs b/CannedNet/Migrations/20260401002616_AddPlayerProgressions.cs new file mode 100644 index 0000000..f9a3f3b --- /dev/null +++ b/CannedNet/Migrations/20260401002616_AddPlayerProgressions.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + /// + public partial class AddPlayerProgressions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "player_progressions", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PlayerId = table.Column(type: "integer", nullable: false), + Level = table.Column(type: "integer", nullable: false), + Xp = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_player_progressions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_player_progressions_PlayerId", + table: "player_progressions", + column: "PlayerId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "player_progressions"); + } + } +} diff --git a/CannedNet/Migrations/20260401123437_AddPlayerSettings.Designer.cs b/CannedNet/Migrations/20260401123437_AddPlayerSettings.Designer.cs new file mode 100644 index 0000000..f665bb1 --- /dev/null +++ b/CannedNet/Migrations/20260401123437_AddPlayerSettings.Designer.cs @@ -0,0 +1,170 @@ +// +using System; +using CannedNet.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260401123437_AddPlayerSettings")] + partial class AddPlayerSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CannedNet.Account", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "createdAt"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "displayName"); + + b.Property("IdentityFlags") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "identityFlags"); + + b.Property("IsJunior") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "isJunior"); + + b.Property("PersonalPronouns") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "personalPronouns"); + + b.Property("Platforms") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platforms"); + + b.Property("ProfileImage") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profileImage"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "username"); + + b.HasKey("AccountId"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("CannedNet.CachedLogin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("LastLoginTime") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "lastLoginTime"); + + b.Property("Platform") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platform"); + + b.Property("PlatformID") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "platformId"); + + b.Property("RequirePassword") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "requirePassword"); + + b.HasKey("Id"); + + b.HasIndex("Platform", "PlatformID") + .IsUnique(); + + b.ToTable("cached_logins", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerProgression", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "level"); + + b.Property("PlayerId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "playerId"); + + b.Property("Xp") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "xp"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("player_progressions", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "key"); + + b.Property("PlayerId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "playerId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "value"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "Key") + .IsUnique(); + + b.ToTable("player_settings", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CannedNet/Migrations/20260401123437_AddPlayerSettings.cs b/CannedNet/Migrations/20260401123437_AddPlayerSettings.cs new file mode 100644 index 0000000..08ab117 --- /dev/null +++ b/CannedNet/Migrations/20260401123437_AddPlayerSettings.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + /// + public partial class AddPlayerSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "player_settings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PlayerId = table.Column(type: "integer", nullable: false), + Key = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_player_settings", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_player_settings_PlayerId_Key", + table: "player_settings", + columns: new[] { "PlayerId", "Key" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "player_settings"); + } + } +} diff --git a/CannedNet/Migrations/20260401125651_AddPlayerAvatars.Designer.cs b/CannedNet/Migrations/20260401125651_AddPlayerAvatars.Designer.cs new file mode 100644 index 0000000..1ef5531 --- /dev/null +++ b/CannedNet/Migrations/20260401125651_AddPlayerAvatars.Designer.cs @@ -0,0 +1,254 @@ +// +using System; +using CannedNet.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260401125651_AddPlayerAvatars")] + partial class AddPlayerAvatars + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CannedNet.Account", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "createdAt"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "displayName"); + + b.Property("IdentityFlags") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "identityFlags"); + + b.Property("IsJunior") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "isJunior"); + + b.Property("PersonalPronouns") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "personalPronouns"); + + b.Property("Platforms") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platforms"); + + b.Property("ProfileImage") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profileImage"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "username"); + + b.HasKey("AccountId"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("CannedNet.AvatarItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "avatarItemDesc"); + + b.Property("AvatarItemType") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "avatarItemType"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "friendlyName"); + + b.Property("OwnerAccountId") + .HasColumnType("integer"); + + b.Property("PlatformMask") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platformMask"); + + b.Property("Rarity") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "rarity"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "tooltip"); + + b.HasKey("Id"); + + b.HasIndex("OwnerAccountId"); + + b.ToTable("avatar_items", (string)null); + }); + + modelBuilder.Entity("CannedNet.CachedLogin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("LastLoginTime") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "lastLoginTime"); + + b.Property("Platform") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platform"); + + b.Property("PlatformID") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "platformId"); + + b.Property("RequirePassword") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "requirePassword"); + + b.HasKey("Id"); + + b.HasIndex("Platform", "PlatformID") + .IsUnique(); + + b.ToTable("cached_logins", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerAvatar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FaceFeatures") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "FaceFeatures"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "HairColor"); + + b.Property("OutfitSelections") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "OutfitSelections"); + + b.Property("OwnerAccountId") + .HasColumnType("integer"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "SkinColor"); + + b.HasKey("Id"); + + b.HasIndex("OwnerAccountId") + .IsUnique(); + + b.ToTable("player_avatars", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerProgression", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "level"); + + b.Property("PlayerId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "playerId"); + + b.Property("Xp") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "xp"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("player_progressions", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "key"); + + b.Property("PlayerId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "playerId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "value"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "Key") + .IsUnique(); + + b.ToTable("player_settings", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CannedNet/Migrations/20260401125651_AddPlayerAvatars.cs b/CannedNet/Migrations/20260401125651_AddPlayerAvatars.cs new file mode 100644 index 0000000..620aa9b --- /dev/null +++ b/CannedNet/Migrations/20260401125651_AddPlayerAvatars.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + /// + public partial class AddPlayerAvatars : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "avatar_items", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AvatarItemType = table.Column(type: "integer", nullable: false), + AvatarItemDesc = table.Column(type: "text", nullable: false), + PlatformMask = table.Column(type: "integer", nullable: false), + FriendlyName = table.Column(type: "text", nullable: false), + Tooltip = table.Column(type: "text", nullable: false), + Rarity = table.Column(type: "integer", nullable: false), + OwnerAccountId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_avatar_items", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "player_avatars", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OwnerAccountId = table.Column(type: "integer", nullable: false), + OutfitSelections = table.Column(type: "text", nullable: false), + FaceFeatures = table.Column(type: "text", nullable: false), + SkinColor = table.Column(type: "text", nullable: false), + HairColor = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_player_avatars", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_avatar_items_OwnerAccountId", + table: "avatar_items", + column: "OwnerAccountId"); + + migrationBuilder.CreateIndex( + name: "IX_player_avatars_OwnerAccountId", + table: "player_avatars", + column: "OwnerAccountId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "avatar_items"); + + migrationBuilder.DropTable( + name: "player_avatars"); + } + } +} diff --git a/CannedNet/Migrations/AppDbContextModelSnapshot.cs b/CannedNet/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..84d2215 --- /dev/null +++ b/CannedNet/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,251 @@ +// +using System; +using CannedNet.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CannedNet.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CannedNet.Account", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "createdAt"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "displayName"); + + b.Property("IdentityFlags") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "identityFlags"); + + b.Property("IsJunior") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "isJunior"); + + b.Property("PersonalPronouns") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "personalPronouns"); + + b.Property("Platforms") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platforms"); + + b.Property("ProfileImage") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profileImage"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "username"); + + b.HasKey("AccountId"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("CannedNet.AvatarItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvatarItemDesc") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "avatarItemDesc"); + + b.Property("AvatarItemType") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "avatarItemType"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "friendlyName"); + + b.Property("OwnerAccountId") + .HasColumnType("integer"); + + b.Property("PlatformMask") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platformMask"); + + b.Property("Rarity") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "rarity"); + + b.Property("Tooltip") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "tooltip"); + + b.HasKey("Id"); + + b.HasIndex("OwnerAccountId"); + + b.ToTable("avatar_items", (string)null); + }); + + modelBuilder.Entity("CannedNet.CachedLogin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "accountId"); + + b.Property("LastLoginTime") + .HasColumnType("timestamp with time zone") + .HasAnnotation("Relational:JsonPropertyName", "lastLoginTime"); + + b.Property("Platform") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "platform"); + + b.Property("PlatformID") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "platformId"); + + b.Property("RequirePassword") + .HasColumnType("boolean") + .HasAnnotation("Relational:JsonPropertyName", "requirePassword"); + + b.HasKey("Id"); + + b.HasIndex("Platform", "PlatformID") + .IsUnique(); + + b.ToTable("cached_logins", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerAvatar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FaceFeatures") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "FaceFeatures"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "HairColor"); + + b.Property("OutfitSelections") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "OutfitSelections"); + + b.Property("OwnerAccountId") + .HasColumnType("integer"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "SkinColor"); + + b.HasKey("Id"); + + b.HasIndex("OwnerAccountId") + .IsUnique(); + + b.ToTable("player_avatars", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerProgression", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "level"); + + b.Property("PlayerId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "playerId"); + + b.Property("Xp") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "xp"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("player_progressions", (string)null); + }); + + modelBuilder.Entity("CannedNet.PlayerSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "key"); + + b.Property("PlayerId") + .HasColumnType("integer") + .HasAnnotation("Relational:JsonPropertyName", "playerId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "value"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "Key") + .IsUnique(); + + b.ToTable("player_settings", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CannedNet.API/Models/Account.cs b/CannedNet/Models/Account.cs similarity index 100% rename from CannedNet.API/Models/Account.cs rename to CannedNet/Models/Account.cs diff --git a/CannedNet/Models/AvatarItem.cs b/CannedNet/Models/AvatarItem.cs new file mode 100644 index 0000000..ee607bb --- /dev/null +++ b/CannedNet/Models/AvatarItem.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace CannedNet; + +public class AvatarItem +{ + public int Id { get; set; } + + [JsonPropertyName("avatarItemType")] + public int AvatarItemType { get; set; } + + [JsonPropertyName("avatarItemDesc")] + public string AvatarItemDesc { get; set; } = ""; + + [JsonPropertyName("platformMask")] + public int PlatformMask { get; set; } = -1; + + [JsonPropertyName("friendlyName")] + public string FriendlyName { get; set; } = ""; + + [JsonPropertyName("tooltip")] + public string Tooltip { get; set; } = ""; + + [JsonPropertyName("rarity")] + public int Rarity { get; set; } = 0; + + public int OwnerAccountId { get; set; } +} diff --git a/CannedNet.API/Models/CachedAccount.cs b/CannedNet/Models/CachedAccount.cs similarity index 100% rename from CannedNet.API/Models/CachedAccount.cs rename to CannedNet/Models/CachedAccount.cs diff --git a/CannedNet.API/Enums/JuniorState.cs b/CannedNet/Models/JuniorState.cs similarity index 100% rename from CannedNet.API/Enums/JuniorState.cs rename to CannedNet/Models/JuniorState.cs diff --git a/CannedNet/Models/NotificationModels.cs b/CannedNet/Models/NotificationModels.cs new file mode 100644 index 0000000..e6baf20 --- /dev/null +++ b/CannedNet/Models/NotificationModels.cs @@ -0,0 +1,231 @@ +using System.Text.Json.Serialization; + +namespace CannedNet; + +public class NotificationMessage +{ + [JsonPropertyName("notificationId")] + public string? NotificationId { get; set; } + + [JsonPropertyName("notificationType")] + public string? NotificationType { get; set; } + + [JsonPropertyName("id")] + public int? Id { get; set; } + + [JsonPropertyName("fromAccountId")] + public int? FromAccountId { get; set; } + + [JsonPropertyName("toAccountId")] + public int? ToAccountId { get; set; } + + [JsonPropertyName("createdAt")] + public DateTime? CreatedAt { get; set; } + + [JsonPropertyName("data")] + public Dictionary? Data { get; set; } +} + +public class SubscriptionUpdateProfile +{ + [JsonPropertyName("accountId")] + public int? AccountId { get; set; } + + [JsonPropertyName("profileImage")] + public string? ProfileImage { get; set; } + + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + [JsonPropertyName("username")] + public string? Username { get; set; } +} + +public class SubscriptionUpdatePresence +{ + [JsonPropertyName("accountId")] + public int? AccountId { get; set; } + + [JsonPropertyName("presence")] + public string? Presence { get; set; } + + [JsonPropertyName("currentRoomId")] + public string? CurrentRoomId { get; set; } + + [JsonPropertyName("currentWorldId")] + public string? CurrentWorldId { get; set; } + + [JsonPropertyName("lastSeen")] + public DateTime? LastSeen { get; set; } +} + +public class SubscriptionUpdateGameSession +{ + [JsonPropertyName("gameSessionId")] + public string? GameSessionId { get; set; } + + [JsonPropertyName("roomId")] + public string? RoomId { get; set; } + + [JsonPropertyName("worldId")] + public string? WorldId { get; set; } + + [JsonPropertyName("players")] + public List? Players { get; set; } +} + +public class SubscriptionUpdateRoom +{ + [JsonPropertyName("roomId")] + public string? RoomId { get; set; } + + [JsonPropertyName("worldId")] + public string? WorldId { get; set; } + + [JsonPropertyName("roomName")] + public string? RoomName { get; set; } + + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +public class RelationshipChangedNotification +{ + [JsonPropertyName("accountId")] + public int? AccountId { get; set; } + + [JsonPropertyName("relationshipType")] + public string? RelationshipType { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } +} + +public class PresenceHeartbeatResponse +{ + [JsonPropertyName("currentRoomId")] + public string? CurrentRoomId { get; set; } + + [JsonPropertyName("currentWorldId")] + public string? CurrentWorldId { get; set; } + + [JsonPropertyName("presence")] + public string? Presence { get; set; } +} + +public class ModerationNotification +{ + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + [JsonPropertyName("duration")] + public int? Duration { get; set; } + + [JsonPropertyName("moderatedBy")] + public string? ModeratedBy { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("gameSessionId")] + public string? GameSessionId { get; set; } +} + +public class GiftPackageNotification +{ + [JsonPropertyName("giftPackageId")] + public string? GiftPackageId { get; set; } + + [JsonPropertyName("fromAccountId")] + public int? FromAccountId { get; set; } + + [JsonPropertyName("giftType")] + public string? GiftType { get; set; } +} + +public class StorefrontBalanceNotification +{ + [JsonPropertyName("balance")] + public decimal? Balance { get; set; } + + [JsonPropertyName("currency")] + public string? Currency { get; set; } + + [JsonPropertyName("purchaseId")] + public string? PurchaseId { get; set; } +} + +public class ConsumableMappingNotification +{ + [JsonPropertyName("mappingId")] + public string? MappingId { get; set; } + + [JsonPropertyName("consumableId")] + public string? ConsumableId { get; set; } + + [JsonPropertyName("quantity")] + public int? Quantity { get; set; } +} + +public class PlayerEventNotification +{ + [JsonPropertyName("eventId")] + public string? EventId { get; set; } + + [JsonPropertyName("eventName")] + public string? EventName { get; set; } + + [JsonPropertyName("eventType")] + public string? EventType { get; set; } + + [JsonPropertyName("worldId")] + public string? WorldId { get; set; } + + [JsonPropertyName("responseStatus")] + public string? ResponseStatus { get; set; } + + [JsonPropertyName("state")] + public string? State { get; set; } +} + +public class ChatMessageNotification +{ + [JsonPropertyName("channelId")] + public string? ChannelId { get; set; } + + [JsonPropertyName("fromAccountId")] + public int? FromAccountId { get; set; } + + [JsonPropertyName("messageId")] + public string? MessageId { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("channelType")] + public string? ChannelType { get; set; } +} + +public class CommunityBoardNotification +{ + [JsonPropertyName("boardId")] + public string? BoardId { get; set; } + + [JsonPropertyName("announcementId")] + public string? AnnouncementId { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("content")] + public string? Content { get; set; } +} + +public class InventionModerationNotification +{ + [JsonPropertyName("inventionId")] + public string? InventionId { get; set; } + + [JsonPropertyName("moderationState")] + public string? ModerationState { get; set; } +} diff --git a/CannedNet/Models/NotificationTypes.cs b/CannedNet/Models/NotificationTypes.cs new file mode 100644 index 0000000..af9e73f --- /dev/null +++ b/CannedNet/Models/NotificationTypes.cs @@ -0,0 +1,40 @@ +namespace CannedNet; + +public enum PushNotificationId +{ + RelationshipChanged = 1, + MessageReceived = 2, + MessageDeleted = 3, + PresenceHeartbeatResponse = 4, + RefreshLogin = 5, + Logout = 6, + SubscriptionUpdateProfile = 11, + SubscriptionUpdatePresence = 12, + SubscriptionUpdateGameSession = 13, + SubscriptionUpdateRoom = 15, + ModerationQuitGame = 20, + ModerationUpdateRequired = 21, + ModerationKick = 22, + ModerationKickAttemptFailed = 23, + ModerationRoomBan = 24, + ServerMaintenance = 25, + GiftPackageReceived = 30, + GiftPackageReceivedImmediate = 31, + ProfileJuniorStatusUpdate = 40, + RelationshipsInvalid = 50, + StorefrontBalanceAdd = 60, + StorefrontBalanceUpdate = 61, + StorefrontBalancePurchase = 62, + ConsumableMappingAdded = 70, + ConsumableMappingRemoved = 71, + PlayerEventCreated = 80, + PlayerEventUpdated = 81, + PlayerEventDeleted = 82, + PlayerEventResponseChanged = 83, + PlayerEventResponseDeleted = 84, + PlayerEventStateChanged = 85, + ChatMessageReceived = 90, + CommunityBoardUpdate = 95, + CommunityBoardAnnouncementUpdate = 96, + InventionModerationStateChanged = 100 +} diff --git a/CannedNet.API/Enums/PlatformType.cs b/CannedNet/Models/PlatformType.cs similarity index 100% rename from CannedNet.API/Enums/PlatformType.cs rename to CannedNet/Models/PlatformType.cs diff --git a/CannedNet/Models/PlayerAvatar.cs b/CannedNet/Models/PlayerAvatar.cs new file mode 100644 index 0000000..a6e0a11 --- /dev/null +++ b/CannedNet/Models/PlayerAvatar.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace CannedNet; + +public class PlayerAvatar +{ + public int Id { get; set; } + public int OwnerAccountId { get; set; } + + [JsonPropertyName("OutfitSelections")] + public string OutfitSelections { get; set; } = ""; + + [JsonPropertyName("FaceFeatures")] + public string FaceFeatures { get; set; } = "{}"; + + [JsonPropertyName("SkinColor")] + public string SkinColor { get; set; } = ""; + + [JsonPropertyName("HairColor")] + public string HairColor { get; set; } = ""; +} diff --git a/CannedNet/Models/PlayerProgression.cs b/CannedNet/Models/PlayerProgression.cs new file mode 100644 index 0000000..7892db8 --- /dev/null +++ b/CannedNet/Models/PlayerProgression.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CannedNet; + +public class PlayerProgression +{ + public int Id { get; set; } + [JsonPropertyName("playerId")] + public int PlayerId { get; set; } + [JsonPropertyName("level")] + public int Level { get; set; } + [JsonPropertyName("xp")] + public int Xp { get; set; } +} + +public class PlayerProgressionBulkResponse +{ + [JsonPropertyName("playerId")] + public int PlayerId { get; set; } + [JsonPropertyName("level")] + public int Level { get; set; } + [JsonPropertyName("xp")] + public int Xp { get; set; } +} diff --git a/CannedNet/Models/PlayerSetting.cs b/CannedNet/Models/PlayerSetting.cs new file mode 100644 index 0000000..dc46d04 --- /dev/null +++ b/CannedNet/Models/PlayerSetting.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace CannedNet; + +public class PlayerSetting +{ + public int Id { get; set; } + [JsonPropertyName("playerId")] + public int PlayerId { get; set; } + [JsonPropertyName("key")] + public string Key { get; set; } + [JsonPropertyName("value")] + public string Value { get; set; } +} + diff --git a/CannedNet.API/Models/RecNetResult.cs b/CannedNet/Models/RecNetResult.cs similarity index 100% rename from CannedNet.API/Models/RecNetResult.cs rename to CannedNet/Models/RecNetResult.cs diff --git a/CannedNet.API/Models/SelfAccount.cs b/CannedNet/Models/SelfAccount.cs similarity index 100% rename from CannedNet.API/Models/SelfAccount.cs rename to CannedNet/Models/SelfAccount.cs diff --git a/CannedNet/Models/SettingPayload.cs b/CannedNet/Models/SettingPayload.cs new file mode 100644 index 0000000..f743ad4 --- /dev/null +++ b/CannedNet/Models/SettingPayload.cs @@ -0,0 +1,7 @@ +namespace CannedNet; + +public class SettingPayload +{ + public string Key { get; set; } + public string Value { get; set; } +} diff --git a/CannedNet/Models/SubscriptionList.cs b/CannedNet/Models/SubscriptionList.cs new file mode 100644 index 0000000..fb51d27 --- /dev/null +++ b/CannedNet/Models/SubscriptionList.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace CannedNet; + +public class SubscriptionList +{ + [JsonPropertyName("playerIds")] + public List? PlayerIds { get; set; } +} diff --git a/CannedNet/Program.cs b/CannedNet/Program.cs new file mode 100644 index 0000000..20937fd --- /dev/null +++ b/CannedNet/Program.cs @@ -0,0 +1,25 @@ +using CannedNet; +using CannedNet.Services; +using Microsoft.EntityFrameworkCore; + +var apps = new List<(WebApplication App, ServiceRegistry Service)>(); + +foreach (var service in Services.All) +{ + var builder = WebApplication.CreateBuilder(); + + service.ConfigureBuilder(builder); + builder.WebHost.UseUrls($"http://*:{service.Port}"); + builder.Configuration.AddJsonFile($"AppConfigs/appsettings.{service.Name}.json", optional: true, reloadOnChange: true); + + apps.Add((builder.Build(), service)); +} + +using var scope = apps[0].App.Services.CreateScope(); +scope.ServiceProvider.GetRequiredService().Database.Migrate(); +var jwtService = scope.ServiceProvider.GetRequiredService(); + +foreach (var (app, service) in apps) + service.MapEndpoints(app, jwtService); + +await Task.WhenAll(apps.Select(t => t.App.RunAsync())); diff --git a/CannedNet/Services/APIService.cs b/CannedNet/Services/APIService.cs new file mode 100644 index 0000000..7afb38d --- /dev/null +++ b/CannedNet/Services/APIService.cs @@ -0,0 +1,249 @@ +using CannedNet; +using CannedNet.Data; +using Microsoft.EntityFrameworkCore; + +namespace CannedNet.Services; + +public class APIService +{ + public WebApplicationBuilder Initialize(string[]? args = null) => ServiceExtensions.CreateRecNetBuilder(args); + + public void MapEndpoints(WebApplication app) + { + var jwtService = app.Services.GetRequiredService(); + + app.MapGet("/api/config/v1/amplitude", () => Results.Ok(new + { + AmplitudeKey = "fd846cd67066d19eec62fbdd00be9258", + StatSigKey = "client-mnew4Ggjuu3Fbh86Ed3tRgWUxapp0D7PPB0i3MXUEao", + RudderStackKey = "37qVlzZyXqdmZckXJ61vQeEJp8Y", + UseRudderStack = false + })); + + app.MapGet("/api/config/v2", () => Results.Content(File.ReadAllText("JSON/configv2.json"), "application/json")); + app.MapGet("/api/versioncheck/v4", () => Results.Content("{\"VersionStatus\":0}", "application/json")); + app.MapGet("/api/gameconfigs/v1/all", () => Results.Content(File.ReadAllText("JSON/gameconfigs.json"), "application/json")); + + app.MapGet("/api/relationships/v2/get", () => Results.Content("[]", "application/json")); + app.MapGet("/api/messages/v2/get", () => Results.Content("[]", "application/json")); + + app.MapGet("/api/playerReputation/v1/{id}", (string id) => + Results.Content($"{{\"AccountId\":{id},\"Noteriety\":0,\"CheerGeneral\":0,\"CheerHelpful\":0,\"CheerCreative\":0,\"CheerGreatHost\":0,\"CheerSportsman\":0,\"CheerCredit\":20,\"SelectedCheer\":null}}", "application/json")); + + app.MapGet("/api/players/v1/progression/{id}", (string id) => + Results.Content($"{{\"PlayerId\":{id},\"Level\":1,\"XP\":0}}", "application/json")); + + app.MapPost("/api/players/v1/progression/bulk", async (HttpRequest httpRequest, AppDbContext db) => + { + var ids = new List(); + + if (httpRequest.ContentLength.HasValue && httpRequest.ContentLength > 0) + { + httpRequest.EnableBuffering(); + using var reader = new StreamReader(httpRequest.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 && keyValue[0] == "Ids") + { + var idString = Uri.UnescapeDataString(keyValue[1]); + foreach (var id in idString.Split(',')) + if (int.TryParse(id, out var parsedId)) + ids.Add(parsedId); + break; + } + } + } + httpRequest.Body.Position = 0; + } + + if (!ids.Any()) + return Results.Json(new List()); + + var progressions = await db.PlayerProgressions + .Where(p => ids.Contains(p.PlayerId)) + .Select(p => new PlayerProgressionBulkResponse { PlayerId = p.PlayerId, Level = p.Level, Xp = p.Xp }) + .ToListAsync(); + + return Results.Json(progressions); + }); + + app.MapGet("/api/avatar/v4/items", 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(); + + var items = await db.AvatarItems + .Where(i => i.OwnerAccountId == id) + .ToListAsync(); + + return Results.Json(items); + }); + + app.MapGet("/api/avatar/v2", 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(); + + var avatar = await db.PlayerAvatars + .FirstOrDefaultAsync(a => a.OwnerAccountId == id); + + if (avatar == null) + { + avatar = new PlayerAvatar + { + OwnerAccountId = id, + OutfitSelections = "", + FaceFeatures = "{}", + SkinColor = "", + HairColor = "" + }; + db.PlayerAvatars.Add(avatar); + await db.SaveChangesAsync(); + } + + return Results.Json(avatar); + }); + + app.MapPost("/api/avatar/v2/set", 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(); + + request.EnableBuffering(); + var avatarUpdate = await System.Text.Json.JsonSerializer.DeserializeAsync(request.Body); + + if (avatarUpdate == null) + return Results.BadRequest(); + + var avatar = await db.PlayerAvatars + .FirstOrDefaultAsync(a => a.OwnerAccountId == id); + + if (avatar == null) + { + avatar = new PlayerAvatar { OwnerAccountId = id }; + db.PlayerAvatars.Add(avatar); + } + + avatar.OutfitSelections = avatarUpdate.OutfitSelections; + avatar.FaceFeatures = avatarUpdate.FaceFeatures; + avatar.SkinColor = avatarUpdate.SkinColor; + avatar.HairColor = avatarUpdate.HairColor; + + await db.SaveChangesAsync(); + return Results.Ok(avatar); + }); + + app.MapGet("/api/PlayerReporting/v1/moderationBlockDetails", () => + Results.Content("{\"ReportCategory\":0,\"Duration\":0,\"GameSessionId\":0,\"IsHostKick\":false,\"Message\":\"\",\"PlayerIdReporter\":null,\"IsBan\":false}", "application/json")); + + app.MapGet("/api/settings/v2" + + "", 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(); + + var settings = await db.PlayerSettings + .Where(s => s.PlayerId == id) + .ToListAsync(); + + return Results.Json(settings); + }); + + app.MapPost("/api/settings/v2/set", 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(); + + request.EnableBuffering(); + request.Body.Position = 0; + using var reader = new StreamReader(request.Body); + var body = await reader.ReadToEndAsync(); + + var settings = new List(); + + if (body.TrimStart().StartsWith("[")) + { + settings = System.Text.Json.JsonSerializer.Deserialize>(body) ?? []; + } + else + { + var single = System.Text.Json.JsonSerializer.Deserialize(body); + if (single != null) settings.Add(single); + } + + if (!settings.Any()) + return Results.BadRequest("No settings provided"); + + db.PlayerSettings.RemoveRange(db.PlayerSettings.Where(s => s.PlayerId == id)); + + foreach (var setting in settings) + { + setting.PlayerId = id; + db.PlayerSettings.Add(setting); + } + + await db.SaveChangesAsync(); + return Results.Ok(); + }); + } +} diff --git a/CannedNet/Services/AccountsService.cs b/CannedNet/Services/AccountsService.cs new file mode 100644 index 0000000..7e64f9d --- /dev/null +++ b/CannedNet/Services/AccountsService.cs @@ -0,0 +1,151 @@ +using CannedNet.Data; + +namespace CannedNet.Services; + +public class AccountsService +{ + public WebApplicationBuilder Initialize(string[]? args = null) => ServiceExtensions.CreateRecNetBuilder(args); + + public void MapEndpoints(WebApplication app, JwtTokenService jwtService) + { + app.MapGet("/", (HttpRequest request) => + { + return Results.Ok("FUCKL MYSJDIOJD"); + }); + + app.MapGet("/account/me", (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(); + + 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, + Email = null, + Phone = null, + JuniorState = null, + Birthday = null, + ParentAccountId = null, + AvailableUsernameChanges = 1 + }; + + return Results.Ok(selfAccount); + }); + + app.MapGet("/account/bulk", (HttpRequest request) => + { + var ids = request.Query["id"]; + var accounts = 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 + }); + } + } + + return Results.Json(accounts); + }); + + app.MapPost("/account/create", async (HttpRequest httpRequest, AppDbContext db) => + { + int platform = 0; + string platformId = ""; + + if (httpRequest.ContentLength.HasValue && httpRequest.ContentLength > 0) + { + try + { + var contentType = httpRequest.ContentType ?? ""; + httpRequest.EnableBuffering(); + + using var reader = new StreamReader(httpRequest.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + + if (!string.IsNullOrWhiteSpace(body) && contentType.Contains("application/x-www-form-urlencoded")) + { + 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 == "platform" && int.TryParse(value, out var parsedPlatform)) + platform = parsedPlatform; + else if (key == "platformId") + platformId = value; + } + } + } + httpRequest.Body.Position = 0; + } + catch { } + } + + var accountId = new Random().Next(10000, 99999); + var account = new Account + { + AccountId = accountId, + ProfileImage = "hdqeamlcmatc6qzoi2ybgf0ddijjcf.jpg", + IsJunior = false, + Platforms = 0, + PersonalPronouns = 0, + IdentityFlags = 0, + Username = $"Player{accountId}", + DisplayName = $"Player{accountId}", + CreatedAt = DateTime.UtcNow + }; + + db.Accounts.Add(account); + + if (!string.IsNullOrEmpty(platformId)) + { + db.CachedLogins.Add(new CachedLogin + { + AccountId = accountId, + Platform = (PlatformType)platform, + PlatformID = platformId, + LastLoginTime = DateTime.UtcNow, + RequirePassword = false + }); + } + + await db.SaveChangesAsync(); + + return Results.Ok(RecNetResultWithValue.Ok(account)); + }); + } +} diff --git a/CannedNet/Services/AuthService.cs b/CannedNet/Services/AuthService.cs new file mode 100644 index 0000000..f8366a1 --- /dev/null +++ b/CannedNet/Services/AuthService.cs @@ -0,0 +1,80 @@ +using CannedNet; +using CannedNet.Data; +using Microsoft.EntityFrameworkCore; + +namespace CannedNet.Services; + +public class AuthService +{ + public WebApplicationBuilder Initialize(string[]? args = null) => ServiceExtensions.CreateRecNetBuilder(args); + + public void MapEndpoints(WebApplication app) + { + var jwtService = app.Services.GetRequiredService(); + + app.MapGet("/eac/challenge", () => + { + var file = File.ReadAllText("JSON/eacchallenge.txt"); + return Results.Content(file, "text/plain"); + }); + + app.MapGet("/cachedlogin/forplatformid/{platform}/{id}", async (string platform, string id, AppDbContext db) => + { + var platformType = int.Parse(platform); + var logins = await db.CachedLogins + .Where(c => c.Platform == (PlatformType)platformType && c.PlatformID == id) + .ToListAsync(); + + return Results.Json(logins.Any() ? logins : new List()); + }); + + app.MapPost("/connect/token", async (HttpRequest httpRequest) => + { + string accountId = ""; + string platformId = ""; + string platform = ""; + + if (httpRequest.ContentLength.HasValue && httpRequest.ContentLength > 0) + { + try + { + httpRequest.EnableBuffering(); + using var reader = new StreamReader(httpRequest.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 == "account_id") + accountId = value; + else if (key == "platform_id") + platformId = value; + } + } + } + httpRequest.Body.Position = 0; + } + catch { } + } + + var accessToken = jwtService.GenerateToken(accountId, platformId, platform); + + return Results.Json(new + { + access_token = accessToken, + expires_in = 3600, + token_type = "Bearer", + refresh_token = Guid.NewGuid().ToString("N").ToUpper() + "-1", + scope = "offline_access profile rn rn.accounts rn.accounts.gc rn.api rn.chat rn.clubs rn.commerce rn.match.read rn.match.write rn.notify rn.rooms rn.storage", + key = "8oQ+e+WQaOBPbEcakhqs3dwZZdOmmyDUmJSD9u4AHMY=" + }); + }); + } +} diff --git a/CannedNet/Services/ChatService.cs b/CannedNet/Services/ChatService.cs new file mode 100644 index 0000000..009bc0e --- /dev/null +++ b/CannedNet/Services/ChatService.cs @@ -0,0 +1,11 @@ +namespace CannedNet.Services; + +public class ChatService +{ + public WebApplicationBuilder Initialize(string[]? args = null) => ServiceExtensions.CreateRecNetBuilder(args); + + public void MapEndpoints(WebApplication app) + { + app.MapGet("/thread", () => Results.Content("[]", "application/json")); + } +} diff --git a/CannedNet.API/JwtTokenService.cs b/CannedNet/Services/JwtTokenService.cs similarity index 94% rename from CannedNet.API/JwtTokenService.cs rename to CannedNet/Services/JwtTokenService.cs index 39708a6..96b116a 100644 --- a/CannedNet.API/JwtTokenService.cs +++ b/CannedNet/Services/JwtTokenService.cs @@ -65,8 +65,8 @@ public class JwtTokenService var handler = new JwtSecurityTokenHandler(); var token = handler.CreateJwtSecurityToken( - issuer: "https://auth.epicquest.live", - audience: "https://auth.epicquest.live/resources", + issuer: "https://auth.lapis.codes", + audience: "https://auth.lapis.codes/resources", subject: identity, notBefore: now, issuedAt: now, @@ -88,9 +88,9 @@ public class JwtTokenService ValidateIssuerSigningKey = true, IssuerSigningKey = _securityKey, ValidateIssuer = true, - ValidIssuer = "https://auth.epicquest.live", + ValidIssuer = "https://auth.lapis.codes", ValidateAudience = true, - ValidAudience = "https://auth.epicquest.live/resources", + ValidAudience = "https://auth.lapis.codes/resources", ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; diff --git a/CannedNet/Services/MatchmakingService.cs b/CannedNet/Services/MatchmakingService.cs new file mode 100644 index 0000000..6f3ad12 --- /dev/null +++ b/CannedNet/Services/MatchmakingService.cs @@ -0,0 +1,35 @@ +namespace CannedNet.Services; + +public class MatchmakingService +{ + public WebApplicationBuilder Initialize(string[]? args = null) => ServiceExtensions.CreateRecNetBuilder(args); + + public void MapEndpoints(WebApplication app) + { + app.MapPost("/player/login", () => Results.Ok()); + + app.MapGet("/player", (HttpRequest request) => + { + var id = request.Query["id"]; + var accounts = new List(); + + 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 + }); + } + + return Results.Json(accounts); + }); + } +} diff --git a/CannedNet/Services/NSService.cs b/CannedNet/Services/NSService.cs new file mode 100644 index 0000000..0fd0f77 --- /dev/null +++ b/CannedNet/Services/NSService.cs @@ -0,0 +1,23 @@ +using CannedNet.Hubs; + +namespace CannedNet.Services; + +public class NSService +{ + public WebApplicationBuilder Initialize(string[]? args = null) => ServiceExtensions.CreateRecNetBuilder(args); + + public void MapEndpoints(WebApplication app) + { + 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/NotificationService.cs b/CannedNet/Services/NotificationService.cs new file mode 100644 index 0000000..22eb101 --- /dev/null +++ b/CannedNet/Services/NotificationService.cs @@ -0,0 +1,117 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.SignalR; + +namespace CannedNet.Services; + +public class NotificationService +{ + private readonly ConcurrentDictionary> _connectionSubscriptions = new(); + private readonly ConcurrentDictionary> _playerConnections = new(); + + public void OnConnected(string connectionId) + { + _connectionSubscriptions.TryAdd(connectionId, new HashSet()); + } + + public void OnDisconnected(string connectionId) + { + if (_connectionSubscriptions.TryRemove(connectionId, out var subscriptions)) + { + foreach (var playerId in subscriptions) + { + if (_playerConnections.TryGetValue(playerId, out var connections)) + { + connections.Remove(connectionId); + if (connections.Count == 0) + { + _playerConnections.TryRemove(playerId, out _); + } + } + } + } + } + + public Task SubscribeToPlayers(string connectionId, SubscriptionList subscriptionList) + { + var oldSubscriptions = _connectionSubscriptions.GetValueOrDefault(connectionId, new HashSet()); + var newPlayerIds = subscriptionList.PlayerIds ?? new List(); + + _connectionSubscriptions[connectionId] = new HashSet(newPlayerIds); + + foreach (var playerId in newPlayerIds) + { + _playerConnections.AddOrUpdate( + playerId, + _ => new HashSet { connectionId }, + (_, existing) => + { + existing.Add(connectionId); + return existing; + }); + } + + foreach (var playerId in oldSubscriptions) + { + if (!newPlayerIds.Contains(playerId) && _playerConnections.TryGetValue(playerId, out var connections)) + { + connections.Remove(connectionId); + } + } + + return Task.CompletedTask; + } + + public HashSet? GetSubscriptions(string connectionId) + { + return _connectionSubscriptions.GetValueOrDefault(connectionId); + } + + public IEnumerable GetConnectionsForPlayer(int playerId) + { + if (_playerConnections.TryGetValue(playerId, out var connections)) + { + return connections.ToList(); + } + return Enumerable.Empty(); + } + + public IEnumerable GetSubscribedPlayers(string connectionId) + { + if (_connectionSubscriptions.TryGetValue(connectionId, out var subscriptions)) + { + return subscriptions.ToList(); + } + return Enumerable.Empty(); + } + + public async Task SendNotificationToPlayer(string callerConnectionId, IHubContext hubContext, int playerId, NotificationMessage notification) + { + var connections = GetConnectionsForPlayer(playerId); + foreach (var connectionId in connections) + { + if (connectionId == callerConnectionId) + continue; + + await hubContext.Clients.Client(connectionId).SendAsync("Notification", notification); + } + } + + public async Task BroadcastNotification(IHubContext hubContext, NotificationMessage notification) + { + await hubContext.Clients.All.SendAsync("Notification", notification); + } + + public NotificationMessage CreateNotification(string notificationType, int? id = null, int? fromAccountId = null, int? toAccountId = null, Dictionary? data = null) + { + return new NotificationMessage + { + NotificationId = Guid.NewGuid().ToString("N"), + NotificationType = notificationType, + Id = id, + FromAccountId = fromAccountId, + ToAccountId = toAccountId, + CreatedAt = DateTime.UtcNow, + Data = data + }; + } +} diff --git a/CannedNet/Services/NotifiyService.cs b/CannedNet/Services/NotifiyService.cs new file mode 100644 index 0000000..5097609 --- /dev/null +++ b/CannedNet/Services/NotifiyService.cs @@ -0,0 +1,13 @@ +using CannedNet.Hubs; + +namespace CannedNet.Services; + +public class NotifyService +{ + public WebApplicationBuilder Initialize(string[]? args = null) => ServiceExtensions.CreateRecNetBuilder(args); + + public void MapEndpoints(WebApplication app) + { + app.MapHub("/hub/v1"); + } +} diff --git a/CannedNet/Services/ServiceExtensions.cs b/CannedNet/Services/ServiceExtensions.cs new file mode 100644 index 0000000..d363d10 --- /dev/null +++ b/CannedNet/Services/ServiceExtensions.cs @@ -0,0 +1,32 @@ +using CannedNet.Data; +using Microsoft.EntityFrameworkCore; + +namespace CannedNet.Services; + +public static class ServiceExtensions +{ + public static void ConfigureRecNetServices(this WebApplicationBuilder builder) + { + builder.Services.ConfigureHttpJsonOptions(options => + options.SerializerOptions.PropertyNamingPolicy = null); + + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") + ?? "Host=localhost;Port=5432;Database=cannednet;Username=postgres;Password=postgres"; + + builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); + + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddSignalR(); + } + + public static void ConfigureHostUrl(this WebApplicationBuilder builder, string defaultUrl) + { + var url = builder.Configuration["Hosting:Url"]; + builder.WebHost.UseUrls(string.IsNullOrWhiteSpace(url) ? defaultUrl : url); + } + + public static WebApplicationBuilder CreateRecNetBuilder(string[]? args) => + WebApplication.CreateBuilder(args ?? []); +} diff --git a/CannedNet/Services/ServiceRegistry.cs b/CannedNet/Services/ServiceRegistry.cs new file mode 100644 index 0000000..f1dce8f --- /dev/null +++ b/CannedNet/Services/ServiceRegistry.cs @@ -0,0 +1,24 @@ +namespace CannedNet.Services; + +public class ServiceRegistry +{ + public required string Name { get; init; } + public required int Port { get; init; } + public required Type ServiceType { get; init; } + public required Action ConfigureBuilder { get; init; } + public required Action MapEndpoints { get; init; } +} + +public static class Services +{ + public static readonly List All = + [ + new() { Name = "NS", Port = 5001, ServiceType = typeof(NSService), ConfigureBuilder = b => b.ConfigureRecNetServices(), MapEndpoints = (app, _) => new NSService().MapEndpoints(app) }, + new() { Name = "API", Port = 5000, ServiceType = typeof(APIService), ConfigureBuilder = b => b.ConfigureRecNetServices(), MapEndpoints = (app, _) => new APIService().MapEndpoints(app) }, + new() { Name = "Auth", Port = 5002, ServiceType = typeof(AuthService), ConfigureBuilder = b => b.ConfigureRecNetServices(), MapEndpoints = (app, jwt) => new AuthService().MapEndpoints(app) }, + new() { Name = "Chat", Port = 5003, ServiceType = typeof(ChatService), ConfigureBuilder = b => b.ConfigureRecNetServices(), MapEndpoints = (app, _) => new ChatService().MapEndpoints(app) }, + new() { Name = "Match", Port = 5004, ServiceType = typeof(MatchmakingService), ConfigureBuilder = b => b.ConfigureRecNetServices(), MapEndpoints = (app, _) => new MatchmakingService().MapEndpoints(app) }, + new() { Name = "Accounts", Port = 5005, ServiceType = typeof(AccountsService), ConfigureBuilder = b => b.ConfigureRecNetServices(), MapEndpoints = (app, jwt) => new AccountsService().MapEndpoints(app, jwt) }, + new() { Name = "Notify", Port = 5006, ServiceType = typeof(NotifyService), ConfigureBuilder = b => b.ConfigureRecNetServices(), MapEndpoints = (app, _) => new NotifyService().MapEndpoints(app) } + ]; +} diff --git a/CannedNet.API/appsettings.Production.json b/CannedNet/appsettings.Production.json similarity index 81% rename from CannedNet.API/appsettings.Production.json rename to CannedNet/appsettings.Production.json index dee1d72..19fb0e6 100644 --- a/CannedNet.API/appsettings.Production.json +++ b/CannedNet/appsettings.Production.json @@ -2,6 +2,10 @@ "ConnectionStrings": { "DefaultConnection": "Host=localhost;Port=5432;Database=cannednet;Username=postgres;Password=postgres" }, + "Ports": { + "API": 5000, + "NS": 5001 + }, "Logging": { "LogLevel": { "Default": "Information" diff --git a/CannedNet.API/appsettings.json b/CannedNet/appsettings.json similarity index 51% rename from CannedNet.API/appsettings.json rename to CannedNet/appsettings.json index c499586..3afb6ba 100644 --- a/CannedNet.API/appsettings.json +++ b/CannedNet/appsettings.json @@ -6,7 +6,11 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Data Source=game.db;Cache=Shared" + "DefaultConnection": "Host=localhost;Port=5432;Database=cannednet;Username=postgres;Password=postgres" + }, + "Ports": { + "Primary": 5000, + "Secondary": 5001 }, "AllowedHosts": "*" }