60 lines
2.4 KiB
C#
60 lines
2.4 KiB
C#
using MongoDB.Entities;
|
|||
|
|
|
||
|
|
using PrivaPub.Federation.Objects;
|
||
|
|
using PrivaPub.Infrastructure.Http;
|
||
|
|
using PrivaPub.Infrastructure.Jobs;
|
||
|
|
using PrivaPub.Models.Jobs;
|
||
|
|
using PrivaPub.Models.User;
|
||
|
|
using PrivaPub.StaticServices;
|
||
|
|
|
||
|
|
namespace PrivaPub.Federation.Actors
|
||
|
|
{
|
||
|
|
// A remote account's follower, following and post counts, as its own server publishes them (the totalItems of its
|
||
|
|
// followers, following and outbox collections). Read when the account is fetched, at most once a day, never when
|
||
|
|
// someone looks at it; a hidden or missing collection leaves its count unknown. Mastodon and GoToSocial do the same.
|
||
|
|
public class AccountCountsJob : IJobHandler
|
||
|
|
{
|
||
|
|
readonly IRemoteActorService _remoteActors;
|
||
|
|
readonly DbEntities _dbEntities;
|
||
|
|
|
||
|
|
public AccountCountsJob(IRemoteActorService remoteActors, DbEntities dbEntities)
|
||
|
|
{
|
||
|
|
_remoteActors = remoteActors;
|
||
|
|
_dbEntities = dbEntities;
|
||
|
|
}
|
||
|
|
|
||
|
|
public JobKind Kind => JobKind.CountAccount;
|
||
|
|
public int Concurrency => 2;
|
||
|
|
public int MaxAttempts => 2;
|
||
|
|
public int PerHostLimit => 1;
|
||
|
|
|
||
|
|
public static string DedupeKey(string actorUri, DateTime day) => $"count|{actorUri}|{day:yyyyMMdd}";
|
||
|
|
|
||
|
|
public async Task<JobOutcome> Handle(Job job, CancellationToken token)
|
||
|
|
{
|
||
|
|
var actor = await _dbEntities.ForeignAvatars.Match(a => a.ActorURI == job.Payload && !a.DeletionAt.HasValue).ExecuteFirstAsync(token);
|
||
|
|
if (actor == default)
|
||
|
|
return JobOutcome.Done;
|
||
|
|
await DB.Default.Update<ForeignAvatar>().MatchID(actor.ID)
|
||
|
|
.Modify(a => a.FollowersCount, await Total(actor, actor.FollowersURL, token))
|
||
|
|
.Modify(a => a.FollowingCount, await Total(actor, actor.FollowingURL, token))
|
||
|
|
.Modify(a => a.StatusesCount, await Total(actor, actor.OutboxURL, token))
|
||
|
|
.Modify(a => a.CountedAt, DateTime.UtcNow)
|
||
|
|
.ExecuteAsync(token);
|
||
|
|
return JobOutcome.Done;
|
||
|
|
}
|
||
|
|
|
||
|
|
// only from the actor's own server, as everything we believe about it
|
||
|
|
async Task<int?> Total(ForeignAvatar actor, string collection, CancellationToken token)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(collection) || !Origin.Same(collection, actor.ActorURI))
|
||
|
|
return default;
|
||
|
|
using var scope = HttpScope.For("collection");
|
||
|
|
using var fetched = await _remoteActors.FetchObject(collection, token);
|
||
|
|
return fetched != default && fetched.Root.ValueKind == System.Text.Json.JsonValueKind.Object
|
||
|
|
&& fetched.Root.TryGetProperty("totalItems", out var total) && total.TryGetInt32(out var count) && count >= 0
|
||
|
|
? count
|
||
|
|
: default;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|