R5 the server's own settings: instance, rules, announcements, relays, media limits, the staff's log
Build / Build (push) Successful in 1m3s
Build / Build (push) Successful in 1m3s
A singleton ServerSettings (title, descriptions, contact, thumbnail, privacy policy, terms, rules, relays, media overrides, PublishActivity), cached by InstanceSettings and read by the instance API, NodeInfo, the instance actor, the web pages and the mailer; its defaults are the old literals, so nothing changes until an administrator edits. /clientapi/admin/settings (and /thumbnail), /announcements, /relays for administrators; /clientapi/moderator/log, the staff's log (AdminAction, written by domain blocks, backups, settings and the CLI; no IP, never a root paired with its personas). The Mastodon API's rules, extended description, privacy policy, terms of service, domain blocks (Published, Obfuscate), announcements (dismiss, reactions, streaming) and activity (only when published). Reports carry rule ids. Media overrides clamped to hard caps. Relays named by the settings reconcile next to the configured ones. Verified: interop.sh instance accounts relay with mastodon, relay, aoderelay and mailpit, 119 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
f67e2ae35e
commit
70d1ffe9a2
46 files changed
+2223
-107
No files matched your search
@@ -0,0 +1,98 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user