Personas have walls their followers write on
Owner decision of 2026-10-06 (G-0009, FEP-400e). A persona's actor names its wall (…/graffiti, sm:wall) and, in
Smithereen's privacySettings, that its followers may write on it. A public post that is not a reply, sent with the wall
as its target by an account following the persona, is hosted: the persona is notified, it reaches the persona's and its
followers' homes, and the followers' servers and the author's are told with Add{Note}. The persona deletes it with
DELETE /api/v1/statuses/:id, which sends Remove{Note}; Smithereen deletes the post then. The wall lists the persona's
public posts that start a thread and what was written on it.
Elsewhere: an account's Add{Note} on its own wall shows the post to its followers here as on that wall (privapub.wall on
the status), and its Remove takes it away. An Add or Remove naming a collection of the account's own server PrivaPub
does not know has its document read again first, at most hourly: accounts kept before walls were read had none.
Checked live: Smithereen 40 checks (G-0009 closed); GoToSocial and Mastodon unchanged (121).
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
e0682fa868
commit
c47b6e5533
26 files changed
+582
-36
No files matched your search
@@ -42,11 +42,13 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
readonly IQuoteService _quotes;
|
||||
readonly IInteractionApprovals _approvals;
|
||||
readonly Relays.IRelays _relays;
|
||||
readonly IWalls _walls;
|
||||
|
||||
public CreateHandler(DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IDeliveryService delivery,
|
||||
IDomainBlocks domainBlocks, IFanout fanout, IRemotePosts remotePosts, IGroupDistributor groups, IObjectRecords records, IPollService polls, ILinkPreviews previews, IQuoteService quotes,
|
||||
IInteractionApprovals approvals = default, Relays.IRelays relays = default)
|
||||
IInteractionApprovals approvals = default, Relays.IRelays relays = default, IWalls walls = default)
|
||||
{
|
||||
_walls = walls;
|
||||
_relays = relays;
|
||||
_approvals = approvals;
|
||||
_quotes = quotes;
|
||||
@@ -189,8 +191,17 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
// forwarded post)
|
||||
var relayed = !followed && visibility == PostVisibility.Public && _relays != default
|
||||
&& await _relays.Passes(Arrival.Current?.ForwardedBy, token);
|
||||
// written on a persona's wall (FEP-400e): public, not a reply, by an account following the persona, as its actor
|
||||
// tells Smithereen
|
||||
var wall = _walls == default || note.Wall == default ? default : await _walls.OwnerOf(note.Wall, token);
|
||||
if (wall != default && (visibility is not (PostVisibility.Public or PostVisibility.Unlisted) || !string.IsNullOrEmpty(note.InReplyTo)
|
||||
|| note.WallOwner != default && note.WallOwner != wall.Uri || !await _walls.MayWrite(wall, author, token)))
|
||||
{
|
||||
Arrival.Drop("wall-refused");
|
||||
return;
|
||||
}
|
||||
if (visibility == PostVisibility.Direct ? persons.Count == 0
|
||||
: group == default && persons.Count == 0 && !repliesToLocal && !followed && !repliesToFollowed && !inFollowedGroup && !relayed)
|
||||
: group == default && wall == default && persons.Count == 0 && !repliesToLocal && !followed && !repliesToFollowed && !inFollowedGroup && !relayed)
|
||||
{
|
||||
Arrival.Drop("not-addressed");
|
||||
return;
|
||||
@@ -212,6 +223,12 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
post.ActivityURI = Id(activity);
|
||||
post.GroupId = group?.Id;
|
||||
post.ConversationId = conversation?.ID;
|
||||
if (wall != default)
|
||||
{
|
||||
post.WallURI = wall.Wall;
|
||||
post.WallOwnerURI = wall.Uri;
|
||||
post.WallOwnerAccountId = wall.Id;
|
||||
}
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(post, token);
|
||||
@@ -235,6 +252,8 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
await Domain.Statuses.ConversationStates.Posted(conversation.ID, post.ID, default, token);
|
||||
if (group is { IsCircle: false })
|
||||
await _groups.Announce(group, activity.AsObject(), note.Id, isNewPost: true, token);
|
||||
if (wall != default)
|
||||
await _walls.Hosted(post, wall, token);
|
||||
}
|
||||
|
||||
async Task<bool> IsCircleMember(LocalActor circle, string actorUri, CancellationToken token) =>
|
||||
|
||||
@@ -6,21 +6,38 @@ using System.Text.Json.Nodes;
|
||||
namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
// An account pins one of its posts (Add to its `featured` collection) or unpins it (Remove), as Mastodon and Akkoma
|
||||
// send it; inside a community's announce, the community features a post made in it. Any other target (Smithereen's
|
||||
// wall, a community's moderators) is not ours to keep.
|
||||
// send it; inside a community's announce, the community features a post made in it. On its wall (Smithereen's, FEP-400e)
|
||||
// it tells of a post someone wrote there, or takes it off. Any other target (a community's moderators) is not ours to
|
||||
// keep.
|
||||
public class FeaturedHandler : IActivityHandler
|
||||
{
|
||||
readonly IFeaturedPosts _featured;
|
||||
readonly IWalls _walls;
|
||||
readonly IRemoteActorService _remoteActors;
|
||||
readonly bool _add;
|
||||
|
||||
public FeaturedHandler(IFeaturedPosts featured, bool add)
|
||||
public FeaturedHandler(IFeaturedPosts featured, bool add, IWalls walls = default, IRemoteActorService remoteActors = default)
|
||||
{
|
||||
_featured = featured;
|
||||
_walls = walls;
|
||||
_remoteActors = remoteActors;
|
||||
_add = add;
|
||||
}
|
||||
|
||||
public string Type => _add ? "Add" : "Remove";
|
||||
|
||||
public Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token) => _featured.Receive(activity, actor, _add, token);
|
||||
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
||||
{
|
||||
var target = Objects.ActivityJson.Id(activity["target"]);
|
||||
// a collection of its own we do not know of: its document read again, at most hourly, in case it was kept before
|
||||
// we read that collection (a wall)
|
||||
if (_remoteActors != default && target != default && target != actor.FeaturedURL && target != actor.WallURL
|
||||
&& Objects.Origin.Same(target, actor.ActorURI) && actor.UpdatedAt < DateTime.UtcNow.AddHours(-1))
|
||||
actor = await _remoteActors.GetActor(actor.ActorURI, refresh: true, token) ?? actor;
|
||||
if (_walls != default && !string.IsNullOrEmpty(actor.WallURL) && target == actor.WallURL)
|
||||
await _walls.Receive(activity, actor, _add, token);
|
||||
else
|
||||
await _featured.Receive(activity, actor, _add, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user