Likes and boosts arrive, count, notify, and can be taken back
- Like: a Favourite per account and post (unique), the post's counter, a Favourite notification for a local author; only on posts the liker could see (public and unlisted, or followers-only and direct when they were addressed). - Announce: the original is always refetched from its own origin (or is ours), and must be public or unlisted. A boost of a local post counts and notifies; a boost by an account a persona follows is stored as a reblog row and fanned out to its followers' home timelines (as a reblog, where reblogs are wanted). A boost by a stranger of a stranger's post is ignored. FEP-1b12 group announces of activities wait for P4. - Undo of a Like or an Announce reverses each of them; an Undo naming only an id is matched against follows, likes and boosts in turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
e99b76dbd7
commit
b34c71775c
7 files changed
+351
-22
No files matched your search
@@ -0,0 +1,106 @@
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Domain.Social;
|
||||
using PrivaPub.Domain.Timelines;
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Infrastructure.Ids;
|
||||
using PrivaPub.Models.Post;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.Models.User;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using static PrivaPub.Federation.Objects.ActivityJson;
|
||||
|
||||
using PostEntity = PrivaPub.Models.Post.Post;
|
||||
|
||||
namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
public class AnnounceHandler : IActivityHandler
|
||||
{
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly ILocalActorService _localActors;
|
||||
readonly IRemotePosts _remotePosts;
|
||||
readonly IFanout _fanout;
|
||||
|
||||
public AnnounceHandler(DbEntities dbEntities, ILocalActorService localActors, IRemotePosts remotePosts, IFanout fanout)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
_remotePosts = remotePosts;
|
||||
_fanout = fanout;
|
||||
}
|
||||
|
||||
public string Type => "Announce";
|
||||
|
||||
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
||||
{
|
||||
var inner = activity["object"];
|
||||
if (inner is JsonObject && Value(inner, "type") is "Create" or "Update" or "Delete" or "Like" or "Dislike" or "Undo" or "Add" or "Remove" or "Block")
|
||||
return;
|
||||
var objectUri = Id(inner);
|
||||
var announceId = Id(activity);
|
||||
if (objectUri == default || announceId == default)
|
||||
return;
|
||||
if (await _dbEntities.Posts.Match(p => p.ObjectURI == announceId).ExecuteAnyAsync(token))
|
||||
return;
|
||||
|
||||
var isLocal = objectUri.StartsWith(_localActors.BaseAddress + "/", StringComparison.OrdinalIgnoreCase);
|
||||
var original = await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri && !p.DeletedAt.HasValue && p.ReblogOfPostId == null).ExecuteFirstAsync(token);
|
||||
var followed = await _dbEntities.Followings.Match(f => f.TargetActorURI == actor.ActorURI && f.State == FollowState.Accepted).ExecuteAnyAsync(token);
|
||||
if (original is not { IsFederatedCopy: false } && !followed)
|
||||
return;
|
||||
if (original == default && !isLocal)
|
||||
original = await _remotePosts.StoreContext(objectUri, 0, token);
|
||||
if (original == default || original.Visibility is not (PostVisibility.Public or PostVisibility.Unlisted))
|
||||
return;
|
||||
|
||||
var to = Strings(activity["to"]);
|
||||
var cc = Strings(activity["cc"]);
|
||||
var published = DateTimeOffset.TryParse(Value(activity, "published"), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var at)
|
||||
? at.UtcDateTime
|
||||
: DateTime.UtcNow;
|
||||
var reblog = new PostEntity
|
||||
{
|
||||
ID = PrivacyIds.At(published),
|
||||
ObjectURI = announceId,
|
||||
ActivityURI = announceId,
|
||||
ActorURI = actor.ActorURI,
|
||||
AuthorAccountId = actor.ID,
|
||||
ReblogOfPostId = original.ID,
|
||||
Visibility = Addressing.Classify(to, cc, actor.FollowersURL),
|
||||
To = to,
|
||||
Cc = cc,
|
||||
IsFederatedCopy = true,
|
||||
CreationDate = published,
|
||||
UpdateDate = published
|
||||
};
|
||||
if (reblog.Visibility == PostVisibility.Direct)
|
||||
return;
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(reblog, token);
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DB.Default.Update<PostEntity>().MatchID(original.ID).Modify(b => b.Inc(p => p.ReblogsCount, 1)).ExecuteAsync(token);
|
||||
if (!original.IsFederatedCopy)
|
||||
await Notifications.Add(original.GroupUserId, NotificationType.Reblog, actor.ID, actor.ActorURI, original.ID, token);
|
||||
await _fanout.Distribute(reblog, token);
|
||||
}
|
||||
|
||||
static List<string> Strings(JsonNode node) => node switch
|
||||
{
|
||||
JsonArray array => array.Select(Id).Where(id => id != default).ToList(),
|
||||
JsonNode single when Id(single) is { } id => new List<string> { id },
|
||||
_ => new List<string>()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user