Files
2026-10-16 23:33:13 -07:00

862 lines
33 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Enums.Messages;
using RadicadeServer.RecNet.Messages;
using RadicadeServer.RecNet.Notifications;
using System.Text.Encodings.Web;
using System.Text.Json;
namespace RadicadeServer.API
{
public enum QueryMode
{
Latest = 0,
NewerThan = 1,
OlderThan = 2
}
[Route("chat/")]
public class ThreadAPI(RecDBContext _db) : Controller
{
private const int Success = 0;
private const int ThreadNotFound = 2;
private const int MembershipNotFound = 3;
private const long SystemSenderPlayerId = -5;
private const int DefaultMessageCount = 50;
private const int MaxMessageCount = 100;
private const int ChatThreadTypeDefault = 0;
[HttpGet("thread/party"), Authorize]
[HttpPost("thread/party"), Authorize]
public IActionResult GetPartyThread()
{
return Ok(Array.Empty<object>());
}
[HttpGet("thread"), Authorize]
public async Task<IActionResult> GetThread([FromQuery] int maxCount, [FromQuery] QueryMode mode, [FromQuery] long? messageId = null, [FromQuery] int count = 0)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var threadLimit = count > 0 ? count : maxCount;
var responses = new List<(DateTime LastMessageTime, long ThreadId, Dictionary<string, object?> Response)>();
foreach (var thread in await _db.Chats.ToListAsync())
{
var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value);
if (!playerIds.Contains(userId.Value))
continue;
var latest = (thread.GetMessages() ?? new List<ChatMessage>())
.OrderByDescending(m => m.chatMessageId)
.FirstOrDefault();
responses.Add((latest?.timeSent ?? DateTime.MinValue, thread.chatThreadId, ToListResponse(thread, latest, playerIds, userId.Value)));
}
var orderedResponses = responses
.OrderByDescending(x => x.LastMessageTime)
.ThenByDescending(x => x.ThreadId)
.Select(x => x.Response);
if (threadLimit > 0)
orderedResponses = orderedResponses.Take(threadLimit);
return Ok(orderedResponses.ToList());
}
[HttpPost("thread/withmembers"), Authorize]
public async Task<IActionResult> CreateThreadWithMembers()
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var request = await ReadChatThreadRequest(userId.Value);
var thread = await FindOrCreateThread(request.PlayerIds, request.ThreadName, userId.Value);
return Ok(ToResponse(thread, userId.Value));
}
[HttpPost("thread"), Authorize]
public async Task<IActionResult> CreateThread()
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var request = await ReadChatThreadRequest(userId.Value);
var thread = await FindOrCreateThread(request.PlayerIds, request.ThreadName, userId.Value);
var posted = false;
if (!string.IsNullOrWhiteSpace(request.MessageContents))
{
var message = await AddMessageToThread(thread, userId.Value, request.MessageContents);
await NotifyChatMessage(thread, message);
posted = true;
}
var chatThreadResponse = ToResponse(thread, userId.Value);
return Ok(new Dictionary<string, object>
{
["chatResult"] = posted ? 0 : 1,
["chatThread"] = chatThreadResponse
});
}
[HttpGet("thread/{threadId:long}"), Authorize]
public async Task<IActionResult> GetThreadById(long threadId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return NotFound();
var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value);
if (!playerIds.Contains(userId.Value))
return NotFound();
var (mode, messageId) = ReadQueryWindow();
var messages = PageMessages(thread.GetMessages() ?? new List<ChatMessage>(), mode, messageId, ReadMessageCount(DefaultMessageCount));
return Ok(ToResponse(thread, userId.Value, messages, playerIds));
}
[HttpGet("thread/{threadId:long}/message"), Authorize]
public async Task<IActionResult> GetThreadMessages(long threadId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return NotFound();
var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value);
if (!playerIds.Contains(userId.Value))
return NotFound();
var (mode, messageId) = ReadQueryWindow();
var messages = PageMessages(thread.GetMessages() ?? new List<ChatMessage>(), mode, messageId, ReadMessageCount(DefaultMessageCount));
return Ok(ToMessageResponses(messages));
}
[HttpPost("thread/{threadId:long}"), Authorize]
[HttpPost("thread/{threadId:long}/message"), Authorize]
public async Task<IActionResult> SendMessageToThread(long threadId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return NotFound();
var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value);
if (!playerIds.Contains(userId.Value))
return NotFound();
var request = await ReadChatThreadRequest(userId.Value);
var posted = !string.IsNullOrWhiteSpace(request.MessageContents);
if (posted)
{
var message = await AddMessageToThread(thread, userId.Value, request.MessageContents);
await NotifyChatMessage(thread, message);
}
var chatThreadResponse = ToResponse(thread, userId.Value);
return Ok(new Dictionary<string, object?>
{
["chatResult"] = posted ? 0 : 1,
["chatThread"] = chatThreadResponse
});
}
[HttpPost("thread/{threadId:long}/rename"), Authorize]
public async Task<IActionResult> RenameThread(long threadId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return NotFound();
var body = await ReadJsonBody();
var newName = GetStringFromBody(body, "chatThreadName", "ChatThreadName", "threadName", "ThreadName", "name", "Name");
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
newName = GetStringFromForm(form, "chatThreadName", "ChatThreadName", "threadName", "ThreadName", "name", "Name") ?? newName;
}
if (string.IsNullOrWhiteSpace(newName))
return Ok(ChatResult(4));
thread.chatThreadName = newName.Trim();
await _db.SaveChangesAsync();
var chatThreadResponse = ToResponse(thread, userId.Value);
return Ok(new Dictionary<string, object>
{
["chatResult"] = 0,
["chatThread"] = chatThreadResponse
});
}
[HttpPost("thread/{threadId:long}/member/{playerId:long}"), Authorize]
public async Task<IActionResult> AddThreadMember(long threadId, long playerId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return NotFound();
if (playerId <= 0) return BadRequest();
var playerIds = thread.GetPlayerIds() ?? new List<long>();
playerIds = playerIds.Where(x => x > 0).Distinct().ToList();
if (!playerIds.Contains(playerId))
{
playerIds.Add(playerId);
thread.playerIds = playerIds.OrderBy(x => x).ToJson();
await _db.SaveChangesAsync();
}
var chatThreadResponse = ToResponse(thread, userId.Value);
return Ok(new Dictionary<string, object>
{
["chatResult"] = 0,
["chatThread"] = chatThreadResponse
});
}
[HttpPut("thread/{threadId:long}/favorite"), Authorize]
[HttpPost("thread/{threadId:long}/favorite"), Authorize]
[HttpDelete("thread/{threadId:long}/favorite"), Authorize]
public async Task<IActionResult> FavoriteThread(long threadId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return Ok(ChatResult(2));
var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value);
if (!playerIds.Contains(userId.Value)) return Ok(ChatResult(3));
var favorite = Request.Method.Equals("DELETE", StringComparison.OrdinalIgnoreCase)
? false
: await ReadBoolValue("isFavorited", "IsFavorited", "favorite", "Favorite", "value", "Value");
var favorited = thread.GetFavoritedBy();
if (favorite)
{
if (!favorited.Contains(userId.Value)) favorited.Add(userId.Value);
}
else
{
favorited.Remove(userId.Value);
}
thread.SetFavoritedBy(favorited);
await _db.SaveChangesAsync();
return Ok(new Dictionary<string, object?>
{
["chatResult"] = 0,
["isFavorited"] = favorite
});
}
[HttpPost("thread/{threadId:long}/snooze"), Authorize]
public async Task<IActionResult> SnoozeThread(long threadId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return Ok(ChatResult(2));
var snooze = await ReadBoolValue("snooze", "Snooze");
var snoozedUntil = snooze ? DateTime.UtcNow.AddYears(1) : (DateTime?)null;
return Ok(new Dictionary<string, object?>
{
["chatResult"] = 0,
["snoozedUntil"] = snoozedUntil
});
}
[HttpPost("thread/{threadId:long}/leave"), Authorize]
public async Task<IActionResult> LeaveThread(long threadId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return Ok(ChatResult(2));
var playerIds = thread.GetPlayerIds() ?? new List<long>();
playerIds = playerIds.Where(x => x != userId.Value).Distinct().OrderBy(x => x).ToList();
thread.playerIds = playerIds.ToJson();
await _db.SaveChangesAsync();
if (playerIds.Count > 0)
await AddMessageToThread(thread, SystemSenderPlayerId, MessageEnvelope($"Player <@U{userId.Value}> left"));
return Ok(ChatResult(0));
}
[HttpPost("thread/{threadId:long}/message/{messageId:long}/read"), Authorize]
public async Task<IActionResult> ReadThreadMessage(long threadId, long messageId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return Ok(ChatResult(ThreadNotFound));
var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId.Value);
if (!playerIds.Contains(userId.Value)) return Ok(ChatResult(MembershipNotFound));
if (messageId > thread.lastReadMessageId)
{
thread.lastReadMessageId = messageId;
await _db.SaveChangesAsync();
}
return Ok(ChatResult(Success));
}
[HttpPost("thread/{threadId:long}/read"), Authorize]
public async Task<IActionResult> ReadThread(long threadId)
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var thread = await _db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
if (thread == null) return Ok(ChatResult(2));
var messageId = await ReadLongValue("messageId", "MessageId", "lastReadMessageId", "LastReadMessageId");
if (messageId.HasValue)
{
thread.lastReadMessageId = messageId.Value;
await _db.SaveChangesAsync();
}
return Ok(ChatResult(0));
}
private async Task<ChatThreadRequest> ReadChatThreadRequest(long userId)
{
var body = await ReadJsonBody();
var playerIds = new List<long> { userId };
AddIdsFromBody(body, playerIds);
AddIdsFromQuery(Request.Query, playerIds);
var threadName = GetStringFromBody(body, "chatThreadName", "ChatThreadName", "threadName", "ThreadName", "name", "Name") ?? "";
var messageContents = GetStringFromBody(body, "messageContents", "MessageContents", "contents", "Contents", "message", "Message") ?? "";
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
AddIdsFromForm(form, playerIds);
threadName = GetStringFromForm(form, "chatThreadName", "ChatThreadName", "threadName", "ThreadName", "name", "Name") ?? threadName;
messageContents = GetStringFromForm(form, "messageContents", "MessageContents", "contents", "Contents", "message", "Message") ?? messageContents;
}
var normalizedPlayerIds = playerIds
.Where(x => x > 0)
.Distinct()
.OrderBy(x => x)
.ToList();
return new ChatThreadRequest(normalizedPlayerIds, threadName, messageContents);
}
private async Task<ChatThread> FindOrCreateThread(List<long> playerIds, string threadName, long callerId)
{
var thread = await FindThread(playerIds);
if (thread == null)
{
thread = new ChatThread
{
chatThreadName = string.IsNullOrWhiteSpace(threadName) ? "" : threadName,
lastReadMessageId = 0,
messages = "[]",
playerIds = playerIds.ToJson()
};
_db.Chats.Add(thread);
await _db.SaveChangesAsync();
var other = playerIds.FirstOrDefault(x => x != callerId);
await AddMessageToThread(thread, SystemSenderPlayerId, ChatStartedNotice(other != 0 ? other : callerId));
}
return thread;
}
private static string MessageEnvelope(string text) => JsonSerializer.Serialize(new Dictionary<string, object>
{
["Type"] = 0,
["Version"] = 1,
["Data"] = text
}, EnvelopeJsonOptions);
private static readonly JsonSerializerOptions EnvelopeJsonOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static string ChatStartedNotice(long playerId) => MessageEnvelope($"Player <@U{playerId}> started a chat");
private async Task<ChatMessage> AddMessageToThread(ChatThread thread, long userId, string messageContents)
{
messageContents = await ResolvePhotoPlaceholder(userId, messageContents);
var messages = thread.GetMessages() ?? new List<ChatMessage>();
var nextMessageId = messages.Count == 0 ? 1 : messages.Max(x => x.chatMessageId) + 1;
var message = new ChatMessage
{
chatMessageId = nextMessageId,
chatThreadeId = thread.chatThreadId,
senderPlayerId = userId,
timeSent = DateTime.UtcNow,
contents = messageContents
};
messages.Add(message);
thread.lastReadMessageId = nextMessageId;
thread.messages = messages.ToJson();
await _db.SaveChangesAsync();
return message;
}
private async Task<string> ResolvePhotoPlaceholder(long userId, string messageContents)
{
if (userId <= 0) return messageContents;
if (!ChatPhotoMessages.IsPhotoEnvelope(messageContents)) return messageContents;
if (!ChatPhotoMessages.HasUnresolvedImageReference(messageContents)) return messageContents;
string source;
long? imageId = null;
if (RecentImageReferences.TryGet(userId, out var referenced))
{
imageId = referenced;
source = "the image metadata it looked up";
}
else
{
var cutoff = DateTime.Now - ChatPhotoMessages.UploadWindow;
var recentImage = await _db.Images.AsNoTracking()
.Where(image => image.OwnerId == userId && image.CreatedAt >= cutoff)
.OrderByDescending(image => image.CreatedAt)
.ThenByDescending(image => image.Id)
.FirstOrDefaultAsync();
imageId = recentImage?.Id;
source = $"its upload {recentImage?.Name}";
}
if (imageId == null) return messageContents;
var resolved = ChatPhotoMessages.ResolveImageReference(messageContents, imageId.Value);
return resolved;
}
private async Task NotifyChatMessage(ChatThread thread, ChatMessage message)
{
var notificationMessage = ToMessageResponse(message);
var playerIds = (thread.GetPlayerIds() ?? new List<long>())
.Where(x => x > 0)
.Distinct()
.ToList();
foreach (var playerId in playerIds)
{
await Notifications.Notify.SendNotify(playerId, new Notification
{
Id = nameof(NotificationType.ChatMessageReceived),
Msg = notificationMessage
});
}
}
private async Task<JsonElement?> ReadJsonBody()
{
if (Request.ContentLength == 0) return null;
if (Request.ContentType?.Contains("json", StringComparison.OrdinalIgnoreCase) != true) return null;
try
{
return await JsonSerializer.DeserializeAsync<JsonElement>(Request.Body);
}
catch
{
return null;
}
}
private async Task<bool> ReadBoolValue(params string[] keys)
{
var body = await ReadJsonBody();
var bodyValue = GetBoolFromBody(body, keys);
if (bodyValue.HasValue) return bodyValue.Value;
foreach (var key in keys)
{
if (Request.Query.TryGetValue(key, out var queryValue) && bool.TryParse(queryValue.FirstOrDefault(), out var queryBool))
return queryBool;
}
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
foreach (var key in keys)
{
if (form.TryGetValue(key, out var formValue) && bool.TryParse(formValue.FirstOrDefault(), out var formBool))
return formBool;
}
}
return false;
}
private async Task<long?> ReadLongValue(params string[] keys)
{
var body = await ReadJsonBody();
var bodyValue = GetLongFromBody(body, keys);
if (bodyValue.HasValue) return bodyValue.Value;
foreach (var key in keys)
{
if (Request.Query.TryGetValue(key, out var queryValue) && long.TryParse(queryValue.FirstOrDefault(), out var queryLong))
return queryLong;
}
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
foreach (var key in keys)
{
if (form.TryGetValue(key, out var formValue) && long.TryParse(formValue.FirstOrDefault(), out var formLong))
return formLong;
}
}
return null;
}
private async Task<ChatThread?> FindThread(List<long> playerIds)
{
var threads = await _db.Chats.ToListAsync();
return threads.FirstOrDefault(thread =>
{
var existingPlayerIds = thread.GetPlayerIds() ?? new List<long>();
return existingPlayerIds
.Where(x => x > 0)
.Distinct()
.OrderBy(x => x)
.SequenceEqual(playerIds);
});
}
private static Dictionary<string, object?> ToResponse(ChatThread thread, long userId, int pageSize = DefaultMessageCount)
{
var playerIds = NormalizePlayerIds(thread.GetPlayerIds(), userId);
var messages = PageMessages(thread.GetMessages() ?? new List<ChatMessage>(), QueryMode.Latest, null, pageSize);
return ToResponse(thread, userId, messages, playerIds);
}
private static Dictionary<string, object?> ToResponse(ChatThread thread, long userId, List<ChatMessage> messages, List<long> playerIds)
{
playerIds = NormalizePlayerIds(playerIds, userId);
var messageResponses = ToMessageResponses(messages);
return new Dictionary<string, object?>
{
["chatThreadId"] = thread.chatThreadId,
["playerIds"] = playerIds,
["lastReadMessageId"] = thread.lastReadMessageId,
["chatThreadName"] = ThreadName(thread),
["chatThreadType"] = ChatThreadTypeDefault,
["snoozedUntil"] = null,
["isFavorited"] = thread.GetFavoritedBy().Contains(userId),
["messages"] = messageResponses
};
}
private static Dictionary<string, object?> ToListResponse(ChatThread thread, ChatMessage? latestMessage, List<long> playerIds, long viewerId)
{
var latest = latestMessage == null ? null : ToMessageResponse(latestMessage);
return new Dictionary<string, object?>
{
["latestMessage"] = latest,
["chatThreadId"] = thread.chatThreadId,
["playerIds"] = playerIds,
["lastReadMessageId"] = thread.lastReadMessageId,
["chatThreadName"] = ThreadName(thread),
["chatThreadType"] = ChatThreadTypeDefault,
["snoozedUntil"] = null,
["isFavorited"] = thread.GetFavoritedBy().Contains(viewerId)
};
}
private static string MessageContents(string? contents)
{
var raw = contents ?? "";
if (raw.Length == 0) return MessageEnvelope("");
var trimmed = raw.TrimStart();
if (trimmed.StartsWith("{", StringComparison.Ordinal))
{
try
{
using var parsed = System.Text.Json.JsonDocument.Parse(raw);
if (parsed.RootElement.ValueKind == JsonValueKind.Object) return raw;
}
catch (System.Text.Json.JsonException)
{
}
}
return MessageEnvelope(raw);
}
private static List<long> NormalizePlayerIds(IEnumerable<long>? playerIds, long userId)
{
var normalized = (playerIds ?? Enumerable.Empty<long>())
.Where(x => x > 0)
.Distinct()
.OrderBy(x => x)
.ToList();
if (normalized.Count == 0)
normalized.Add(userId);
return normalized;
}
private static string ThreadName(ChatThread thread)
=> string.IsNullOrWhiteSpace(thread.chatThreadName) ? "" : thread.chatThreadName;
private static List<Dictionary<string, object>> ToMessageResponses(IEnumerable<ChatMessage> messages)
{
return messages.Select(ToMessageResponse).ToList();
}
private static Dictionary<string, object> ToMessageResponse(ChatMessage message)
{
var contents = MessageContents(message.contents);
return new Dictionary<string, object>
{
["chatMessageId"] = message.chatMessageId,
["chatThreadId"] = message.chatThreadeId,
["senderPlayerId"] = message.senderPlayerId,
["timeSent"] = message.timeSent,
["contents"] = contents,
["moderationState"] = 0
};
}
private static void AddIdsFromBody(JsonElement? body, List<long> playerIds)
{
if (!body.HasValue || body.Value.ValueKind != JsonValueKind.Object) return;
foreach (var key in new[] { "ids", "Ids", "playerIds", "PlayerIds", "recipientPlayerIds", "RecipientPlayerIds", "memberIds", "MemberIds", "memberPlayerIds", "MemberPlayerIds", "playerId", "PlayerId", "toPlayerId", "ToPlayerId" })
{
if (body.Value.TryGetProperty(key, out var value))
AddIdsFromJson(value, playerIds);
}
}
private static void AddIdsFromJson(JsonElement value, List<long> playerIds)
{
switch (value.ValueKind)
{
case JsonValueKind.Array:
foreach (var item in value.EnumerateArray())
AddIdsFromJson(item, playerIds);
break;
case JsonValueKind.Number:
if (value.TryGetInt64(out var id) && id > 0)
playerIds.Add(id);
break;
case JsonValueKind.String:
AddIdsFromStrings(new[] { value.GetString() ?? "" }, playerIds);
break;
}
}
private static void AddIdsFromQuery(IQueryCollection query, List<long> playerIds)
{
foreach (var key in new[] { "ids", "Ids", "playerIds", "PlayerIds", "recipientPlayerIds", "RecipientPlayerIds", "memberIds", "MemberIds", "memberPlayerIds", "MemberPlayerIds", "playerId", "PlayerId", "toPlayerId", "ToPlayerId" })
{
if (query.TryGetValue(key, out var values))
AddIdsFromStrings(values, playerIds);
}
}
private static void AddIdsFromForm(IFormCollection form, List<long> playerIds)
{
foreach (var key in new[] { "ids", "Ids", "playerIds", "PlayerIds", "recipientPlayerIds", "RecipientPlayerIds", "memberIds", "MemberIds", "memberPlayerIds", "MemberPlayerIds", "playerId", "PlayerId", "toPlayerId", "ToPlayerId" })
{
if (form.TryGetValue(key, out var values))
AddIdsFromStrings(values, playerIds);
}
}
private static void AddIdsFromStrings(IEnumerable<string?> values, List<long> playerIds)
{
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value)) continue;
foreach (var part in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
var clean = part.Trim('[', ']', '"', '\'');
if (long.TryParse(clean, out var id) && id > 0)
playerIds.Add(id);
}
}
}
private static string? GetStringFromBody(JsonElement? body, params string[] keys)
{
if (!body.HasValue || body.Value.ValueKind != JsonValueKind.Object) return null;
foreach (var key in keys)
{
if (body.Value.TryGetProperty(key, out var value) && value.ValueKind == JsonValueKind.String)
return value.GetString();
}
return null;
}
private static bool? GetBoolFromBody(JsonElement? body, params string[] keys)
{
if (!body.HasValue || body.Value.ValueKind != JsonValueKind.Object) return null;
foreach (var key in keys)
{
if (!body.Value.TryGetProperty(key, out var value)) continue;
if (value.ValueKind == JsonValueKind.True) return true;
if (value.ValueKind == JsonValueKind.False) return false;
if (value.ValueKind == JsonValueKind.String && bool.TryParse(value.GetString(), out var parsed))
return parsed;
}
return null;
}
private static long? GetLongFromBody(JsonElement? body, params string[] keys)
{
if (!body.HasValue || body.Value.ValueKind != JsonValueKind.Object) return null;
foreach (var key in keys)
{
if (!body.Value.TryGetProperty(key, out var value)) continue;
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var parsedNumber))
return parsedNumber;
if (value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out var parsedString))
return parsedString;
}
return null;
}
private static string? GetStringFromForm(IFormCollection form, params string[] keys)
{
foreach (var key in keys)
{
if (form.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value.FirstOrDefault()))
return value.FirstOrDefault();
}
return null;
}
private static int ChatResult(int result) => result;
private int ReadMessageCount(int fallback)
{
foreach (var key in new[] { "MessageCount", "messageCount", "count", "Count" })
{
if (Request.Query.TryGetValue(key, out var values) && int.TryParse(values.FirstOrDefault(), out var parsed))
return Math.Clamp(parsed, 1, MaxMessageCount);
}
return Math.Clamp(fallback, 1, MaxMessageCount);
}
private (QueryMode Mode, long? MessageId) ReadQueryWindow()
{
var mode = QueryMode.Latest;
foreach (var key in new[] { "Mode", "mode" })
{
if (Request.Query.TryGetValue(key, out var values) && TryParseQueryMode(values.FirstOrDefault(), out var parsed))
{
mode = parsed;
break;
}
}
long? messageId = null;
foreach (var key in new[] { "ReferenceMessageId", "referenceMessageId", "MessageId", "messageId", "LastMessageId", "lastMessageId" })
{
if (Request.Query.TryGetValue(key, out var values) && long.TryParse(values.FirstOrDefault(), out var parsed))
{
messageId = parsed;
break;
}
}
return (mode, messageId);
}
private static bool TryParseQueryMode(string? raw, out QueryMode mode)
{
mode = QueryMode.Latest;
if (string.IsNullOrWhiteSpace(raw)) return false;
if (int.TryParse(raw, out var numeric) && Enum.IsDefined(typeof(QueryMode), numeric))
{
mode = (QueryMode)numeric;
return true;
}
return Enum.TryParse(raw, ignoreCase: true, out mode);
}
private static List<ChatMessage> PageMessages(List<ChatMessage> stored, QueryMode mode, long? messageId, int pageSize)
{
var window = mode switch
{
QueryMode.NewerThan when messageId.HasValue => stored.Where(m => m.chatMessageId > messageId.Value),
QueryMode.OlderThan when messageId.HasValue => stored.Where(m => m.chatMessageId < messageId.Value),
_ => stored
};
return window
.OrderByDescending(m => m.chatMessageId)
.Take(Math.Clamp(pageSize, 1, MaxMessageCount))
.ToList();
}
private sealed record ChatThreadRequest(List<long> PlayerIds, string ThreadName, string MessageContents);
}
}