Replies to a persona's posts reach its followers; personas join remote events
Two owner decisions of 2026-10-05, recorded in the roadmap.
Replies passed on ("the fediverse is broken without"): a public or unlisted
reply from another server to a persona's public, unlisted or followers-only
post goes on to the persona's followers as its author's server sent it, as
Mastodon forwards it, never to the replier's own server, never for a
local-only or group post; its edit and deletion follow. Only an activity its
own actor delivered is passed on (Arrival.Raw), so nothing forwarded is
forwarded again. The town checks it as relay.reply cells (specs/relay-five:
882 checks pass); Mastodon takes a passed-on activity only with an LD
signature, which GoToSocial and Akkoma don't add, and the checker knows it.
Events: a persona joins another server's event with a Join and leaves it with
a Leave, both to the organiser only, through
POST /api/privapub/v1/statuses/:id/join|leave; the organiser's Accept or
Reject is routed by our join id and shows as privapub.event.participation.
Events by invitation or taken on another site are refused before anything
is sent. Mobilizon's scenario joins and leaves an event (28 checks) and keeps
one for decePubClient's e2e.
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
2b42377b3f
commit
0646de22bd
24 files changed
+676
-26
No files matched your search
@@ -33,9 +33,12 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
readonly PrivaPub.Infrastructure.Jobs.IJobQueue _jobs;
|
||||
readonly Domain.Media.IMediaService _media;
|
||||
|
||||
readonly IParticipations _participations;
|
||||
|
||||
public StatusesController(IStatusService statuses, MastodonMapper mapper, DbEntities dbEntities, IMemoryCache cache, IQuoteService quotes,
|
||||
IOutboxPublisher outbox, PrivaPub.Infrastructure.Jobs.IJobQueue jobs, Domain.Media.IMediaService media)
|
||||
IOutboxPublisher outbox, PrivaPub.Infrastructure.Jobs.IJobQueue jobs, Domain.Media.IMediaService media, IParticipations participations = default)
|
||||
{
|
||||
_participations = participations;
|
||||
_jobs = jobs;
|
||||
_media = media;
|
||||
_quotes = quotes;
|
||||
@@ -248,6 +251,22 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
return post == default ? NotFoundError() : Json(new StatusSource { Id = post.ID, Text = post.Text ?? string.Empty, SpoilerText = post.SpoilerText ?? string.Empty });
|
||||
}
|
||||
|
||||
// a persona joins or leaves a remote event (owner decision 2026-10-05); the status says how it stands
|
||||
// (privapub.event.participation)
|
||||
[HttpPost("/api/privapub/v1/statuses/{id}/join"), Scope("write:statuses")]
|
||||
public async Task<IActionResult> Join(string id, CancellationToken token)
|
||||
{
|
||||
var outcome = await _participations.Join(Me, id, token);
|
||||
return outcome.Ok ? Json(await _mapper.Status(outcome.Post, MyId, token)) : Error(outcome.Status, outcome.Error);
|
||||
}
|
||||
|
||||
[HttpPost("/api/privapub/v1/statuses/{id}/leave"), Scope("write:statuses")]
|
||||
public async Task<IActionResult> Leave(string id, CancellationToken token)
|
||||
{
|
||||
var outcome = await _participations.Leave(Me, id, token);
|
||||
return outcome.Ok ? Json(await _mapper.Status(outcome.Post, MyId, token)) : Error(outcome.Status, outcome.Error);
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/statuses/{id}/favourite"), Scope("write:favourites")]
|
||||
public Task<IActionResult> Favourite(string id, CancellationToken token) => Toggle(_statuses.Favourite(Me, id, true, token), id, token);
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
public int? Capacity { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string Category { get; set; }
|
||||
public string Participation { get; set; }//the viewer's: "pending", "accepted" or "rejected"
|
||||
}
|
||||
|
||||
public class PrivaPubPlace
|
||||
|
||||
@@ -215,6 +215,11 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
})
|
||||
.OrderByDescending(e => e.Count)
|
||||
.ToList());
|
||||
var eventIds = all.Where(p => p.Event != default && p.IsFederatedCopy).Select(p => p.ID).ToList();
|
||||
var participations = viewerId == default || eventIds.Count == 0
|
||||
? new Dictionary<string, Models.Social.ParticipationState>()
|
||||
: (await DB.Default.Find<Models.Social.Participation>().Match(p => p.AvatarId == viewerId && eventIds.Contains(p.PostId)).ExecuteAsync(token))
|
||||
.ToDictionary(p => p.PostId, p => p.State);
|
||||
var pollIds = all.Where(p => p.Poll != default).Select(p => p.ID).ToList();
|
||||
var ownVotes = viewerId == default || pollIds.Count == 0
|
||||
? new Dictionary<string, List<int>>()
|
||||
@@ -289,6 +294,8 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
status.QuotesCount = post.QuotesCount;
|
||||
status.QuoteApproval = post.IsFederatedCopy ? Approval(post, viewerId) : LocalApproval(post, viewerId, followsAuthor.Contains(post.GroupUserId));
|
||||
status.InteractionPolicy = Policies(post);
|
||||
if (status.Privapub?.Event != default && participations.TryGetValue(post.ID, out var participation))
|
||||
status.Privapub.Event.Participation = participation.ToString().ToLowerInvariant();
|
||||
if (post.QuoteState == QuoteState.None)
|
||||
return;
|
||||
var state = post.QuoteState.ToString().ToLowerInvariant();
|
||||
|
||||
Reference in new issue
Block a user