✨ Message attachments expires at
This commit is contained in:
parent
2eff4364c9
commit
8da8c4bedd
@ -5,13 +5,14 @@ using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using SystemClock = NodaTime.SystemClock;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
[ApiController]
|
||||
[Route("/chat")]
|
||||
public partial class ChatController(AppDatabase db, ChatService cs) : ControllerBase
|
||||
public partial class ChatController(AppDatabase db, ChatService cs, FileService fs) : ControllerBase
|
||||
{
|
||||
public class MarkMessageReadRequest
|
||||
{
|
||||
@ -63,7 +64,8 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
}
|
||||
|
||||
[HttpGet("{roomId:guid}/messages")]
|
||||
public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset, [FromQuery] int take = 20)
|
||||
public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset,
|
||||
[FromQuery] int take = 20)
|
||||
{
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
|
||||
|
||||
@ -133,7 +135,6 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
}
|
||||
|
||||
|
||||
|
||||
[GeneratedRegex("@([A-Za-z0-9_-]+)")]
|
||||
private static partial Regex MentionRegex();
|
||||
|
||||
@ -143,7 +144,8 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (string.IsNullOrWhiteSpace(request.Content) && (request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
||||
if (string.IsNullOrWhiteSpace(request.Content) &&
|
||||
(request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
||||
return BadRequest("You cannot send an empty message.");
|
||||
|
||||
var member = await db.ChatMembers
|
||||
@ -153,7 +155,8 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
.Include(m => m.Account)
|
||||
.Include(m => m.Account.Profile)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member == null || member.Role < ChatMemberRole.Member) return StatusCode(403, "You need to be a normal member to send messages here.");
|
||||
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
|
||||
{
|
||||
@ -204,9 +207,9 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
if (mentioned.Count > 0)
|
||||
{
|
||||
var mentionedMembers = await db.ChatMembers
|
||||
.Where(m => mentioned.Contains(m.Account.Name))
|
||||
.Select(m => m.Id)
|
||||
.ToListAsync();
|
||||
.Where(m => mentioned.Contains(m.Account.Name))
|
||||
.Select(m => m.Id)
|
||||
.ToListAsync();
|
||||
message.MembersMentioned = mentionedMembers;
|
||||
}
|
||||
}
|
||||
@ -217,7 +220,6 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPatch("{roomId:guid}/messages/{messageId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> UpdateMessage([FromBody] SendMessageRequest request, Guid roomId, Guid messageId)
|
||||
@ -227,7 +229,7 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
var message = await db.ChatMessages
|
||||
.Include(m => m.Sender)
|
||||
.Include(m => m.Sender.Account)
|
||||
.Include(m => m.Sender.Account.Profile)
|
||||
.Include(m => m.Sender.Account.Profile).Include(message => message.Attachments)
|
||||
.FirstOrDefaultAsync(m => m.Id == messageId && m.ChatRoomId == roomId);
|
||||
if (message == null) return NotFound();
|
||||
|
||||
@ -261,23 +263,8 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
|
||||
if (request.AttachmentsId is not null)
|
||||
{
|
||||
var records = await db.Files.Where(f => request.AttachmentsId.Contains(f.Id)).ToListAsync();
|
||||
|
||||
var previous = message.Attachments?.ToDictionary(f => f.Id) ?? new Dictionary<string, CloudFile>();
|
||||
var current = records.ToDictionary(f => f.Id);
|
||||
|
||||
// Detect added files
|
||||
var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList();
|
||||
// Detect removed files
|
||||
var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList();
|
||||
|
||||
// Update attachments
|
||||
message.Attachments = request.AttachmentsId.Select(id => current[id]).ToList();
|
||||
|
||||
// Call mark usage
|
||||
var fs = HttpContext.RequestServices.GetRequiredService<Storage.FileService>();
|
||||
await fs.MarkUsageRangeAsync(added, 1);
|
||||
await fs.MarkUsageRangeAsync(removed, -1);
|
||||
message.Attachments = (await fs.DiffAndMarkFilesAsync(request.AttachmentsId, message.Attachments)).current;
|
||||
await fs.DiffAndSetExpiresAsync(request.AttachmentsId, Duration.FromDays(30), message.Attachments);
|
||||
}
|
||||
|
||||
message.EditedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
@ -309,8 +296,7 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
|
||||
public class SyncRequest
|
||||
{
|
||||
[Required]
|
||||
public long LastSyncTimestamp { get; set; }
|
||||
[Required] public long LastSyncTimestamp { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("{roomId:guid}/sync")]
|
||||
|
@ -18,7 +18,11 @@ public class ChatService(AppDatabase db, FileService fs, IServiceScopeFactory sc
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var files = message.Attachments.Distinct().ToList();
|
||||
if (files.Count != 0) await fs.MarkUsageRangeAsync(files, 1);
|
||||
if (files.Count != 0)
|
||||
{
|
||||
await fs.MarkUsageRangeAsync(files, 1);
|
||||
await fs.SetExpiresRangeAsync(files, Duration.FromDays(30));
|
||||
}
|
||||
|
||||
// Then start the delivery process
|
||||
// Using ConfigureAwait(false) is correct here since we don't need context to flow
|
||||
|
@ -111,22 +111,7 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act)
|
||||
|
||||
if (attachments is not null)
|
||||
{
|
||||
var records = await db.Files.Where(e => attachments.Contains(e.Id)).ToListAsync();
|
||||
|
||||
var previous = post.Attachments.ToDictionary(f => f.Id);
|
||||
var current = records.ToDictionary(f => f.Id);
|
||||
|
||||
// Detect added files
|
||||
var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList();
|
||||
// Detect removed files
|
||||
var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList();
|
||||
|
||||
// Update attachments
|
||||
post.Attachments = attachments.Select(id => current[id]).ToList();
|
||||
|
||||
// Call mark usage
|
||||
await fs.MarkUsageRangeAsync(added, 1);
|
||||
await fs.MarkUsageRangeAsync(removed, -1);
|
||||
post.Attachments = (await fs.DiffAndMarkFilesAsync(attachments, post.Attachments)).current;
|
||||
}
|
||||
|
||||
if (tags is not null)
|
||||
|
@ -399,6 +399,64 @@ public class FileService(
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task SetExpiresRangeAsync(ICollection<CloudFile> files, Duration? duration)
|
||||
{
|
||||
var ids = files.Select(f => f.Id).ToArray();
|
||||
await db.Files.Where(o => ids.Contains(o.Id))
|
||||
.ExecuteUpdateAsync(setter => setter.SetProperty(
|
||||
b => b.ExpiredAt,
|
||||
duration.HasValue
|
||||
? b => SystemClock.Instance.GetCurrentInstant() + duration.Value
|
||||
: _ => null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<(ICollection<CloudFile> current, ICollection<CloudFile> added, ICollection<CloudFile> removed)> DiffAndMarkFilesAsync(
|
||||
ICollection<string>? newFileIds,
|
||||
ICollection<CloudFile>? previousFiles = null
|
||||
)
|
||||
{
|
||||
if (newFileIds == null) return ([], [], previousFiles ?? []);
|
||||
|
||||
var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync();
|
||||
var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary<string, CloudFile>();
|
||||
var current = records.ToDictionary(f => f.Id);
|
||||
|
||||
var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList();
|
||||
var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList();
|
||||
|
||||
if (added.Count > 0) await MarkUsageRangeAsync(added, 1);
|
||||
if (removed.Count > 0) await MarkUsageRangeAsync(removed, -1);
|
||||
|
||||
return (newFileIds.Select(id => current[id]).ToList(), added, removed);
|
||||
}
|
||||
|
||||
public async Task<(ICollection<CloudFile> current, ICollection<CloudFile> added, ICollection<CloudFile> removed)> DiffAndSetExpiresAsync(
|
||||
ICollection<string>? newFileIds,
|
||||
Duration? duration,
|
||||
ICollection<CloudFile>? previousFiles = null
|
||||
)
|
||||
{
|
||||
if (newFileIds == null) return ([], [], previousFiles ?? []);
|
||||
|
||||
var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync();
|
||||
var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary<string, CloudFile>();
|
||||
var current = records.ToDictionary(f => f.Id);
|
||||
|
||||
var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList();
|
||||
var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList();
|
||||
|
||||
if (added.Count > 0) await SetExpiresRangeAsync(added, duration);
|
||||
if (removed.Count > 0) await SetExpiresRangeAsync(removed, null);
|
||||
|
||||
return (newFileIds.Select(id => current[id]).ToList(), added, removed);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class CloudFileUnusedRecyclingJob(AppDatabase db, FileService fs, ILogger<CloudFileUnusedRecyclingJob> logger)
|
||||
|
Loading…
x
Reference in New Issue
Block a user