Media are reserved for one post, atomically
An upload's owner checked that it was free, then attached it in a second step, so two posts asking for the same upload at once could both get it; a post now, or an edit, could take media a scheduled post held, which then failed when its time came (only logged) and was deleted; and a persona deleted or banned after scheduling a post still published it. Now a post claims its media in one conditional update before anything is written (its id is minted first), and a post that loses the race is refused with 422, the media it took put back. Scheduling reserves media the same way. Media held by a scheduled post are that post's alone, and its job publishes only for a persona still there whose root is neither deleted nor banned; otherwise what it held is trashed. MastodonScheduledStatusesTests: a post now and an edit can't take scheduled media, two racing posts never both get an upload, a gone persona's scheduled post never publishes. 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
bb680e8cb8
commit
65938bb2a0
4 files changed
+117
-14
No files matched your search
@@ -7,6 +7,8 @@ using PrivaPub.Models.Jobs;
|
||||
using PrivaPub.Models.Media;
|
||||
using PrivaPub.Models.Post;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.Models.User;
|
||||
using PrivaPub.Domain.Media;
|
||||
|
||||
namespace PrivaPub.Domain.Statuses
|
||||
{
|
||||
@@ -19,8 +21,9 @@ namespace PrivaPub.Domain.Statuses
|
||||
public const int MaxWaiting = 300;
|
||||
public const int MaxPerDay = 25;
|
||||
|
||||
public static StatusDraft Draft(ScheduledParams p) => new()
|
||||
public static StatusDraft Draft(ScheduledParams p, string scheduledId = default) => new()
|
||||
{
|
||||
ScheduledStatusId = scheduledId,
|
||||
Text = p.Text,
|
||||
PlainText = true,
|
||||
SpoilerText = p.SpoilerText,
|
||||
@@ -66,6 +69,15 @@ namespace PrivaPub.Domain.Statuses
|
||||
RunAt = scheduled.ScheduledAt
|
||||
};
|
||||
|
||||
// whether a persona may still publish: neither deleted nor its root deleted or banned
|
||||
public static async Task<bool> MayPublish(string avatarId, CancellationToken token)
|
||||
{
|
||||
if (!await DB.Default.Find<Avatar>().Match(a => a.ID == avatarId && !a.DeletionAt.HasValue).ExecuteAnyAsync(token))
|
||||
return false;
|
||||
var rootIds = (await DB.Default.Find<RootToAvatar>().Match(r => r.AvatarId == avatarId).ExecuteAsync(token)).Select(r => r.RootId).ToList();
|
||||
return await DB.Default.Find<RootUser>().Match(u => rootIds.Contains(u.ID) && u.DeletedAt == null && !u.IsBanned).ExecuteAnyAsync(token);
|
||||
}
|
||||
|
||||
// the post's media go back to being plain uploads (the janitor's after a day), or are attached to the post by now
|
||||
public static Task Release(string scheduledId, CancellationToken token) =>
|
||||
DB.Default.Update<MediaAttachment>().Match(m => m.ScheduledStatusId == scheduledId).Modify(m => m.ScheduledStatusId, null).ExecuteAsync(token);
|
||||
@@ -93,10 +105,17 @@ namespace PrivaPub.Domain.Statuses
|
||||
if (scheduled == default || scheduled.ScheduledAt > DateTime.UtcNow.AddSeconds(30))
|
||||
return JobOutcome.Done;//dropped, or moved to a later time that has its own job
|
||||
using var scope = _scopes.CreateScope();
|
||||
// a persona deleted or banned since it was scheduled publishes nothing, and what it held is trashed
|
||||
if (!await ScheduledStatuses.MayPublish(scheduled.AvatarId, token))
|
||||
{
|
||||
await scope.ServiceProvider.GetRequiredService<IMediaService>().Trash(m => m.ScheduledStatusId == scheduled.ID, "its author is gone", token);
|
||||
await DB.Default.DeleteAsync<ScheduledStatus>(scheduled.ID);
|
||||
return JobOutcome.Done;
|
||||
}
|
||||
var author = await scope.ServiceProvider.GetRequiredService<ILocalActorService>().FindById(LocalActorKind.Person, scheduled.AvatarId, token);
|
||||
if (author != default)
|
||||
{
|
||||
var outcome = await scope.ServiceProvider.GetRequiredService<IStatusService>().Publish(author, ScheduledStatuses.Draft(scheduled.Params), token);
|
||||
var outcome = await scope.ServiceProvider.GetRequiredService<IStatusService>().Publish(author, ScheduledStatuses.Draft(scheduled.Params, scheduled.ID), token);
|
||||
if (!outcome.Ok)
|
||||
_logger.LogWarning("Scheduled status {Id} of {Avatar} could not be published: {Error}", scheduled.ID, scheduled.AvatarId, outcome.Error);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user