using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using DorkNet.Server.Services; namespace DorkNet.Server.Controllers.API.Sanitize; /// /// api.{rec.net,localhost}/api/sanitize/* — server-side profanity filter /// the watch's text-input UI calls before allowing a room name / /// invention name / chat message to be submitted. Delegates to /// for the actual scrubbing. Response /// shape: {Text (purified), IsClean (bool)}. /// [ApiController] public class SanitizeController(ILogger logger) : ControllerBase { [HttpPost("api/sanitize/v1")] [HttpPost("api/sanitize/v1/text")] public async Task Sanitize() { // RecRoom.StringSanitization.PurifyString sends JSON body // {"Value":"","ReplacementChar":42} — verified from a // live chat-send error trace ("Received malformed RecNet // response: ...Text:{\"Value\":...}..."). Earlier we used to // pass the whole body through as "text", which made Text in // the response contain the literal JSON string, and the watch's // LitJson then choked on the malformed wrap. string text = string.Empty; try { if (Request.HasFormContentType) text = Request.Form["Text"].ToString(); } catch { } if (string.IsNullOrEmpty(text)) { using var reader = new StreamReader(Request.Body); var raw = await reader.ReadToEndAsync(); if (!string.IsNullOrEmpty(raw)) { var first = raw.AsSpan().TrimStart(); if (first.Length > 0 && first[0] == '{') { try { using var doc = System.Text.Json.JsonDocument.Parse(raw); var root = doc.RootElement; if (root.ValueKind == System.Text.Json.JsonValueKind.Object) { if (root.TryGetProperty("Value", out var vEl) && vEl.ValueKind == System.Text.Json.JsonValueKind.String) text = vEl.GetString() ?? string.Empty; else if (root.TryGetProperty("Text", out var tEl) && tEl.ValueKind == System.Text.Json.JsonValueKind.String) text = tEl.GetString() ?? string.Empty; else if (root.TryGetProperty("Input", out var iEl) && iEl.ValueKind == System.Text.Json.JsonValueKind.String) text = iEl.GetString() ?? string.Empty; } } catch { text = raw; } } else { text = raw; } } } // StringSanitization.PurifyString_d__9 chains Core.Post to // ApiCallback via ExpectPrimitiveResponse, so the watch's // LitJson importer wants a BARE JSON string (e.g. "dwadwa"), not // an object body. Returning {Text, IsClean} as we did before // makes LitJson hand the Dictionary to Convert.ChangeType(_, typeof(string)) // which throws "Object must implement IConvertible". Strip the // envelope — but use Content() with explicit application/json so // ASP.NET Core's StringOutputFormatter doesn't serialize as // text/plain (raw string sans quotes), which the watch then sees // as an empty cleanVersion after LitJson decode. var purified = ProfanityFilter.Purify(text); var json = System.Text.Json.JsonSerializer.Serialize(purified); logger.LogInformation("[sanitize] input=\"{Input}\" purified=\"{Purified}\" json={Json}", text, purified, json); return Content(json, "application/json"); } public sealed class SanitizeBody { public string? Text { get; set; } public string? Input { get; set; } public string? Value { get; set; } public string? Context { get; set; } public int? Intent { get; set; } [System.Text.Json.Serialization.JsonPropertyName("ruleset")] public int? Ruleset { get; set; } } /// POST api/sanitize/v1/purifyString — returns /// {Text} with profanity replaced. Wire shape matches /// StringSanitization.PurifyString. [HttpPost("api/sanitize/v1/purifyString")] public IActionResult PurifyString( [FromBody] SanitizeBody? body, [FromForm(Name = "Text")] string? textForm, [FromForm(Name = "Input")] string? inputForm, [FromForm(Name = "Value")] string? valueForm, [FromQuery(Name = "text")] string? textQuery) { var input = body?.Text ?? body?.Input ?? body?.Value ?? textForm ?? inputForm ?? valueForm ?? textQuery ?? string.Empty; return Ok(new { Text = ProfanityFilter.Purify(input) }); } /// POST api/sanitize/v1/requestIsStringPure — /// returns a bare JSON bool. Watch's ExpectPrimitiveResponse /// rejects object bodies here. [HttpPost("api/sanitize/v1/requestIsStringPure")] public IActionResult IsStringPure( [FromBody] SanitizeBody? body, [FromForm(Name = "Text")] string? textForm, [FromQuery(Name = "text")] string? textQuery) { var input = body?.Text ?? body?.Input ?? body?.Value ?? textForm ?? textQuery ?? string.Empty; return Ok(ProfanityFilter.IsClean(input)); } /// POST api/sanitize/v1/isPure — 2023 route used /// by CV2 maker pen text such as port names. It imports an object /// field named IsPure. [HttpPost("api/sanitize/v1/isPure")] public IActionResult IsPureV1( [FromBody] SanitizeBody? body, [FromForm(Name = "Text")] string? textForm, [FromForm(Name = "Input")] string? inputForm, [FromForm(Name = "Value")] string? valueForm, [FromQuery(Name = "text")] string? textQuery) { var input = body?.Text ?? body?.Input ?? body?.Value ?? textForm ?? inputForm ?? valueForm ?? textQuery ?? string.Empty; var isPure = ProfanityFilter.IsClean(input); logger.LogInformation( "[sanitize] isPure context={Context} intent={Intent} ruleset={Ruleset} valueLength={ValueLength} isPure={IsPure}", body?.Context ?? string.Empty, body?.Intent, body?.Ruleset, input.Length, isPure); var json = System.Text.Json.JsonSerializer.Serialize(new { IsPure = isPure }); return Content(json, "application/json"); } /// POST api/sanitize/isPure — December 2020 route /// used by room/settings forms. Its importer reads an IsPure /// object field, unlike the newer bare-bool route above. [HttpPost("api/sanitize/isPure")] public IActionResult IsPure( [FromBody] SanitizeBody? body, [FromForm(Name = "Text")] string? textForm, [FromQuery(Name = "text")] string? textQuery) { var input = body?.Text ?? body?.Input ?? body?.Value ?? textForm ?? textQuery ?? string.Empty; return Ok(new { IsPure = ProfanityFilter.IsClean(input) }); } }