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)] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] public async Task 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) .Paginate(p => p.ThreadId ?? p.Id, pq, ControllerContext) .Select(p => new Conversation { Id = p.ThreadId ?? p.Id, 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).ToListAsync(); var notes = await noteRenderer.RenderManyAsync(conversations.Select(p => p.LastNote), user, accounts: accounts); var res = 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() }); return Ok(res); } [HttpDelete("{id}")] [Authorize("write:conversations")] [ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(MastodonErrorResponse))] 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")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ConversationEntity))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] public async Task MarkRead(string id) { var user = HttpContext.GetUserOrFail(); var conversation = await db.Conversations(user) .IncludeCommonProperties() .Where(p => (p.ThreadId ?? p.Id) == id) .Select(p => new Conversation { Id = p.ThreadId ?? p.Id, 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).ToListAsync(); var noteRendererDto = new NoteRenderer.NoteRendererDto { Accounts = accounts }; var res = new ConversationEntity { Id = conversation.Id, Unread = conversation.Unread, LastStatus = await noteRenderer.RenderAsync(conversation.LastNote, user, data: noteRendererDto), Accounts = accounts }; return Ok(res); } private class Conversation { public required Note LastNote; public required bool Unread; public required List UserIds; public required string Id { get; init; } } }