Mbin joins the pasture; a magazine's own threads and locks are taken

Mbin 1.10.1 runs in the pasture (its image, a messenger worker, a RabbitMQ
of its own, its API limits raised), and peers/mbin_token.py gets mbuser's
token through the authorization-code flow. scenarios/mbin.sh: 24 checks and
one known gap, magazines both ways, titled threads, a Note to a magazine as
a microblog post, comments, favourites and upvotes both ways, a moderator's
lock, unlock and removal, the unfollow and statistics.

What it showed:
- Mbin sends a magazine's threads to its subscribers as the author's Create,
  the magazine as its audience, never announced. A post whose group is
  followed here and lives on the post's own server is now kept as if
  announced; the same from another server is not.
- A moderator's lock is a bare Lock (and Undo{Lock}): LockHandler takes it
  from the post's own server only.
- Mbin takes private messages only as ChatMessage and its actors say
  nothing about it; PrivaPub never decides by a server's software, so this
  stays open as G-0008 for the owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 16:41:50 +02:00
1 parent 47d6e22988
commit 26346cde30
15 files changed
+493 -8

No files matched your search

@@ -174,7 +174,13 @@ namespace PrivaPub.Federation.Inbox.Handlers
// a reply in the thread of someone followed here, as Mastodon keeps them: what the thread's server forwards
var repliesToFollowed = !followed && parent is { IsFederatedCopy: true } && visibility is PostVisibility.Public or PostVisibility.Unlisted
&& await _dbEntities.Followings.Match(f => f.TargetActorURI == parent.ActorURI && f.State == FollowState.Accepted).ExecuteAnyAsync(token);
if (visibility == PostVisibility.Direct ? persons.Count == 0 : group == default && persons.Count == 0 && !repliesToLocal && !followed && !repliesToFollowed)
// a post in a remote group followed here that the group's own server sends itself rather than announcing it (Mbin
// delivers a magazine's threads to its subscribers as their author's Create): the group's server speaks for it
var inFollowedGroup = !followed && visibility is PostVisibility.Public or PostVisibility.Unlisted && note.Audience != default
&& Origin.Same(note.Audience, author.ActorURI) && Origin.Same(note.Id, note.Audience)
&& await _dbEntities.Followings.Match(f => f.TargetActorURI == note.Audience && f.State == FollowState.Accepted).ExecuteAnyAsync(token);
if (visibility == PostVisibility.Direct ? persons.Count == 0
: group == default && persons.Count == 0 && !repliesToLocal && !followed && !repliesToFollowed && !inFollowedGroup)
{
Arrival.Drop("not-addressed");
return;
@@ -0,0 +1,50 @@
using MongoDB.Entities;
using PrivaPub.Federation.Objects;
using PrivaPub.Models.User;
using PrivaPub.StaticServices;
using System.Text.Json.Nodes;
using static PrivaPub.Federation.Objects.ActivityJson;
using PostEntity = PrivaPub.Models.Post.Post;
namespace PrivaPub.Federation.Inbox.Handlers
{
// A moderator's lock of a thread sent as it is, not inside its community's announce (Mbin sends it to the magazine's
// subscribers itself): taken only from the post's own server, which speaks for it. Replies are refused until the
// Undo{Lock}, as for a lock a community announces.
public class LockHandler : IActivityHandler
{
readonly DbEntities _dbEntities;
public LockHandler(DbEntities dbEntities)
{
_dbEntities = dbEntities;
}
public string Type => "Lock";
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
{
if (await Set(_dbEntities, Id(activity["object"]), actor, locked: true, token))
Arrival.Accept("locked");
else
Arrival.Drop("unknown-object");
}
// whether the post is ours to lock or unlock for that actor, and was
public static async Task<bool> Set(DbEntities dbEntities, string objectUri, ForeignAvatar actor, bool locked, CancellationToken token)
{
if (objectUri == default || !Origin.Same(objectUri, actor.ActorURI))
return false;
var post = await dbEntities.Posts.Match(p => p.ObjectURI == objectUri && p.IsFederatedCopy).ExecuteFirstAsync(token);
if (post == default)
return false;
Arrival.About(visibility: post.Visibility, created: post.CreationDate);
await DB.Default.Update<PostEntity>().MatchID(post.ID).Modify(p => p.LockedAt, locked ? DateTime.UtcNow : null).ExecuteAsync(token);
return true;
}
}
}
@@ -49,6 +49,8 @@ namespace PrivaPub.Federation.Inbox.Handlers
undone |= await UndoDislike(inner, innerId, actor, token);
if (innerType is null or "Block")
undone |= await BlockHandler.Undo(inner, innerId, actor, _localActors, token);
if (innerType == "Lock")
undone |= await LockHandler.Set(_dbEntities, Id(inner["object"]), actor, locked: false, token);
if (innerType is null or "Like" or "EmojiReact")
undone |= await _reactions.Withdraw(actor, innerId, inner is JsonObject ? Id(inner["object"]) : default,
inner is JsonObject ? Value(inner, "_misskey_reaction") ?? Value(inner, "content") : default, token);
@@ -93,6 +93,7 @@ namespace PrivaPub.Middleware
.AddSingleton<IActivityHandler, AnnounceHandler>()
.AddSingleton<IActivityHandler, FlagHandler>()
.AddSingleton<IActivityHandler, BlockHandler>()
.AddSingleton<IActivityHandler, LockHandler>()
.AddSingleton<IActivityHandler, CreateHandler>()
.AddSingleton<IActivityHandler, DeleteHandler>()
.AddSingleton<IActivityHandler, UpdateHandler>()