💥 ♻️ Refactor cloud files' references, and loading system

This commit is contained in:
2025-06-01 19:18:23 +08:00
parent 02ae634690
commit 00229fd406
32 changed files with 5204 additions and 582 deletions

View File

@ -92,7 +92,6 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
.Include(m => m.Sender)
.Include(m => m.Sender.Account)
.Include(m => m.Sender.Account.Profile)
.Include(m => m.Attachments)
.Skip(offset)
.Take(take)
.ToListAsync();
@ -169,6 +168,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
.ToListAsync();
message.Attachments = attachments
.OrderBy(f => request.AttachmentsId.IndexOf(f.Id))
.Select(f => f.ToReferenceObject())
.ToList();
}
@ -270,7 +270,6 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
var message = await db.ChatMessages
.Include(m => m.Sender)
.Include(m => m.ChatRoom)
.Include(m => m.Attachments)
.FirstOrDefaultAsync(m => m.Id == messageId && m.ChatRoomId == roomId);
if (message == null) return NotFound();

View File

@ -12,7 +12,7 @@ public enum ChatRoomType
DirectMessage
}
public class ChatRoom : ModelBase
public class ChatRoom : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; }
[MaxLength(1024)] public string? Name { get; set; }
@ -21,10 +21,8 @@ public class ChatRoom : ModelBase
public bool IsCommunity { get; set; }
public bool IsPublic { get; set; }
[MaxLength(32)] public string? PictureId { get; set; }
public CloudFile? Picture { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; }
public CloudFile? Background { 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>();
@ -35,6 +33,8 @@ public class ChatRoom : ModelBase
[JsonPropertyName("members")]
public ICollection<ChatMemberTransmissionObject> DirectMembers { get; set; } =
new List<ChatMemberTransmissionObject>();
public string ResourceIdentifier => $"chatroom/{Id}";
}
public enum ChatMemberRole

View File

