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
@@ -5,8 +5,12 @@ using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.ClientModels;
|
||||
using PrivaPub.ClientModels.Admin;
|
||||
using PrivaPub.Domain.Instance;
|
||||
using PrivaPub.Domain.Moderation;
|
||||
using PrivaPub.Extensions;
|
||||
using PrivaPub.Models;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.Models.User;
|
||||
|
||||
namespace PrivaPub.Controllers.ClientToServer
|
||||
{
|
||||
@@ -15,9 +19,22 @@ namespace PrivaPub.Controllers.ClientToServer
|
||||
Authorize(Policy = Policies.IsModerator)]
|
||||
public class ModeratorController : ControllerBase
|
||||
{
|
||||
const int MaxLogPage = 100;
|
||||
|
||||
readonly IInstanceSettings _settings;
|
||||
readonly IModerationLog _log;
|
||||
|
||||
public ModeratorController(IInstanceSettings settings, IModerationLog log)
|
||||
{
|
||||
_settings = settings;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
[HttpGet, Route("/clientapi/moderator/reports")]
|
||||
public async Task<IActionResult> Reports([FromQuery] bool resolved, CancellationToken token) =>
|
||||
Ok((await DB.Default.Find<Report>().Match(r => r.IsResolved == resolved).Sort(r => r.ID, Order.Descending).Limit(100).ExecuteAsync(token))
|
||||
public async Task<IActionResult> Reports([FromQuery] bool resolved, CancellationToken token)
|
||||
{
|
||||
var rules = _settings.Current.Rules.ToDictionary(r => r.Id, r => r.Text ?? string.Empty);
|
||||
return Ok((await DB.Default.Find<Report>().Match(r => r.IsResolved == resolved).Sort(r => r.ID, Order.Descending).Limit(100).ExecuteAsync(token))
|
||||
.Select(r => new ViewReport
|
||||
{
|
||||
ID = r.ID,
|
||||
@@ -27,6 +44,8 @@ namespace PrivaPub.Controllers.ClientToServer
|
||||
PostIds = r.PostIds,
|
||||
ObjectURIs = r.ObjectURIs,
|
||||
Category = r.Category,
|
||||
RuleIds = r.RuleIds ?? new(),
|
||||
Rules = (r.RuleIds ?? new()).Select(id => rules.GetValueOrDefault(id, string.Empty)).ToList(),
|
||||
Comment = r.Comment,
|
||||
Forwarded = r.Forwarded,
|
||||
IsResolved = r.IsResolved,
|
||||
@@ -35,16 +54,71 @@ namespace PrivaPub.Controllers.ClientToServer
|
||||
RemoteResolvedAt = r.RemoteResolvedAt,
|
||||
RemoteResolvedBy = r.RemoteResolvedBy
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpPost, Route("/clientapi/moderator/reports/{id}/resolve")]
|
||||
public async Task<IActionResult> Resolve(string id, CancellationToken token)
|
||||
{
|
||||
var by = User.GetUserId();
|
||||
var result = await DB.Default.Update<Report>().MatchID(id)
|
||||
.Modify(r => r.IsResolved, true)
|
||||
.Modify(r => r.ResolvedAt, DateTime.UtcNow)
|
||||
.Modify(r => r.ResolvedBy, User.GetUserId())
|
||||
.Modify(r => r.ResolvedBy, by)
|
||||
.ExecuteAsync(token);
|
||||
return result.MatchedCount == 0 ? NotFound() : Ok();
|
||||
if (result.MatchedCount == 0)
|
||||
return NotFound();
|
||||
var report = await DB.Default.Find<Report>().MatchID(id).ExecuteFirstAsync(token);
|
||||
await _log.Write(by, AdminActions.ReportResolve, AdminTargets.Report, id, report?.Category, reportId: id, token: token);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// The staff's log, newest first, a page at a time (before: the last id of the page before). Administrators read all
|
||||
// of it; a moderator who isn't one reads what isn't about a login, and who acted only when it was the command line or
|
||||
// themselves.
|
||||
[HttpGet, Route("/clientapi/moderator/log")]
|
||||
public async Task<IActionResult> Log([FromQuery] string before, [FromQuery] int limit = 50, CancellationToken token = default)
|
||||
{
|
||||
var me = await DB.Default.Find<RootUser>().MatchID(User.GetUserId()).ExecuteFirstAsync(token);
|
||||
if (me is not { IsBanned: false, DeletedAt: null } || !me.Policies.Contains(Policies.IsModerator))
|
||||
return Forbid();
|
||||
var admin = me.Policies.Contains(Policies.IsAdmin);
|
||||
limit = Math.Clamp(limit, 1, MaxLogPage);
|
||||
var query = DB.Default.Find<AdminAction>();
|
||||
if (!string.IsNullOrEmpty(before))
|
||||
{
|
||||
if (!MongoDB.Bson.ObjectId.TryParse(before, out _))
|
||||
return BadRequest();
|
||||
query.Match(f => f.Lt(a => a.ID, before));
|
||||
}
|
||||
if (!admin)
|
||||
query.Match(a => a.TargetKind != AdminTargets.Root);
|
||||
var actions = await query.Sort(a => a.ID, Order.Descending).Limit(limit).ExecuteAsync(token);
|
||||
var actorIds = actions.Select(a => a.ByRootId).Where(id => id != default && id != AdminAction.Cli).Distinct().ToList();
|
||||
var names = admin && actorIds.Count > 0
|
||||
? (await DB.Default.Find<RootUser>().Match(u => actorIds.Contains(u.ID)).Project(u => u.Include(x => x.UserName)).ExecuteAsync(token))
|
||||
.ToDictionary(u => u.ID, u => u.UserName)
|
||||
: new Dictionary<string, string>();
|
||||
return Ok(new ViewAdminActionsPage
|
||||
{
|
||||
Items = actions.Select(a => new ViewAdminAction
|
||||
{
|
||||
Id = a.ID,
|
||||
At = a.At,
|
||||
By = a.ByRootId == AdminAction.Cli ? AdminAction.Cli
|
||||
: a.ByRootId == default ? default
|
||||
: admin ? names.GetValueOrDefault(a.ByRootId, a.ByRootId)
|
||||
: a.ByRootId == me.ID ? me.UserName
|
||||
: default,
|
||||
ByYou = a.ByRootId == me.ID,
|
||||
Action = a.Action,
|
||||
TargetKind = a.TargetKind,
|
||||
TargetId = a.TargetId,
|
||||
TargetLabel = a.TargetLabel,
|
||||
ReportId = a.ReportId,
|
||||
Note = a.Note
|
||||
}).ToList(),
|
||||
Next = actions.Count == limit ? actions[^1].ID : default
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user