Files

137 lines
7.2 KiB
C#
Raw Permalink Normal View History

using MongoDB.Driver;
using MongoDB.Entities;
using PrivaPub.Models.Federation;
using PrivaPub.Models.Post;
using PrivaPub.Models.Social;
using PrivaPub.Models.User;
using PostEntity = PrivaPub.Models.Post.Post;
namespace PrivaPub.Domain.Social
{
// A persona's notification policy (Mastodon 4.3), applied as each notification is made: accepted, filtered into a
// request per account, or dropped. Polls ending and edits of posts the persona answered are never filtered.
public static class NotificationPolicies
{
static readonly TimeSpan NewAccount = TimeSpan.FromDays(30);
// someone who followed the persona lately is not yet a follower for the policy: following, then writing, is how
// a newcomer gets past it
static readonly TimeSpan NewFollower = TimeSpan.FromDays(3);
public static async Task<NotificationPolicy> Of(string avatarId, CancellationToken token) =>
await DB.Default.Find<NotificationPolicy>().Match(p => p.AvatarId == avatarId).ExecuteFirstAsync(token)
?? new NotificationPolicy { AvatarId = avatarId };
public static async Task Save(NotificationPolicy policy, CancellationToken token) =>
await DB.Default.Update<NotificationPolicy>().Match(p => p.AvatarId == policy.AvatarId)
.Modify(p => p.ForNotFollowing, policy.ForNotFollowing)
.Modify(p => p.ForNotFollowers, policy.ForNotFollowers)
.Modify(p => p.ForNewAccounts, policy.ForNewAccounts)
.Modify(p => p.ForPrivateMentions, policy.ForPrivateMentions)
.Modify(p => p.ForLimitedAccounts, policy.ForLimitedAccounts)
.Option(o => o.IsUpsert = true)
.ExecuteAsync(token);
public static async Task<NotificationAction> Judge(string avatarId, NotificationType type, string fromAccountId, string fromActorUri, string postId,
CancellationToken token)
{
if (type is NotificationType.Poll or NotificationType.Update || string.IsNullOrEmpty(fromActorUri))
return NotificationAction.Accept;
var policy = await DB.Default.Find<NotificationPolicy>().Match(p => p.AvatarId == avatarId).ExecuteFirstAsync(token);
if (policy is null or { ForNotFollowing: NotificationAction.Accept, ForNotFollowers: NotificationAction.Accept, ForNewAccounts: NotificationAction.Accept,
ForPrivateMentions: NotificationAction.Accept, ForLimitedAccounts: NotificationAction.Accept })
return NotificationAction.Accept;
if (await DB.Default.Find<NotificationPermission>().Match(p => p.AvatarId == avatarId && p.FromActorURI == fromActorUri).ExecuteAnyAsync(token))
return NotificationAction.Accept;
var actions = new List<NotificationAction>();
var following = await DB.Default.Find<Following>()
.Match(f => f.AvatarId == avatarId && f.TargetActorURI == fromActorUri && f.State == FollowState.Accepted).ExecuteAnyAsync(token);
if (!following)
actions.Add(policy.ForNotFollowing);
if (policy.ForNotFollowers != NotificationAction.Accept)
{
var since = DateTime.UtcNow - NewFollower;
if (!await DB.Default.Find<Follower>()
.Match(f => f.LocalActorId == avatarId && f.ActorURI == fromActorUri && f.IsAccepted && f.CreationDate <= since).ExecuteAnyAsync(token))
actions.Add(policy.ForNotFollowers);
}
if (policy.ForNewAccounts != NotificationAction.Accept || policy.ForLimitedAccounts != NotificationAction.Accept)
{
var (since, silenced) = await Sender(fromAccountId, fromActorUri, token);
if (since > DateTime.UtcNow - NewAccount)
actions.Add(policy.ForNewAccounts);
if (silenced)
actions.Add(policy.ForLimitedAccounts);
}
// a private mention it did not ask for: from someone it does not follow, answering nothing of the persona's
if (policy.ForPrivateMentions != NotificationAction.Accept && type == NotificationType.Mention && !following && !string.IsNullOrEmpty(postId)
&& await DB.Default.Find<PostEntity>().Match(p => p.ID == postId && p.Visibility == PostVisibility.Direct && p.InReplyToAccountId != avatarId)
.ExecuteAnyAsync(token))
actions.Add(policy.ForPrivateMentions);
return actions.Contains(NotificationAction.Drop) ? NotificationAction.Drop
: actions.Contains(NotificationAction.Filter) ? NotificationAction.Filter
: NotificationAction.Accept;
}
// since when the sender has been on the fediverse (its actor's published, else when this server first met it), and
// whether it is silenced here, itself or its server
static async Task<(DateTime Since, bool Silenced)> Sender(string accountId, string actorUri, CancellationToken token)
{
var host = Uri.TryCreate(actorUri, UriKind.Absolute, out var uri) ? uri.Host : default;
var silencedHere = host != default && (await DB.Default.Find<DomainBlock>().Match(b => b.Severity == DomainBlockSeverity.Silence).ExecuteAsync(token))
.Any(b => host.Equals(b.Domain, StringComparison.OrdinalIgnoreCase) || host.EndsWith("." + b.Domain, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(accountId) && await DB.Default.Find<Avatar>().Match(a => a.ID == accountId).ExecuteFirstAsync(token) is { } local)
return (local.CreatedAt, local.SilencedAt.HasValue);
var foreign = await DB.Default.Find<ForeignAvatar>().Match(a => a.ActorURI == actorUri).ExecuteFirstAsync(token);
return foreign == default ? (DateTime.UtcNow, silencedHere) : (foreign.Published ?? foreign.CreatedAt, silencedHere || foreign.SilencedAt.HasValue);
}
// a notification filtered from an account: its request gathers it
public static async Task Requested(Notification notification, CancellationToken token)
{
var now = DateTime.UtcNow;
var update = DB.Default.Update<NotificationRequest>()
.Match(r => r.AvatarId == notification.AvatarId && r.FromActorURI == notification.FromActorURI)
.Modify(b => b.Inc(r => r.NotificationsCount, 1))
.Modify(r => r.FromAccountId, notification.FromAccountId)
.Modify(r => r.UpdatedAt, now)
.Modify(b => b.SetOnInsert(r => r.CreatedAt, now))
.Option(o => o.IsUpsert = true);
if (!string.IsNullOrEmpty(notification.PostId))
update.Modify(r => r.LastPostId, notification.PostId);
try
{
await update.ExecuteAsync(token);
}
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
// (two filtered at once from one account: the other made the request)
await update.Option(o => o.IsUpsert = false).ExecuteAsync(token);
}
}
// the persona lets an account's notifications in, now and from then on
public static async Task Accept(NotificationRequest request, CancellationToken token)
{
try
{
await DB.Default.SaveAsync(new NotificationPermission { AvatarId = request.AvatarId, FromActorURI = request.FromActorURI }, token);
}
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
}
await DB.Default.Update<Notification>().Match(n => n.AvatarId == request.AvatarId && n.FromActorURI == request.FromActorURI && n.Filtered)
.Modify(n => n.Filtered, false).ExecuteAsync(token);
await DB.Default.DeleteAsync<NotificationRequest>(request.ID);
}
// or dismisses them
public static async Task Dismiss(NotificationRequest request, CancellationToken token)
{
await DB.Default.DeleteAsync<Notification>(n => n.AvatarId == request.AvatarId && n.FromActorURI == request.FromActorURI && n.Filtered);
await DB.Default.DeleteAsync<NotificationRequest>(request.ID);
}
}
}