A boost or a favourite given again is sent again
Every boost of a post was announce-{post}, and every favourite like-{post}:
after an Undo, giving it again made the same activity id, which the
delivery queue had already delivered and dropped (and a server that
remembers ids would ignore). Found by decePub's e2e actions against the
pasture, which unboost and boost again. Now each boost and favourite is an
activity of its own, as Mastodon's are, and an Undo names the one it ends;
a concurrent second boost is removed after saving. An edit's Update is
named to the millisecond, a quote approval's to the tick.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
ce473f2741
commit
b710493701
5 files changed
+64
-14
No files matched your search
@@ -1,3 +1,4 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Entities;
|
||||
|
||||
@@ -321,7 +322,7 @@ namespace PrivaPub.Domain.Statuses
|
||||
var update = new JsonObject
|
||||
{
|
||||
["@context"] = ActivityPubRenderer.Context(),
|
||||
["id"] = author.ActivityUri($"update-{post.ID}-{new DateTimeOffset(post.EditedAt.Value).ToUnixTimeSeconds()}"),
|
||||
["id"] = author.ActivityUri($"update-{post.ID}-{new DateTimeOffset(post.EditedAt.Value).ToUnixTimeMilliseconds()}"),
|
||||
["type"] = "Update",
|
||||
["actor"] = author.Uri,
|
||||
["to"] = note["to"]!.DeepClone(),
|
||||
@@ -378,12 +379,16 @@ namespace PrivaPub.Domain.Statuses
|
||||
if (post == default)
|
||||
return StatusOutcome.Fail(StatusCodes.Status404NotFound, "Record not found");
|
||||
|
||||
var activityId = me.ActivityUri($"like-{post.ID}");
|
||||
// each favourite is a Like of its own, as Mastodon's are: a favourite after an unfavourite is a new Like, which no
|
||||
// server (and no delivery queue) takes for the one already undone; an unfavourite undoes the Like it ends
|
||||
var favourite = default(Favourite);
|
||||
if (on)
|
||||
{
|
||||
favourite = new Favourite { ID = ObjectId.GenerateNewId().ToString(), AccountId = me.Id, ActorURI = me.Uri, PostId = post.ID };
|
||||
favourite.ActivityURI = me.ActivityUri($"like-{favourite.ID}");
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(new Favourite { AccountId = me.Id, ActorURI = me.Uri, PostId = post.ID, ActivityURI = activityId }, token);
|
||||
await DB.Default.SaveAsync(favourite, token);
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
@@ -396,8 +401,8 @@ namespace PrivaPub.Domain.Statuses
|
||||
}
|
||||
else
|
||||
{
|
||||
var removed = await DB.Default.DeleteAsync<Favourite>(f => f.AccountId == me.Id && f.PostId == post.ID);
|
||||
if (removed.DeletedCount == 0)
|
||||
favourite = await DB.Default.Find<Favourite>().Match(f => f.AccountId == me.Id && f.PostId == post.ID).ExecuteFirstAsync(token);
|
||||
if (favourite == default || (await DB.Default.DeleteAsync<Favourite>(favourite.ID)).DeletedCount == 0)
|
||||
return new StatusOutcome(post);
|
||||
await DB.Default.Update<PostEntity>().MatchID(post.ID).Modify(b => b.Inc(p => p.FavouritesCount, -1)).ExecuteAsync(token);
|
||||
post.FavouritesCount--;
|
||||
@@ -408,12 +413,12 @@ namespace PrivaPub.Domain.Statuses
|
||||
var like = new JsonObject
|
||||
{
|
||||
["@context"] = ActivityPubRenderer.ActivityStreams,
|
||||
["id"] = activityId,
|
||||
["id"] = favourite.ActivityURI ?? me.ActivityUri($"like-{post.ID}"),//a favourite from before every Like had its own id
|
||||
["type"] = "Like",
|
||||
["actor"] = me.Uri,
|
||||
["object"] = post.ObjectURI
|
||||
};
|
||||
await _delivery.Enqueue(me, new[] { inbox }, on ? like : Undo(me, like, $"undo-like-{post.ID}-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"), token);
|
||||
await _delivery.Enqueue(me, new[] { inbox }, on ? like : Undo(me, like, $"undo-like-{favourite.ID}"), token);
|
||||
}
|
||||
return new StatusOutcome(post);
|
||||
}
|
||||
@@ -429,7 +434,10 @@ namespace PrivaPub.Domain.Statuses
|
||||
return StatusOutcome.Fail(StatusCodes.Status422UnprocessableEntity, "Validation failed: This post can't be boosted");
|
||||
|
||||
var existing = await _dbEntities.Posts.Match(p => p.ReblogOfPostId == original.ID && p.AuthorAccountId == me.Id && !p.DeletedAt.HasValue).ExecuteFirstAsync(token);
|
||||
var announceId = me.ActivityUri($"announce-{original.ID}");
|
||||
// each boost is an activity of its own, as Mastodon's are: a boost after an unboost is a new Announce, which no
|
||||
// server (and no delivery queue) takes for the one already undone; an unboost undoes the boost it ends
|
||||
var reblogId = existing?.ID ?? ObjectId.GenerateNewId().ToString();
|
||||
var announceId = existing?.ActivityURI ?? me.ActivityUri($"announce-{reblogId}");
|
||||
var announce = new JsonObject
|
||||
{
|
||||
["@context"] = ActivityPubRenderer.ActivityStreams,
|
||||
@@ -448,6 +456,7 @@ namespace PrivaPub.Domain.Statuses
|
||||
return new StatusOutcome(existing);
|
||||
var reblog = new PostEntity
|
||||
{
|
||||
ID = reblogId,
|
||||
GroupUserId = me.Id,
|
||||
AuthorAccountId = me.Id,
|
||||
ReblogOfPostId = original.ID,
|
||||
@@ -466,6 +475,14 @@ namespace PrivaPub.Domain.Statuses
|
||||
{
|
||||
return new StatusOutcome(await _dbEntities.Posts.Match(p => p.ObjectURI == announceId).ExecuteFirstAsync(token));
|
||||
}
|
||||
// two boosts at once: the first one saved stands, and the other goes
|
||||
var boosts = await _dbEntities.Posts.Match(p => p.ReblogOfPostId == original.ID && p.AuthorAccountId == me.Id && !p.DeletedAt.HasValue)
|
||||
.Sort(p => p.ID, Order.Ascending).ExecuteAsync(token);
|
||||
if (boosts.Count > 1 && boosts[0].ID != reblog.ID)
|
||||
{
|
||||
await DB.Default.DeleteAsync<PostEntity>(reblog.ID);
|
||||
return new StatusOutcome(boosts[0]);
|
||||
}
|
||||
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, me.Id, me.Uri, original.ID, token);
|
||||
@@ -479,7 +496,7 @@ namespace PrivaPub.Domain.Statuses
|
||||
await DB.Default.DeleteAsync<PostEntity>(existing.ID);
|
||||
await DB.Default.DeleteAsync<TimelineEntry>(e => e.PostId == existing.ID);
|
||||
await DB.Default.Update<PostEntity>().MatchID(original.ID).Modify(b => b.Inc(p => p.ReblogsCount, -1)).ExecuteAsync(token);
|
||||
await _delivery.EnqueueToFollowers(me, Undo(me, announce, $"undo-announce-{original.ID}-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"), token,
|
||||
await _delivery.EnqueueToFollowers(me, Undo(me, announce, $"undo-announce-{existing.ID}"), token,
|
||||
await AuthorInbox(original, token) is { } authorInbox ? new[] { authorInbox } : default);
|
||||
return new StatusOutcome(original);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user