using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; 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 { [ApiController, Route("clientapi/moderator"), 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 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().Match(r => r.IsResolved == resolved).Sort(r => r.ID, Order.Descending).Limit(100).ExecuteAsync(token)) .Select(r => new ViewReport { ID = r.ID, Reporter = r.ReporterAvatarId != default ? "local" : r.ReporterActorURI, TargetAccountId = r.TargetAccountId, TargetActorURI = r.TargetActorURI, 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, CreatedAt = r.CreatedAt, ResolvedAt = r.ResolvedAt, RemoteResolvedAt = r.RemoteResolvedAt, RemoteResolvedBy = r.RemoteResolvedBy })); } [HttpPost, Route("/clientapi/moderator/reports/{id}/resolve")] public async Task Resolve(string id, CancellationToken token) { var by = User.GetUserId(); var result = await DB.Default.Update().MatchID(id) .Modify(r => r.IsResolved, true) .Modify(r => r.ResolvedAt, DateTime.UtcNow) .Modify(r => r.ResolvedBy, by) .ExecuteAsync(token); if (result.MatchedCount == 0) return NotFound(); var report = await DB.Default.Find().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 Log([FromQuery] string before, [FromQuery] int limit = 50, CancellationToken token = default) { var me = await DB.Default.Find().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(); 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().Match(u => actorIds.Contains(u.ID)).Project(u => u.Include(x => x.UserName)).ExecuteAsync(token)) .ToDictionary(u => u.ID, u => u.UserName) : new Dictionary(); 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 }); } } }