diff --git a/ReferenceClientProxyImplementation.slnx b/ReferenceClientProxyImplementation.slnx
new file mode 100644
index 0000000..dc01632
--- /dev/null
+++ b/ReferenceClientProxyImplementation.slnx
@@ -0,0 +1,3 @@
+<Solution>
+ <Project Path="ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj" />
+</Solution>
diff --git a/ReferenceClientProxyImplementation/.gitignore b/ReferenceClientProxyImplementation/.gitignore
new file mode 100644
index 0000000..0219682
--- /dev/null
+++ b/ReferenceClientProxyImplementation/.gitignore
@@ -0,0 +1,9 @@
+downloadCache
+server-cs-d9282bd218a35e2399d27e98cdd6f7a0a8552bb7
+cache
+cache_formatted
+clientRepository
+error_reports
+prettier
+node
+biome
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Configuration/AssetCacheConfig.cs b/ReferenceClientProxyImplementation/Configuration/AssetCacheConfig.cs
new file mode 100644
index 0000000..7360a0a
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Configuration/AssetCacheConfig.cs
@@ -0,0 +1,15 @@
+namespace ReferenceClientProxyImplementation.Configuration;
+
+public class AssetCacheConfig {
+ public bool MemoryCache { get; set; } = true;
+ public bool DiskCache { get; set; } = true;
+ public string DiskCachePath { get; set; } = "cache";
+ public bool WipeOnStartup { get; set; } = false;
+ public string DiskCacheBaseDirectory { get; set; } = "./clientRepository";
+ public List<List<string>> ExecOnRevisionChange { get; set; } = [];
+ public bool DitchPatchedOnStartup { get; set; } = false;
+ public string BiomePath { get; set; } = "biome";
+ public string PrettierPath { get; set; } = "prettier";
+
+ public string NodePath { get; set; } = "node";
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Configuration/ProxyConfiguration.cs b/ReferenceClientProxyImplementation/Configuration/ProxyConfiguration.cs
new file mode 100644
index 0000000..84afacd
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Configuration/ProxyConfiguration.cs
@@ -0,0 +1,8 @@
+namespace ReferenceClientProxyImplementation.Configuration;
+
+public class ProxyConfiguration {
+ public ProxyConfiguration(IConfiguration configuration) => configuration.GetRequiredSection("ProxyConfiguration").Bind(this);
+
+ public TestClientConfig TestClient { get; set; } = new();
+ public AssetCacheConfig AssetCache { get; set; } = new();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Configuration/TestClientConfig.cs b/ReferenceClientProxyImplementation/Configuration/TestClientConfig.cs
new file mode 100644
index 0000000..1776eef
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Configuration/TestClientConfig.cs
@@ -0,0 +1,13 @@
+namespace ReferenceClientProxyImplementation.Configuration;
+
+public class TestClientConfig {
+ public TestClientDebug DebugOptions = new();
+ public bool Enabled { get; set; } = true;
+ // public bool UseLatest = true;
+ public string Revision { get; set; } = "canary";
+ public Dictionary<string, object> GlobalEnv { get; set; } = new();
+
+ // internal
+ public string RevisionPath { get; set; } = null!;
+ public string RevisionBaseUrl { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Configuration/TestClientDebug.cs b/ReferenceClientProxyImplementation/Configuration/TestClientDebug.cs
new file mode 100644
index 0000000..b7370b5
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Configuration/TestClientDebug.cs
@@ -0,0 +1,7 @@
+namespace ReferenceClientProxyImplementation.Configuration;
+
+public class TestClientDebug {
+ public bool DumpWebsocketTraffic = false;
+ public bool DumpWebsocketTrafficToBrowserConsole = false;
+ public TestClientPatchOptions PatchOptions = new();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Configuration/TestClientPatchOptions.cs b/ReferenceClientProxyImplementation/Configuration/TestClientPatchOptions.cs
new file mode 100644
index 0000000..e95dfa5
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Configuration/TestClientPatchOptions.cs
@@ -0,0 +1,7 @@
+namespace ReferenceClientProxyImplementation.Configuration;
+
+public class TestClientPatchOptions {
+ public bool GatewayImmediateReconnect = false;
+ public bool GatewayPlaintext = true;
+ public bool NoXssWarning = true;
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs b/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs
new file mode 100644
index 0000000..8bd78bc
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs
@@ -0,0 +1,72 @@
+//#define MEMCACHE
+using System.Collections.Frozen;
+using ArcaneLibs.Extensions.Streams;
+using Microsoft.AspNetCore.Mvc;
+using ReferenceClientProxyImplementation.Configuration;
+using ReferenceClientProxyImplementation.Services;
+
+namespace ReferenceClientProxyImplementation.Controllers;
+
+[Controller]
+[Route("/")]
+public class AssetsController(ProxyConfiguration proxyConfiguration, ClientStoreService clientStore) : Controller {
+#if MEMCACHE
+ private static FrozenDictionary<string, ReadOnlyMemory<byte>> memCache = new Dictionary<string, ReadOnlyMemory<byte>>().ToFrozenDictionary();
+#endif
+
+ [HttpGet("/assets/{*res:required}")]
+ public async Task<IActionResult> Asset(string res) {
+ if (res is "version.staging.json" or "version.internal.json")
+ res = $"version.{proxyConfiguration.TestClient.Revision}.json";
+ else if (res.EndsWith(".map")) {
+ return NotFound();
+ }
+
+ var ext = res.Split(".").Last();
+ var contentType = ext switch {
+ //text types
+ "html" => "text/html",
+ "js" => "text/javascript",
+ "css" => "text/css",
+ "txt" => "text/plain",
+ "csv" => "text/csv",
+ "json" => "application/json",
+ //image types
+ "apng" => "image/apng",
+ "gif" => "image/gif",
+ "jpg" => "image/jpeg",
+ "png" => "image/png",
+ "svg" => "image/svg+xml",
+ "webp" => "image/webp",
+ "ico" => "image/x-icon",
+ //script types
+ "wasm" => "application/wasm",
+ _ => "application/octet-stream"
+ };
+ // Response.Headers.ContentType = contentType;
+#if MEMCACHE
+ if (memCache.TryGetValue(res, out var value)) {
+ // value.Position = 0;
+ var cms = new MemoryStream(value.ToArray());
+ // await value.CopyToAsync(cms);
+ // ms.Position = 0;
+
+ return new FileStreamResult(cms, contentType);
+ }
+#endif
+
+#if MEMCACHE
+ var stream = await clientStore.GetPatchedClientAsset("assets/" + res);
+ stream.Position = 0;
+ // var ms = new MemoryStream(stream.ReadToEnd().ToArray(), false);
+ // memCache = memCache.Append(new KeyValuePair<string, MemoryStream>(res, ms)).ToFrozenDictionary();
+ // return new FileStreamResult(ms, contentType);
+ var mem = new ReadOnlyMemory<byte>(stream.ReadToEnd().ToArray());
+ memCache = memCache.Append(new KeyValuePair<string, ReadOnlyMemory<byte>>(res, mem)).ToFrozenDictionary();
+ return new FileStreamResult(new MemoryStream(mem.ToArray()), contentType);
+#else
+ return new FileStreamResult(await clientStore.GetPatchedClientAsset("assets/" + res), contentType);
+#endif
+ // return ;
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs.bak b/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs.bak
new file mode 100644
index 0000000..ea5909a
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Controllers/AssetsControllers.cs.bak
@@ -0,0 +1,124 @@
+//using System.Collections.Concurrent;
+//using System.Text;
+//using Microsoft.AspNetCore.Mvc;
+//using ReferenceClientProxyImplementation.Configuration;
+//using ReferenceClientProxyImplementation.Services;
+//using Spacebar.API.Helpers;
+//
+//namespace ReferenceClientProxyImplementation.Controllers;
+//
+//[Controller]
+//[Route("/")]
+//public class AssetsController(ProxyConfiguration proxyConfiguration, ClientStoreService clientStore) : Controller {
+// private static readonly ConcurrentDictionary<string, byte[]> cache = new();
+//
+// [HttpGet("/assets/{*res:required}")]
+// public async Task<object> Asset(string res) {
+// var ext = res.Split(".").Last();
+// var contentType = ext switch {
+// //text types
+// "html" => "text/html",
+// "js" => "text/javascript",
+// "css" => "text/css",
+// "txt" => "text/plain",
+// "csv" => "text/csv",
+// //image types
+// "apng" => "image/apng",
+// "gif" => "image/gif",
+// "jpg" => "image/jpeg",
+// "png" => "image/png",
+// "svg" => "image/svg+xml",
+// "webp" => "image/webp",
+// "ico" => "image/x-icon",
+// _ => "application/octet-stream"
+// };
+// if (cache.ContainsKey(res))
+// return File(cache[res], contentType);
+//
+// if (System.IO.File.Exists("./Resources/Assets/" + res))
+// cache.TryAdd(res, await System.IO.File.ReadAllBytesAsync("./Resources/Assets/" + res));
+// else if (System.IO.File.Exists("./cache_formatted/" + res))
+// cache.TryAdd(res, await System.IO.File.ReadAllBytesAsync("./cache_formatted/" + res));
+// else if (System.IO.File.Exists("./cache/" + res))
+// cache.TryAdd(res, await System.IO.File.ReadAllBytesAsync($"{proxyConfiguration.AssetCache.DiskCachePath}/{res}"));
+// else {
+// if (!Directory.Exists(proxyConfiguration.AssetCache.DiskCachePath)) Directory.CreateDirectory(proxyConfiguration.AssetCache.DiskCachePath);
+// if (res.EndsWith(".map")) return NotFound();
+// Console.WriteLine($"[Asset cache] Downloading {"https://discord.com/assets/" + res} -> {proxyConfiguration.AssetCache.DiskCachePath}/{res}");
+// try {
+// using (var hc = new HttpClient()) {
+// var resp = await hc.GetAsync("https://discord.com/assets/" + res);
+//
+// if (!resp.IsSuccessStatusCode) return NotFound();
+// //save to file
+// var bytes = await resp.Content.ReadAsByteArrayAsync();
+// //check if cloudflare
+// if (bytes.Length == 0) {
+// Console.WriteLine(
+// $"[Asset cache] Cloudflare detected, retrying {"https://discord.com/assets/" + res} -> {proxyConfiguration.AssetCache.DiskCachePath}/{res}");
+// await Task.Delay(1000);
+// resp = await hc.GetAsync("https://discord.com/assets/" + res);
+// if (!resp.IsSuccessStatusCode) return NotFound();
+// bytes = await resp.Content.ReadAsByteArrayAsync();
+// }
+//
+// //check if cloudflare html
+// /*if (bytes.Length < 1000 && bytes.ToList().Contains<byte[]>(Encoding.UTF8.GetBytes("Cloudflare")))
+// {
+// Console.WriteLine($"[Asset cache] Cloudflare detected, retrying {"https://discord.com/assets/" + res} -> ./cache/{res}");
+// await Task.Delay(1000);
+// resp = await hc.GetAsync("https://discord.com/assets/" + res);
+// if (!resp.IsSuccessStatusCode) return NotFound();
+// bytes = await resp.Content.ReadAsByteArrayAsync();
+// }*/
+// if (res.EndsWith(".js") || res.EndsWith(".css")) {
+// //remove sourcemap
+// var str = Encoding.UTF8.GetString(bytes);
+// str = PatchClient(str);
+// bytes = Encoding.UTF8.GetBytes(str);
+// }
+//
+// if (proxyConfiguration.AssetCache.DiskCache) await System.IO.File.WriteAllBytesAsync($"{proxyConfiguration.AssetCache.DiskCachePath}/{res}", bytes);
+// cache.TryAdd(res, bytes);
+// }
+// //await new WebClient().DownloadFileTaskAsync("https://discord.com/assets/" + res, "./cache/" + res);
+// //cache.TryAdd(res, await System.IO.File.ReadAllBytesAsync("./cache/" + res));
+// }
+// catch (Exception e) {
+// Console.WriteLine(e);
+// return NotFound();
+// }
+// }
+//
+// if (cache.ContainsKey(res)) {
+// var result = cache[res];
+// if (!proxyConfiguration.AssetCache.MemoryCache) cache.TryRemove(res, out _);
+// return File(result, contentType);
+// }
+//
+// return NotFound();
+// }
+//
+// public string PatchClient(string str) {
+// var patchOptions = proxyConfiguration.TestClient.DebugOptions.PatchOptions;
+// str = str.Replace("//# sourceMappingURL=", "//# disabledSourceMappingURL=");
+// str = str.Replace("https://fa97a90475514c03a42f80cd36d147c4@sentry.io/140984", "https://6bad92b0175d41a18a037a73d0cff282@sentry.thearcanebrony.net/12");
+// if (patchOptions.GatewayPlaintext)
+// str = str.Replace("e.isDiscordGatewayPlaintextSet=function(){0;return!1};", "e.isDiscordGatewayPlaintextSet = function() { return true };");
+//
+// if (patchOptions.NoXssWarning) {
+// str = str.Replace("console.log(\"%c\"+n.SELF_XSS_", "console.valueOf(n.SELF_XSS_");
+// str = str.Replace("console.log(\"%c\".concat(n.SELF_XSS_", "console.valueOf(console.valueOf(n.SELF_XSS_");
+// }
+//
+// if (patchOptions.GatewayImmediateReconnect) str = str.Replace("nextReconnectIsImmediate=!1", "nextReconnectIsImmediate = true");
+//
+// return str;
+// }
+//
+// [HttpGet("/robots.txt")]
+// public object Robots() => Resolvers.ReturnFile("./Resources/robots.txt");
+//
+// [HttpGet("/favicon.ico")]
+// public object Favicon() => Resolvers.ReturnFile("./Resources/RunData/favicon.png");
+//}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Controllers/ErrorReportingProxy.cs b/ReferenceClientProxyImplementation/Controllers/ErrorReportingProxy.cs
new file mode 100644
index 0000000..acc186b
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Controllers/ErrorReportingProxy.cs
@@ -0,0 +1,34 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ReferenceClientProxyImplementation.Controllers;
+
+[Controller]
+[Route("/")]
+public class ErrorReportingProxy : Controller {
+ [HttpPost("/error-reporting-proxy/web")]
+ public async Task<ActionResult> HandleErrorReport() {
+ // read body as string
+ var body = await new StreamReader(Request.Body).ReadToEndAsync();
+ var lines = body.Split('\n');
+ var data = new JsonObject() {
+ ["eventInfo"] = JsonSerializer.Deserialize<JsonObject>(lines[0]),
+ ["typeInfo"] = JsonSerializer.Deserialize<JsonObject>(lines[1]),
+ ["stackTrace"] = JsonSerializer.Deserialize<JsonObject>(lines[2]),
+ };
+
+ if (lines.Length > 3)
+ for (var i = 3; i < lines.Length; i++) {
+ data[$"unk_line_{i}"] = JsonSerializer.Deserialize<JsonValue>(lines[i]);
+ }
+
+ if (!System.IO.Directory.Exists("error_reports"))
+ System.IO.Directory.CreateDirectory("error_reports");
+ await System.IO.File.WriteAllTextAsync($"error_reports/web_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.json", data.ToJsonString(new JsonSerializerOptions {
+ WriteIndented = true
+ }));
+
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Controllers/FrontendController.cs b/ReferenceClientProxyImplementation/Controllers/FrontendController.cs
new file mode 100644
index 0000000..b12996d
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Controllers/FrontendController.cs
@@ -0,0 +1,77 @@
+using Microsoft.AspNetCore.Mvc;
+using ReferenceClientProxyImplementation.Configuration;
+using ReferenceClientProxyImplementation.Helpers;
+using ReferenceClientProxyImplementation.Patches.Implementations;
+
+namespace ReferenceClientProxyImplementation.Controllers;
+
+[Controller]
+[Route("/")]
+public class FrontendController(ProxyConfiguration proxyConfiguration, PatchSet patches) : Controller {
+ [HttpGet]
+ [HttpGet("/app")]
+ [HttpGet("/login")]
+ [HttpGet("/register")]
+ [HttpGet("/channels/@me")]
+ [HttpGet("/channels/{*_}")]
+ [HttpGet("/shop")]
+ [HttpGet("/app/{*_}")]
+ [HttpGet("/open")]
+ [HttpGet("/settings/{*_}")]
+ [HttpGet("/action/{*_}")]
+ [HttpGet("/library/{*_}")]
+ public async Task<Stream> Home() {
+ var patchedPath = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "patched", "app.html");
+ if (!System.IO.File.Exists(patchedPath)) {
+ var path = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "src", "app.html");
+ var patchedContent = await patches.ApplyPatches("app.html", await System.IO.File.ReadAllBytesAsync(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(patchedPath)!);
+ await System.IO.File.WriteAllBytesAsync(patchedPath, patchedContent);
+ }
+
+ return System.IO.File.OpenRead(patchedPath);
+ // return null;
+ // if (!proxyConfiguration.TestClient.Enabled)
+ // return NotFound("Test client is disabled");
+ // var html = await System.IO.File.ReadAllTextAsync(proxyConfiguration.TestClient.UseLatest ? "Resources/Pages/index-updated.html" : "Resources/Pages/index.html");
+ //
+ // //inject debug utilities
+ // var debugOptions = proxyConfiguration.TestClient.DebugOptions;
+ // if (debugOptions.DumpWebsocketTrafficToBrowserConsole)
+ // html = html.Replace("<!-- preload plugin marker -->",
+ // await System.IO.File.ReadAllTextAsync("Resources/Private/Injections/WebSocketDataLog.html") + "\n<!-- preload plugin marker -->");
+ // if (debugOptions.DumpWebsocketTraffic)
+ // html = html.Replace("<!-- preload plugin marker -->",
+ // await System.IO.File.ReadAllTextAsync("Resources/Private/Injections/WebSocketDumper.html") + "\n<!-- preload plugin marker -->");
+ //
+ // return File(Encoding.UTF8.GetBytes(html), "text/html");
+ }
+
+ [HttpGet("/developers")]
+ [HttpGet("/developers/{*_}")]
+ public async Task<object> Developers() {
+ var patchedPath = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "patched", "developers.html");
+ if (!System.IO.File.Exists(patchedPath)) {
+ var path = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "src", "developers.html");
+ var patchedContent = await patches.ApplyPatches("developers.html", await System.IO.File.ReadAllBytesAsync(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(patchedPath)!);
+ await System.IO.File.WriteAllBytesAsync(patchedPath, patchedContent);
+ }
+
+ return System.IO.File.OpenRead(patchedPath);
+ }
+
+ [HttpGet("/popout")]
+ [HttpGet("/popout/{*_}")]
+ public async Task<object> Popout() {
+ var patchedPath = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "patched", "popout.html");
+ if (!System.IO.File.Exists(patchedPath)) {
+ var path = Path.Combine(proxyConfiguration.TestClient.RevisionPath, "src", "popout.html");
+ var patchedContent = await patches.ApplyPatches("popout.html", await System.IO.File.ReadAllBytesAsync(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(patchedPath)!);
+ await System.IO.File.WriteAllBytesAsync(patchedPath, patchedContent);
+ }
+
+ return System.IO.File.OpenRead(patchedPath);
+ }
+}
diff --git a/ReferenceClientProxyImplementation/Controllers/StaticController.cs b/ReferenceClientProxyImplementation/Controllers/StaticController.cs
new file mode 100644
index 0000000..a2ecf2c
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Controllers/StaticController.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Mvc;
+using ReferenceClientProxyImplementation.Helpers;
+
+namespace ReferenceClientProxyImplementation.Controllers;
+
+[Controller]
+[Route("/")]
+public class StaticController : Controller {
+ [HttpGet("/resources/{*res:required}")]
+ public object Resource(string res) {
+ if (System.IO.File.Exists("./Resources/Static/" + res))
+ return Resolvers.ReturnFile("./Resources/Static/" + res);
+ return new RedirectResult("https://discord.gg/assets/" + res);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Helpers/HtmlUtils.cs b/ReferenceClientProxyImplementation/Helpers/HtmlUtils.cs
new file mode 100644
index 0000000..a8ee447
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Helpers/HtmlUtils.cs
@@ -0,0 +1,16 @@
+using AngleSharp.Html;
+using AngleSharp.Html.Parser;
+
+namespace ReferenceClientProxyImplementation.Helpers;
+
+public class HtmlUtils {
+ public static string CleanupHtml(string input) {
+ var parser = new HtmlParser();
+
+ var document = parser.ParseDocument(input);
+
+ var sw = new StringWriter();
+ document.ToHtml(sw, new PrettyMarkupFormatter());
+ return sw.ToString();
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Helpers/Resolvers.cs b/ReferenceClientProxyImplementation/Helpers/Resolvers.cs
new file mode 100644
index 0000000..813ffcb
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Helpers/Resolvers.cs
@@ -0,0 +1,80 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ReferenceClientProxyImplementation.Helpers;
+
+public static class Resolvers {
+ private static readonly string Navbar = File.Exists("Resources/Parts/Navbar.html") ? File.ReadAllText("Resources/Parts/Navbar.html") : "Navbar not found!";
+
+ public static object ReturnFile(string path) {
+ if (!File.Exists(path)) return new NotFoundObjectResult("File doesn't exist!");
+ var ext = path.Split(".").Last();
+ var contentType = ext switch {
+ //text types
+ "html" => "text/html",
+ "js" => "text/javascript",
+ "css" => "text/css",
+ "txt" => "text/plain",
+ "csv" => "text/csv",
+ //image types
+ "apng" => "image/apng",
+ "gif" => "image/gif",
+ "jpg" => "image/jpeg",
+ "png" => "image/png",
+ "svg" => "image/svg+xml",
+ "webp" => "image/webp",
+ "ico" => "image/x-icon",
+ _ => "application/octet-stream"
+ };
+ switch (ext) {
+ case "html":
+ return new ContentResult {
+ ContentType = contentType,
+ Content = File.ReadAllText(path)
+ };
+ case "js":
+ case "css":
+ case "txt":
+ case "csv":
+ case "svg":
+ return new ContentResult {
+ ContentType = contentType,
+ Content = File.ReadAllText(path)
+ };
+ case "png":
+ case "webp":
+ case "jpg":
+ case "gif":
+ case "apng":
+ case "7z":
+ case "gz":
+ case "tar":
+ case "rar":
+ case "zip":
+ case "webm":
+ case "woff":
+ case "jar":
+ case "mp3":
+ case "mp4":
+ return new PhysicalFileResult(Path.GetFullPath(path), contentType);
+ default:
+ Console.WriteLine($"Unsupported filetype: {ext} ({path})");
+ return new PhysicalFileResult(Path.GetFullPath(path), "application/octet-stream");
+ }
+ }
+
+ public static object ReturnFileWithVars(string path, Dictionary<string, object>? customVars = null) {
+ if (!File.Exists(path)) return new NotFoundObjectResult(Debugger.IsAttached ? $"File {path} doesn't exist!" : "File doesn't exist!");
+ var result = ReturnFile(path);
+ if (result.GetType() != typeof(ContentResult)) return result;
+ var contentResult = (ContentResult)result;
+ contentResult.Content = contentResult.Content?.Replace("$NAVBAR", Navbar);
+ if (customVars != null)
+ foreach (var (key, value) in customVars)
+ contentResult.Content = contentResult.Content?.Replace(key, value.ToString());
+
+ result = contentResult;
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs b/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs
new file mode 100644
index 0000000..09a00cc
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs
@@ -0,0 +1,6 @@
+namespace ReferenceClientProxyImplementation.Patches.Constants;
+
+public class Constant {
+ public required string SourceValue { get; set; }
+ public required string TargetValue { get; set; }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/IPatch.cs b/ReferenceClientProxyImplementation/Patches/IPatch.cs
new file mode 100644
index 0000000..92cab93
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/IPatch.cs
@@ -0,0 +1,8 @@
+namespace ReferenceClientProxyImplementation.Patches;
+
+public interface IPatch {
+ public int GetOrder();
+ public string GetName();
+ public bool Applies(string relativeName, byte[] content);
+ public Task<byte[]> Execute(string relativeName, byte[] content);
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs
new file mode 100644
index 0000000..649625f
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs
@@ -0,0 +1,91 @@
+using System.Diagnostics;
+using ArcaneLibs;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations;
+
+public partial class FormatJsFilePatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => -100;
+
+ public string GetName() => "Format JS file";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js") || relativeName.EndsWith(".css") || relativeName.EndsWith(".html");
+
+ public async Task<byte[]> Execute(string relativeName, byte[] content) {
+ var cachePath = Path.Combine(config.TestClient.RevisionPath, "formatted", relativeName);
+ if (File.Exists(cachePath)) {
+ Console.WriteLine($"Using cached formatted file for {relativeName}");
+ return await File.ReadAllBytesAsync(cachePath);
+ }
+
+ // temporary: add some newlines
+ var stringContent = System.Text.Encoding.UTF8.GetString(content);
+ // stringContent = stringContent.Replace("function(){", "function() {\n");
+ content = System.Text.Encoding.UTF8.GetBytes(stringContent);
+
+
+ Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
+ var tmpPath = $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{Random.Shared.NextInt64()}_{Path.GetFileName(relativeName)}";
+ await File.WriteAllBytesAsync(tmpPath, content);
+ var sw = Stopwatch.StartNew();
+ ProcessStartInfo psi;
+
+ // Biome doesn't support HTML and struggles with upstream emitting Sass directives
+ if (relativeName.EndsWith(".html") || relativeName.EndsWith(".css"))
+ psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ else
+ psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") {
+ // psi = new ProcessStartInfo(config.AssetCache.BiomePath, $"format --write --line-width 240 --files-max-size=100000000 {tmpPath}") {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(psi);
+ if (process == null) {
+ throw new InvalidOperationException("Failed to start the formatting process.");
+ }
+
+ // var stdout = await process.StandardOutput.ReadToEndAsync();
+ // var stderr = await process.StandardError.ReadToEndAsync();
+ // await process.WaitForExitAsync();
+
+ Dictionary<ulong, string> stdoutLines = new();
+ Dictionary<ulong, string> stderrLines = new();
+
+ process.OutputDataReceived += (sender, args) => {
+ if (args.Data != null) {
+ stdoutLines[(ulong)sw.ElapsedMilliseconds] = args.Data;
+ Console.Write("O");
+ }
+ };
+ process.ErrorDataReceived += (sender, args) => {
+ if (args.Data != null) {
+ stderrLines[(ulong)sw.ElapsedMilliseconds] = args.Data;
+ Console.Write("E");
+ }
+ };
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ await process.WaitForExitAsync();
+
+ Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+
+ if (process.ExitCode != 0) {
+ Console.WriteLine($"Failed to format {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+ Console.WriteLine("Standard Output:\n" + string.Join("\n", stdoutLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}")));
+ Console.WriteLine("Standard Error:\n" + string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}")));
+ throw new Exception($"Failed to exec({psi.FileName} {psi.Arguments}): {string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => kv.Value))}");
+ }
+
+ var result = await File.ReadAllBytesAsync(tmpPath);
+ File.Move(tmpPath, cachePath);
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs
new file mode 100644
index 0000000..877a31a
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs
@@ -0,0 +1,72 @@
+using System.Diagnostics;
+using ArcaneLibs;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations;
+
+public partial class FormatHtmlCssPatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => -100;
+
+ public string GetName() => "Format HTML/CSS file";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".css") || relativeName.EndsWith(".html");
+
+ public async Task<byte[]> Execute(string relativeName, byte[] content) {
+ var cachePath = Path.Combine(config.TestClient.RevisionPath, "formatted", relativeName);
+ if (File.Exists(cachePath)) {
+ Console.WriteLine($"Using cached formatted file for {relativeName}");
+ return await File.ReadAllBytesAsync(cachePath);
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
+ var tmpPath = $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{Random.Shared.NextInt64()}_{Path.GetFileName(relativeName)}";
+ await File.WriteAllBytesAsync(tmpPath, content);
+ var sw = Stopwatch.StartNew();
+ ProcessStartInfo psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(psi);
+ if (process == null) {
+ throw new InvalidOperationException("Failed to start the formatting process.");
+ }
+
+ // var stdout = await process.StandardOutput.ReadToEndAsync();
+ // var stderr = await process.StandardError.ReadToEndAsync();
+ // await process.WaitForExitAsync();
+
+ Dictionary<ulong, string> stdoutLines = new();
+ Dictionary<ulong, string> stderrLines = new();
+
+ while (!process.HasExited) {
+ while (!process.StandardOutput.EndOfStream) {
+ var line = await process.StandardOutput.ReadLineAsync();
+ if (line == null) continue;
+ stdoutLines[(ulong)sw.ElapsedMilliseconds] = line;
+ Console.Write("O");
+ }
+
+ while (!process.StandardError.EndOfStream) {
+ var line = await process.StandardError.ReadLineAsync();
+ if (line == null) continue;
+ stderrLines[(ulong)sw.ElapsedMilliseconds] = line;
+ Console.Write("E");
+ }
+ }
+
+ // Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+
+ if (process.ExitCode != 0) {
+ Console.WriteLine($"Failed to format {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+ Console.WriteLine("Standard Output:\n" + string.Join("\n", stdoutLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}")));
+ Console.WriteLine("Standard Error:\n" + string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}")));
+ throw new Exception($"Failed to format file {relativeName}: {string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => kv.Value))}");
+ }
+
+ var result = await File.ReadAllBytesAsync(tmpPath);
+ File.Move(tmpPath, cachePath);
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs
new file mode 100644
index 0000000..dc54850
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs
@@ -0,0 +1,22 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.HTMLPatches;
+
+public class GlobalEnvPatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Patch GLOBAL_ENV";
+ public bool Applies(string relativeName, byte[] content) => relativeName is "app.html" or "developers.html" or "popout.html";
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ foreach(var (key, value) in config.TestClient.GlobalEnv) {
+ stringContent = new Regex($"{key}: \".*?\"").Replace(stringContent, $"{key}: \"{value}\"");
+ }
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs
new file mode 100644
index 0000000..6c5f312
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs
@@ -0,0 +1,36 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.HTMLPatches;
+
+public partial class StripNoncesPatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => "Strip nonces/integrity from html";
+ public bool Applies(string relativeName, byte[] content) => relativeName is "app.html" or "developers.html" or "popout.html";
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+ stringContent = HtmlScriptIntegrityRegex().Replace(
+ HtmlScriptNonceRegex().Replace(
+ JsElementNonceRegex().Replace(
+ stringContent,
+ ""
+ ),
+ ""
+ ),
+ ""
+ );
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex("\\snonce=\"[a-zA-Z0-9+/=]+\"")]
+ private static partial Regex HtmlScriptNonceRegex();
+
+ [GeneratedRegex("\\w.nonce='[a-zA-Z0-9+/=]+';")]
+ private static partial Regex JsElementNonceRegex();
+
+ [GeneratedRegex(@"\sintegrity=""[a-zA-Z0-9+/=\-\s]+""")]
+ private static partial Regex HtmlScriptIntegrityRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs
new file mode 100644
index 0000000..6e7f7a2
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs
@@ -0,0 +1,32 @@
+using System.Text;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public class ApiProtocolPatch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => "API: Use GLOBAL_ENV.API_PROTOCOL instead of hardcoded https:";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ // TODO: regex
+ stringContent = stringContent
+ .Replace(
+ "return \"https:\" + window.GLOBAL_ENV.API_ENDPOINT + (e ? \"/v\".concat(window.GLOBAL_ENV.API_VERSION) : \"\");",
+ "return window.GLOBAL_ENV.API_PROTOCOL + window.GLOBAL_ENV.API_ENDPOINT + (e ? \"/v\".concat(window.GLOBAL_ENV.API_VERSION) : \"\");"
+ )
+ .Replace(
+ "api_endpoint: \"\".concat(\"https:\").concat(window.GLOBAL_ENV.API_ENDPOINT)",
+ "api_endpoint: window.GLOBAL_ENV.API_PROTOCOL.concat(window.GLOBAL_ENV.API_ENDPOINT)"
+ )
+ .Replace(
+ "f = null != d ? \"https://\".concat(d) : location.protocol + window.GLOBAL_ENV.API_ENDPOINT,",
+ "f = null != d ? window.GLOBAL_ENV.API_PROTOCOL.concat(d) : location.protocol + window.GLOBAL_ENV.API_ENDPOINT,"
+ )
+ ;
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs
new file mode 100644
index 0000000..152a3f3
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs
@@ -0,0 +1,21 @@
+using System.Text;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public class BooleanPatch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => "Use real booleans in JS files";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = stringContent
+ .Replace("return!", "return !")
+ .Replace("!0", "true")
+ .Replace("!1", "false");
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs
new file mode 100644
index 0000000..fe73c8e
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs
@@ -0,0 +1,21 @@
+// using System.Text;
+// using System.Text.RegularExpressions;
+//
+// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+//
+// public partial class BooleanPropagationPatch : IPatch {
+// public int GetOrder() => 3;
+//
+// public string GetName() => "Patch pointless boolean comparisons in JS";
+// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+//
+// public async Task<byte[]> Execute(string relativePath, byte[] content) {
+// var stringContent = Encoding.UTF8.GetString(content);
+//
+// stringContent = stringContent
+// .Replace(" && true", "").Replace(" || false", "").Replace("false || ", "")
+// ;
+//
+// return Encoding.UTF8.GetBytes(stringContent);
+// }
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs
new file mode 100644
index 0000000..c44bf95
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs
@@ -0,0 +1,26 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class DisableSciencePatch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => @"JS(web): Disable /science calls";
+ public bool Applies(string relativeName, byte[] content) => relativeName.StartsWith("assets/web.") && relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ var match = HandleTrackDefinitionRegex().Match(stringContent);
+ stringContent = stringContent.Insert(match.Index + match.Length, @"
+ return (new Promise(() => { }), false); // ReferenceClientProxyImplementation: Disable /science calls
+ ");
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@".\.handleTrack = function \(.\) \{", RegexOptions.Compiled)]
+ private static partial Regex HandleTrackDefinitionRegex();
+
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs
new file mode 100644
index 0000000..9eed00c
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs
@@ -0,0 +1,26 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class ExpandUnicodeEscapesPatch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => @"JS: expand \x?? to \u00??";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = XToURegex().Replace(
+ stringContent,
+ m => $"\\u00{m.Groups[1].Value}"
+ );
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@"\\x([0-9A-Fa-f]{2})", RegexOptions.Compiled)]
+ private static partial Regex XToURegex();
+
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs
new file mode 100644
index 0000000..b741f56
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs
@@ -0,0 +1,33 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class IsStaffPatch : IPatch {
+ public int GetOrder() => 2;
+
+ public string GetName() => "Patch isStaff/isStaffPersonal in JS";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string relativePath, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = IsNullableStaffRegex().Replace(
+ stringContent,
+ m => $"{m.Groups[1].Value}!!{m.Groups[2].Value}"
+ );
+
+ stringContent = IsStaffRegex().Replace(
+ stringContent,
+ m => $"{m.Groups[1].Value}true"
+ );
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@"(\W)(\w|this|\w\.user)\.isStaff(Personal)?\(\)", RegexOptions.Compiled)]
+ private static partial Regex IsStaffRegex();
+
+ [GeneratedRegex(@"(\W)(\w|this|\w\.user)\?\.isStaff(Personal)?\(\)", RegexOptions.Compiled)]
+ private static partial Regex IsNullableStaffRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs
new file mode 100644
index 0000000..b5e7d77
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs
@@ -0,0 +1,79 @@
+using System.Diagnostics;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class JsonParseMultilinePatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Patch null-coalescing expressions in JS";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string relativePath, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ var matches = JsonParseRegex().Matches(stringContent);
+ Console.WriteLine($"Found {matches.Count} JSON.parse calls in {relativePath}");
+
+
+
+ await Parallel.ForEachAsync(matches, async (match, ct) => {
+ string formattedJson = match.Groups[1].Value;
+ try {
+ var jsonElement = JsonSerializer.Deserialize<JsonElement>(formattedJson.Replace("\\", "\\\\") + "waef");
+ formattedJson = JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions { WriteIndented = true });
+ } catch (JsonException je) {
+ // Console.WriteLine($"STJ: Failed to parse JSON in {relativePath} at index {match.Index}: {je.Message}"); // intentinally broken
+ try {
+ formattedJson = await formatJsonWithNodejs(relativePath, match, ct);
+ } catch (Exception e) {
+ Console.WriteLine($"Node.js: Failed to parse JSON in {relativePath} at index {match.Index}: {e.Message}");
+ return;
+ }
+ }
+
+ lock (matches) stringContent = stringContent.Replace(match.Value, $"JSON.parse(`{formattedJson.Replace("\\", "\\\\")}`);");
+ });
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ private async Task<string> formatJsonWithNodejs(string relativePath, Match match, CancellationToken ct) {
+ // Extract the JSON string from the match
+ var id = "dcp_" + Path.GetFileName(relativePath).Replace('.', '_') + "_" + match.Index;
+ await File.WriteAllTextAsync($"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js", $"console.log(JSON.stringify(JSON.parse(`{match.Groups[1].Value.Replace("`", "\\\\\\`")}`), null, 2))");
+ var sw = Stopwatch.StartNew();
+
+ var psi = new ProcessStartInfo(config.AssetCache.NodePath, $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true };
+
+ using var process = Process.Start(psi);
+ if (process == null) {
+ throw new InvalidOperationException("Failed to start the formatting process.");
+ }
+
+ var stdout = await process.StandardOutput.ReadToEndAsync();
+ var stderr = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+ // Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+
+ if (process.ExitCode != 0) {
+ Console.WriteLine($"Failed to run {Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+ Console.WriteLine("Standard Output: " + stdout);
+ Console.WriteLine("Standard Error: " + stderr);
+ throw new Exception($"Failed to execute {Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js: {stderr}");
+ }
+
+ var formattedJson = stdout.Trim();
+ Console.WriteLine($"Parsed JSON({id}) in {sw.ElapsedMilliseconds}ms: {formattedJson.Length} bytes");
+ // stringContent = stringContent.Replace(match.Value, $"JSON.parse(`{formattedJson.Replace("\\n", "\\\\n")}`);");
+ await File.WriteAllTextAsync($"{config.TestClient.RevisionPath}/patched/assets/{Path.GetFileName(relativePath)}-{match.Index}.json", formattedJson);
+ return formattedJson;
+ }
+
+ [GeneratedRegex(@"JSON\.parse\(\n\s*'(.*?)',?\s*\);", RegexOptions.Compiled | RegexOptions.Multiline)]
+ private static partial Regex JsonParseRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs
new file mode 100644
index 0000000..a94e312
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs
@@ -0,0 +1,16 @@
+using System.Text;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public class KnownConstantsPatch : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Use named constants";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs
new file mode 100644
index 0000000..e7f78a0
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs
@@ -0,0 +1,32 @@
+// using System.Text;
+// using System.Text.RegularExpressions;
+//
+// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+//
+// public partial class LegacyJsPathces : IPatch {
+// public int GetOrder() => 1;
+//
+// public string GetName() => "Patch deprecated JS constructs";
+// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+//
+// public async Task<byte[]> Execute(string relativePath, byte[] content) {
+// var stringContent = Encoding.UTF8.GetString(content);
+//
+// while(MozInputSourceRegex().IsMatch(stringContent)) {
+// var match = MozInputSourceRegex().Match(stringContent);
+// var replacement = match.Groups[1].Value switch {
+// "0" => "",
+// "1" => "mouse",
+// "2" => "pen",
+// "3" => "pen",
+// "4" => "touch",
+// _ => throw new InvalidOperationException("Unreachable")
+// };
+// }
+//
+// return Encoding.UTF8.GetBytes(stringContent);
+// }
+//
+// [GeneratedRegex(@"([0-6]) === (\w).mozInputSource", RegexOptions.Compiled)]
+// private static partial Regex MozInputSourceRegex();
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs
new file mode 100644
index 0000000..2005c4c
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs
@@ -0,0 +1,34 @@
+// using System.Text;
+// using System.Text.RegularExpressions;
+//
+// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+//
+// public partial class LogErrorContextPatch : IPatch {
+// public int GetOrder() => 2;
+//
+// public string GetName() => "Patch assertions to log more context";
+// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+//
+// public async Task<byte[]> Execute(string relativePath, byte[] content) {
+// var stringContent = Encoding.UTF8.GetString(content);
+//
+// stringContent = NotNullAssertionRegex().Replace(
+// stringContent,
+// m => {
+// var methodName = m.Groups[1].Value;
+// var objectName = m.Groups[2].Value;
+// var message = m.Groups[3].Value;
+// Console.WriteLine($"Patching not-null assertion in {relativePath}: {methodName} - {message}");
+//
+// return $@"{methodName}()(null != {objectName}, ""{message} - Context: "" + JSON.stringify({objectName}))";
+// }
+// );
+//
+// return Encoding.UTF8.GetBytes(stringContent);
+// }
+//
+// // null assertion: u()(null != o, "PrivateChannel.renderAvatar: Invalid prop configuration - no user or channel");
+// // capture: method name, object name, message
+// [GeneratedRegex(@"([a-zA-Z0-9_]+)\(\)\(\s*null != ([a-zA-Z0-9_]+),\s*""([^""]+)""\s*\)", RegexOptions.Compiled)]
+// private static partial Regex NotNullAssertionRegex();
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs
new file mode 100644
index 0000000..98156c8
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs
@@ -0,0 +1,32 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class NullCoalescingPatch : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Patch null-coalescing expressions in JS";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string relativePath, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = NullCoalescingRegex().Replace(
+ stringContent,
+ m => $"{m.Groups[1].Value}?.{m.Groups[2].Value}"
+ );
+ // stringContent = ParenNullCheckRegex().Replace(
+ // stringContent,
+ // m => $"{m.Groups[1].Value} == null"
+ // );
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@"null == ([a-zA-Z0-9_]+?) \? undefined : \1\.([a-zA-Z0-9_]+?)", RegexOptions.Compiled)]
+ private static partial Regex NullCoalescingRegex();
+
+ [GeneratedRegex(@"\(([^()]+?)\) == null", RegexOptions.Compiled)]
+ private static partial Regex ParenNullCheckRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs
new file mode 100644
index 0000000..bab0756
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs
@@ -0,0 +1,56 @@
+// using System.Text;
+// using System.Text.RegularExpressions;
+// using ReferenceClientProxyImplementation.Services;
+//
+// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+//
+// public partial class PrefetchAssetsPatch(IServiceProvider sp) : IPatch {
+// public int GetOrder() => 1000000;
+//
+// public string GetName() => "Prefetch assets";
+// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+//
+// private static SemaphoreSlim ss = new(2, 2);
+// private static HashSet<string> alreadyKnownAssets = new();
+//
+// public async Task<byte[]> Execute(string relativePath, byte[] content) {
+// // Can't inject service due to loop
+// var stringContent = Encoding.UTF8.GetString(content);
+// var matches = PrefetchAssetsRegex().Matches(stringContent);
+//
+// Console.WriteLine($"Found {matches.Count} prefetch assets in {relativePath}");
+// if (matches.Count == 0) {
+// return content; // No matches found, return original content
+// }
+//
+// var clientStore = sp.GetRequiredService<ClientStoreService>();
+//
+// var newAssets = matches
+// .Select(x => x.Groups[1].Value)
+// .Distinct()
+// .Where(x => !clientStore.HasRawAsset(x) && alreadyKnownAssets.Add(x));
+//
+// var tasks = newAssets
+// .Select(async match => {
+// await ss.WaitAsync();
+// Console.WriteLine($"Discovered prefetch asset in {relativePath}: {match}");
+// // var patches = sp.GetRequiredService<PatchSet>();
+// var res = await clientStore.GetOrDownloadRawAsset(match);
+// await res.DisposeAsync();
+// ss.Release();
+// Console.WriteLine($"Prefetched asset {match} in {relativePath}");
+// }).ToList();
+//
+// if (tasks.Count == 0) {
+// Console.WriteLine($"No new prefetch assets found in {relativePath}, returning original content.");
+// return content; // No new assets to prefetch, return original content
+// }
+//
+// await Task.WhenAny(tasks);
+//
+// return content;
+// }
+//
+// [GeneratedRegex(@".\.exports = ""((?:[a-z\d/\.]*?)\.\w{2,4})""", RegexOptions.Compiled)]
+// private static partial Regex PrefetchAssetsRegex();
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs
new file mode 100644
index 0000000..9d819c7
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs
@@ -0,0 +1,27 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class Void0Patch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => "Use literal undefined instead of void 0";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = stringContent
+ .Replace("void 0", "undefined");
+ stringContent = VoidFunctionRegex().Replace(
+ stringContent,
+ m => $"{m.Groups[1].Value}("
+ );
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@"\(0, ([a-zA-Z0-9_.$]+?)\)\(", RegexOptions.Compiled)]
+ private static partial Regex VoidFunctionRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs
new file mode 100644
index 0000000..277bf8a
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs
@@ -0,0 +1,19 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class WhileTruePatch : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Patch while(true) expressions in JS";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string relativePath, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = stringContent.Replace("for (;;)", "while (true)");
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs b/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs
new file mode 100644
index 0000000..c3dba59
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs
@@ -0,0 +1,26 @@
+namespace ReferenceClientProxyImplementation.Patches.Implementations;
+
+public class PatchSet(IServiceProvider sp) {
+ public List<IPatch> Patches { get; } = sp.GetServices<IPatch>().OrderBy(x => x.GetOrder()).ToList();
+
+ public async Task<byte[]> ApplyPatches(string relativeName, byte[] content) {
+ var i = 0;
+ var patches = Patches
+ .Where(p => p.Applies(relativeName, content))
+ .OrderBy(p => p.GetOrder())
+ .ToList();
+ foreach (var patch in patches) {
+ if (patch.Applies(relativeName, content)) {
+ var defaultColor = Console.ForegroundColor;
+ Console.ForegroundColor = ConsoleColor.DarkBlue;
+ Console.Write("==> ");
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.WriteLine($"Running task {++i}/{patches.Count}: {patch.GetName()} (Type<{patch.GetType().Name}>)");
+ Console.ForegroundColor = defaultColor;
+ content = await patch.Execute(relativeName, content);
+ }
+ }
+
+ return content;
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Program.cs b/ReferenceClientProxyImplementation/Program.cs
new file mode 100644
index 0000000..c35248b
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Program.cs
@@ -0,0 +1,107 @@
+using ArcaneLibs;
+using Microsoft.AspNetCore.HttpLogging;
+using Microsoft.Extensions.Hosting.Systemd;
+using ReferenceClientProxyImplementation.Configuration;
+using ReferenceClientProxyImplementation.Patches;
+using ReferenceClientProxyImplementation.Patches.Implementations;
+using ReferenceClientProxyImplementation.Services;
+using ReferenceClientProxyImplementation.Tasks;
+
+// using Spacebar.API.Tasks.Startup;
+
+if (!Directory.Exists("cache_formatted")) Directory.CreateDirectory("cache_formatted");
+if (!Directory.Exists("cache")) Directory.CreateDirectory("cache");
+/*foreach (var file in Directory.GetFiles("cache").Where(x => x.EndsWith(".js")))
+{
+ //JsFormatter.FormatJsFile(File.OpenRead(file), File.OpenWrite(file.Replace("cache", "cache_formatted")));
+}*/
+
+/*var processes = Directory.GetFiles("cache").Where(x => x.EndsWith(".js")).Select(file => JsFormatter.SafeFormat(file, file.Replace("cache", "cache_formatted"))).ToList();
+while (processes.Any(x => !x.HasExited))
+{
+ Thread.Sleep(100);
+}*/
+
+//Environment.Exit(0);
+// Tasks.RunStartup();
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Host.ConfigureHostOptions(host => {
+ host.ServicesStartConcurrently = true;
+ host.ServicesStopConcurrently = true;
+ host.ShutdownTimeout = TimeSpan.FromSeconds(5);
+});
+
+// builder.Services.AddHostedService<TemporaryTestJob>();
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+builder.Services.AddHttpLogging(o => { o.LoggingFields = HttpLoggingFields.All; });
+builder.Services.AddLogging(o => {
+ if (SystemdHelpers.IsSystemdService())
+ o.AddSystemdConsole();
+ else o.AddConsole();
+});
+
+builder.Services.AddSingleton<ProxyConfiguration>();
+// builder.Services.AddSingleton<BuildDownloadService>();
+
+foreach (var taskType in ClassCollector<ITask>.ResolveFromAllAccessibleAssemblies())
+{
+ builder.Services.AddSingleton(typeof(ITask), taskType);
+}
+builder.Services.AddHostedService<Tasks>();
+
+foreach (var taskType in ClassCollector<IPatch>.ResolveFromAllAccessibleAssemblies())
+{
+ builder.Services.AddSingleton(typeof(IPatch), taskType);
+}
+builder.Services.AddSingleton<PatchSet>();
+
+builder.Services.AddSingleton<ClientStoreService>();
+
+var app = builder.Build();
+
+//
+if (app.Environment.IsDevelopment()) {
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpLogging();
+app.UseRouting();
+// app.UseSentryTracing();
+
+// app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
+
+app.Use((context, next) => {
+ context.Response.Headers["Content-Type"] += "; charset=utf-8";
+ context.Response.Headers["Access-Control-Allow-Origin"] = "*";
+ return next.Invoke();
+});
+app.UseCors("*");
+
+app.MapControllers();
+app.UseDeveloperExceptionPage();
+
+//
+app.UseEndpoints(endpoints => { endpoints.MapControllerRoute("default", "{controller=FrontendController}/{action=Index}/{id?}"); });
+
+Console.WriteLine("Starting web server!");
+if (args.Contains("--exit-on-modified")) {
+ Console.WriteLine("[WARN] --exit-on-modified enabled, exiting on source file change!");
+ new FileSystemWatcher {
+ Path = Environment.CurrentDirectory,
+ Filter = "*.cs",
+ NotifyFilter = NotifyFilters.LastWrite,
+ EnableRaisingEvents = true
+ }.Changed += async (sender, args) => {
+ Console.WriteLine("Source modified. Exiting...");
+ await app.StopAsync();
+ Environment.Exit(0);
+ };
+}
+
+app.Run();
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Properties/launchSettings.json b/ReferenceClientProxyImplementation/Properties/launchSettings.json
new file mode 100644
index 0000000..3cac55a
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Properties/launchSettings.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:60588",
+ "sslPort": 44301
+ }
+ },
+ "profiles": {
+ "Spacebar.API": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:2000;https://localhost:2001;http://localhost:2002",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "CONFIG_PATH": "../Config.json"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj b/ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj
new file mode 100644
index 0000000..14bdb55
--- /dev/null
+++ b/ReferenceClientProxyImplementation/ReferenceClientProxyImplementation.csproj
@@ -0,0 +1,34 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net10.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <EnableDefaultContentItems>false</EnableDefaultContentItems>
+ <LangVersion>preview</LangVersion>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)'=='Release'">
+ <DebugSymbols>False</DebugSymbols>
+ <DebugType>None</DebugType>
+ <Optimize>True</Optimize>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AngleSharp" Version="1.3.0"/>
+ <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20250630-114950"/>
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6"/>
+ <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.6"/>
+ <PackageReference Include="Sentry.AspNetCore" Version="5.11.2"/>
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Remove="cache\**"/>
+ <Compile Remove="cache_formatted\**"/>
+ <Compile Remove="server-cs-d9282bd218a35e2399d27e98cdd6f7a0a8552bb7\**"/>
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Resources\**" CopyToOutputDirectory="Always"/>
+ </ItemGroup>
+
+</Project>
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/checkLocale.js b/ReferenceClientProxyImplementation/Resources/Assets/checkLocale.js
new file mode 100644
index 0000000..2f46120
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/checkLocale.js
@@ -0,0 +1,47 @@
+const localStorage = window.localStorage;
+// TODO: remote auth
+// window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, "");
+localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT);
+localStorage.setItem(
+ "DeveloperOptionsStore",
+ `{"trace":false,"canary":false,"logGatewayEvents":true,"logOverlayEvents":true,"logAnalyticsEvents":true,"sourceMapsEnabled":false,"axeEnabled":false}`
+);
+
+const supportedLocales = [
+ "bg",
+ "cs",
+ "da",
+ "de",
+ "el",
+ "en-GB",
+ "es-ES",
+ "fi",
+ "fr",
+ "hi",
+ "hr",
+ "hu",
+ "it",
+ "ja",
+ "ko",
+ "lt",
+ "nl",
+ "no",
+ "pl",
+ "pt-BR",
+ "ro",
+ "ru",
+ "sv-SE",
+ "th",
+ "tr",
+ "uk",
+ "vi",
+ "zh-CN",
+ "zh-TW"
+];
+
+const settings = JSON.parse(localStorage.getItem("UserSettingsStore"));
+if (settings && !supportedLocales.includes(settings.locale)) {
+ // fix client locale wrong and client not loading at all
+ settings.locale = "en-US";
+ localStorage.setItem("UserSettingsStore", JSON.stringify(settings));
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/dff87c953f43b561d71fbcfe8a93a79a.png b/ReferenceClientProxyImplementation/Resources/Assets/dff87c953f43b561d71fbcfe8a93a79a.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/dff87c953f43b561d71fbcfe8a93a79a.png
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/endpoints.json b/ReferenceClientProxyImplementation/Resources/Assets/endpoints.json
new file mode 100644
index 0000000..d5478e7
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/endpoints.json
@@ -0,0 +1,115 @@
+{
+ "USER_CHANNELS": "/users/@me/channels",
+ "USER_ACTIVITY_STATISTICS": "/users/@me/activities/statistics/applications",
+ "ACTIVITIES": "/activities",
+ "LOBBIES": "/lobbies",
+ "LOBBY_SEARCH": "/lobbies/search",
+ "NETWORKING_TOKEN": "/networking/token",
+ "USER_GAMES_NOTIFICATIONS": "/users/@me/settings/game-notifications",
+ "USER_GAMES_NOTIFICATIONS_OVERRIDES": "/users/@me/settings/game-notifications/overrides",
+ "UNVERIFIED_APPLICATIONS": "/unverified-applications",
+ "UNVERIFIED_APPLICATIONS_ICONS": "/unverified-applications/icons",
+ "BULK_ACK": "/read-states/ack-bulk",
+ "GUILDS": "/guilds",
+ "CHANNELS": "/channels",
+ "TUTORIAL_INDICATORS": "/tutorial/indicators",
+ "TUTORIAL_INDICATORS_SUPPRESS": "/tutorial/indicators/suppress",
+ "USERS": "/users",
+ "ME": "/users/@me",
+ "DELETE_ACCOUNT": "/users/@me/delete",
+ "DISABLE_ACCOUNT": "/users/@me/disable",
+ "DEVICES": "/users/@me/devices",
+ "SETTINGS": "/users/@me/settings",
+ "SETTINGS_CONSENT": "/users/@me/consent",
+ "PHONE": "/users/@me/phone",
+ "VERIFY_PHONE": "/users/@me/phone/verify",
+ "VERIFY_PHONE_NEW": "/phone-verifications/verify",
+ "RESEND_PHONE": "/phone-verifications/resend",
+ "CONNECTIONS": "/users/@me/connections",
+ "CONNECTION_SYNC_CONTACTS": "/users/@me/connections/contacts/@me/external-friend-list-entries",
+ "NOTES": "/users/@me/notes",
+ "MENTIONS": "/users/@me/mentions",
+ "CAPTCHA": "/users/@me/captcha/verify",
+ "EXPERIMENTS": "/experiments",
+ "LOGIN": "/auth/login",
+ "LOGIN_MFA": "/auth/mfa/totp",
+ "LOGIN_SMS": "/auth/mfa/sms",
+ "LOGIN_SMS_SEND": "/auth/mfa/sms/send",
+ "REMOTE_AUTH_INITIALIZE": "/users/@me/remote-auth",
+ "REMOTE_AUTH_CANCEL": "/users/@me/remote-auth/cancel",
+ "REMOTE_AUTH_FINISH": "/users/@me/remote-auth/finish",
+ "LOGOUT": "/auth/logout",
+ "REGISTER": "/auth/register",
+ "REGISTER_PHONE": "/auth/register/phone",
+ "TRACK": "/science",
+ "SSO": "/sso",
+ "VERIFY": "/auth/verify",
+ "AUTHORIZE_IP": "/auth/authorize-ip",
+ "VERIFY_RESEND": "/auth/verify/resend",
+ "FORGOT_PASSWORD": "/auth/forgot",
+ "RESET_PASSWORD": "/auth/reset",
+ "ICE": "/voice/ice",
+ "REPORT": "/report",
+ "REPORT_V2": "/reports",
+ "REPORT_OPTIONS": "/report/options",
+ "INTEGRATIONS": "/integrations",
+ "GATEWAY": "/gateway",
+ "APPLICATIONS_DETECTABLE": "/applications/detectable",
+ "OAUTH2_AUTHORIZE": "/oauth2/authorize",
+ "OAUTH2_AUTHORIZE_WEBHOOK_CHANNELS": "/oauth2/authorize/webhook-channels",
+ "OAUTH2_CURRENT_AUTH": "/oauth2/@me",
+ "OAUTH2_TOKENS": "/oauth2/tokens",
+ "OAUTH2_WHITELIST_ACCEPT": "/oauth2/whitelist/accept",
+ "MFA_TOTP_ENABLE": "/users/@me/mfa/totp/enable",
+ "MFA_TOTP_DISABLE": "/users/@me/mfa/totp/disable",
+ "MFA_SMS_ENABLE": "/users/@me/mfa/sms/enable",
+ "MFA_SMS_DISABLE": "/users/@me/mfa/sms/disable",
+ "MFA_CODES": "/users/@me/mfa/codes",
+ "DISABLE_EMAIL_NOTIFICATIONS": "/users/disable-email-notifications",
+ "GUILD_PREMIUM_SUBSCRIPTION_COOLDOWN": "/users/@me/guilds/premium/subscriptions/cooldown",
+ "USER_GUILD_PREMIUM_SUBSCRIPTIONS": "/users/@me/guilds/premium/subscriptions",
+ "USER_PREMIUM_GUILD_SUBSCRIPTION_SLOTS": "/users/@me/guilds/premium/subscription-slots",
+ "BILLING_STRIPE_SETUP_INTENT_SECRET": "/users/@me/billing/stripe/setup-intents",
+ "BILLING_PAYMENT_SOURCES": "/users/@me/billing/payment-sources",
+ "BILLING_PAYMENTS": "/users/@me/billing/payments",
+ "BILLING_BRAINTREE_POPUP_BRIDGE": "/billing/braintree/popup-bridge",
+ "BILLING_BRAINTREE_POPUP_BRIDGE_CALLBACK": "/billing/braintree/popup-bridge/callback",
+ "BILLING_SUBSCRIPTIONS": "/users/@me/billing/subscriptions",
+ "BILLING_APPLY_APPLE_RECEIPT": "/billing/apple/apply-receipt",
+ "BILLING_INVOICE_PREVIEW": "/users/@me/billing/invoices/preview",
+ "USER_AGREEMENTS": "/users/@me/agreements",
+ "HANDOFF": "/auth/handoff",
+ "HANDOFF_EXCHANGE": "/auth/handoff/exchange",
+ "LIBRARY": "/users/@me/library",
+ "AUTH_CONSENT_REQUIRED": "/auth/consent-required",
+ "USER_HARVEST": "/users/@me/harvest",
+ "APPLICATION_BRANCHES": "/branches",
+ "APPLICATIONS_PUBLIC": "/applications/public",
+ "APPLICATIONS_TRENDING": "/applications/trending/global",
+ "STORE_PUBLISHED_LISTINGS_APPLICATIONS": "/store/published-listings/applications",
+ "STORE_PUBLISHED_LISTINGS_SKUS": "/store/published-listings/skus",
+ "ENTITLEMENTS_GIFTABLE": "/users/@me/entitlements/gifts",
+ "PROMOTIONS": "/promotions",
+ "PROMOTION_ACK": "/promotions/ack",
+ "HYPESQUAD_ONLINE": "/hypesquad/online",
+ "GIFS_SEARCH": "/gifs/search",
+ "GIFS_TRENDING": "/gifs/trending",
+ "GIFS_TRENDING_GIFS": "/gifs/trending-gifs",
+ "GIFS_SELECT": "/gifs/select",
+ "GIFS_SUGGEST": "/gifs/suggest",
+ "GIFS_TRENDING_SEARCH": "/gifs/trending-search",
+ "USER_GIFT_CODE_CREATE": "/users/@me/entitlements/gift-codes",
+ "USER_GIFT_CODES": "/users/@me/entitlements/gift-codes",
+ "GUILD_DISCOVERY": "/discoverable-guilds",
+ "GUILD_DISCOVERY_CATEGORIES": "/discovery/categories",
+ "GUILD_DISCOVERY_VALID_TERM": "/discovery/valid-term",
+ "USER_AFFINITIES": "/users/@me/affinities/users",
+ "GUILD_AFFINITIES": "/users/@me/affinities/guilds",
+ "XBOX_GAME_PASS_PROMOTION": "/promotions/xbox-game-pass",
+ "XBOX_GAME_PASS_PROMOTION_REDEEM": "/promotions/xbox-game-pass/redeem",
+ "FUNIMATION_PROMOTION": "/promotions/funimation",
+ "PARTNERS_CONNECTIONS": "/partners/connections",
+ "PARTNERS_APPLY": "/partners/apply",
+ "USER_STICKER_PACKS": "/users/@me/sticker-packs",
+ "INTERACTIONS": "/interactions"
+}
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/features.json b/ReferenceClientProxyImplementation/Resources/Assets/features.json
new file mode 100644
index 0000000..1e1ebb1
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/features.json
@@ -0,0 +1,26 @@
+[
+ "ANIMATED_ICON",
+ "BANNER",
+ "COMMERCE",
+ "COMMUNITY",
+ "DISCOVERABLE",
+ "DISCOVERABLE_DISABLED",
+ "ENABLED_DISCOVERABLE_BEFORE",
+ "HUB",
+ "INVITE_SPLASH",
+ "MONETIZATION_ENABLED",
+ "MORE_EMOJI",
+ "MORE_STICKERS",
+ "NEWS",
+ "PARTNERED",
+ "PREVIEW_ENABLED",
+ "PRIVATE_THREADS",
+ "SEVEN_DAY_THREAD_ARCHIVE",
+ "THREE_DAY_THREAD_ARCHIVE",
+ "THREADS_ENABLED",
+ "TICKETED_EVENTS_ENABLED",
+ "VANITY_URL",
+ "VERIFIED",
+ "VIP_REGIONS",
+ "WELCOME_SCREEN_ENABLED"
+]
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/fosscord-login.css b/ReferenceClientProxyImplementation/Resources/Assets/fosscord-login.css
new file mode 100644
index 0000000..37bb111
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/fosscord-login.css
@@ -0,0 +1,71 @@
+/* replace tos acceptance popup */
+#app-mount > div:nth-child(7) > div > div > div.tooltipContent-bqVLWK {
+ visibility: hidden;
+}
+
+#app-mount > div:nth-child(7) > div > div > div.tooltipContent-bqVLWK::after {
+ visibility: visible;
+ display: block;
+ content: "You need to agree to this instance's rules to continue";
+ margin-top: -32px;
+}
+
+/* replace login header */
+#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.mainLoginContainer-1ddwnR > h3 {
+ visibility: hidden;
+}
+
+h3.title-jXR8lp.marginBottom8-AtZOdT.base-1x0h_U.size24-RIRrxO::after {
+ margin-top: -32px;
+ content: "Welcome to Spacebar!";
+ visibility: visible;
+ display: block;
+}
+
+/* Logo in top left when bg removed */
+#app-mount > div.app-1q1i1E > div > a {
+ /* replace me: original dimensions: 130x36 */
+ background: url(https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Spacebar-Wordmark-Gradient.svg);
+ width: 130px;
+ height: 23px;
+ background-size: contain;
+}
+
+/* replace TOS text */
+
+#app-mount
+> div.app-1q1i1E
+> div
+> div
+> div
+> form
+> div
+> div
+> div.flex-1xMQg5.flex-1O1GKY.horizontal-1ae9ci.horizontal-2EEEnY.flex-1O1GKY.directionRow-3v3tfG.justifyStart-2NDFzi.alignCenter-1dQNNs.noWrap-3jynv6.marginTop20-3TxNs6
+> label
+> div.label-cywgfr.labelClickable-11AuB8.labelForward-1wfipV
+> * {
+ visibility: hidden;
+}
+
+#app-mount
+> div.app-1q1i1E
+> div
+> div
+> div
+> form
+> div
+> div
+> div.flex-1xMQg5.flex-1O1GKY.horizontal-1ae9ci.horizontal-2EEEnY.flex-1O1GKY.directionRow-3v3tfG.justifyStart-2NDFzi.alignCenter-1dQNNs.noWrap-3jynv6.marginTop20-3TxNs6
+> label
+> div.label-cywgfr.labelClickable-11AuB8.labelForward-1wfipV::after {
+ visibility: visible;
+ content: "I have read and agree with the rules set by this instance.";
+ display: block;
+ margin-top: -16px;
+}
+
+/* shrink login box to same size as register */
+.authBoxExpanded-2jqaBe {
+ width: 480px !important;
+}
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/fosscord.css b/ReferenceClientProxyImplementation/Resources/Assets/fosscord.css
new file mode 100644
index 0000000..283c29f
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/fosscord.css
@@ -0,0 +1,44 @@
+/* loading spinner */
+#app-mount > div.app-1q1i1E > div.container-16j22k.fixClipping-3qAKRb > div.content-1-zrf2 > video {
+ filter: opacity(1);
+ background: url("http://www.clipartbest.com/cliparts/7ca/6Rr/7ca6RrLAi.gif");
+ background-size: contain;
+ /* width: 64px;
+ height: 64px; */
+ padding-bottom: 64px;
+ background-repeat: no-repeat;
+}
+
+/* home button icon */
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div {
+ background-image: url(https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Spacebar-Icon-Rounded-Subtract.svg);
+ background-size: contain;
+ border-radius: 50%;
+}
+
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div, #app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div:hover {
+ background-color: white;
+}
+
+/* Login QR */
+#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.transitionGroup-aR7y1d.qrLogin-1AOZMt,
+#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.verticalSeparator-3huAjp,
+ /* Remove login bg */
+#app-mount > div.app-1q1i1E > div > svg,
+ /* Download bar */
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.notice-3bPHh-.colorDefault-22HBa0,
+ /* Connection problem links */
+#app-mount > div.app-1q1i1E > div.container-16j22k.fixClipping-3qAKRb > div.problems-3mgf6w.slideIn-sCvzGz > div:nth-child(2),
+ /* Downloads button */
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div:nth-child(7) > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div,
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div:nth-child(6) > div,
+ /* help button */
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.content-98HsJk > div.chat-3bRxxu > section > div.toolbar-1t6TWx > a,
+ /* download button start of guild */
+#chat-messages-899316648933185083 > div > div > div:nth-child(5),
+ /* Thread permissions etc popups */
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.content-98HsJk > div.sidebar-2K8pFh.hasNotice-1XRy4h > nav > div.container-3O_wAf,
+ /* home button icon */
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div > svg {
+ display: none;
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/plugins/.gitkeep b/ReferenceClientProxyImplementation/Resources/Assets/plugins/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/plugins/.gitkeep
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/preload-plugins/autoRegister.js b/ReferenceClientProxyImplementation/Resources/Assets/preload-plugins/autoRegister.js
new file mode 100644
index 0000000..15ef582
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/preload-plugins/autoRegister.js
@@ -0,0 +1,62 @@
+// Auto register guest account:
+const prefix = [
+ "mysterious",
+ "adventurous",
+ "courageous",
+ "precious",
+ "cynical",
+ "flamer ",
+ "despicable",
+ "suspicious",
+ "gorgeous",
+ "impeccable",
+ "lovely",
+ "stunning",
+ "keyed",
+ "phoned",
+ "glorious",
+ "amazing",
+ "strange",
+ "arcane"
+];
+const suffix = [
+ "Anonymous",
+ "Boy",
+ "Lurker",
+ "Keyhitter",
+ "User",
+ "Enjoyer",
+ "Hunk",
+ "Coolstar",
+ "Wrestling",
+ "TylerTheCreator",
+ "Ad",
+ "Gamer",
+ "Games",
+ "Programmer"
+];
+
+Array.prototype.random = function () {
+ return this[Math.floor(Math.random() * this.length)];
+};
+
+function _generateName() {
+ return `${prefix.random()}${suffix.random()}`;
+}
+
+var token = JSON.parse(localStorage.getItem("token"));
+if (!token && location.pathname !== "/login" && location.pathname !== "/register") {
+ fetch(`${window.GLOBAL_ENV.API_ENDPOINT}/auth/register`, {
+ method: "POST",
+ headers: {"content-type": "application/json"},
+ body: JSON.stringify({username: `${_generateName()}`, consent: true}) //${Date.now().toString().slice(-4)}
+ })
+ .then((x) => x.json())
+ .then((x) => {
+ localStorage.setItem("token", `"${x.token}"`);
+ if (!window.localStorage) {
+ // client already loaded -> need to reload to apply the newly registered user token
+ location.reload();
+ }
+ });
+}
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/tools/rightsCalculator.js b/ReferenceClientProxyImplementation/Resources/Assets/tools/rightsCalculator.js
new file mode 100644
index 0000000..c324891
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/tools/rightsCalculator.js
@@ -0,0 +1,17 @@
+String.prototype.replaceAt = function (index, replacement) {
+ return this.substring(0, index) + replacement + this.substring(index + 1);
+}
+
+var legacyRights = 0n;
+var modernRights = '0'.repeat(document.getElementsByTagName("input").length);
+var configRights = {};
+
+function calculate(a) {
+ console.log(a)
+ legacyRights += a.checked ? (1n << BigInt(a.value)) : -(1n << BigInt(a.value))
+ modernRights = modernRights.replaceAt(a.value, a.checked ? '1' : '0')
+ configRights[a.name] = a.checked
+ document.getElementById("legacyRights").innerText = "Legacy rights (fosscord-server-ts): " + legacyRights
+ document.getElementById("modernRights").innerText = "User rights: " + modernRights
+ document.getElementById("configRights").value = JSON.stringify(configRights, null, 4)
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/user.css b/ReferenceClientProxyImplementation/Resources/Assets/user.css
new file mode 100644
index 0000000..a7e5c4f
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/user.css
@@ -0,0 +1 @@
+/* Your custom CSS goes here, enjoy! */
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/banner1.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner1.png
new file mode 100644
index 0000000..ed9bd5c
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner1.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/banner2.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner2.png
new file mode 100644
index 0000000..90d3713
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner2.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/banner3.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner3.png
new file mode 100644
index 0000000..2235189
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner3.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/banner4.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner4.png
new file mode 100644
index 0000000..e6bd7b6
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/banner4.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Assets/widget/shield.png b/ReferenceClientProxyImplementation/Resources/Assets/widget/shield.png
new file mode 100644
index 0000000..30277db
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Assets/widget/shield.png
Binary files differdiff --git a/ReferenceClientProxyImplementation/Resources/Pages/developers.html b/ReferenceClientProxyImplementation/Resources/Pages/developers.html
new file mode 100644
index 0000000..9798554
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Pages/developers.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html class="theme-dark" data-theme="dark">
+ <head>
+ <meta charset="utf-8"/>
+ <meta content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no" name="viewport"/>
+
+ <link href="/assets/532.03aaeef88460fae60534.css" integrity="" rel="stylesheet"/>
+ <link href="/assets/07dca80a102d4149e9736d4b162cff6f.ico" rel="icon"/>
+ <title>Discord Test Client Developer Portal</title>
+ <meta charset="utf-8" data-react-helmet="true"/>
+ </head>
+
+ <body>
+ <div id="app-mount"></div>
+ <script>
+ window.GLOBAL_ENV = {
+ API_VERSION: 9,
+ API_ENDPOINT: "/api",
+ WEBAPP_ENDPOINT: "",
+ CDN_HOST: `${location.hostname}:3003`,
+
+ BRAINTREE_KEY: "production_5st77rrc_49pp2rp4phym7387",
+ STRIPE_KEY: "pk_live_CUQtlpQUF0vufWpnpUmQvcdi",
+ MARKETING_ENDPOINT: "//discord.com",
+ RELEASE_CHANNEL: "stable",
+ ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0"
+ };
+ GLOBAL_ENV.MEDIA_PROXY_ENDPOINT = location.protocol + "//" + GLOBAL_ENV.CDN_HOST;
+ const localStorage = window.localStorage;
+ // TODO: remote auth
+ // window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, "");
+ localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT);
+ localStorage.setItem(
+ "DeveloperOptionsStore",
+ JSON.stringify({
+ trace: false,
+ canary: true,
+ logGatewayEvents: true,
+ logOverlayEvents: true,
+ logAnalyticsEvents: true,
+ sourceMapsEnabled: false,
+ axeEnabled: true,
+ bugReporterEnabled: true,
+ idleStatusIndicatorEnabled: false
+ })
+ );
+ </script>
+ <script integrity="" src="/assets/41fde19fdf180f3d4315.js"></script>
+ <script integrity="" src="/assets/7b04a3ab10e05dd9054e.js"></script>
+ <script integrity="" src="/assets/d1f811da193e5648048b.js"></script>
+ </body>
+</html>
diff --git a/ReferenceClientProxyImplementation/Resources/Pages/index-template.html b/ReferenceClientProxyImplementation/Resources/Pages/index-template.html
new file mode 100644
index 0000000..ef77e22
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Pages/index-template.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8"/>
+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
+ <title>Discord Test Client</title>
+ <link href="/assets/fosscord.css" rel="stylesheet"/>
+ <link href="/assets/fosscord-login.css" id="logincss" rel="stylesheet"/>
+ <link href="/assets/user.css" id="customcss" rel="stylesheet"/>
+ <!-- preload plugin marker -->
+ </head>
+
+ <body>
+ <div id="app-mount"></div>
+ <script>
+ window.__OVERLAY__ = /overlay/.test(location.pathname);
+ window.__BILLING_STANDALONE__ = /^\/billing/.test(location.pathname);
+
+ var xmlhttp = new XMLHttpRequest();
+ var url = "/api/_fosscord/v1/global_env";
+ xmlhttp.onreadystatechange = function () {
+ if (this.readyState == 4 && this.status == 200) {
+ window.GLOBAL_ENV = JSON.parse(this.responseText);
+ }
+ }
+
+ xmlhttp.open("GET", url, false);
+ xmlhttp.send();
+
+
+ const localStorage = window.localStorage;
+ // TODO: remote auth
+ // window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, "");
+ localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT);
+ localStorage.setItem(
+ "DeveloperOptionsStore",
+ JSON.stringify({
+ trace: false,
+ canary: true,
+ logGatewayEvents: true,
+ logOverlayEvents: true,
+ logAnalyticsEvents: true,
+ sourceMapsEnabled: false,
+ axeEnabled: true,
+ bugReporterEnabled: true,
+ idleStatusIndicatorEnabled: false
+ })
+ );
+
+ setInterval(() => {
+ var token = JSON.parse(localStorage.getItem("token"));
+ if (token) {
+ var logincss = document.querySelector('#logincss'),
+ canRemove = logincss ? logincss : "";
+ if (canRemove !== "") {
+ document.querySelector("#logincss").remove();
+ canRemove = "";
+ }
+ }
+ }, 1000)
+
+ const settings = JSON.parse(localStorage.getItem("UserSettingsStore"));
+ if (settings && settings.locale.length <= 2) {
+ // fix client locale wrong and client not loading at all
+ settings.locale = "en-US";
+ localStorage.setItem("UserSettingsStore", JSON.stringify(settings));
+ }
+ </script>
+ <!--prefetch_script-->
+ <!--client_css-->
+ <script src="/assets/checkLocale.js"></script>
+ <!--client_script-->
+ <!-- plugin marker -->
+ </body>
+</html>
diff --git a/ReferenceClientProxyImplementation/Resources/Pages/index.html b/ReferenceClientProxyImplementation/Resources/Pages/index.html
new file mode 100644
index 0000000..0b52736
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Pages/index.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8"/>
+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
+ <title>Discord Test Client</title>
+ <link href="/assets/fosscord.css" rel="stylesheet"/>
+ <link href="/assets/fosscord-login.css" id="logincss" rel="stylesheet"/>
+ <link href="/assets/user.css" id="customcss" rel="stylesheet"/>
+ <!-- preload plugin marker -->
+ </head>
+
+ <body>
+ <div id="app-mount"></div>
+ <script>
+ window.__OVERLAY__ = /overlay/.test(location.pathname);
+ window.__BILLING_STANDALONE__ = /^\/billing/.test(location.pathname);
+ window.GLOBAL_ENV = {
+ API_ENDPOINT: "/api",
+ API_VERSION: 9,
+ GATEWAY_ENDPOINT: `${location.protocol === "https:" ? "wss://" : "ws://"}${location.hostname}:2001`,
+ WEBAPP_ENDPOINT: "",
+ CDN_HOST: `${location.hostname}:3003`,
+ ASSET_ENDPOINT: "",
+ MEDIA_PROXY_ENDPOINT: "https://media.discordapp.net",
+ WIDGET_ENDPOINT: `//${location.host}/widget`,
+ INVITE_HOST: `${location.host}/invite`,
+ GUILD_TEMPLATE_HOST: "discord.new",
+ GIFT_CODE_HOST: "discord.gift",
+ RELEASE_CHANNEL: "staging",
+ MARKETING_ENDPOINT: "//discord.com",
+ BRAINTREE_KEY: "production_5st77rrc_49pp2rp4phym7387",
+ STRIPE_KEY: "pk_live_CUQtlpQUF0vufWpnpUmQvcdi",
+ NETWORKING_ENDPOINT: "//router.discordapp.net",
+ RTC_LATENCY_ENDPOINT: "//latency.discord.media/rtc",
+ ACTIVITY_APPLICATION_HOST: 'discordsays.com',
+ PROJECT_ENV: "staging",
+ REMOTE_AUTH_ENDPOINT: "//localhost:3020",
+ SENTRY_TAGS: {buildId: "75e36d9", buildType: "normal"},
+ MIGRATION_SOURCE_ORIGIN: "https://discordapp.com",
+ MIGRATION_DESTINATION_ORIGIN: "https://discord.com",
+ HTML_TIMESTAMP: Date.now(),
+ ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0"
+ };
+ GLOBAL_ENV.MEDIA_PROXY_ENDPOINT = location.protocol + "//" + GLOBAL_ENV.CDN_HOST;
+ const localStorage = window.localStorage;
+ // TODO: remote auth
+ // window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, "");
+ localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT);
+ localStorage.setItem(
+ "DeveloperOptionsStore",
+ JSON.stringify({
+ trace: false,
+ canary: true,
+ logGatewayEvents: true,
+ logOverlayEvents: true,
+ logAnalyticsEvents: true,
+ sourceMapsEnabled: false,
+ axeEnabled: true,
+ bugReporterEnabled: true,
+ idleStatusIndicatorEnabled: false
+ })
+ );
+
+ setInterval(() => {
+ var token = JSON.parse(localStorage.getItem("token"));
+ if (token) {
+ var logincss = document.querySelector('#logincss'),
+ canRemove = logincss ? logincss : "";
+ if (canRemove !== "") {
+ document.querySelector("#logincss").remove();
+ canRemove = "";
+ }
+ }
+ }, 1000)
+
+ const settings = JSON.parse(localStorage.getItem("UserSettingsStore"));
+ if (settings && settings.locale.length <= 2) {
+ // fix client locale wrong and client not loading at all
+ settings.locale = "en-US";
+ localStorage.setItem("UserSettingsStore", JSON.stringify(settings));
+ }
+ </script>
+ <link as="script" href="/assets/d110af2d26131beb5874.js" rel="prefetch">
+ <link as="script" href="/assets/b4499d2a6b9046b1b402.js" rel="prefetch">
+ <link as="script" href="/assets/4b58fa778cc38e586a72.js" rel="prefetch">
+ <link as="script" href="/assets/2e45b6b321248222f12a.js" rel="prefetch">
+ <link as="script" href="/assets/2a6b8d1c4c54837fbc1c.js" rel="prefetch">
+ <link as="script" href="/assets/32a385a6252512863141.js" rel="prefetch">
+ <link as="script" href="/assets/66fb582b4c0e8351bd05.js" rel="prefetch">
+ <link as="script" href="/assets/d5b25e619c7d3c54072a.js" rel="prefetch">
+ <link as="script" href="/assets/e03405baa0373b35d236.js" rel="prefetch">
+ <link as="script" href="/assets/7ac1c95d059085cc0b95.js" rel="prefetch">
+ <link as="script" href="/assets/0778eb7c5d969638777f.js" rel="prefetch">
+ <link as="script" href="/assets/d2888bff1a9b35045d7a.js" rel="prefetch">
+ <link as="script" href="/assets/46f8b7fcfcafca99414f.js" rel="prefetch">
+ <link as="script" href="/assets/e2d458306e3a43a03786.js" rel="prefetch">
+ <link as="script" href="/assets/2f6fc578446032c0c0a5.js" rel="prefetch">
+ <link as="script" href="/assets/14a17120248ea20823d1.js" rel="prefetch">
+ <link as="script" href="/assets/0cfed40d2532db2e560f.js" rel="prefetch">
+ <link as="script" href="/assets/f66efd328f8ca3ad6f1d.js" rel="prefetch">
+ <link as="script" href="/assets/a1ad32825016ef835b51.js" rel="prefetch">
+ <link as="script" href="/assets/916a3fd42b56f5b5b0ff.js" rel="prefetch">
+ <link as="script" href="/assets/e4c9aec8e2ccf49c6a43.js" rel="prefetch">
+ <link as="script" href="/assets/4eb8ce79510c19e425f0.js" rel="prefetch">
+ <link as="script" href="/assets/669ce420727060b05505.js" rel="prefetch">
+ <link as="script" href="/assets/90adae2a1e530ee2388a.js" rel="prefetch">
+ <link as="script" href="/assets/95785439b23503f2a4bc.js" rel="prefetch">
+ <link as="script" href="/assets/4dcc4abc98689f0e601b.js" rel="prefetch">
+ <link as="script" href="/assets/50b1f37a5e2c8602463a.js" rel="prefetch">
+ <link as="script" href="/assets/0d7b3701d6136866c7ec.js" rel="prefetch">
+ <link as="script" href="/assets/d84f21f10b08966b8903.js" rel="prefetch">
+ <link as="script" href="/assets/60385fdbdf9f90d8c4c7.js" rel="prefetch">
+ <link as="script" href="/assets/7f327522a62dfbc3fa17.js" rel="prefetch">
+ <link as="script" href="/assets/50ef6fe50ba0096918d9.js" rel="prefetch">
+ <link as="script" href="/assets/6f81e4e1883133e82730.js" rel="prefetch">
+ <link as="script" href="/assets/ec34e6ac622f2adf141a.js" rel="prefetch">
+ <link as="script" href="/assets/1839152a11550e6304c2.js" rel="prefetch">
+ <link as="script" href="/assets/e7ab6471de10e3f701e6.js" rel="prefetch">
+ <link as="script" href="/assets/efb278e1216055e3d1a8.js" rel="prefetch">
+ <link as="script" href="/assets/70093dfc536e766dbb27.js" rel="prefetch">
+ <link href="/assets/40532.bd1a2e262154456bad22.css" rel="stylesheet">
+ <script src="/assets/checkLocale.js"></script>
+ <script src="/assets/81d16d948535e8d77831.js"></script>
+ <script src="/assets/d110af2d26131beb5874.js"></script>
+ <script src="/assets/c44d54de4c931fca740e.js"></script>
+ <script src="/assets/2cc7dd42639b0c3ae762.js"></script>
+ <!-- plugin marker -->
+ </body>
+</html>
diff --git a/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDataLog.html b/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDataLog.html
new file mode 100644
index 0000000..ef16e72
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDataLog.html
@@ -0,0 +1,7 @@
+<script>
+ window.toHexString = function (byteArray) {
+ return byteArray.reduce((output, elem) =>
+ (output + (elem.toString(16).padStart(2, '0').toUpperCase() + ' ')),
+ '');
+ }
+</script>
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDumper.html b/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDumper.html
new file mode 100644
index 0000000..14dc989
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Resources/Private/Injections/WebSocketDumper.html
@@ -0,0 +1,22 @@
+<script>
+ window.sockets = [];
+ var lastBuff = '';
+ const nativeWebSocket = window.WebSocket;
+ window.WebSocket = function (...args) {
+ console.log("Starting new websocket");
+ const socket = new nativeWebSocket(...args);
+ window.sockets.push(socket);
+ if (!args[0].includes('spotify'))
+ socket.addEventListener("message", ev => {
+ console.log("Dumping message...");
+ lastBuff = ev.data;
+ var dat = new Uint8Array(lastBuff);
+ if (window.toHexString) console.log(window.toHexString(dat));
+ var xhr = new XMLHttpRequest;
+ xhr.open("POST", "http://localhost:2001/dump/cs", false);
+ xhr.send(ev.data);
+ });
+ console.log("Websocket hooked!", socket);
+ return socket;
+ };
+</script>
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs b/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs
new file mode 100644
index 0000000..364c6c5
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs
@@ -0,0 +1,76 @@
+// using System.Net;
+// using AngleSharp.Html.Parser;
+//
+// namespace ReferenceClientProxyImplementation.Services;
+//
+// public class BuildDownloadService(ILogger<BuildDownloadService> logger) {
+// private static readonly HttpClient hc = new();
+//
+// public async Task DownloadBuildFromArchiveOrg(string outputDirectory, DateTime timestamp) {
+// // 20150906025145
+// var paddedTimestamp = timestamp.ToString("yyyyMMddHHmmss");
+// await DownloadBuildFromUrl(outputDirectory, $"https://web.archive.org/web/{paddedTimestamp}im_/https://discordapp.com/login");
+// }
+//
+// public async Task DownloadBuildFromUrl(string outputDirectory, string url) {
+// logger.LogInformation("Downloading build from {url} to {outDir}", url, outputDirectory);
+// var response = await hc.GetAsync(url);
+// if (!response.IsSuccessStatusCode)
+// throw new Exception($"Failed to download build from {url}");
+// var html = await response.Content.ReadAsStringAsync();
+// File.WriteAllText(outputDirectory + "/index.html", html);
+// var parser = new HtmlParser();
+// var document = parser.ParseDocument(html);
+// var assets = document.QuerySelectorAll("link[rel=stylesheet], link[rel=icon], script, img");
+// foreach (var asset in assets) {
+// var assetUrl = asset.GetAttribute("href") ?? asset.GetAttribute("src");
+// if (assetUrl == null)
+// continue;
+// if (assetUrl.StartsWith("//")) {
+// logger.LogWarning("Skipping asset {assetUrl} as it is a protocol-relative URL", assetUrl);
+// continue;
+// }
+//
+// var assetStream = await GetAssetStream(assetUrl);
+// var assetPath = Path.Combine(outputDirectory, assetUrl.TrimStart('/'));
+// Console.WriteLine($"Downloading asset {assetUrl} to {assetPath}");
+// Directory.CreateDirectory(Path.GetDirectoryName(assetPath));
+// await using var fs = File.Create(assetPath);
+// await assetStream.CopyToAsync(fs);
+// }
+//
+// logger.LogInformation("Downloading build from {url} complete!", url);
+// }
+//
+// public async Task<Stream> GetAssetStream(string asset) {
+// asset = asset.Replace("/assets/", "");
+// var urlsToTry = new Stack<string>(new[] {
+// $"https://web.archive.org/web/0id_/https://discordapp.com/assets/{asset}",
+// $"https://web.archive.org/web/0id_/https://discord.com/assets/{asset}",
+// $"https://discord.com/assets/{asset}"
+// });
+// while (urlsToTry.TryPop(out var urlToTry)) {
+// if (string.IsNullOrWhiteSpace(urlToTry)) continue;
+// try {
+// var response = await hc.GetAsync(urlToTry, HttpCompletionOption.ResponseHeadersRead);
+// if (response.IsSuccessStatusCode) {
+// Console.WriteLine($"Got success for asset {asset} from {urlToTry}");
+// return await response.Content.ReadAsStreamAsync();
+// }
+// //redirect
+//
+// if (response.StatusCode == HttpStatusCode.Found) {
+// var redirectUrl = response.Headers.Location?.ToString();
+// if (string.IsNullOrWhiteSpace(redirectUrl)) continue;
+// urlsToTry.Push(redirectUrl);
+// }
+// else logger.LogWarning("Failed to download asset {asset} from {urlToTry}", asset, urlToTry);
+// }
+// catch {
+// // ignored
+// }
+// }
+//
+// throw new Exception($"Failed to download asset {asset}");
+// }
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Services/ClientStoreService.cs b/ReferenceClientProxyImplementation/Services/ClientStoreService.cs
new file mode 100644
index 0000000..6bd7418
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Services/ClientStoreService.cs
@@ -0,0 +1,62 @@
+using ArcaneLibs.Extensions.Streams;
+using ReferenceClientProxyImplementation.Configuration;
+using ReferenceClientProxyImplementation.Patches.Implementations;
+
+namespace ReferenceClientProxyImplementation.Services;
+
+public class ClientStoreService(ProxyConfiguration config, PatchSet patches) {
+ private static readonly HttpClient HttpClient = new();
+
+ public async Task<Stream> GetPatchedClientAsset(string relativePath) {
+ if (relativePath.StartsWith("/")) {
+ relativePath = relativePath[1..];
+ }
+
+ var path = Path.Combine(config.TestClient.RevisionPath, "patched", relativePath);
+
+ if (File.Exists(path))
+ return File.OpenRead(path);
+
+ var srcAsset = (await GetOrDownloadRawAsset(relativePath)).ReadToEnd().ToArray();
+ var result = await patches.ApplyPatches(relativePath, srcAsset);
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ if (!result.SequenceEqual(srcAsset)) {
+ await File.WriteAllBytesAsync(path, result);
+ return File.OpenRead(path);
+ }
+
+ Console.WriteLine($"No patches applied for {relativePath}, returning original asset.");
+ return new MemoryStream(srcAsset);
+ }
+
+ public async Task<Stream> GetOrDownloadRawAsset(string relativePath) {
+ relativePath = relativePath.TrimStart('/');
+ var assetPath = Path.Combine(config.TestClient.RevisionPath, "src", relativePath);
+ if (File.Exists(assetPath)) {
+ Console.WriteLine($"Asset {relativePath} already exists at {assetPath}, returning existing file.");
+ return File.OpenRead(assetPath);
+ }
+
+ var url = $"{config.TestClient.RevisionBaseUrl}/{relativePath}";
+ var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
+ if (!response.IsSuccessStatusCode) {
+ Console.WriteLine($"Failed to download asset {relativePath} from {url}, status code: {response.StatusCode}");
+ throw new FileNotFoundException($"Asset not found: {relativePath}");
+ }
+ var contentStream = await response.Content.ReadAsStreamAsync();
+ Directory.CreateDirectory(Path.GetDirectoryName(assetPath)!);
+ await using var fileStream = File.Create(assetPath);
+ await contentStream.CopyToAsync(fileStream);
+ fileStream.Close();
+ contentStream.Close();
+ Console.WriteLine($"Downloaded asset {relativePath} to {assetPath}");
+
+ return File.OpenRead(assetPath);
+ }
+
+ public bool HasRawAsset(string relativePath) {
+ relativePath = relativePath.TrimStart('/');
+ var assetPath = Path.Combine(config.TestClient.RevisionPath, "src", relativePath);
+ return File.Exists(assetPath);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs b/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs
new file mode 100644
index 0000000..039fc74
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs
@@ -0,0 +1,3 @@
+namespace ReferenceClientProxyImplementation.Services;
+
+public class ModernAssetLocator { }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs b/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs
new file mode 100644
index 0000000..b8998f1
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs
@@ -0,0 +1,14 @@
+// namespace ReferenceClientProxyImplementation.Services;
+//
+// public class TemporaryTestJob(BuildDownloadService buildDownloadService) : BackgroundService {
+// protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
+// Console.WriteLine("Running test job");
+// var outDir =
+// "/home/Rory/git/spacebar/server-cs/DevUtils/ReferenceClientProxyImplementation/downloadCache/today/raw/";
+// if (Directory.Exists(outDir))
+// Directory.Delete(outDir, true);
+// Directory.CreateDirectory(outDir);
+// // await buildDownloadService.DownloadBuildFromArchiveOrg(outDir, new DateTime(2014, 1, 1));
+// await buildDownloadService.DownloadBuildFromUrl(outDir, "https://canary.discord.com/app");
+// }
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Tasks/Startup/BuildClientTask.cs b/ReferenceClientProxyImplementation/Tasks/Startup/BuildClientTask.cs
new file mode 100644
index 0000000..8718fc5
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Tasks/Startup/BuildClientTask.cs
@@ -0,0 +1,39 @@
+// using ReferenceClientProxyImplementation.Configuration;
+// using ReferenceClientProxyImplementation.Helpers;
+//
+// namespace ReferenceClientProxyImplementation.Tasks.Startup;
+//
+// public class BuildClientTask(ProxyConfiguration proxyConfig) : ITask {
+// public int GetOrder() => 10;
+//
+// public string GetName() => "Build updated test client";
+//
+// public async Task Execute() {
+// var hc = new HttpClient();
+// if (proxyConfig.AssetCache.WipeOnStartup) {
+// Directory.Delete(proxyConfig.AssetCache.DiskCachePath, true);
+// Directory.CreateDirectory(proxyConfig.AssetCache.DiskCachePath);
+// }
+//
+// // if (!proxyConfig.TestClient.Enabled ||
+// // !proxyConfig.TestClient.UseLatest) {
+// // Console.WriteLine("[Client Updater] Test client is disabled or not set to use latest version, skipping!");
+// return;
+// // }
+//
+// Console.WriteLine("[Client updater] Fetching client");
+// var client = HtmlUtils.CleanupHtml(await hc.GetStringAsync("https://canary.discord.com/channels/@me"));
+// Console.WriteLine("[Client updater] Building client...");
+// var target = File.ReadAllText("Resources/Pages/index-template.html");
+// var lines = client.Split("\n");
+// target = target.Replace("<!--prefetch_script-->",
+// string.Join("\n", lines.Where(x => x.Contains("link rel=\"prefetch\" as=\"script\""))));
+// target = target.Replace("<!--client_script-->",
+// string.Join("\n", lines.Where(x => x.Contains("<script src="))));
+// target = target.Replace("<!--client_css-->",
+// string.Join("\n", lines.Where(x => x.Contains("link rel=\"stylesheet\""))));
+// target = target.Replace("integrity", "hashes");
+// File.WriteAllText("Resources/Pages/index-updated.html", target);
+// Console.WriteLine("[Client updater] Finished building client!");
+// }
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Tasks/Startup/InitClientStoreTask.cs b/ReferenceClientProxyImplementation/Tasks/Startup/InitClientStoreTask.cs
new file mode 100644
index 0000000..4aeab96
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Tasks/Startup/InitClientStoreTask.cs
@@ -0,0 +1,173 @@
+using System.Diagnostics;
+using System.Text;
+using System.Text.RegularExpressions;
+using ArcaneLibs.Extensions;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Tasks.Startup;
+
+public partial class InitClientStoreService(ProxyConfiguration proxyConfig) : ITask {
+ public int GetOrder() => 0;
+
+ public string GetName() => "Get client revision";
+
+ public async Task Execute() {
+ switch (proxyConfig.TestClient.Revision) {
+ case "canary":
+ proxyConfig.TestClient.RevisionBaseUrl = "https://canary.discord.com";
+ proxyConfig.TestClient.RevisionPath = await GetRevisionPathFromUrl("canary", "https://canary.discord.com/app");
+ break;
+ case "ptb":
+ proxyConfig.TestClient.RevisionBaseUrl = "https://ptb.discord.com";
+ proxyConfig.TestClient.RevisionPath = await GetRevisionPathFromUrl("ptb", "https://ptb.discord.com/app");
+ break;
+ case "stable":
+ proxyConfig.TestClient.RevisionBaseUrl = "https://discord.com";
+ proxyConfig.TestClient.RevisionPath = await GetRevisionPathFromUrl("stable", "https://discord.com/app");
+ break;
+ default:
+ if (proxyConfig.TestClient.RevisionPath == null) {
+ throw new Exception("Test client revision path is not set!");
+ }
+
+ break;
+ }
+ }
+
+ private async Task<string> GetRevisionPathFromUrl(string rev, string url) {
+ using var hc = new HttpClient();
+ using var response = await hc.GetAsync(url);
+ var content = await response.Content.ReadAsStringAsync();
+ var normalisedContent = StripNonces(content);
+ var hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(normalisedContent));
+ var knownHashes = await GetKnownRevisionHashes("src/app.html");
+ var currentRevisionFilePath = Path.Combine(proxyConfig.AssetCache.DiskCacheBaseDirectory, "currentRevision");
+ var previousRevision = Path.Exists(currentRevisionFilePath) ? await File.ReadAllTextAsync(currentRevisionFilePath) : "";
+ var revisionName = rev;
+
+ if (knownHashes.Any(x => x.Value.SequenceEqual(hash))) {
+ Console.WriteLine($"[InitClientStoreTask] Found known revision '{rev}' with hash {hash.AsHexString().Replace(" ", "")}!");
+ revisionName = knownHashes.First(x => x.Value.SequenceEqual(hash)).Key;
+ }
+ else {
+ Console.WriteLine($"[InitClientStoreTask] No known revision found for hash {hash.AsHexString().Replace(" ", "")}, creating new revision directory!");
+ if (response.Headers.Contains("X-Build-Id")) {
+ revisionName = "buildId_" + response.Headers.GetValues("X-Build-Id").FirstOrDefault();
+ Console.WriteLine("[InitClientStoreTask] Using build ID from X-Build-Id header: " + revisionName);
+ }
+ }
+
+ var revisionPath = Path.Combine(proxyConfig.AssetCache.DiskCacheBaseDirectory, revisionName);
+ Console.WriteLine($"[InitClientStoreTask] Saving revision '{revisionName}' to {revisionPath}...");
+ PrepareRevisionDirectory(revisionPath);
+ await File.WriteAllTextAsync(Path.Combine(revisionPath, "src", "app.html"), content);
+ await File.WriteAllTextAsync(Path.Combine(proxyConfig.AssetCache.DiskCacheBaseDirectory, "currentRevision"), revisionName);
+
+ //also download dev page
+ using var devResponse = await hc.GetAsync(url.Replace("/app", "/developers/applications"));
+ var devContent = await devResponse.Content.ReadAsStringAsync();
+ await File.WriteAllTextAsync(Path.Combine(revisionPath, "src", "developers.html"), devContent);
+
+ //...and popout
+ using var popoutResponse = await hc.GetAsync(url.Replace("/app", "/popout"));
+ var popoutContent = await popoutResponse.Content.ReadAsStringAsync();
+ await File.WriteAllTextAsync(Path.Combine(revisionPath, "src", "popout.html"), popoutContent);
+
+ if (proxyConfig.AssetCache.DitchPatchedOnStartup) {
+ Directory.Delete(Path.Combine(revisionPath, "patched"), true);
+ Directory.CreateDirectory(Path.Combine(revisionPath, "patched"));
+ }
+
+ if (previousRevision != revisionName || true) {
+ foreach (var argv in proxyConfig.AssetCache.ExecOnRevisionChange) {
+ try {
+ var psi = new ProcessStartInfo(argv[0], argv[1..].Select(a => a.Replace("{revisionPath}", revisionPath))) {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ using var process = Process.Start(psi);
+ if (process != null) {
+ _ = process.StandardOutput.ReadToEndAsync();
+ _ = process.StandardError.ReadToEndAsync();
+ Console.WriteLine($"[InitClientStoreTask] Executing post-revision change command: {argv[0]} {string.Join(" ", argv[1..])}");
+ }
+ else {
+ Console.WriteLine($"[InitClientStoreTask] Failed to start post-revision change command: {argv[0]} {string.Join(" ", argv[1..])}");
+ }
+ }
+ catch (Exception e) {
+ Console.WriteLine($"[InitClientStoreTask] Failed to start post-revision change command: {argv[0]} {string.Join(" ", argv[1..])}\n{e}");
+ }
+ }
+ }
+
+ return revisionPath;
+ }
+
+ private static void PrepareRevisionDirectory(string revisionPath, bool dropPatched = false) {
+ Directory.CreateDirectory(revisionPath);
+ Directory.CreateDirectory(Path.Combine(revisionPath, "src"));
+ Directory.CreateDirectory(Path.Combine(revisionPath, "formatted"));
+ Directory.CreateDirectory(Path.Combine(revisionPath, "patched"));
+ }
+
+ private async Task<Dictionary<string, byte[]>> GetKnownRevisionHashes(string file) {
+ if (!Directory.Exists(proxyConfig.AssetCache.DiskCacheBaseDirectory))
+ Directory.CreateDirectory(proxyConfig.AssetCache.DiskCacheBaseDirectory);
+
+ var revisionHashTasks = Directory
+ .GetDirectories(proxyConfig.AssetCache.DiskCacheBaseDirectory)
+ .Select(dir => GetKnownRevisionHash(dir, file));
+
+ var revisionHashes = await Task.WhenAll(revisionHashTasks);
+ return revisionHashes
+ .OfType<(string RevisionId, byte[] Hash)>()
+ .ToDictionary(
+ x => x.RevisionId,
+ x => x.Hash
+ );
+ }
+
+ private async Task<(string RevisionId, byte[] Hash)?> GetKnownRevisionHash(string dir, string file) {
+ var hashFile = Path.Combine(dir, file);
+ if (File.Exists(hashFile)) {
+ var content = StripNonces(await File.ReadAllTextAsync(hashFile));
+ var hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(content));
+ var result = (new DirectoryInfo(dir).Name, hash);
+ return result;
+ }
+
+ Console.WriteLine($"[InitClientStoreTask] '{file}' not found in client revision directory '{dir}'!");
+ return null;
+ }
+
+ private static string StripNonces(string content) =>
+ // most specific first
+ HtmlScriptIntegrityRegex().Replace(
+ HtmlScriptNonceRegex().Replace(
+ JsElementNonceRegex().Replace(
+ CFParamsRegex().Replace(
+ content,
+ ""
+ ),
+ ""
+ ),
+ ""),
+ ""
+ );
+
+ [GeneratedRegex("nonce=\"[a-zA-Z0-9+/=]+\"")]
+ private static partial Regex HtmlScriptNonceRegex();
+
+ [GeneratedRegex(@"\sintegrity=""[a-zA-Z0-9+/=\-\s]+""")]
+ private static partial Regex HtmlScriptIntegrityRegex();
+
+ [GeneratedRegex("\\w.nonce='[a-zA-Z0-9+/=]+';")]
+ private static partial Regex JsElementNonceRegex();
+
+ [GeneratedRegex(
+ @"var\s+\w+\s*=\s*b\.createElement\('script'\);\s*\w+\.nonce='[a-zA-Z0-9+/=]+'\s*;\s*\w+\.innerHTML=""window\.__CF\$cv\$\w+=\{r:'[a-zA-Z0-9+/=]+',t:'[a-zA-Z0-9+/=]+'\};var\s+\w+=document\.createElement\('script'\);\s*\w+\.nonce='[a-zA-Z0-9+/=]+'\s*;\s*\w+\.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document\.getElementsByTagName\('head'\)\[0\]\.appendChild\(\w+\);")]
+ public static partial Regex CFParamsRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Tasks/Startup/PatchClientAssetsTask.cs b/ReferenceClientProxyImplementation/Tasks/Startup/PatchClientAssetsTask.cs
new file mode 100644
index 0000000..60e7ddc
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Tasks/Startup/PatchClientAssetsTask.cs
@@ -0,0 +1,37 @@
+// using ReferenceClientProxyImplementation.Configuration;
+//
+// namespace ReferenceClientProxyImplementation.Tasks.Startup;
+//
+// public class PatchClientAssetsTask(ProxyConfiguration proxyConfig) : ITask {
+// public int GetOrder() => 100;
+//
+// public string GetName() => "Patch client assets";
+//
+// public async Task Execute() {
+// // foreach (var file in Directory.GetFiles(proxyConfig.AssetCache.DiskCachePath).Where(x => x.EndsWith(".js")))
+// // {
+// // var start = DateTime.Now;
+// // if(proxyConfig.Logging.LogClientPatching) Console.Write($"[Client Patcher] Patching file {file}...");
+// // var contents = File.ReadAllText(file);
+// // contents = AssetsController.PatchClient(contents);
+// // File.WriteAllText(file, contents);
+// // if(proxyConfig.Logging.LogClientPatching) Console.WriteLine($" Done in {DateTime.Now - start}!");
+// // }
+// //
+// // if (proxyConfig.Debug.ReformatAssets)
+// // {
+// // Console.WriteLine("[Client Patcher] Reformatting assets...");
+// // foreach (var file in Directory.GetFiles(proxyConfig.AssetCache.DiskCachePath))
+// // {
+// // var target = file.Replace(proxyConfig.AssetCache.DiskCachePath, proxyConfig.TestClient.DebugOptions.FormattedAssetPath);
+// // if(!File.Exists(target))
+// // File.Copy(file, target, false);
+// // }
+// //
+// // Process.Start("npx", "prettier -w " + proxyConfig.Debug.FormattedAssetPath).WaitForExit();
+// // Console.WriteLine("[Client Patcher] Done!");
+// // if (proxyConfig.Debug.OpenFormattedDirAfterReformat)
+// // Process.Start(proxyConfig.Debug.OpenFormattedDirCommand.Command, proxyConfig.Debug.OpenFormattedDirCommand.Args.Replace("$dir", proxyConfig.Debug.FormattedAssetPath));
+// // }
+// }
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Tasks/Startup/SanitiseConfigPathsTask.cs b/ReferenceClientProxyImplementation/Tasks/Startup/SanitiseConfigPathsTask.cs
new file mode 100644
index 0000000..2534362
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Tasks/Startup/SanitiseConfigPathsTask.cs
@@ -0,0 +1,15 @@
+using ArcaneLibs.Extensions;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Tasks.Startup;
+
+public partial class SanitiseConfigPathTask(ProxyConfiguration proxyConfig) : ITask {
+ public int GetOrder() => int.MinValue;
+
+ public string GetName() => "Sanitise config path";
+
+ public async Task Execute() {
+ // proxyConfig.AssetCache.DiskCacheBaseDirectory = Path.GetFullPath(proxyConfig.AssetCache.DiskCacheBaseDirectory);
+ Console.WriteLine(proxyConfig.ToJson());
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Tasks/Tasks.cs b/ReferenceClientProxyImplementation/Tasks/Tasks.cs
new file mode 100644
index 0000000..5038002
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Tasks/Tasks.cs
@@ -0,0 +1,28 @@
+namespace ReferenceClientProxyImplementation.Tasks;
+
+public class Tasks(IServiceProvider serviceProvider) : BackgroundService {
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
+ var defaultColor = Console.ForegroundColor;
+ var tasks = serviceProvider.GetServices<ITask>().ToList();
+ // List<ITask> tasks = new()
+ // {
+ // new BuildClientTask(),
+ // new PatchClientAssetsTask()
+ // };
+ var i = 0;
+ foreach (var task in tasks.OrderBy(x => x.GetOrder())) {
+ Console.ForegroundColor = ConsoleColor.DarkBlue;
+ Console.Write("==> ");
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.WriteLine($"Running task {++i}/{tasks.Count}: {task.GetName()} (Type<{task.GetType().Name}>)");
+ Console.ForegroundColor = defaultColor;
+ task.Execute();
+ }
+ }
+}
+
+public interface ITask {
+ public int GetOrder();
+ public string GetName();
+ public Task Execute();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/appsettings.Development.json b/ReferenceClientProxyImplementation/appsettings.Development.json
new file mode 100644
index 0000000..6cb9d8a
--- /dev/null
+++ b/ReferenceClientProxyImplementation/appsettings.Development.json
@@ -0,0 +1,62 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Information",
+ "Microsoft.AspNetCore.HttpLogging": "Warning",
+ "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Warning",
+ "Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor": "Warning",
+ "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Warning"
+ }
+ },
+ "ProxyConfiguration": {
+ "TestClient": {
+ "Enabled": true,
+ "Revision": "canary",
+ // This only does text properties!
+ "GlobalEnv": {
+// "IMAGE_PROXY_ENDPOINTS": "//images-ext-1.discordapp.net,//images-ext-2.discordapp.net",
+
+
+ "API_ENDPOINT": "//api.old.server.spacebar.chat/api",
+ "GATEWAY_ENDPOINT": "wss://gateway.old.server.spacebar.chat",
+ "MEDIA_PROXY_ENDPOINT": "//cdn.old.server.spacebar.chat",
+ "IMAGE_PROXY_ENDPOINTS": "//cdn.old.server.spacebar.chat",
+ "API_PROTOCOL": "https:",
+ "CDN_HOST": "cdn.old.server.spacebar.chat",
+
+
+ //"API_ENDPOINT": "//localhost:3001/api",
+ //"GATEWAY_ENDPOINT": "ws://localhost:3002",
+ //"MEDIA_PROXY_ENDPOINT": "//localhost:3003",
+ //"IMAGE_PROXY_ENDPOINTS": "//localhost:3003",
+ //"CDN_HOST": "localhost:3003",
+ //"API_PROTOCOL": "http:",
+
+
+ "NODE_ENV": "production",
+ "PROJECT_ENV": "development", //internal
+ "RELEASE_CHANNEL": "internal",
+ "PUBLIC_PATH": "/assets/",
+ "API_VERSION": 9
+ }
+ },
+ "AssetCache": {
+ "MemoryCache": true,
+ "DiskCache": true,
+ "DiskCachePath": "cache",
+ "DiskCacheBaseDirectory": "./clientRepository",
+ "DitchPatchedOnStartup": true,
+ "WipeOnStartup": false,
+ "ExecOnRevisionChange": [
+ ["code", "-n", "{revisionPath}/patched"]
+ ],
+ //"BiomePath": "/nix/store/cahp9z8wkr9kazb2qx54ddp1fgjgrrxa-biome-2.2.5/bin/biome",
+ //"PrettierPath": "/nix/store/6all2hvcnqjwbgi8nhdrffbydv7np5vx-prettier-3.6.2/bin/prettier",
+ //"NodePath": "/nix/store/cwxxy2is6hb4dcqsx8rhnvwic3hfzpcd-nodejs-22.19.0/bin/node"
+ "BiomePath": "./biome/bin/biome",
+ "PrettierPath": "./prettier/bin/prettier",
+ "NodePath": "./node/bin/node"
+ }
+ }
+}
diff --git a/ReferenceClientProxyImplementation/appsettings.json b/ReferenceClientProxyImplementation/appsettings.json
new file mode 100644
index 0000000..23039ed
--- /dev/null
+++ b/ReferenceClientProxyImplementation/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/ReferenceClientProxyImplementation/formatCache.sh b/ReferenceClientProxyImplementation/formatCache.sh
new file mode 100755
index 0000000..d041b5d
--- /dev/null
+++ b/ReferenceClientProxyImplementation/formatCache.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+rm -rfv ./cache_formatted/*
+cp -rv ./cache/*.js ./cache_formatted/
+cp -rv ./cache/*.css ./cache_formatted/
+npx prettier --write ./cache_formatted/
\ No newline at end of file
|