using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MongoDB.Entities; using PrivaPub.Api.Mastodon.Infrastructure; using PrivaPub.Domain.Social; using PrivaPub.Models.Social; namespace PrivaPub.Api.Mastodon.Controllers { // Mastodon's announcements: the administrators' (Domain/Social/Announcements.cs), each persona's dismissals and // reactions its own. Anonymous readers get the list without `read` or `me`. public class AnnouncementsController : MastodonController { readonly IAnnouncements _announcements; public AnnouncementsController(IAnnouncements announcements) { _announcements = announcements; } [HttpGet("/api/v1/announcements"), Scope("read", requiresUser: false), AllowAnonymous] public async Task List(CancellationToken token) => Json(await Map(await _announcements.Shown(token), MyId, token)); [HttpPost("/api/v1/announcements/{id}/dismiss"), Scope("write:accounts")] public async Task Dismiss(string id, CancellationToken token) => await _announcements.Dismiss(id, MyId, token) ? Json(new { }) : NotFoundError(); [HttpPut("/api/v1/announcements/{id}/reactions/{name}"), Scope("write:favourites")] public Task React(string id, string name, CancellationToken token) => Reaction(id, name, add: true, token); [HttpDelete("/api/v1/announcements/{id}/reactions/{name}"), Scope("write:favourites")] public Task Unreact(string id, string name, CancellationToken token) => Reaction(id, name, add: false, token); async Task Reaction(string id, string name, bool add, CancellationToken token) { var (status, error) = await _announcements.React(id, MyId, name, add, token); return error == default ? Json(new { }) : Error(status, error); } /// Mastodon's Announcement entities, as the viewer (a persona, or nobody) sees them. public static async Task>> Map(IReadOnlyList announcements, string viewerId, CancellationToken token) { var ids = announcements.Select(a => a.ID).ToList(); if (ids.Count == 0) return new List>(); var reactions = await DB.Default.Find().Match(r => ids.Contains(r.AnnouncementId)).ExecuteAsync(token); var read = viewerId == default ? new HashSet() : (await DB.Default.Find().Match(r => r.AvatarId == viewerId && ids.Contains(r.AnnouncementId)).ExecuteAsync(token)) .Select(r => r.AnnouncementId).ToHashSet(); return announcements.Select(a => { var entity = new Dictionary { ["id"] = a.ID, ["content"] = a.Html ?? string.Empty, ["starts_at"] = a.StartsAt == default ? default : MastodonJson.Time(a.StartsAt.Value), ["ends_at"] = a.EndsAt == default ? default : MastodonJson.Time(a.EndsAt.Value), ["all_day"] = a.AllDay, ["published_at"] = MastodonJson.Time(a.PublishedAt ?? a.CreatedAt), ["updated_at"] = MastodonJson.Time(a.UpdatedAt), ["mentions"] = Array.Empty(), ["statuses"] = Array.Empty(), ["tags"] = Array.Empty(), ["emojis"] = Array.Empty(), ["reactions"] = reactions.Where(r => r.AnnouncementId == a.ID) .GroupBy(r => r.Name) .OrderBy(g => g.Min(r => r.CreatedAt)) .Select(g => Reaction(g.Key, g.Count(), viewerId == default ? default : g.Any(r => r.AvatarId == viewerId))) .ToList() }; if (viewerId != default) entity["read"] = read.Contains(a.ID); return entity; }).ToList(); } // a unicode emoji's reaction: no url (custom emoji come later); `me` only for a persona static Dictionary Reaction(string name, int count, bool? me) { var reaction = new Dictionary { ["name"] = name, ["count"] = count }; if (me is { } mine) reaction["me"] = mine; return reaction; } /// The `announcement.reaction` event's payload: the emoji, how many chose it now, and on which. public static async Task> ReactionEvent(string announcementId, string name, CancellationToken token) => new() { ["name"] = name, ["count"] = await DB.Default.CountAsync(r => r.AnnouncementId == announcementId && r.Name == name, token), ["announcement_id"] = announcementId }; } }