Files
SocialPub/PrivaPub.Tests/Support/Harness.cs
T
thepraandClaude Opus 5.5 8f75317050 Blocks, mutes, bookmarks, pins and reports
- Blocks are per persona and never federate (a Block activity would tell
  the other server who blocked whom): the blocked account is removed as a
  follower, with a Reject{Follow} if it is remote, unfollowed, cleared from
  home and notifications, and refused with Reject if it follows again.
- Mutes (optionally timed, optionally sparing notifications) and per-
  persona domain blocks keep authors out of home timelines, notifications
  and every status list the API returns; the boosts of a hidden author's
  posts are hidden too.
- Bookmarks and pins (at most five, public or unlisted, own posts) with
  their Mastodon endpoints and flags; pinned posts are the actor's
  `featured` collection at /trophies, and `featuredTags` points at
  /tattoos.
- Reports: /api/v1/reports stores the report and, when forwarding to a
  remote account, sends Flag from the instance actor, so the reporting
  persona is never named to the other server. An inbound Flag about a local
  persona or its posts becomes a report; moderators list and resolve them
  under /clientapi/moderator/reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 12:13:57 +02:00

136 lines
5.5 KiB
C#

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Entities;
using PrivaPub.Domain.Content;
using PrivaPub.Domain.Relationships;
using PrivaPub.Domain.Social;
using PrivaPub.Domain.Statuses;
using PrivaPub.Domain.Timelines;
using PrivaPub.Federation.Actors;
using PrivaPub.Federation.Inbox;
using PrivaPub.Federation.Inbox.Handlers;
using PrivaPub.Federation.Outbox;
using PrivaPub.Infrastructure.Jobs;
using PrivaPub.Models;
using PrivaPub.Models.Federation;
using PrivaPub.Models.Jobs;
using PrivaPub.Models.User;
using PrivaPub.Resources;
using PrivaPub.Services;
using PrivaPub.StaticServices;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace PrivaPub.Tests.Support
{
public sealed class Harness : IAsyncDisposable
{
public const string Host = "privapub.test";
public const string Base = "https://" + Host;
Harness(Peer peer)
{
Peer = peer;
var cache = new MemoryCache(new MemoryCacheOptions());
Local = new LocalActorService(Db, new StaticOptions<AppConfiguration>(new AppConfiguration { BackendBaseAddress = Base }));
Remote = new RemoteActorService(Peer.Http(cache), Local, cache, Db);
Delivery = new DeliveryService(Db, Queue);
Fanout = new Fanout(Db);
RemotePosts = new RemotePosts(Db, Local, Remote, new NoBlocks(), Queue);
Receiver = new InboxReceiver(Local, Remote, Queue, new NoBlocks(), NullLogger<InboxReceiver>.Instance);
Processor = new InboxProcessor(Remote, new IActivityHandler[]
{
new FollowHandler(Db, Local, Remote, Delivery),
new AcceptHandler(Db, Local),
new RejectHandler(Db, Local),
new UndoHandler(Db, Local),
new LikeHandler(Db),
new AnnounceHandler(Db, Local, RemotePosts, Fanout),
new CreateHandler(Db, Local, Remote, Delivery, new NoBlocks(), Fanout, RemotePosts),
new DeleteHandler(Db, Local, Remote, Delivery),
new UpdateHandler(Db, Local, Remote),
new FlagHandler(Db, Local)
}, NullLogger<InboxProcessor>.Instance);
Follows = new FollowService(Db, Local, Remote, Delivery, new KeyLocalizer<GenericRes>(), NullLogger<FollowService>.Instance);
Content = new ContentRenderer(Local, Remote);
Outbox = new OutboxPublisher(Db, Local, Delivery);
Statuses = new StatusService(Db, Local, Remote, Delivery, Content, Outbox, Fanout);
Posts = new PostsService(Db, Local, Statuses, new KeyLocalizer<GenericRes>(), NullLogger<PostsService>.Instance);
Timelines = new TimelineService(Db, new KeyLocalizer<GenericRes>());
Relationships = new RelationshipService(Db, Follows, Delivery);
Reports = new ReportService(Db, Local, Delivery);
}
public Peer Peer { get; }
public DbEntities Db { get; } = new();
public JobQueue Queue { get; } = new();
public LocalActorService Local { get; }
public RemoteActorService Remote { get; }
public DeliveryService Delivery { get; }
public InboxReceiver Receiver { get; }
public InboxProcessor Processor { get; }
public FollowService Follows { get; }
public ContentRenderer Content { get; }
public OutboxPublisher Outbox { get; }
public PostsService Posts { get; }
public StatusService Statuses { get; }
public Fanout Fanout { get; }
public RemotePosts RemotePosts { get; }
public TimelineService Timelines { get; }
public RelationshipService Relationships { get; }
public ReportService Reports { get; }
public async Task FollowedBy(LocalActor local, RemoteActor follower) =>
await DB.Default.SaveAsync(new Follower
{
LocalActorId = local.Id,
LocalActorKind = local.Kind,
ActorURI = follower.Id,
InboxURL = follower.Id + "/inbox",
SharedInboxURL = follower.SharedInbox
});
public static async Task<Harness> Start() => new(await Peer.Start());
public async Task<(string RootId, LocalActor Avatar)> Persona(string name, string rootId = default)
{
rootId ??= Guid.NewGuid().ToString("N")[..24];
var (privateKey, publicKey) = Keys.NewKeyPair();
var avatar = new Avatar { UserName = $"{name}{Guid.NewGuid():N}"[..20], Name = name, PrivateKey = privateKey, PublicKey = publicKey };
await DB.Default.SaveAsync(avatar);
await DB.Default.SaveAsync(new RootToAvatar { RootId = rootId, AvatarId = avatar.ID });
return (rootId, Local.FromAvatar(avatar));
}
public async Task<InboxResult> Deliver(RemoteActor sender, string path, JsonNode activity)
{
var result = await Receiver.Receive(sender.Post(Host, path, activity), default, CancellationToken.None);
var dedupe = "inbox|" + (activity is JsonObject ? activity["id"]?.GetValue<string>() : default);
var job = await DB.Default.Find<Job>().Match(j => j.DedupeKey == dedupe).ExecuteFirstAsync();
if (job != default)
Assert.Equal(JobResult.Done, (await Processor.Handle(job, CancellationToken.None)).Result);
return result;
}
public async Task<List<JsonObject>> Outgoing(string inbox) =>
(await DB.Default.Find<Job>().Match(j => j.Kind == JobKind.Deliver).ExecuteAsync())
.Select(j => JsonSerializer.Deserialize<DeliveryPayload>(j.Payload))
.Where(p => p.Inbox == inbox)
.Select(p => JsonNode.Parse(p.Body)!.AsObject())
.ToList();
public async ValueTask DisposeAsync() => await Peer.DisposeAsync();
}
public sealed class KeyLocalizer<T> : IStringLocalizer<T>
{
public LocalizedString this[string name] => new(name, name);
public LocalizedString this[string name, params object[] arguments] => new(name, string.Format(name, arguments));
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) => Enumerable.Empty<LocalizedString>();
}
}