2025-08-19 03:08:30 -05:00
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
|
using LANCommander.Server.Data.Models;
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using ZiggyCreatures.Caching.Fusion;
|
|
|
|
|
|
|
|
|
|
|
|
namespace LANCommander.Server.Services
|
|
|
|
|
|
{
|
|
|
|
|
|
public sealed class ChatService(
|
|
|
|
|
|
ILogger<ChatService> logger,
|
2025-11-16 12:31:25 -06:00
|
|
|
|
SettingsProvider<Settings.Settings> settingsProvider,
|
2025-08-19 03:08:30 -05:00
|
|
|
|
IFusionCache cache,
|
|
|
|
|
|
ChatMessageService chatMessageService,
|
2025-08-28 19:57:23 -05:00
|
|
|
|
ChatThreadService chatThreadService,
|
2025-11-12 18:26:25 -06:00
|
|
|
|
ChatThreadReadStatusService chatThreadReadStatusService,
|
2025-11-16 12:31:25 -06:00
|
|
|
|
UserService userService) : BaseService(logger, settingsProvider)
|
2025-08-19 03:08:30 -05:00
|
|
|
|
{
|
|
|
|
|
|
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
|
|
|
|
|
|
private readonly int _maxCachedMessages = 200;
|
|
|
|
|
|
|
|
|
|
|
|
private static string ThreadCacheKey(Guid threadId) => $"Chat/Thread/{threadId}";
|
2025-10-28 22:19:48 -05:00
|
|
|
|
private static string UserThreadCacheKey(Guid userId) => $"Chat/User/{userId}/Threads";
|
2025-08-20 20:54:09 -05:00
|
|
|
|
|
|
|
|
|
|
public async Task<ChatThread> StartThreadAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
var thread = await chatThreadService.AddAsync(new ChatThread());
|
|
|
|
|
|
|
|
|
|
|
|
return thread;
|
|
|
|
|
|
}
|
2025-10-28 22:19:48 -05:00
|
|
|
|
|
|
|
|
|
|
public async Task AddParticipantAsync(Guid threadId, Guid userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var thread = await chatThreadService.Include(t => t.Participants).GetAsync(threadId);
|
|
|
|
|
|
|
|
|
|
|
|
var user = await userService.GetAsync(userId);
|
2025-11-14 00:16:11 -06:00
|
|
|
|
|
|
|
|
|
|
if (thread != null && (thread.Participants == null || thread.Participants.All(p => p.Id != user.Id)))
|
2025-10-28 22:19:48 -05:00
|
|
|
|
{
|
2025-11-13 18:17:45 -06:00
|
|
|
|
logger.LogInformation("Adding participant {UserId} to thread {ThreadId}", user.UserName, thread.Id);
|
2025-11-14 00:16:11 -06:00
|
|
|
|
|
|
|
|
|
|
if (thread.Participants == null)
|
|
|
|
|
|
thread.Participants = new List<User>();
|
2025-11-13 18:17:45 -06:00
|
|
|
|
|
2025-10-28 22:19:48 -05:00
|
|
|
|
thread.Participants.Add(user);
|
|
|
|
|
|
|
|
|
|
|
|
await chatThreadService.UpdateAsync(thread);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-19 03:08:30 -05:00
|
|
|
|
|
|
|
|
|
|
public async Task<ChatMessage> SendMessageAsync(Guid threadId, string content)
|
|
|
|
|
|
{
|
|
|
|
|
|
var message = await chatMessageService.AddAsync(new ChatMessage
|
|
|
|
|
|
{
|
|
|
|
|
|
ThreadId = threadId,
|
|
|
|
|
|
Content = content,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var cacheKey = ThreadCacheKey(threadId);
|
|
|
|
|
|
var gate = _locks.GetOrAdd(cacheKey, _ => new SemaphoreSlim(1, 1));
|
|
|
|
|
|
|
|
|
|
|
|
await gate.WaitAsync();
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var current = await cache.TryGetAsync<List<ChatMessage>>(cacheKey);
|
|
|
|
|
|
var messages = current.HasValue
|
|
|
|
|
|
? new List<ChatMessage>(current.Value)
|
|
|
|
|
|
: new List<ChatMessage>(capacity: _maxCachedMessages);
|
|
|
|
|
|
|
|
|
|
|
|
messages.RemoveAll(m => m.Id == message.Id);
|
|
|
|
|
|
messages.Add(message);
|
|
|
|
|
|
|
|
|
|
|
|
if (messages.Count > _maxCachedMessages)
|
|
|
|
|
|
messages.RemoveRange(0, messages.Count - _maxCachedMessages);
|
|
|
|
|
|
|
|
|
|
|
|
await cache.SetAsync(cacheKey, messages);
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
gate.Release();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<List<ChatMessage>> GetMessagesAsync(Guid threadId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cacheKey = ThreadCacheKey(threadId);
|
|
|
|
|
|
|
|
|
|
|
|
var messages = await cache.GetOrSetAsync(cacheKey, async _ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var dbMessages = await chatMessageService.Query(q =>
|
|
|
|
|
|
{
|
|
|
|
|
|
return q
|
|
|
|
|
|
.OrderByDescending(m => m.CreatedOn)
|
|
|
|
|
|
.Take(_maxCachedMessages);
|
|
|
|
|
|
}).GetAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return dbMessages.Reverse().ToList();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return messages;
|
|
|
|
|
|
}
|
2025-08-28 19:57:23 -05:00
|
|
|
|
|
2025-11-03 23:27:05 -06:00
|
|
|
|
public async Task<ChatThread> GetThreadAsync(Guid threadId)
|
|
|
|
|
|
=> await chatThreadService.Include(t => t.Participants).GetAsync(threadId);
|
|
|
|
|
|
|
2025-10-28 22:19:48 -05:00
|
|
|
|
public async Task<List<ChatThread>> GetThreadsAsync(Guid userId)
|
2025-08-28 19:57:23 -05:00
|
|
|
|
{
|
2025-10-28 22:19:48 -05:00
|
|
|
|
var cacheKey = UserThreadCacheKey(userId);
|
|
|
|
|
|
|
2025-08-28 19:57:23 -05:00
|
|
|
|
var threads = await cache.GetOrSetAsync(cacheKey, async _ =>
|
|
|
|
|
|
{
|
2025-10-28 22:19:48 -05:00
|
|
|
|
var user = await userService.GetAsync(userId);
|
2025-11-05 23:42:36 -06:00
|
|
|
|
|
|
|
|
|
|
if (user == null)
|
|
|
|
|
|
return new List<ChatThread>();
|
2025-08-28 19:57:23 -05:00
|
|
|
|
|
|
|
|
|
|
var dbThreads = await chatThreadService.Query(q =>
|
|
|
|
|
|
{
|
|
|
|
|
|
return q
|
2025-10-28 22:19:48 -05:00
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.AsSplitQuery()
|
2025-11-09 19:23:54 -06:00
|
|
|
|
.Where(t => t.Participants.Any(p => p.Id == userId))
|
|
|
|
|
|
.Include(t => t.Participants);
|
2025-08-28 19:57:23 -05:00
|
|
|
|
}).GetAsync();
|
|
|
|
|
|
|
2025-10-28 22:19:48 -05:00
|
|
|
|
return dbThreads.OrderByDescending(t => t.CreatedOn).ToList();
|
2025-08-28 19:57:23 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return threads;
|
|
|
|
|
|
}
|
2025-11-10 18:06:55 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets a list of users stripped down to only IDs and usernames
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<List<User>> GetUsersAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
var users = await userService.AsNoTracking().GetAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return users.Select(u => new User
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = u.Id,
|
|
|
|
|
|
UserName = u.UserName,
|
|
|
|
|
|
Alias = u.Alias,
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
}
|
2025-11-12 18:26:25 -06:00
|
|
|
|
|
|
|
|
|
|
public async Task UpdateReadStatus(Guid threadId, Guid userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
await chatThreadReadStatusService.UpdateReadStatus(threadId, userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<int> GetUnreadMessageCountAsync(Guid threadId, Guid userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var lastRead = await chatThreadReadStatusService.GetLastReadAsync(threadId, userId);
|
|
|
|
|
|
|
|
|
|
|
|
if (lastRead == null)
|
|
|
|
|
|
return await chatThreadReadStatusService.GetUnreadCountAsync(threadId, lastRead.GetValueOrDefault());
|
|
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
2025-08-19 03:08:30 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|