Iceshrimp.NET/Iceshrimp.Backend/Controllers/Mastodon/ConversationsController.cs
kopper 7173373330
[backend/federation] Improved reply backfill
Instead of backfilling every note we come across that has a reply
collection, only schedule a backfill job if someone wants to see the
replies (on GET MastoAPI /context, or Iceshrimp API /descendants)

Reply backfilling is also done on a ThreadIdOrId basis as opposed to the
previous way of backfilling individual notes. This allows us finer
grained control over the recursion and frees up the job queue, alongside
allowing for easier implementation of context collection backfill in the
future (by mapping each context collection to a thread)

---

Currently, note threads are implicit based on the "threadId" column of a
note, which can be null (where it's the same as the note's "id")

This commit turns note threads into an actual entity, and as a part of
that, makes "threadId" non-nullable (by specifically setting it to "id"
for those cases)

This is done to attach extra metadata to the entire thread, currently
just the time of when it was last backfilled, but more may be added in
the future (the context collection associated with this thread, for example)

---

The data format for backfill jobs have backwards-incompatibly changed
since the introduction of the feature. We can drop all old jobs without
causing too much trouble as they will be re-scheduled on demand

---

Signed-off-by: Laura Hausmann <laura@hausmann.dev>
2024-10-24 21:18:57 +02:00

155 lines
No EOL
6.5 KiB
C#

using System.Net;
using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Controllers.Mastodon;
[MastodonApiController]
[Route("/api/v1/conversations")]
[Authenticate]
[EnableRateLimiting("sliding")]
[EnableCors("mastodon")]
[Produces(MediaTypeNames.Application.Json)]
public class ConversationsController(
DatabaseContext db,
NoteRenderer noteRenderer,
UserRenderer userRenderer
) : ControllerBase
{
[HttpGet]
[Authorize("read:statuses")]
[LinkPagination(20, 40)]
[ProducesResults(HttpStatusCode.OK)]
public async Task<IEnumerable<ConversationEntity>> GetConversations(MastodonPaginationQuery pq)
{
var user = HttpContext.GetUserOrFail();
//TODO: rewrite using .DistinctBy when https://github.com/npgsql/efcore.pg/issues/894 is implemented
var conversations = await db.Conversations(user)
.IncludeCommonProperties()
.FilterHiddenConversations(user, db)
.FilterMutedThreads(user, db)
.Paginate(p => p.ThreadId, pq, ControllerContext)
.Select(p => new Conversation
{
Id = p.ThreadId,
LastNote = p,
UserIds = p.VisibleUserIds,
Unread = db.Notifications.Any(n => n.Note == p &&
n.Notifiee == user &&
!n.IsRead &&
(n.Type ==
Notification.NotificationType.Reply ||
n.Type ==
Notification.NotificationType.Mention))
})
.ToListAsync();
var userIds = conversations.SelectMany(i => i.UserIds)
.Concat(conversations.Select(p => p.LastNote.UserId))
.Distinct();
var users = await db.Users.IncludeCommonProperties()
.Where(p => userIds.Contains(p.Id))
.ToListAsync();
var accounts = await userRenderer.RenderManyAsync(users, user).ToListAsync();
var notes = await noteRenderer.RenderManyAsync(conversations.Select(p => p.LastNote), user, accounts: accounts);
return conversations.Select(p => new ConversationEntity
{
Id = p.Id,
Unread = p.Unread,
LastStatus = notes.First(n => n.Id == p.LastNote.Id),
Accounts = accounts.Where(a => p.UserIds.Any(u => u == a.Id))
.DefaultIfEmpty(accounts.First(a => a.Id == user.Id))
.ToList()
});
}
[HttpDelete("{id}")]
[Authorize("write:conversations")]
[ProducesErrors(HttpStatusCode.NotImplemented)]
public IActionResult RemoveConversation(string id) => throw new GracefulException(HttpStatusCode.NotImplemented,
"Iceshrimp.NET does not support this endpoint due to database schema differences to Mastodon");
[HttpPost("{id}/read")]
[Authorize("write:conversations")]
[ProducesResults(HttpStatusCode.OK)]
[ProducesErrors(HttpStatusCode.NotFound)]
public async Task<ConversationEntity> MarkRead(string id)
{
var user = HttpContext.GetUserOrFail();
var conversation = await db.Conversations(user)
.IncludeCommonProperties()
.Where(p => p.ThreadId == id)
.Select(p => new Conversation
{
Id = p.ThreadId,
LastNote = p,
UserIds = p.VisibleUserIds,
Unread = db.Notifications.Any(n => n.Note == p &&
n.Notifiee == user &&
!n.IsRead &&
(n.Type ==
Notification.NotificationType.Reply ||
n.Type ==
Notification.NotificationType.Mention))
})
.FirstOrDefaultAsync() ??
throw GracefulException.RecordNotFound();
if (conversation.Unread)
{
await db.Notifications.Where(n => n.Note == conversation.LastNote &&
n.Notifiee == user &&
!n.IsRead &&
(n.Type ==
Notification.NotificationType.Reply ||
n.Type ==
Notification.NotificationType.Mention))
.ExecuteUpdateAsync(p => p.SetProperty(n => n.IsRead, true));
conversation.Unread = false;
}
var userIds = conversation.UserIds.Append(conversation.LastNote.UserId).Distinct();
var users = await db.Users.IncludeCommonProperties()
.Where(p => userIds.Contains(p.Id))
.ToListAsync();
var accounts = await userRenderer.RenderManyAsync(users, user).ToListAsync();
var noteRendererDto = new NoteRenderer.NoteRendererDto { Accounts = accounts };
return new ConversationEntity
{
Id = conversation.Id,
Unread = conversation.Unread,
LastStatus = await noteRenderer.RenderAsync(conversation.LastNote, user, data: noteRendererDto),
Accounts = accounts
};
}
private class Conversation
{
public required Note LastNote;
public required bool Unread;
public required List<string> UserIds;
public required string Id { get; init; }
}
}