98 lines
4.3 KiB
C#
98 lines
4.3 KiB
C#
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<IActionResult> List(CancellationToken token) =>
|
||
|
|
Json(await Map(await _announcements.Shown(token), MyId, token));
|
||
|
|
|
||
|
|
[HttpPost("/api/v1/announcements/{id}/dismiss"), Scope("write:accounts")]
|
||
|
|
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> Unreact(string id, string name, CancellationToken token) => Reaction(id, name, add: false, token);
|
||
|
|
|
||
|
|
async Task<IActionResult> 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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Mastodon's Announcement entities, as the viewer (a persona, or nobody) sees them.</summary>
|
||
|
|
public static async Task<List<Dictionary<string, object>>> Map(IReadOnlyList<Announcement> announcements, string viewerId, CancellationToken token)
|
||
|
|
{
|
||
|
|
var ids = announcements.Select(a => a.ID).ToList();
|
||
|
|
if (ids.Count == 0)
|
||
|
|
return new List<Dictionary<string, object>>();
|
||
|
|
var reactions = await DB.Default.Find<AnnouncementReaction>().Match(r => ids.Contains(r.AnnouncementId)).ExecuteAsync(token);
|
||
|
|
var read = viewerId == default
|
||
|
|
? new HashSet<string>()
|
||
|
|
: (await DB.Default.Find<AnnouncementRead>().Match(r => r.AvatarId == viewerId && ids.Contains(r.AnnouncementId)).ExecuteAsync(token))
|
||
|
|
.Select(r => r.AnnouncementId).ToHashSet();
|
||
|
|
return announcements.Select(a =>
|
||
|
|
{
|
||
|
|
var entity = new Dictionary<string, object>
|
||
|
|
{
|
||
|
|
["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<object>(),
|
||
|
|
["statuses"] = Array.Empty<object>(),
|
||
|
|
["tags"] = Array.Empty<object>(),
|
||
|
|
["emojis"] = Array.Empty<object>(),
|
||
|
|
["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<string, object> Reaction(string name, int count, bool? me)
|
||
|
|
{
|
||
|
|
var reaction = new Dictionary<string, object> { ["name"] = name, ["count"] = count };
|
||
|
|
if (me is { } mine)
|
||
|
|
reaction["me"] = mine;
|
||
|
|
return reaction;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>The `announcement.reaction` event's payload: the emoji, how many chose it now, and on which.</summary>
|
||
|
|
public static async Task<Dictionary<string, object>> ReactionEvent(string announcementId, string name, CancellationToken token) => new()
|
||
|
|
{
|
||
|
|
["name"] = name,
|
||
|
|
["count"] = await DB.Default.CountAsync<AnnouncementReaction>(r => r.AnnouncementId == announcementId && r.Name == name, token),
|
||
|
|
["announcement_id"] = announcementId
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|