From e74618ddcec31d140754b744dc8a63c925943411 Mon Sep 17 00:00:00 2001
From: Lapis <47952866+LapisGit@users.noreply.github.com>
Date: Tue, 31 Mar 2026 17:06:41 -0400
Subject: [PATCH] current src
---
.gitignore | 8 +
CannedNet.API/CannedNet.API.csproj | 17 +
CannedNet.API/Data/AppDbContext.cs | 38 +++
CannedNet.API/Enums/JuniorState.cs | 8 +
CannedNet.API/Enums/PlatformType.cs | 15 +
CannedNet.API/JwtTokenService.cs | 110 +++++++
CannedNet.API/Models/Account.cs | 16 +
CannedNet.API/Models/CachedAccount.cs | 24 ++
CannedNet.API/Models/RecNetResult.cs | 20 ++
CannedNet.API/Models/SelfAccount.cs | 15 +
CannedNet.API/Program.cs | 337 +++++++++++++++++++
CannedNet.API/appsettings.Production.json | 11 +
CannedNet.API/appsettings.json | 13 +
CannedNet.API/configv2.json | 373 ++++++++++++++++++++++
CannedNet.API/eacchallenge.txt | 1 +
CannedNet.API/endpoints.json | 26 ++
CannedNet.API/endpointsreference.json | 26 ++
CannedNet.API/gameconfigs.json | 170 ++++++++++
CannedNet.API/photonsettings.json | 13 +
CannedNet.slnx | 4 +
20 files changed, 1245 insertions(+)
create mode 100644 .gitignore
create mode 100644 CannedNet.API/CannedNet.API.csproj
create mode 100644 CannedNet.API/Data/AppDbContext.cs
create mode 100644 CannedNet.API/Enums/JuniorState.cs
create mode 100644 CannedNet.API/Enums/PlatformType.cs
create mode 100644 CannedNet.API/JwtTokenService.cs
create mode 100644 CannedNet.API/Models/Account.cs
create mode 100644 CannedNet.API/Models/CachedAccount.cs
create mode 100644 CannedNet.API/Models/RecNetResult.cs
create mode 100644 CannedNet.API/Models/SelfAccount.cs
create mode 100644 CannedNet.API/Program.cs
create mode 100644 CannedNet.API/appsettings.Production.json
create mode 100644 CannedNet.API/appsettings.json
create mode 100644 CannedNet.API/configv2.json
create mode 100644 CannedNet.API/eacchallenge.txt
create mode 100644 CannedNet.API/endpoints.json
create mode 100644 CannedNet.API/endpointsreference.json
create mode 100644 CannedNet.API/gameconfigs.json
create mode 100644 CannedNet.API/photonsettings.json
create mode 100644 CannedNet.slnx
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5ef5b7c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/
+/.idea
+/CannedNet.API/Migrations
+CannedNet.sln.DotSettings.user
diff --git a/CannedNet.API/CannedNet.API.csproj b/CannedNet.API/CannedNet.API.csproj
new file mode 100644
index 0000000..6afdecd
--- /dev/null
+++ b/CannedNet.API/CannedNet.API.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net10.0
+ enable
+ enable
+ CannedNet
+
+
+
+
+
+
+
+
+
+
diff --git a/CannedNet.API/Data/AppDbContext.cs b/CannedNet.API/Data/AppDbContext.cs
new file mode 100644
index 0000000..0a0cddc
--- /dev/null
+++ b/CannedNet.API/Data/AppDbContext.cs
@@ -0,0 +1,38 @@
+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/Enums/JuniorState.cs b/CannedNet.API/Enums/JuniorState.cs
new file mode 100644
index 0000000..e5edf99
--- /dev/null
+++ b/CannedNet.API/Enums/JuniorState.cs
@@ -0,0 +1,8 @@
+namespace CannedNet;
+
+public enum JuniorState
+{
+ BasedOnBirthday = 0,
+ ModerationOverride = 1,
+ PendingOverride = 2
+}
\ No newline at end of file
diff --git a/CannedNet.API/Enums/PlatformType.cs b/CannedNet.API/Enums/PlatformType.cs
new file mode 100644
index 0000000..d035b31
--- /dev/null
+++ b/CannedNet.API/Enums/PlatformType.cs
@@ -0,0 +1,15 @@
+namespace CannedNet;
+
+public enum PlatformType
+{
+ All = -1,
+ Steam = 0,
+ Oculus = 1,
+ PlayStation = 2,
+ Xbox = 3,
+ RecNet = 4,
+ IOS = 5,
+ GooglePlay = 6,
+ Standalone = 7,
+ Pico = 8
+}
\ No newline at end of file
diff --git a/CannedNet.API/JwtTokenService.cs b/CannedNet.API/JwtTokenService.cs
new file mode 100644
index 0000000..39708a6
--- /dev/null
+++ b/CannedNet.API/JwtTokenService.cs
@@ -0,0 +1,110 @@
+using System.Security.Cryptography;
+using System.Security.Claims;
+using Microsoft.IdentityModel.Tokens;
+using System.IdentityModel.Tokens.Jwt;
+
+namespace CannedNet;
+
+public class JwtTokenService
+{
+ private static readonly RSA _rsa = RSA.Create(2048);
+ private readonly RsaSecurityKey _securityKey;
+ private readonly SigningCredentials _signingCredentials;
+ private const string KeyId = "7C2F041398671515B0862CB23FAF95B03";
+
+ public JwtTokenService()
+ {
+ _securityKey = new RsaSecurityKey(_rsa) { KeyId = KeyId };
+ _signingCredentials = new SigningCredentials(_securityKey, SecurityAlgorithms.RsaSha256);
+ }
+
+ public string GenerateToken(string accountId, string platformId, string platform = "Steam")
+ {
+ var now = DateTime.UtcNow;
+ var exp = now.AddHours(1);
+
+ // theres gotta be some other better way to do this, but im fucking stupid so :shrug:
+ // if it works it works ig idk, someone can clean this up later or i will later
+
+ var claims = new List
+ {
+ new Claim("iss", "https://rr.lapis.codes"),
+ new Claim("nbf", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
+ new Claim("iat", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
+ new Claim("exp", ((DateTimeOffset)exp).ToUnixTimeSeconds().ToString()),
+ new Claim("aud", "https://rr.lapis.codes/resources"),
+ new Claim("amr", "cached_login"),
+ new Claim("client_id", "recroom"),
+ new Claim("sub", accountId),
+ new Claim("auth_time", ((DateTimeOffset)now).ToUnixTimeSeconds().ToString()),
+ new Claim("idp", "local"),
+ new Claim("platform", platform),
+ new Claim("platform_id", platformId),
+ new Claim("rn.ver", "20210129"),
+ new Claim("rn.plat", "0"),
+ new Claim("role", "gameClient"),
+ new Claim("jti", Guid.NewGuid().ToString("N").Substring(0, 32).ToUpper())
+ };
+
+ claims.Add(new Claim("scope", "profile"));
+ claims.Add(new Claim("scope", "rn"));
+ claims.Add(new Claim("scope", "rn.accounts"));
+ claims.Add(new Claim("scope", "rn.accounts.gc"));
+ claims.Add(new Claim("scope", "rn.api"));
+ claims.Add(new Claim("scope", "rn.chat"));
+ claims.Add(new Claim("scope", "rn.clubs"));
+ claims.Add(new Claim("scope", "rn.commerce"));
+ claims.Add(new Claim("scope", "rn.match.read"));
+ claims.Add(new Claim("scope", "rn.match.write"));
+ claims.Add(new Claim("scope", "rn.notify"));
+ claims.Add(new Claim("scope", "rn.rooms"));
+ claims.Add(new Claim("scope", "rn.storage"));
+ claims.Add(new Claim("scope", "offline_access"));
+
+ var identity = new ClaimsIdentity(claims, "jwt");
+
+ var handler = new JwtSecurityTokenHandler();
+ var token = handler.CreateJwtSecurityToken(
+ issuer: "https://auth.epicquest.live",
+ audience: "https://auth.epicquest.live/resources",
+ subject: identity,
+ notBefore: now,
+ issuedAt: now,
+ expires: exp,
+ signingCredentials: _signingCredentials);
+
+ return handler.WriteToken(token);
+ }
+
+ public string? ValidateAndGetAccountId(string token)
+ {
+ try
+ {
+ var handler = new JwtSecurityTokenHandler();
+ handler.InboundClaimTypeMap.Clear();
+
+ var parameters = new TokenValidationParameters
+ {
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = _securityKey,
+ ValidateIssuer = true,
+ ValidIssuer = "https://auth.epicquest.live",
+ ValidateAudience = true,
+ ValidAudience = "https://auth.epicquest.live/resources",
+ ValidateLifetime = true,
+ ClockSkew = TimeSpan.Zero
+ };
+
+ var principal = handler.ValidateToken(token, parameters, out SecurityToken validatedToken);
+
+ var accountIdClaim = principal.Claims.FirstOrDefault(c => c.Type == "sub");
+
+ return accountIdClaim?.Value;
+ }
+ catch (Exception ex)
+ {
+ return null;
+ }
+ }
+}
+
diff --git a/CannedNet.API/Models/Account.cs b/CannedNet.API/Models/Account.cs
new file mode 100644
index 0000000..3d37962
--- /dev/null
+++ b/CannedNet.API/Models/Account.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace CannedNet;
+
+public class Account
+{
+ [JsonPropertyName("accountId")] public int? AccountId { get; set; }
+ [JsonPropertyName("profileImage")] public string? ProfileImage { get; set; }
+ [JsonPropertyName("isJunior")] public bool IsJunior { get; set; }
+ [JsonPropertyName("platforms")] public int? Platforms { get; set; }
+ [JsonPropertyName("personalPronouns")] public int? PersonalPronouns { get; set; }
+ [JsonPropertyName("identityFlags")] public int? IdentityFlags { get; set; }
+ [JsonPropertyName("username")] public string? Username { get; set; }
+ [JsonPropertyName("displayName")] public string? DisplayName { get; set; }
+ [JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; }
+}
\ No newline at end of file
diff --git a/CannedNet.API/Models/CachedAccount.cs b/CannedNet.API/Models/CachedAccount.cs
new file mode 100644
index 0000000..6a539f4
--- /dev/null
+++ b/CannedNet.API/Models/CachedAccount.cs
@@ -0,0 +1,24 @@
+using System.Text.Json.Serialization;
+
+namespace CannedNet;
+
+public class CachedLogin
+{
+ [JsonIgnore]
+ public int Id { get; set; }
+
+ [JsonPropertyName("accountId")]
+ public int AccountId { get; set; }
+
+ [JsonPropertyName("platform")]
+ public PlatformType Platform { get; set; }
+
+ [JsonPropertyName("platformId")]
+ public string PlatformID { get; set; }
+
+ [JsonPropertyName("lastLoginTime")]
+ public DateTime LastLoginTime { get; set; }
+
+ [JsonPropertyName("requirePassword")]
+ public bool RequirePassword { get; set; } = false;
+}
\ No newline at end of file
diff --git a/CannedNet.API/Models/RecNetResult.cs b/CannedNet.API/Models/RecNetResult.cs
new file mode 100644
index 0000000..aa737e5
--- /dev/null
+++ b/CannedNet.API/Models/RecNetResult.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace CannedNet;
+
+public class RecNetResultWithValue : RecNetResult
+{
+ [JsonPropertyName("value")] public required T? Value { get; set; }
+}
+public class RecNetResult
+{
+ public static RecNetResult Ok() => new RecNetResult { Success = true };
+ public static RecNetResult Err(string error) => new RecNetResult { Success = false, Error = error };
+ public static RecNetResultWithValue Ok(T value) => new RecNetResultWithValue { Success = true, Value = value };
+ public static RecNetResultWithValue Err(string error) => new RecNetResultWithValue { Success = false, Error = error, Value = default };
+
+ [JsonPropertyName("success")] public bool Success { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("error")] public string? Error { get; set; }
+}
\ No newline at end of file
diff --git a/CannedNet.API/Models/SelfAccount.cs b/CannedNet.API/Models/SelfAccount.cs
new file mode 100644
index 0000000..d18386f
--- /dev/null
+++ b/CannedNet.API/Models/SelfAccount.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace CannedNet;
+
+public class SelfAccount : Account
+{
+ [JsonPropertyName("email")] public string? Email { get; set; }
+ [JsonPropertyName("phone")] public string? Phone { get; set; }
+ [JsonPropertyName("juniorState")] public JuniorState? JuniorState { get; set; }
+ [JsonPropertyName("birthday")] public DateTime? Birthday { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("parentAccountId")] public string? ParentAccountId { get; set; }
+ [JsonPropertyName("availableUsernameChanges")] public int? AvailableUsernameChanges { get; set; }
+}
\ No newline at end of file
diff --git a/CannedNet.API/Program.cs b/CannedNet.API/Program.cs
new file mode 100644
index 0000000..93a344e
--- /dev/null
+++ b/CannedNet.API/Program.cs
@@ -0,0 +1,337 @@
+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