Add chat message handling and WebSocket integration

Introduce new `ChatService` for managing chat messages, including marking messages as read and checking read status. Add `WebSocketPacket` class for handling WebSocket communication and integrate it with `WebSocketService` to process chat-related packets. Enhance `ChatRoom` and `ChatMember` models with additional fields and relationships. Update `AppDatabase` to include new chat-related entities and adjust permissions for chat creation.
This commit is contained in:
2025-05-02 19:51:32 +08:00
parent da6a891b5f
commit 17de9a0f23
18 changed files with 483 additions and 1819 deletions

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("/chat")]
public class ChatController : ControllerBase
{
public class MarkMessageReadRequest
{
public Guid MessageId { get; set; }
public long ChatRoomId { get; set; }
}
}

View File

@ -37,6 +37,7 @@ public enum ChatMemberRole
public class ChatMember : ModelBase
{
public Guid Id { get; set; }
public long ChatRoomId { get; set; }
[JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!;
public long AccountId { get; set; }

View File

@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Realm;
using DysonNetwork.Sphere.Storage;
using Microsoft.AspNetCore.Authorization;
namespace DysonNetwork.Sphere.Chat;
@ -17,6 +19,7 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase
.Where(c => c.Id == id)
.Include(e => e.Picture)
.Include(e => e.Background)
.Include(e => e.Realm)
.FirstOrDefaultAsync();
if (chatRoom is null) return NotFound();
return Ok(chatRoom);
@ -51,6 +54,8 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase
}
[HttpPost]
[Authorize]
[RequiredPermission("global", "chat.create")]
public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
@ -65,7 +70,8 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase
new()
{
Role = ChatMemberRole.Owner,
AccountId = currentUser.Id
AccountId = currentUser.Id,
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
}
}
};
@ -105,7 +111,7 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase
return Ok(chatRoom);
}
[HttpPut("{id:long}")]
[HttpPatch("{id:long}")]
public async Task<ActionResult<ChatRoom>> UpdateChatRoom(long id, [FromBody] ChatRoomRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();

View File

@ -0,0 +1,48 @@
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Chat;
using Microsoft.EntityFrameworkCore;
public class ChatService(AppDatabase db)
{
public async Task MarkMessageAsReadAsync(Guid messageId, long roomId, long userId)
{
var existingStatus = await db.ChatStatuses
.FirstOrDefaultAsync(x => x.MessageId == messageId && x.Sender.AccountId == userId);
var sender = await db.ChatMembers
.Where(m => m.AccountId == userId && m.ChatRoomId == roomId)
.FirstOrDefaultAsync();
if (sender is null) throw new ArgumentException("User is not a member of the chat room.");
if (existingStatus == null)
{
existingStatus = new MessageStatus
{
MessageId = messageId,
SenderId = sender.Id,
};
db.ChatStatuses.Add(existingStatus);
}
await db.SaveChangesAsync();
}
public async Task<bool> GetMessageReadStatus(Guid messageId, long userId)
{
return await db.ChatStatuses
.AnyAsync(x => x.MessageId == messageId && x.Sender.AccountId == userId);
}
public async Task<int> CountUnreadMessage(long userId, long chatRoomId)
{
var messages = await db.ChatMessages
.Where(m => m.ChatRoomId == chatRoomId)
.Select(m => new MessageStatusResponse
{
MessageId = m.Id,
IsRead = m.Statuses.Any(rs => rs.Sender.AccountId == userId)
})
.ToListAsync();
return messages.Count(m => !m.IsRead);
}
}

View File

@ -0,0 +1,67 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Net.Mail;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Sphere.Chat;
public class Message : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Type { get; set; } = null!;
[MaxLength(4096)] public string Content { get; set; } = string.Empty;
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
[Column(TypeName = "jsonb")] public List<Guid>? MembersMetioned { get; set; }
public Instant? EditedAt { get; set; }
public ICollection<Attachment> Attachments { get; set; } = new List<Attachment>();
public ICollection<MessageReaction> Reactions { get; set; } = new List<MessageReaction>();
public ICollection<MessageStatus> Statuses { get; set; } = new List<MessageStatus>();
public Guid? RepliedMessageId { get; set; }
public Message? RepliedMessage { get; set; }
public Guid? ForwardedMessageId { get; set; }
public Message? ForwardedMessage { get; set; }
public Guid SenderId { get; set; }
public ChatMember Sender { get; set; } = null!;
public long ChatRoomId { get; set; }
[JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!;
}
public enum MessageReactionAttitude
{
Positive,
Neutral,
Negative,
}
public class MessageReaction : ModelBase
{
public Guid MessageId { get; set; }
[JsonIgnore] public Message Message { get; set; } = null!;
public Guid SenderId { get; set; }
public ChatMember Sender { get; set; } = null!;
[MaxLength(256)] public string Symbol { get; set; } = null!;
public MessageReactionAttitude Attitude { get; set; }
}
/// If the status is exist, means the user has read the message.
public class MessageStatus : ModelBase
{
public Guid MessageId { get; set; }
public Message Message { get; set; } = null!;
public Guid SenderId { get; set; }
public ChatMember Sender { get; set; } = null!;
public Instant ReadAt { get; set; }
}
[NotMapped]
public class MessageStatusResponse
{
public Guid MessageId { get; set; }
public bool IsRead { get; set; }
}