Typing indicator, mark as read server-side

This commit is contained in:
2025-05-18 05:35:14 +08:00
parent 5951dab6f1
commit c597df3937
13 changed files with 3646 additions and 36 deletions

View File

@ -1,16 +1,27 @@
using System.Net.WebSockets;
using DysonNetwork.Sphere.Chat;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Internal;
using SystemClock = NodaTime.SystemClock;
namespace DysonNetwork.Sphere.Connection.Handlers;
public class MessageReadHandler(AppDatabase db) : IWebSocketPacketHandler
public class MessageReadHandler(AppDatabase db, IMemoryCache cache, ChatRoomService crs) : IWebSocketPacketHandler
{
public string PacketType => "message.read";
public string PacketType => "messages.read";
public async Task HandleAsync(Account.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket)
public const string ChatMemberCacheKey = "ChatMember_{0}_{1}";
public async Task HandleAsync(
Account.Account currentUser,
string deviceId,
WebSocketPacket packet,
WebSocket socket,
WebSocketService srv
)
{
var request = packet.GetData<Chat.ChatController.MarkMessageReadRequest>();
var request = packet.GetData<ChatController.MarkMessageReadRequest>();
if (request is null)
{
await socket.SendAsync(
@ -26,12 +37,24 @@ public class MessageReadHandler(AppDatabase db) : IWebSocketPacketHandler
return;
}
var existingStatus = await db.ChatStatuses
.FirstOrDefaultAsync(x => x.MessageId == request.MessageId && x.Sender.AccountId == currentUser.Id);
var sender = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
.FirstOrDefaultAsync();
ChatMember? sender = null;
var cacheKey = string.Format(ChatMemberCacheKey, currentUser.Id, request.ChatRoomId);
if (cache.TryGetValue(cacheKey, out ChatMember? cachedMember))
sender = cachedMember;
else
{
sender = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
.FirstOrDefaultAsync();
if (sender != null)
{
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
cache.Set(cacheKey, sender, cacheOptions);
}
}
if (sender is null)
{
await socket.SendAsync(
@ -47,16 +70,25 @@ public class MessageReadHandler(AppDatabase db) : IWebSocketPacketHandler
return;
}
if (existingStatus == null)
db.ChatStatuses.Add(new MessageStatus
{
existingStatus = new MessageStatus
{
MessageId = request.MessageId,
SenderId = sender.Id,
};
db.ChatStatuses.Add(existingStatus);
}
MessageId = request.MessageId,
SenderId = sender.Id,
ReadAt = SystemClock.Instance.GetCurrentInstant(),
});
await db.SaveChangesAsync();
try
{
await db.SaveChangesAsync();
// Broadcast read statuses
var otherMembers = (await crs.ListRoomMembers(request.ChatRoomId)).Select(m => m.AccountId).ToList();
foreach (var member in otherMembers)
srv.SendPacketToAccount(member, packet);
}
catch
{
// ignored
}
}
}

View File

@ -0,0 +1,74 @@
using System.Net.WebSockets;
using DysonNetwork.Sphere.Chat;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace DysonNetwork.Sphere.Connection.Handlers;
public class MessageTypingHandler(AppDatabase db, ChatRoomService crs, IMemoryCache cache) : IWebSocketPacketHandler
{
public string PacketType => "messages.typing";
public async Task HandleAsync(
Account.Account currentUser,
string deviceId,
WebSocketPacket packet,
WebSocket socket,
WebSocketService srv
)
{
var request = packet.GetData<ChatController.TypingMessageRequest>();
if (request is null)
{
await socket.SendAsync(
new ArraySegment<byte>(new WebSocketPacket
{
Type = WebSocketPacketType.Error,
ErrorMessage = "Mark message as read requires you provide the ChatRoomId"
}.ToBytes()),
WebSocketMessageType.Binary,
true,
CancellationToken.None
);
return;
}
ChatMember? sender = null;
var cacheKey = string.Format(MessageReadHandler.ChatMemberCacheKey, currentUser.Id, request.ChatRoomId);
if (cache.TryGetValue(cacheKey, out ChatMember? cachedMember))
sender = cachedMember;
else
{
sender = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
.FirstOrDefaultAsync();
if (sender != null)
{
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
cache.Set(cacheKey, sender, cacheOptions);
}
}
if (sender is null)
{
await socket.SendAsync(
new ArraySegment<byte>(new WebSocketPacket
{
Type = WebSocketPacketType.Error,
ErrorMessage = "User is not a member of the chat room."
}.ToBytes()),
WebSocketMessageType.Binary,
true,
CancellationToken.None
);
return;
}
// Broadcast read statuses
var otherMembers = (await crs.ListRoomMembers(request.ChatRoomId)).Select(m => m.AccountId).ToList();
foreach (var member in otherMembers)
srv.SendPacketToAccount(member, packet);
}
}

View File

@ -5,5 +5,5 @@ namespace DysonNetwork.Sphere.Connection;
public interface IWebSocketPacketHandler
{
string PacketType { get; }
Task HandleAsync(Account.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket);
Task HandleAsync(Account.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket, WebSocketService srv);
}

View File

@ -37,13 +37,17 @@ public class WebSocketPacket
/// <returns>Deserialized data of type T</returns>
public T? GetData<T>()
{
if (Data == null)
return default;
if (Data is T typedData)
return typedData;
var jsonOpts = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
};
return JsonSerializer.Deserialize<T>(
JsonSerializer.Serialize(Data)
JsonSerializer.Serialize(Data, jsonOpts),
jsonOpts
);
}

View File

@ -85,7 +85,7 @@ public class WebSocketService
{
if (_handlerMap.TryGetValue(packet.Type, out var handler))
{
await handler.HandleAsync(currentUser, deviceId, packet, socket);
await handler.HandleAsync(currentUser, deviceId, packet, socket, this);
return;
}