137 lines
7.2 KiB
C#
137 lines
7.2 KiB
C#
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|