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 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().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 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; } } }