diff --git a/Iceshrimp.Backend/Controllers/Mastodon/AnnouncementController.cs b/Iceshrimp.Backend/Controllers/Mastodon/AnnouncementController.cs new file mode 100644 index 00000000..847c5aeb --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/AnnouncementController.cs @@ -0,0 +1,103 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Mime; +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Extensions; +using Iceshrimp.Backend.Core.Helpers; +using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; +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/announcements")] +[EnableCors("mastodon")] +[Authenticate] +[EnableRateLimiting("sliding")] +[Produces(MediaTypeNames.Application.Json)] +public class AnnouncementController(DatabaseContext db, MfmConverter mfmConverter) : ControllerBase +{ + [HttpGet] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] + public async Task GetAnnouncements([FromQuery(Name = "with_dismissed")] bool withDismissed) + { + var user = HttpContext.GetUserOrFail(); + var announcements = db.Announcements.AsQueryable(); + + if (!withDismissed) + { + announcements = announcements.Where(p => p.IsReadBy(user)); + } + + var res = await announcements.OrderByDescending(p => p.UpdatedAt ?? p.CreatedAt) + .Select(p => new AnnouncementEntity + { + Id = p.Id, + PublishedAt = p.CreatedAt.ToStringIso8601Like(), + UpdatedAt = (p.UpdatedAt ?? p.CreatedAt).ToStringIso8601Like(), + IsRead = p.IsReadBy(user), + Content = $""" + **{p.Title}** + {p.Text} + """, + Mentions = new List(), //TODO + Emoji = new List() //TODO + }) + .ToListAsync(); + + await res.Select(async p => p.Content = await mfmConverter.ToHtmlAsync(p.Content, [], null)).AwaitAllAsync(); + + return Ok(res); + } + + [HttpPost("{id}/dismiss")] + [Authorize("write:accounts")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] + [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] + public async Task DismissAnnouncement(string id) + { + var user = HttpContext.GetUserOrFail(); + var announcement = await db.Announcements.FirstOrDefaultAsync(p => p.Id == id) ?? + throw GracefulException.RecordNotFound(); + + if (await db.Announcements.AnyAsync(p => p == announcement && !p.IsReadBy(user))) + { + var announcementRead = new AnnouncementRead + { + Id = IdHelpers.GenerateSlowflakeId(), + CreatedAt = DateTime.UtcNow, + Announcement = announcement, + User = user + }; + await db.AnnouncementReads.AddAsync(announcementRead); + await db.SaveChangesAsync(); + } + + return Ok(new object()); + } + + [HttpPut("{id}/reactions/{name}")] + [Authorize("write:favourites")] + [ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(MastodonErrorResponse))] + public IActionResult ReactToAnnouncement(string id, string name) => + throw new GracefulException(HttpStatusCode.NotImplemented, + "Iceshrimp.NET does not support this endpoint due to database schema differences to Mastodon"); + + [HttpDelete("{id}/reactions/{name}")] + [Authorize("write:favourites")] + [ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(MastodonErrorResponse))] + public IActionResult RemoveAnnouncementReaction(string id, string name) => + throw new GracefulException(HttpStatusCode.NotImplemented, + "Iceshrimp.NET does not support this endpoint due to database schema differences to Mastodon"); +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/AnnouncementEntity.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/AnnouncementEntity.cs new file mode 100644 index 00000000..6dabd7a6 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/AnnouncementEntity.cs @@ -0,0 +1,21 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; + +public class AnnouncementEntity +{ + [J("id")] public required string Id { get; set; } + [J("content")] public required string Content { get; set; } + [J("published_at")] public required string PublishedAt { get; set; } + [J("updated_at")] public required string UpdatedAt { get; set; } + [J("read")] public required bool IsRead { get; set; } + [J("mentions")] public required List Mentions { get; set; } + [J("emojis")] public required List Emoji { get; set; } + + [J("statuses")] public List Statuses = []; //FIXME + [J("reactions")] public List Reactions = []; //FIXME + [J("tags")] public List Tags = []; //FIXME + + [J("published")] public bool Published => true; + [J("all_day")] public bool AllDay => false; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Database/Tables/Announcement.cs b/Iceshrimp.Backend/Core/Database/Tables/Announcement.cs index 36ff32c9..c89c6049 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/Announcement.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/Announcement.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using EntityFrameworkCore.Projectables; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Database.Tables; @@ -39,4 +40,9 @@ public class Announcement [InverseProperty(nameof(AnnouncementRead.Announcement))] public virtual ICollection AnnouncementReads { get; set; } = new List(); + + [NotMapped] [Projectable] public virtual IEnumerable ReadBy => AnnouncementReads.Select(p => p.User); + + [Projectable] + public bool IsReadBy(User user) => ReadBy.Contains(user); } \ No newline at end of file