using Microsoft.AspNetCore.Mvc; using MongoDB.Driver; using MongoDB.Entities; using PrivaPub.Api.Mastodon.Infrastructure; using PrivaPub.Api.Mastodon.Mappers; using PrivaPub.Domain.Social; using PrivaPub.Models.Social; using PrivaPub.StaticServices; namespace PrivaPub.Api.Mastodon.Controllers { // Mastodon's notification policy and requests (4.3): what the persona's notifications from strangers, new accounts, // unasked private mentions and silenced accounts become, and the requests that gather the filtered ones, one per // account, let in or dismissed public class NotificationRequestsController : MastodonController { readonly MastodonMapper _mapper; readonly DbEntities _dbEntities; public NotificationRequestsController(MastodonMapper mapper, DbEntities dbEntities) { _mapper = mapper; _dbEntities = dbEntities; } static string Name(NotificationAction action) => action switch { NotificationAction.Filter => "filter", NotificationAction.Drop => "drop", _ => "accept" }; static NotificationAction? Parse(string value) => value?.ToLowerInvariant() switch { "accept" => NotificationAction.Accept, "filter" => NotificationAction.Filter, "drop" => NotificationAction.Drop, _ => null }; async Task Summary(CancellationToken token) => new { pending_requests_count = await DB.Default.CountAsync(r => r.AvatarId == MyId, token), pending_notifications_count = await DB.Default.CountAsync(n => n.AvatarId == MyId && n.Filtered, token) }; async Task V2(NotificationPolicy policy, CancellationToken token) => new { for_not_following = Name(policy.ForNotFollowing), for_not_followers = Name(policy.ForNotFollowers), for_new_accounts = Name(policy.ForNewAccounts), for_private_mentions = Name(policy.ForPrivateMentions), for_limited_accounts = Name(policy.ForLimitedAccounts), summary = await Summary(token) }; // the first version spoke of filtering only: dropped is filtered for it async Task V1(NotificationPolicy policy, CancellationToken token) => new { filter_not_following = policy.ForNotFollowing != NotificationAction.Accept, filter_not_followers = policy.ForNotFollowers != NotificationAction.Accept, filter_new_accounts = policy.ForNewAccounts != NotificationAction.Accept, filter_private_mentions = policy.ForPrivateMentions != NotificationAction.Accept, summary = await Summary(token) }; [HttpGet("/api/v2/notifications/policy"), Scope("read:notifications")] public async Task Policy(CancellationToken token) => Json(await V2(await NotificationPolicies.Of(MyId, token), token)); [HttpPatch("/api/v2/notifications/policy"), Scope("write:notifications")] public async Task SetPolicy(CancellationToken token) { var policy = await NotificationPolicies.Of(MyId, token); policy.ForNotFollowing = Parse(Params.Get("for_not_following")) ?? policy.ForNotFollowing; policy.ForNotFollowers = Parse(Params.Get("for_not_followers")) ?? policy.ForNotFollowers; policy.ForNewAccounts = Parse(Params.Get("for_new_accounts")) ?? policy.ForNewAccounts; policy.ForPrivateMentions = Parse(Params.Get("for_private_mentions")) ?? policy.ForPrivateMentions; policy.ForLimitedAccounts = Parse(Params.Get("for_limited_accounts")) ?? policy.ForLimitedAccounts; await NotificationPolicies.Save(policy, token); return Json(await V2(policy, token)); } [HttpGet("/api/v1/notifications/policy"), Scope("read:notifications")] public async Task PolicyV1(CancellationToken token) => Json(await V1(await NotificationPolicies.Of(MyId, token), token)); [HttpPut("/api/v1/notifications/policy"), HttpPatch("/api/v1/notifications/policy"), Scope("write:notifications")] public async Task SetPolicyV1(CancellationToken token) { var policy = await NotificationPolicies.Of(MyId, token); static NotificationAction Filter(bool? filter, NotificationAction current) => filter switch { true => current == NotificationAction.Drop ? NotificationAction.Drop : NotificationAction.Filter, false => NotificationAction.Accept, _ => current }; policy.ForNotFollowing = Filter(Params.Bool("filter_not_following"), policy.ForNotFollowing); policy.ForNotFollowers = Filter(Params.Bool("filter_not_followers"), policy.ForNotFollowers); policy.ForNewAccounts = Filter(Params.Bool("filter_new_accounts"), policy.ForNewAccounts); policy.ForPrivateMentions = Filter(Params.Bool("filter_private_mentions"), policy.ForPrivateMentions); await NotificationPolicies.Save(policy, token); return Json(await V1(policy, token)); } async Task Map(NotificationRequest request, CancellationToken token) { var account = await _mapper.Account(request.FromAccountId, token); if (account == default) return default; var post = string.IsNullOrEmpty(request.LastPostId) ? default : await _dbEntities.Posts.Match(p => p.ID == request.LastPostId && !p.DeletedAt.HasValue).ExecuteFirstAsync(token); return new { id = request.ID, created_at = MastodonJson.Time(request.CreatedAt), updated_at = MastodonJson.Time(request.UpdatedAt), notifications_count = request.NotificationsCount.ToString(System.Globalization.CultureInfo.InvariantCulture), account, last_status = post == default ? default : await _mapper.Status(post, MyId, token) }; } [HttpGet("/api/v1/notifications/requests"), Scope("read:notifications")] public async Task Requests(CancellationToken token) { var requests = await Page.From(Params, Limit(40, 80)).Fetch(DB.Default.Find().Match(r => r.AvatarId == MyId), r => r.ID, token); Link("/api/v1/notifications/requests", requests.LastOrDefault()?.ID, requests.FirstOrDefault()?.ID); var mapped = new List(); foreach (var request in requests) if (await Map(request, token) is { } one) mapped.Add(one); return Json(mapped); } Task Mine(string id, CancellationToken token) => DB.Default.Find().Match(r => r.ID == id && r.AvatarId == MyId).ExecuteFirstAsync(token); [HttpGet("/api/v1/notifications/requests/merged"), Scope("read:notifications")] public IActionResult Merged() => Json(new { merged = true }); [HttpGet("/api/v1/notifications/requests/{id}"), Scope("read:notifications")] public async Task Request(string id, CancellationToken token) => await Mine(id, token) is { } request && await Map(request, token) is { } mapped ? Json(mapped) : NotFoundError(); [HttpPost("/api/v1/notifications/requests/{id}/accept"), Scope("write:notifications")] public async Task Accept(string id, CancellationToken token) { if (await Mine(id, token) is not { } request) return NotFoundError(); await NotificationPolicies.Accept(request, token); return Json(new { }); } [HttpPost("/api/v1/notifications/requests/{id}/dismiss"), Scope("write:notifications")] public async Task Dismiss(string id, CancellationToken token) { if (await Mine(id, token) is not { } request) return NotFoundError(); await NotificationPolicies.Dismiss(request, token); return Json(new { }); } [HttpPost("/api/v1/notifications/requests/accept"), Scope("write:notifications")] public async Task AcceptMany(CancellationToken token) { foreach (var id in Params.List("id").Take(100)) if (await Mine(id, token) is { } request) await NotificationPolicies.Accept(request, token); return Json(new { }); } [HttpPost("/api/v1/notifications/requests/dismiss"), Scope("write:notifications")] public async Task DismissMany(CancellationToken token) { foreach (var id in Params.List("id").Take(100)) if (await Mine(id, token) is { } request) await NotificationPolicies.Dismiss(request, token); return Json(new { }); } } }