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) });
}
}