♻️ Centralized data models (wip)

This commit is contained in:
2025-09-27 14:09:28 +08:00
parent 51b6f7309e
commit e70d8371f8
206 changed files with 1352 additions and 2128 deletions

View File

@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Content;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -33,7 +33,7 @@ public partial class ChatController(
public class ChatSummaryResponse
{
public int UnreadCount { get; set; }
public Message? LastMessage { get; set; }
public SnChatMessage? LastMessage { get; set; }
}
[HttpGet("summary")]
@@ -71,7 +71,7 @@ public partial class ChatController(
}
[HttpGet("{roomId:guid}/messages")]
public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset,
public async Task<ActionResult<List<SnChatMessage>>> ListMessages(Guid roomId, [FromQuery] int offset,
[FromQuery] int take = 20)
{
var currentUser = HttpContext.Items["CurrentUser"] as Account;
@@ -114,7 +114,7 @@ public partial class ChatController(
}
[HttpGet("{roomId:guid}/messages/{messageId:guid}")]
public async Task<ActionResult<Message>> GetMessage(Guid roomId, Guid messageId)
public async Task<ActionResult<SnChatMessage>> GetMessage(Guid roomId, Guid messageId)
{
var currentUser = HttpContext.Items["CurrentUser"] as Account;
@@ -165,7 +165,7 @@ public partial class ChatController(
if (member == null || member.Role < ChatMemberRole.Member)
return StatusCode(403, "You need to be a normal member to send messages here.");
var message = new Message
var message = new SnChatMessage
{
Type = "text",
SenderId = member.Id,
@@ -182,7 +182,7 @@ public partial class ChatController(
var queryResponse = await files.GetFileBatchAsync(queryRequest);
message.Attachments = queryResponse.Files
.OrderBy(f => request.AttachmentsId.IndexOf(f.Id))
.Select(CloudFileReferenceObject.FromProtoValue)
.Select(SnCloudFileReferenceObject.FromProtoValue)
.ToList();
}

View File

@@ -1,145 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using NodaTime;
namespace DysonNetwork.Sphere.Chat;
public enum ChatRoomType
{
Group,
DirectMessage
}
public class ChatRoom : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; }
[MaxLength(1024)] public string? Name { get; set; }
[MaxLength(4096)] public string? Description { get; set; }
public ChatRoomType Type { get; set; }
public bool IsCommunity { get; set; }
public bool IsPublic { get; set; }
// Outdated fields, for backward compability
[MaxLength(32)] public string? PictureId { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>();
public Guid? RealmId { get; set; }
public Realm.Realm? Realm { get; set; }
[NotMapped]
[JsonPropertyName("members")]
public ICollection<ChatMemberTransmissionObject> DirectMembers { get; set; } =
new List<ChatMemberTransmissionObject>();
public string ResourceIdentifier => $"chatroom:{Id}";
}
public abstract class ChatMemberRole
{
public const int Owner = 100;
public const int Moderator = 50;
public const int Member = 0;
}
public enum ChatMemberNotify
{
All,
Mentions,
None
}
public enum ChatTimeoutCauseType
{
ByModerator = 0,
BySlowMode = 1,
}
public class ChatTimeoutCause
{
public ChatTimeoutCauseType Type { get; set; }
public Guid? SenderId { get; set; }
}
public class ChatMember : ModelBase
{
public Guid Id { get; set; }
public Guid ChatRoomId { get; set; }
public ChatRoom ChatRoom { get; set; } = null!;
public Guid AccountId { get; set; }
[NotMapped] public AccountReference? Account { get; set; }
[NotMapped] public AccountStatusReference? Status { get; set; }
[MaxLength(1024)] public string? Nick { get; set; }
public int Role { get; set; } = ChatMemberRole.Member;
public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All;
public Instant? LastReadAt { get; set; }
public Instant? JoinedAt { get; set; }
public Instant? LeaveAt { get; set; }
public bool IsBot { get; set; } = false;
/// <summary>
/// The break time is the user doesn't receive any message from this member for a while.
/// Expect mentioned him or her.
/// </summary>
public Instant? BreakUntil { get; set; }
/// <summary>
/// The timeout is the user can't send any message.
/// Set by the moderator of the chat room.
/// </summary>
public Instant? TimeoutUntil { get; set; }
/// <summary>
/// The timeout cause is the reason why the user is timeout.
/// </summary>
[Column(TypeName = "jsonb")] public ChatTimeoutCause? TimeoutCause { get; set; }
}
public class ChatMemberTransmissionObject : ModelBase
{
public Guid Id { get; set; }
public Guid ChatRoomId { get; set; }
public Guid AccountId { get; set; }
[NotMapped] public AccountReference Account { get; set; } = null!;
[MaxLength(1024)] public string? Nick { get; set; }
public int Role { get; set; } = ChatMemberRole.Member;
public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All;
public Instant? JoinedAt { get; set; }
public Instant? LeaveAt { get; set; }
public bool IsBot { get; set; } = false;
public Instant? BreakUntil { get; set; }
public Instant? TimeoutUntil { get; set; }
public ChatTimeoutCause? TimeoutCause { get; set; }
public static ChatMemberTransmissionObject FromEntity(ChatMember member)
{
return new ChatMemberTransmissionObject
{
Id = member.Id,
ChatRoomId = member.ChatRoomId,
AccountId = member.AccountId,
Account = member.Account!,
Nick = member.Nick,
Role = member.Role,
Notify = member.Notify,
JoinedAt = member.JoinedAt,
LeaveAt = member.LeaveAt,
IsBot = member.IsBot,
BreakUntil = member.BreakUntil,
TimeoutUntil = member.TimeoutUntil,
TimeoutCause = member.TimeoutCause,
CreatedAt = member.CreatedAt,
UpdatedAt = member.UpdatedAt,
DeletedAt = member.DeletedAt
};
}
}

View File

@@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere.Localization;
@@ -12,6 +11,7 @@ using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Localization;
using NodaTime;
using DysonNetwork.Shared.Models;
namespace DysonNetwork.Sphere.Chat;
@@ -31,7 +31,7 @@ public class ChatRoomController(
) : ControllerBase
{
[HttpGet("{id:guid}")]
public async Task<ActionResult<ChatRoom>> GetChatRoom(Guid id)
public async Task<ActionResult<SnChatRoom>> GetChatRoom(Guid id)
{
var chatRoom = await db.ChatRooms
.Where(c => c.Id == id)
@@ -48,7 +48,7 @@ public class ChatRoomController(
[HttpGet]
[Authorize]
public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms()
public async Task<ActionResult<List<SnChatRoom>>> ListJoinedChatRooms()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
@@ -74,7 +74,7 @@ public class ChatRoomController(
[HttpPost("direct")]
[Authorize]
public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request)
public async Task<ActionResult<SnChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
@@ -106,11 +106,11 @@ public class ChatRoomController(
return BadRequest("You already have a DM with this user.");
// Create new DM chat room
var dmRoom = new ChatRoom
var dmRoom = new SnChatRoom
{
Type = ChatRoomType.DirectMessage,
IsPublic = false,
Members = new List<ChatMember>
Members = new List<SnChatMember>
{
new()
{
@@ -148,7 +148,7 @@ public class ChatRoomController(
[HttpGet("direct/{accountId:guid}")]
[Authorize]
public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid accountId)
public async Task<ActionResult<SnChatRoom>> GetDirectChatRoom(Guid accountId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
@@ -178,19 +178,19 @@ public class ChatRoomController(
[HttpPost]
[Authorize]
[RequiredPermission("global", "chat.create")]
public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request)
public async Task<ActionResult<SnChatRoom>> CreateChatRoom(ChatRoomRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
if (request.Name is null) return BadRequest("You cannot create a chat room without a name.");
var chatRoom = new ChatRoom
var chatRoom = new SnChatRoom
{
Name = request.Name,
Description = request.Description ?? string.Empty,
IsCommunity = request.IsCommunity ?? false,
IsPublic = request.IsPublic ?? false,
Type = ChatRoomType.Group,
Members = new List<ChatMember>
Members = new List<SnChatMember>
{
new()
{
@@ -215,7 +215,7 @@ public class ChatRoomController(
{
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
chatRoom.Picture = CloudFileReferenceObject.FromProtoValue(fileResponse);
chatRoom.Picture = SnCloudFileReferenceObject.FromProtoValue(fileResponse);
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
{
@@ -236,7 +236,7 @@ public class ChatRoomController(
{
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud.");
chatRoom.Background = CloudFileReferenceObject.FromProtoValue(fileResponse);
chatRoom.Background = SnCloudFileReferenceObject.FromProtoValue(fileResponse);
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
{
@@ -290,7 +290,7 @@ public class ChatRoomController(
[HttpPatch("{id:guid}")]
public async Task<ActionResult<ChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request)
public async Task<ActionResult<SnChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
@@ -341,7 +341,7 @@ public class ChatRoomController(
ResourceId = chatRoom.ResourceIdentifier
});
chatRoom.Picture = CloudFileReferenceObject.FromProtoValue(fileResponse);
chatRoom.Picture = SnCloudFileReferenceObject.FromProtoValue(fileResponse);
}
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
{
@@ -371,7 +371,7 @@ public class ChatRoomController(
ResourceId = chatRoom.ResourceIdentifier
});
chatRoom.Background = CloudFileReferenceObject.FromProtoValue(fileResponse);
chatRoom.Background = SnCloudFileReferenceObject.FromProtoValue(fileResponse);
}
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
{
@@ -468,7 +468,7 @@ public class ChatRoomController(
[HttpGet("{roomId:guid}/members/me")]
[Authorize]
public async Task<ActionResult<ChatMember>> GetRoomIdentity(Guid roomId)
public async Task<ActionResult<SnChatMember>> GetRoomIdentity(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
return Unauthorized();
@@ -484,7 +484,7 @@ public class ChatRoomController(
}
[HttpGet("{roomId:guid}/members")]
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId,
public async Task<ActionResult<List<SnChatMember>>> ListMembers(Guid roomId,
[FromQuery] int take = 20,
[FromQuery] int offset = 0,
[FromQuery] bool withStatus = false
@@ -561,7 +561,7 @@ public class ChatRoomController(
[HttpPost("invites/{roomId:guid}")]
[Authorize]
public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId,
public async Task<ActionResult<SnChatMember>> InviteMember(Guid roomId,
[FromBody] ChatMemberRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
@@ -620,7 +620,7 @@ public class ChatRoomController(
if (hasExistingMember)
return BadRequest("This user has been joined the chat cannot be invited again.");
var newMember = new ChatMember
var newMember = new SnChatMember
{
AccountId = Guid.Parse(relatedUser.Id),
ChatRoomId = roomId,
@@ -651,7 +651,7 @@ public class ChatRoomController(
[HttpGet("invites")]
[Authorize]
public async Task<ActionResult<List<ChatMember>>> ListChatInvites()
public async Task<ActionResult<List<SnChatMember>>> ListChatInvites()
{
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
@@ -674,7 +674,7 @@ public class ChatRoomController(
[HttpPost("invites/{roomId:guid}/accept")]
[Authorize]
public async Task<ActionResult<ChatRoom>> AcceptChatInvite(Guid roomId)
public async Task<ActionResult<SnChatRoom>> AcceptChatInvite(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
@@ -731,7 +731,7 @@ public class ChatRoomController(
[HttpPatch("{roomId:guid}/members/me/notify")]
[Authorize]
public async Task<ActionResult<ChatMember>> UpdateChatMemberNotify(
public async Task<ActionResult<SnChatMember>> UpdateChatMemberNotify(
Guid roomId,
[FromBody] ChatMemberNotifyRequest request
)
@@ -763,7 +763,7 @@ public class ChatRoomController(
[HttpPatch("{roomId:guid}/members/{memberId:guid}/role")]
[Authorize]
public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole)
public async Task<ActionResult<SnChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole)
{
if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role.");
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
@@ -885,7 +885,7 @@ public class ChatRoomController(
[HttpPost("{roomId:guid}/members/me")]
[Authorize]
public async Task<ActionResult<ChatRoom>> JoinChatRoom(Guid roomId)
public async Task<ActionResult<SnChatRoom>> JoinChatRoom(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
@@ -901,7 +901,7 @@ public class ChatRoomController(
if (existingMember != null)
return BadRequest("You are already a member of this chat room.");
var newMember = new ChatMember
var newMember = new SnChatMember
{
AccountId = Guid.Parse(currentUser.Id),
ChatRoomId = roomId,
@@ -966,7 +966,7 @@ public class ChatRoomController(
return NoContent();
}
private async Task _SendInviteNotify(ChatMember member, Account sender)
private async Task _SendInviteNotify(SnChatMember member, Account sender)
{
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
CultureService.SetCultureInfo(account);

View File

@@ -1,8 +1,9 @@
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Account = DysonNetwork.Shared.Data.AccountReference;
using Account = DysonNetwork.Shared.Data.SnAccount;
namespace DysonNetwork.Sphere.Chat;
@@ -16,10 +17,10 @@ public class ChatRoomService(
private const string RoomMembersCacheKeyPrefix = "chatroom:members:";
private const string ChatMemberCacheKey = "chatroom:{0}:member:{1}";
public async Task<List<ChatMember>> ListRoomMembers(Guid roomId)
public async Task<List<SnChatMember>> ListRoomMembers(Guid roomId)
{
var cacheKey = RoomMembersCacheKeyPrefix + roomId;
var cachedMembers = await cache.GetAsync<List<ChatMember>>(cacheKey);
var cachedMembers = await cache.GetAsync<List<SnChatMember>>(cacheKey);
if (cachedMembers != null)
return cachedMembers;
@@ -38,10 +39,10 @@ public class ChatRoomService(
return members;
}
public async Task<ChatMember?> GetRoomMember(Guid accountId, Guid chatRoomId)
public async Task<SnChatMember?> GetRoomMember(Guid accountId, Guid chatRoomId)
{
var cacheKey = string.Format(ChatMemberCacheKey, accountId, chatRoomId);
var member = await cache.GetAsync<ChatMember?>(cacheKey);
var member = await cache.GetAsync<SnChatMember?>(cacheKey);
if (member is not null) return member;
member = await db.ChatMembers
@@ -66,7 +67,7 @@ public class ChatRoomService(
await cache.RemoveGroupAsync(chatRoomGroup);
}
public async Task<List<ChatRoom>> SortChatRoomByLastMessage(List<ChatRoom> rooms)
public async Task<List<SnChatRoom>> SortChatRoomByLastMessage(List<SnChatRoom> rooms)
{
var roomIds = rooms.Select(r => r.Id).ToList();
var lastMessages = await db.ChatMessages
@@ -83,7 +84,7 @@ public class ChatRoomService(
return sortedRooms;
}
public async Task<List<ChatRoom>> LoadDirectMessageMembers(List<ChatRoom> rooms, Guid userId)
public async Task<List<SnChatRoom>> LoadDirectMessageMembers(List<SnChatRoom> rooms, Guid userId)
{
var directRoomsId = rooms
.Where(r => r.Type == ChatRoomType.DirectMessage)
@@ -91,7 +92,7 @@ public class ChatRoomService(
.ToList();
if (directRoomsId.Count == 0) return rooms;
List<ChatMember> members = directRoomsId.Count != 0
List<SnChatMember> members = directRoomsId.Count != 0
? await db.ChatMembers
.Where(m => directRoomsId.Contains(m.ChatRoomId))
.Where(m => m.AccountId != userId)
@@ -100,7 +101,7 @@ public class ChatRoomService(
: [];
members = await LoadMemberAccounts(members);
Dictionary<Guid, List<ChatMember>> directMembers = new();
Dictionary<Guid, List<SnChatMember>> directMembers = new();
foreach (var member in members)
{
if (!directMembers.ContainsKey(member.ChatRoomId))
@@ -116,7 +117,7 @@ public class ChatRoomService(
}).ToList();
}
public async Task<ChatRoom> LoadDirectMessageMembers(ChatRoom room, Guid userId)
public async Task<SnChatRoom> LoadDirectMessageMembers(SnChatRoom room, Guid userId)
{
if (room.Type != ChatRoomType.DirectMessage) return room;
var members = await db.ChatMembers
@@ -143,14 +144,14 @@ public class ChatRoomService(
return member?.Role >= maxRequiredRole;
}
public async Task<ChatMember> LoadMemberAccount(ChatMember member)
public async Task<SnChatMember> LoadMemberAccount(SnChatMember member)
{
var account = await accountsHelper.GetAccount(member.AccountId);
member.Account = Account.FromProtoValue(account);
return member;
}
public async Task<List<ChatMember>> LoadMemberAccounts(ICollection<ChatMember> members)
public async Task<List<SnChatMember>> LoadMemberAccounts(ICollection<SnChatMember> members)
{
var accountIds = members.Select(m => m.AccountId).ToList();
var accounts = (await accountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);

View File

@@ -1,5 +1,5 @@
using System.Text.RegularExpressions;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.Chat.Realtime;
using DysonNetwork.Sphere.WebReader;
@@ -29,7 +29,7 @@ public partial class ChatService(
/// This method is designed to be called from a background task
/// </summary>
/// <param name="message">The message to process link previews for</param>
private async Task ProcessMessageLinkPreviewAsync(Message message)
private async Task ProcessMessageLinkPreviewAsync(SnChatMessage message)
{
try
{
@@ -66,7 +66,7 @@ public partial class ChatService(
logger.LogDebug($"Updated message {message.Id} with {embedsList.Count} link previews");
// Create and store sync message for link preview update
var syncMessage = new Message
var syncMessage = new SnChatMessage
{
Type = "messages.update.links",
ChatRoomId = dbMessage.ChatRoomId,
@@ -114,7 +114,7 @@ public partial class ChatService(
/// <param name="message">The message to process</param>
/// <param name="webReader">The web reader service</param>
/// <returns>The message with link previews added to its meta data</returns>
public async Task<Message> PreviewMessageLinkAsync(Message message, WebReaderService? webReader = null)
public async Task<SnChatMessage> PreviewMessageLinkAsync(SnChatMessage message, WebReaderService? webReader = null)
{
if (string.IsNullOrEmpty(message.Content))
return message;
@@ -172,9 +172,9 @@ public partial class ChatService(
}
private async Task DeliverWebSocketMessage(
Message message,
SnChatMessage message,
string type,
List<ChatMember> members,
List<SnChatMember> members,
IServiceScope scope
)
{
@@ -195,7 +195,7 @@ public partial class ChatService(
logger.LogInformation($"Delivered message to {request.UserIds.Count} accounts.");
}
public async Task<Message> SendMessageAsync(Message message, ChatMember sender, ChatRoom room)
public async Task<SnChatMessage> SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room)
{
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
message.CreatedAt = SystemClock.Instance.GetCurrentInstant();
@@ -230,9 +230,9 @@ public partial class ChatService(
}
private async Task DeliverMessageAsync(
Message message,
ChatMember sender,
ChatRoom room,
SnChatMessage message,
SnChatMember sender,
SnChatRoom room,
string type = WebSocketPacketType.MessageNew,
bool notify = true
)
@@ -254,11 +254,11 @@ public partial class ChatService(
}
private async Task SendPushNotificationsAsync(
Message message,
ChatMember sender,
ChatRoom room,
SnChatMessage message,
SnChatMember sender,
SnChatRoom room,
string type,
List<ChatMember> members,
List<SnChatMember> members,
IServiceScope scope
)
{
@@ -292,7 +292,7 @@ public partial class ChatService(
logger.LogInformation($"Delivered message to {accountsToNotify.Count} accounts.");
}
private PushNotification BuildNotification(Message message, ChatMember sender, ChatRoom room, string roomSubject,
private PushNotification BuildNotification(SnChatMessage message, SnChatMember sender, SnChatRoom room, string roomSubject,
string type)
{
var metaDict = new Dictionary<string, object>
@@ -325,7 +325,7 @@ public partial class ChatService(
return notification;
}
private string BuildNotificationBody(Message message, string type)
private string BuildNotificationBody(SnChatMessage message, string type)
{
if (message.DeletedAt is not null)
return "Deleted a message";
@@ -356,7 +356,7 @@ public partial class ChatService(
}
}
private List<Account> FilterAccountsForNotification(List<ChatMember> members, Message message, ChatMember sender)
private List<Account> FilterAccountsForNotification(List<SnChatMember> members, SnChatMessage message, SnChatMember sender)
{
var now = SystemClock.Instance.GetCurrentInstant();
@@ -377,7 +377,7 @@ public partial class ChatService(
return accountsToNotify.Where(a => a.Id != sender.AccountId.ToString()).ToList();
}
private async Task CreateFileReferencesForMessageAsync(Message message)
private async Task CreateFileReferencesForMessageAsync(SnChatMessage message)
{
var files = message.Attachments.Distinct().ToList();
if (files.Count == 0) return;
@@ -391,7 +391,7 @@ public partial class ChatService(
await fileRefs.CreateReferenceBatchAsync(request);
}
private async Task UpdateFileReferencesForMessageAsync(Message message, List<string> attachmentsId)
private async Task UpdateFileReferencesForMessageAsync(SnChatMessage message, List<string> attachmentsId)
{
// Delete existing references for this message
await fileRefs.DeleteResourceReferencesAsync(
@@ -411,10 +411,10 @@ public partial class ChatService(
var queryRequest = new GetFileBatchRequest();
queryRequest.Ids.AddRange(attachmentsId);
var queryResult = await filesClient.GetFileBatchAsync(queryRequest);
message.Attachments = queryResult.Files.Select(CloudFileReferenceObject.FromProtoValue).ToList();
message.Attachments = queryResult.Files.Select(SnCloudFileReferenceObject.FromProtoValue).ToList();
}
private async Task DeleteFileReferencesForMessageAsync(Message message)
private async Task DeleteFileReferencesForMessageAsync(SnChatMessage message)
{
var messageResourceId = $"message:{message.Id}";
await fileRefs.DeleteResourceReferencesAsync(
@@ -474,7 +474,7 @@ public partial class ChatService(
);
}
public async Task<Dictionary<Guid, Message?>> ListLastMessageForUser(Guid userId)
public async Task<Dictionary<Guid, SnChatMessage?>> ListLastMessageForUser(Guid userId)
{
var userRooms = await db.ChatMembers
.Where(m => m.LeaveAt == null && m.JoinedAt != null)
@@ -517,9 +517,9 @@ public partial class ChatService(
return messages;
}
public async Task<RealtimeCall> CreateCallAsync(ChatRoom room, ChatMember sender)
public async Task<SnRealtimeCall> CreateCallAsync(SnChatRoom room, SnChatMember sender)
{
var call = new RealtimeCall
var call = new SnRealtimeCall
{
RoomId = room.Id,
SenderId = sender.Id,
@@ -547,7 +547,7 @@ public partial class ChatService(
db.ChatRealtimeCall.Add(call);
await db.SaveChangesAsync();
await SendMessageAsync(new Message
await SendMessageAsync(new SnChatMessage
{
Type = "call.start",
ChatRoomId = room.Id,
@@ -561,7 +561,7 @@ public partial class ChatService(
return call;
}
public async Task EndCallAsync(Guid roomId, ChatMember sender)
public async Task EndCallAsync(Guid roomId, SnChatMember sender)
{
var call = await GetCallOngoingAsync(roomId);
if (call is null) throw new InvalidOperationException("No ongoing call was not found.");
@@ -592,7 +592,7 @@ public partial class ChatService(
db.ChatRealtimeCall.Update(call);
await db.SaveChangesAsync();
await SendMessageAsync(new Message
await SendMessageAsync(new SnChatMessage
{
Type = "call.ended",
ChatRoomId = call.RoomId,
@@ -605,7 +605,7 @@ public partial class ChatService(
}, call.Sender, call.Room);
}
public async Task<RealtimeCall?> GetCallOngoingAsync(Guid roomId)
public async Task<SnRealtimeCall?> GetCallOngoingAsync(Guid roomId)
{
return await db.ChatRealtimeCall
.Where(c => c.RoomId == roomId)
@@ -660,8 +660,8 @@ public partial class ChatService(
}
public async Task<Message> UpdateMessageAsync(
Message message,
public async Task<SnChatMessage> UpdateMessageAsync(
SnChatMessage message,
Dictionary<string, object>? meta = null,
string? content = null,
Guid? repliedMessageId = null,
@@ -705,7 +705,7 @@ public partial class ChatService(
await db.SaveChangesAsync();
// Create and store sync message for the update
var syncMessage = new Message
var syncMessage = new SnChatMessage
{
Type = "messages.update",
ChatRoomId = message.ChatRoomId,
@@ -751,7 +751,7 @@ public partial class ChatService(
/// Soft deletes a message and notifies other chat members
/// </summary>
/// <param name="message">The message to delete</param>
public async Task DeleteMessageAsync(Message message)
public async Task DeleteMessageAsync(SnChatMessage message)
{
// Only allow deleting regular text messages
if (message.Type != "text")
@@ -770,7 +770,7 @@ public partial class ChatService(
await db.SaveChangesAsync();
// Create and store sync message for the deletion
var syncMessage = new Message
var syncMessage = new SnChatMessage
{
Type = "messages.delete",
ChatRoomId = message.ChatRoomId,
@@ -805,6 +805,6 @@ public partial class ChatService(
public class SyncResponse
{
public List<Message> Messages { get; set; } = [];
public List<SnChatMessage> Messages { get; set; } = [];
public Instant CurrentTimestamp { get; set; }
}

View File

@@ -1,80 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using NodaTime;
namespace DysonNetwork.Sphere.Chat;
public class Message : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Type { get; set; } = null!;
[MaxLength(4096)] public string? Content { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
[Column(TypeName = "jsonb")] public List<Guid>? MembersMentioned { get; set; }
[MaxLength(36)] public string Nonce { get; set; } = null!;
public Instant? EditedAt { get; set; }
[Column(TypeName = "jsonb")] public List<CloudFileReferenceObject> Attachments { get; set; } = [];
public ICollection<MessageReaction> Reactions { get; set; } = new List<MessageReaction>();
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 Guid ChatRoomId { get; set; }
[JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!;
public string ResourceIdentifier => $"message:{Id}";
/// <summary>
/// Creates a shallow clone of this message for sync operations
/// </summary>
/// <returns>A new Message instance with copied properties</returns>
public Message Clone()
{
return new Message
{
Id = Id,
Type = Type,
Content = Content,
Meta = Meta,
MembersMentioned = MembersMentioned,
Nonce = Nonce,
EditedAt = EditedAt,
Attachments = Attachments,
RepliedMessageId = RepliedMessageId,
ForwardedMessageId = ForwardedMessageId,
SenderId = SenderId,
Sender = Sender,
ChatRoomId = ChatRoomId,
ChatRoom = ChatRoom,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt
};
}
}
public enum MessageReactionAttitude
{
Positive,
Neutral,
Negative,
}
public class MessageReaction : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
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; }
}

View File

@@ -1,51 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using DysonNetwork.Sphere.Chat.Realtime;
using NodaTime;
namespace DysonNetwork.Sphere.Chat;
public class RealtimeCall : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Instant? EndedAt { get; set; }
public Guid SenderId { get; set; }
public ChatMember Sender { get; set; } = null!;
public Guid RoomId { get; set; }
public ChatRoom Room { get; set; } = null!;
/// <summary>
/// Provider name (e.g., "cloudflare", "agora", "twilio")
/// </summary>
public string? ProviderName { get; set; }
/// <summary>
/// Service provider's session identifier
/// </summary>
public string? SessionId { get; set; }
/// <summary>
/// JSONB column containing provider-specific configuration
/// </summary>
[Column(name: "upstream", TypeName = "jsonb")]
public string? UpstreamConfigJson { get; set; }
/// <summary>
/// Deserialized upstream configuration
/// </summary>
[NotMapped]
public Dictionary<string, object> UpstreamConfig
{
get => string.IsNullOrEmpty(UpstreamConfigJson)
? new Dictionary<string, object>()
: JsonSerializer.Deserialize<Dictionary<string, object>>(UpstreamConfigJson) ?? new Dictionary<string, object>();
set => UpstreamConfigJson = value.Count > 0
? JsonSerializer.Serialize(value)
: null;
}
}

View File

@@ -1,3 +1,4 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.Chat.Realtime;
using Livekit.Server.Sdk.Dotnet;
@@ -46,7 +47,7 @@ public class RealtimeCallController(
[HttpGet("{roomId:guid}")]
[Authorize]
public async Task<ActionResult<RealtimeCall>> GetOngoingCall(Guid roomId)
public async Task<ActionResult<SnRealtimeCall>> GetOngoingCall(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
@@ -145,7 +146,7 @@ public class RealtimeCallController(
[HttpPost("{roomId:guid}")]
[Authorize]
public async Task<ActionResult<RealtimeCall>> StartCall(Guid roomId)
public async Task<ActionResult<SnRealtimeCall>> StartCall(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
@@ -165,7 +166,7 @@ public class RealtimeCallController(
[HttpDelete("{roomId:guid}")]
[Authorize]
public async Task<ActionResult<RealtimeCall>> EndCall(Guid roomId)
public async Task<ActionResult<SnRealtimeCall>> EndCall(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
@@ -250,7 +251,7 @@ public class CallParticipant
/// <summary>
/// The participant's profile in the chat
/// </summary>
public ChatMember? Profile { get; set; }
public SnChatMember? Profile { get; set; }
/// <summary>
/// When the participant joined the call