Once a follow holds, OutboxBackfill reads the account's latest public posts from its outbox's first page (twenty at most, once a day) and keeps them as any fetched post: its profile shows them at once instead of only what it posts from then on. Homes still get only what arrives afterwards, as on Mastodon. Announces and other servers' objects in the outbox are left out. Checked live against Mastodon (scenarios/pins.sh, now 9 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
116 lines
4.3 KiB
C#
116 lines
4.3 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Domain.Statuses;
|
|
using PrivaPub.Federation.Actors;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Models.User;
|
|
using PrivaPub.StaticServices;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
using static PrivaPub.Federation.Objects.ActivityJson;
|
|
|
|
namespace PrivaPub.Federation.Inbox.Handlers
|
|
{
|
|
public class AcceptHandler : IActivityHandler
|
|
{
|
|
readonly DbEntities _dbEntities;
|
|
readonly ILocalActorService _localActors;
|
|
readonly IQuoteService _quotes;
|
|
readonly IInteractionApprovals _approvals;
|
|
readonly IParticipations _participations;
|
|
readonly Relays.IRelays _relays;
|
|
readonly Infrastructure.Jobs.IJobQueue _queue;
|
|
|
|
public AcceptHandler(DbEntities dbEntities, ILocalActorService localActors, IQuoteService quotes, IInteractionApprovals approvals = default,
|
|
IParticipations participations = default, Relays.IRelays relays = default, Infrastructure.Jobs.IJobQueue queue = default)
|
|
{
|
|
_queue = queue;
|
|
_relays = relays;
|
|
_participations = participations;
|
|
_dbEntities = dbEntities;
|
|
_localActors = localActors;
|
|
_quotes = quotes;
|
|
_approvals = approvals;
|
|
}
|
|
|
|
public virtual string Type => "Accept";
|
|
|
|
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
|
{
|
|
if (await _quotes.Answered(activity, actor, accepted: Type == "Accept", token))
|
|
{
|
|
Arrival.Accept("quote-answer");
|
|
return;
|
|
}
|
|
if (_approvals != default && await _approvals.Answered(activity, actor, accepted: Type == "Accept", token))
|
|
{
|
|
Arrival.Accept("interaction-answer");
|
|
return;
|
|
}
|
|
if (_participations != default && await _participations.Answered(activity, actor, accepted: Type == "Accept", token))
|
|
{
|
|
Arrival.Accept("participation-answer");
|
|
return;
|
|
}
|
|
if (_relays != default && await _relays.Answered(activity, actor, accepted: Type == "Accept", token))
|
|
{
|
|
Arrival.Accept("relay-answer");
|
|
return;
|
|
}
|
|
var following = await FindFollowing(activity["object"], actor, _dbEntities, _localActors, token);
|
|
if (following == default)
|
|
{
|
|
Arrival.Drop("unknown-object");
|
|
return;
|
|
}
|
|
Arrival.About("Follow");
|
|
Arrival.Accept("follow-answer");
|
|
await Apply(following, token);
|
|
}
|
|
|
|
// the follow holds: the account's latest public posts are read from its outbox (OutboxBackfill), once a day at most
|
|
protected virtual async Task Apply(Following following, CancellationToken token)
|
|
{
|
|
await DB.Default.Update<Following>().MatchID(following.ID).Modify(f => f.State, FollowState.Accepted).ExecuteAsync(token);
|
|
if (_queue != default)
|
|
await _queue.Enqueue(Models.Jobs.JobKind.BackfillOutbox, following.TargetActorURI, new Uri(following.TargetActorURI).Host,
|
|
Actors.OutboxBackfill.DedupeKey(following.TargetActorURI, DateTime.UtcNow), token);
|
|
}
|
|
|
|
public static async Task<Following> FindFollowing(JsonNode follow, ForeignAvatar target, DbEntities dbEntities, ILocalActorService localActors,
|
|
CancellationToken token)
|
|
{
|
|
if (follow is JsonObject && Value(follow, "type") is { } type && type != "Follow")
|
|
return default;
|
|
// (a follow sent again names the first one under a new id: FollowService.AgainId)
|
|
var followId = Id(follow) is { } named && named.IndexOf(Domain.Social.FollowService.Again, StringComparison.Ordinal) is var at and > 0
|
|
? named[..at]
|
|
: Id(follow);
|
|
var byId = followId == default
|
|
? default
|
|
: await dbEntities.Followings.Match(f => f.FollowActivityURI == followId && f.TargetActorURI == target.ActorURI).ExecuteFirstAsync(token);
|
|
if (byId != default || follow is not JsonObject)
|
|
return byId;
|
|
|
|
var follower = await localActors.FindByUri(Id(follow["actor"]), token);
|
|
return follower == default
|
|
? default
|
|
: await dbEntities.Followings.Match(f => f.AvatarId == follower.Id && f.TargetActorURI == target.ActorURI).ExecuteFirstAsync(token);
|
|
}
|
|
}
|
|
|
|
public class RejectHandler : AcceptHandler
|
|
{
|
|
public RejectHandler(DbEntities dbEntities, ILocalActorService localActors, IQuoteService quotes, IInteractionApprovals approvals = default,
|
|
IParticipations participations = default, Relays.IRelays relays = default)
|
|
: base(dbEntities, localActors, quotes, approvals, participations, relays, queue: default)
|
|
{
|
|
}
|
|
|
|
public override string Type => "Reject";
|
|
|
|
protected override Task Apply(Following following, CancellationToken token) => DB.Default.DeleteAsync<Following>(following.ID);
|
|
}
|
|
}
|