Build / Build (push) Successful in 36s
- ObjectRecord, one per stored remote object: the raw JSON (up to 256 KB, always hashed), delivered or fetched, refetched from origin or not, the activity that brought it (or caused the fetch), shared or personal inbox, the signature's key, algorithm and signed headers, received time, published and updated, the delivering activity's @context, and up to ten later revisions from Update. - Delivery details travel from InboxReceiver through the inbox job to the handlers as Arrival.Current. - A host is described from its NodeInfo when we first hear from it, at most weekly (DescribeInstance job), never when someone opens the details view. - GET /api/privapub/v1/statuses/:id/provenance and /api/privapub/v1/instances/:host, with the extensions an object used detected from its raw form (044f quotes, interaction policies, contexts, proofs, Misskey fields, MFM, FEP-8967 links, emoji, polls, language maps, url variants, Markdown content). Checked live: a GoToSocial reply shows as delivered to the shared inbox, signed hs2019 with GoToSocial's fragment-less key id, with its interaction policy detected, and gts.test is described as gotosocial 0.22.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
145 lines
6.0 KiB
C#
145 lines
6.0 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.Media;
|
|
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.Federation.Objects;
|
|
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);
|
|
Groups = new GroupDistributor(Delivery);
|
|
Records = new ObjectRecords(Queue);
|
|
RemotePosts = new RemotePosts(Db, Local, Remote, new NoBlocks(), Queue, Records);
|
|
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, Remote),
|
|
new CreateHandler(Db, Local, Remote, Delivery, new NoBlocks(), Fanout, RemotePosts, Groups, Records),
|
|
new DeleteHandler(Db, Local, Remote, Delivery, Groups),
|
|
new UpdateHandler(Db, Local, Remote, Groups, Records),
|
|
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);
|
|
Media = new MediaService(new StaticOptions<MediaOptions>(new MediaOptions { Root = Path.Combine(Path.GetTempPath(), $"privapub-media-{Guid.NewGuid():N}") }),
|
|
Local, default, NullLogger<MediaService>.Instance);
|
|
Statuses = new StatusService(Db, Local, Remote, Delivery, Content, Outbox, Fanout, Media, Groups);
|
|
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 MediaService Media { get; }
|
|
public GroupDistributor Groups { get; }
|
|
public Fanout Fanout { get; }
|
|
public RemotePosts RemotePosts { get; }
|
|
public ObjectRecords Records { 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>();
|
|
}
|
|
}
|