using System.Collections;
namespace CampusNet.Services;
///
/// Room chat crosses the game server as RPC event 36 carrying the text wrapped as
/// "<noparse>...</noparse>". A normal client has already run it through the Campus
/// server's chat filter, so what arrives is masked. Each chat message is held until the Campus
/// server (POST /photon/chatguard, localhost) says whether its filter would still change it -
/// if so the text skipped the filter (a chat prompt echoed into chat by a circuit, or a client
/// that never asked), the message is dropped and the sender disconnected; the Campus server
/// times them out. If the Campus server does not answer in time the message goes through:
/// chat must never stop working because of this check. CAMPUSNET_CHAT_GUARD=0 turns it off.
///
public static class ChatGuard
{
public static readonly bool Enabled = Environment.GetEnvironmentVariable("CAMPUSNET_CHAT_GUARD") != "0";
private static readonly string Url = Environment.GetEnvironmentVariable("CAMPUSNET_CHATGUARD_URL") ?? "http://127.0.0.1:8080/photon/chatguard";
private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromMilliseconds(1500) };
private const string Open = "", Close = "";
/// The chat text inside an event-36 payload, or null when it is not a chat message.
public static string? ChatText(object? o, int depth = 0)
{
if (o == null || depth > 4) return null;
if (o is string s)
return s.Length > Open.Length + Close.Length && s.StartsWith(Open, StringComparison.Ordinal) && s.EndsWith(Close, StringComparison.Ordinal)
? s.Substring(Open.Length, s.Length - Open.Length - Close.Length) : null;
if (o is Hashtable ht) { foreach (DictionaryEntry e in ht) { var t = ChatText(e.Value, depth + 1); if (t != null) return t; } return null; }
if (o is IEnumerable en and not byte[]) { int n = 0; foreach (var x in en) { if (n++ > 64) break; var t = ChatText(x, depth + 1); if (t != null) return t; } }
return null;
}
/// True only when the Campus server says the text skipped the filter; false on any doubt.
public static async Task IsBypass(string? userId, string room, string text)
{
if (string.IsNullOrEmpty(userId) || !ulong.TryParse(userId, out _)) return false;
try
{
using var body = new FormUrlEncodedContent(new[]
{
new KeyValuePair("id", userId),
new KeyValuePair("room", room),
new KeyValuePair("text", text),
});
using var resp = await Http.PostAsync(Url, body);
return resp.IsSuccessStatusCode && (await resp.Content.ReadAsStringAsync()).Trim() == "bypass";
}
catch { return false; }
}
}