mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
862 lines
33 KiB
C#
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);
|
|
}
|
|
}
|