A persona's notification policy and its requests
Mastodon 4.3's policy was a stub that accepted everything. It is now kept per persona: notifications from accounts it does not follow, accounts that do not follow it (or only for three days), accounts newer than 30 days, private mentions it did not ask for and silenced accounts are accepted, filtered or dropped. Filtered ones stay out of every list and count unless asked for, gathered in a request per account; letting a request in lets that account in for good, dismissing it deletes them. Everything is accepted until the persona chooses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
9872b78677
commit
5d5f1d6909
10 files changed
+511
-29
No files matched your search
@@ -0,0 +1,178 @@
|
||||
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<object> Summary(CancellationToken token) => new
|
||||
{
|
||||
pending_requests_count = await DB.Default.CountAsync<NotificationRequest>(r => r.AvatarId == MyId, token),
|
||||
pending_notifications_count = await DB.Default.CountAsync<Notification>(n => n.AvatarId == MyId && n.Filtered, token)
|
||||
};
|
||||
|
||||
async Task<object> 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<object> 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<IActionResult> Policy(CancellationToken token) => Json(await V2(await NotificationPolicies.Of(MyId, token), token));
|
||||
|
||||
[HttpPatch("/api/v2/notifications/policy"), Scope("write:notifications")]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<object> 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<IActionResult> Requests(CancellationToken token)
|
||||
{
|
||||
var requests = await Page.From(Params, Limit(40, 80)).Fetch(DB.Default.Find<NotificationRequest>().Match(r => r.AvatarId == MyId), r => r.ID, token);
|
||||
Link("/api/v1/notifications/requests", requests.LastOrDefault()?.ID, requests.FirstOrDefault()?.ID);
|
||||
var mapped = new List<object>();
|
||||
foreach (var request in requests)
|
||||
if (await Map(request, token) is { } one)
|
||||
mapped.Add(one);
|
||||
return Json(mapped);
|
||||
}
|
||||
|
||||
Task<NotificationRequest> Mine(string id, CancellationToken token) =>
|
||||
DB.Default.Find<NotificationRequest>().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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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 { });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user