@ -17,6 +17,7 @@ namespace DysonNetwork.Sphere.Chat;
public class ChatRoomController(
AppDatabase db,
FileService fs,
FileReferenceService fileRefService,
ChatRoomService crs,
RealmService rs,
ActionLogService als,
@ -176,13 +177,13 @@ public class ChatRoomController(
if (request.PictureId is not null)
{
chatRoom.Picture = await db.Files.FindAsync(request.PictureId);
chatRoom.Picture = (await db.Files.FindAsync(request.PictureId))?.ToReferenceObject();
if (chatRoom.Picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
}
if (request.BackgroundId is not null)
{
chatRoom.Background = await db.Files.FindAsync(request.BackgroundId);
chatRoom.Background = (await db.Files.FindAsync(request.BackgroundId))?.ToReferenceObject();
if (chatRoom.Background is null)
return BadRequest("Invalid background id, unable to find the file on cloud.");
}
@ -190,10 +191,21 @@ public class ChatRoomController(
db.ChatRooms.Add(chatRoom);
await db.SaveChangesAsync();
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
if (chatRoom.Picture is not null)
await fs.MarkUsageAsync(chatRoom.Picture, 1);
await fileRefService.CreateReferenceAsync(
chatRoom.Picture.Id,
"chat.room.picture",
chatRoomResourceId
);
if (chatRoom.Background is not null)
await fs.MarkUsageAsync(chatRoom.Background, 1);
await fileRefService.CreateReferenceAsync(
chatRoom.Background.Id,
"chat.room.background",
chatRoomResourceId
);
als.CreateActionLogFromRequest(
ActionLogType.ChatroomCreate,
@ -235,22 +247,50 @@ public class ChatRoomController(
chatRoom.RealmId = member.RealmId;
}
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
if (request.PictureId is not null)
{
var picture = await db.Files.FindAsync(request.PictureId);
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
await fs.MarkUsageAsync(picture, 1);
if (chatRoom.Picture is not null) await fs.MarkUsageAsync(chatRoom.Picture, -1);
chatRoom.Picture = picture;
// Remove old references for pictures
var oldPictureRefs = await fileRefService.GetResourceReferencesAsync(chatRoomResourceId, "chat.room.picture");
foreach (var oldRef in oldPictureRefs)
{
await fileRefService.DeleteReferenceAsync(oldRef.Id);
}
// Add a new reference
await fileRefService.CreateReferenceAsync(
picture.Id,
"chat.room.picture",
chatRoomResourceId
);
chatRoom.Picture = picture.ToReferenceObject();
}
if (request.BackgroundId is not null)
{
var background = await db.Files.FindAsync(request.BackgroundId);
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
await fs.MarkUsageAsync(background, 1);
if (chatRoom.Background is not null) await fs.MarkUsageAsync(chatRoom.Background, -1);
chatRoom.Background = background;
// Remove old references for backgrounds
var oldBackgroundRefs = await fileRefService.GetResourceReferencesAsync(chatRoomResourceId, "chat.room.background");
foreach (var oldRef in oldBackgroundRefs)
{
await fileRefService.DeleteReferenceAsync(oldRef.Id);
}
// Add a new reference
await fileRefService.CreateReferenceAsync(
background.Id,
"chat.room.background",
chatRoomResourceId
);
chatRoom.Background = background.ToReferenceObject();
}
if (request.Name is not null)
@ -293,14 +333,14 @@ public class ChatRoomController(
else if (!await crs.IsMemberWithRole(chatRoom.Id, currentUser.Id, ChatMemberRole.Owner))
return StatusCode(403, "You need at least be the owner to delete the chat.");
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
// Delete all file references for this chat room
await fileRefService.DeleteResourceReferencesAsync(chatRoomResourceId);
db.ChatRooms.Remove(chatRoom);
await db.SaveChangesAsync();
if (chatRoom.Picture is not null)
await fs.MarkUsageAsync(chatRoom.Picture, -1);
if (chatRoom.Background is not null)
await fs.MarkUsageAsync(chatRoom.Background, -1);
als.CreateActionLogFromRequest(
ActionLogType.ChatroomDelete,
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request

View File

@ -10,6 +10,7 @@ namespace DysonNetwork.Sphere.Chat;
public class ChatService(
AppDatabase db,
FileService fs,
FileReferenceService fileRefService,
IServiceScopeFactory scopeFactory,
IRealtimeService realtime,
ILogger<ChatService> logger
@ -30,9 +31,16 @@ public class ChatService(
var files = message.Attachments.Distinct().ToList();
if (files.Count != 0)
{
await fs.MarkUsageRangeAsync(files, 1);
await fs.SetExpiresRangeAsync(files, Duration.FromDays(30));
await fs.SetUsageRangeAsync(files, ChatFileUsageIdentifier);
var messageResourceId = $"message:{message.Id}";
foreach (var file in files)
{
await fileRefService.CreateReferenceAsync(
file.Id,
ChatFileUsageIdentifier,
messageResourceId,
duration: Duration.FromDays(30)
);
}
}
// Then start the delivery process
@ -64,7 +72,7 @@ public class ChatService(
{
message.Sender = sender;
message.ChatRoom = room;
using var scope = scopeFactory.CreateScope();
var scopedWs = scope.ServiceProvider.GetRequiredService<WebSocketService>();
var scopedNty = scope.ServiceProvider.GetRequiredService<NotificationService>();
@ -87,8 +95,8 @@ public class ChatService(
.Where(a => a.MimeType != null && a.MimeType.StartsWith("image"))
.Select(a => a.Id).ToList()
};
if (sender.Account.Profile is not { PictureId: null })
metaDict["pfp"] = sender.Account.Profile.PictureId;
if (sender.Account.Profile is not { Picture: null })
metaDict["pfp"] = sender.Account.Profile.Picture.Id;
if (!string.IsNullOrEmpty(room.Name))
metaDict["room_name"] = room.Name;
@ -346,9 +354,28 @@ public class ChatService(
if (attachmentsId is not null)
{
message.Attachments = (await fs.DiffAndMarkFilesAsync(attachmentsId, message.Attachments)).current;
await fs.DiffAndSetExpiresAsync(attachmentsId, Duration.FromDays(30), message.Attachments);
await fs.DiffAndSetUsageAsync(attachmentsId, ChatFileUsageIdentifier, message.Attachments);
var messageResourceId = $"message:{message.Id}";
// Delete existing references for this message
await fileRefService.DeleteResourceReferencesAsync(messageResourceId);
// Create new references for each attachment
foreach (var fileId in attachmentsId)
{
await fileRefService.CreateReferenceAsync(
fileId,
ChatFileUsageIdentifier,
messageResourceId,
duration: Duration.FromDays(30)
);
}
// Update message attachments by getting files from database
var files = await db.Files
.Where(f => attachmentsId.Contains(f.Id))
.ToListAsync();
message.Attachments = files.Select(x => x.ToReferenceObject()).ToList();
}
message.EditedAt = SystemClock.Instance.GetCurrentInstant();
@ -371,9 +398,9 @@ public class ChatService(
/// <param name="message">The message to delete</param>
public async Task DeleteMessageAsync(Message message)
{
var files = message.Attachments.Distinct().ToList();
if (files.Count != 0)
await fs.MarkUsageRangeAsync(files, -1);
// Remove all file references for this message
var messageResourceId = $"message:{message.Id}";
await fileRefService.DeleteResourceReferencesAsync(messageResourceId);
db.ChatMessages.Remove(message);
await db.SaveChangesAsync();

View File

@ -7,7 +7,7 @@ using NodaTime;
namespace DysonNetwork.Sphere.Chat;
public class Message : ModelBase
public class Message : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Type { get; set; } = null!;
@ -17,7 +17,7 @@ public class Message : ModelBase
[MaxLength(36)] public string Nonce { get; set; } = null!;
public Instant? EditedAt { get; set; }
public ICollection<CloudFile> Attachments { get; set; } = new List<CloudFile>();
[Column(TypeName = "jsonb")] public List<CloudFileReferenceObject> Attachments { get; set; } = [];
public ICollection<MessageReaction> Reactions { get; set; } = new List<MessageReaction>();
public Guid? RepliedMessageId { get; set; }
@ -29,32 +29,8 @@ public class Message : ModelBase
public ChatMember Sender { get; set; } = null!;
public Guid ChatRoomId { get; set; }
[JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!;
public Message Clone()
{
return new Message
{
Id = Id,
Content = Content,
Meta = Meta?.ToDictionary(entry => entry.Key, entry => entry.Value),
MembersMentioned = MembersMentioned?.ToList(),
Nonce = Nonce,
EditedAt = EditedAt,
Attachments = new List<CloudFile>(Attachments),
Reactions = new List<MessageReaction>(Reactions),
RepliedMessageId = RepliedMessageId,
RepliedMessage = RepliedMessage?.Clone() as Message,
ForwardedMessageId = ForwardedMessageId,
ForwardedMessage = ForwardedMessage?.Clone() as Message,
SenderId = SenderId,
Sender = Sender,
ChatRoomId = ChatRoomId,
ChatRoom = ChatRoom,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt,
DeletedAt = DeletedAt
};
}
public string ResourceIdentifier => $"message/{Id}";
}
public enum MessageReactionAttitude