Refactor AP core

This commit is contained in:
Laura Hausmann 2024-01-27 02:39:16 +01:00
parent e4211fde56
commit 831a1ceeba
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
9 changed files with 120 additions and 53 deletions

View file

@ -0,0 +1,29 @@
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub;
public class ActivityRenderer(IOptions<Config.InstanceSection> config) {
public static ASActivity RenderCreate(ASObject obj, ASObject actor) {
return new ASActivity {
Id = $"{obj.Id}#Create",
Type = "https://www.w3.org/ns/activitystreams#Create",
Actor = new ASActor { Id = actor.Id },
Object = obj
};
}
public ASActivity RenderAccept(ASObject followeeActor, string requestId) {
return new ASActivity {
Id = $"https://{config.Value.WebDomain}/activities/{new Guid().ToString().ToLowerInvariant()}",
Type = "https://www.w3.org/ns/activitystreams#Accept",
Actor = new ASActor {
Id = followeeActor.Id
},
Object = new ASObject {
Id = requestId
}
};
}
}

View file

@ -14,19 +14,21 @@ public static class ServiceExtensions {
//services.AddTransient<T>();
// Scoped = instantiated per request
services.AddScoped<UserResolver>();
services.AddScoped<ActivityRenderer>();
services.AddScoped<UserRenderer>();
services.AddScoped<NoteRenderer>();
services.AddScoped<UserResolver>();
services.AddScoped<UserService>();
services.AddScoped<NoteService>();
services.AddScoped<APService>();
services.AddScoped<ActivityDeliverService>();
services.AddScoped<ActivityHandlerService>();
services.AddScoped<WebFingerService>();
services.AddScoped<AuthorizedFetchMiddleware>();
// Singleton = instantiated once across application lifetime
services.AddSingleton<HttpClient>();
services.AddSingleton<HttpRequestService>();
services.AddSingleton<APFetchService>();
services.AddSingleton<ActivityFetcherService>();
services.AddSingleton<QueueService>();
services.AddSingleton<ErrorHandlerMiddleware>();
services.AddSingleton<RequestBufferingMiddleware>();

View file

@ -0,0 +1,38 @@
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Queues;
using Iceshrimp.Backend.Core.Services;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
public class ActivityDeliverService(
ILogger<ActivityDeliverService> logger,
DatabaseContext db,
HttpRequestService httpRqSvc,
QueueService queueService
) {
public async Task DeliverToFollowers(ASActivity activity, User actor) {
logger.LogDebug("Delivering activity {id} to followers", activity.Id);
if (activity.Actor == null) throw new Exception("Actor must not be null");
var inboxUrls = await db.Followings.Where(p => p.Followee == actor)
.Select(p => p.FollowerSharedInbox ?? p.FollowerInbox)
.Where(p => p != null)
.Select(p => p!)
.Distinct()
.ToListAsync();
if (inboxUrls.Count == 0) return;
var keypair = await db.UserKeypairs.FirstAsync(p => p.User == actor);
var payload = await activity.SignAndCompact(keypair);
foreach (var inboxUrl in inboxUrls) {
var request = await httpRqSvc.PostSigned(inboxUrl, payload, "application/activity+json", actor, keypair);
queueService.DeliverQueue.Enqueue(new DeliverJob(request));
}
}
}

View file

@ -11,7 +11,7 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
//TODO: required attribute doesn't work with Newtonsoft.Json it appears
//TODO: enforce @type values
public class APFetchService(HttpClient client, HttpRequestService httpRqSvc) {
public class ActivityFetcherService(HttpClient client, HttpRequestService httpRqSvc) {
private static readonly JsonSerializerSettings JsonSerializerSettings =
new();
//FIXME: not doing this breaks ld signatures, but doing this breaks mapping the object to datetime properties

View file

@ -1,4 +1,4 @@
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Controllers.Renderers.ActivityPub;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
@ -8,18 +8,17 @@ using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Queues;
using Iceshrimp.Backend.Core.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
public class APService(
IOptions<Config.InstanceSection> config,
ILogger<APService> logger,
public class ActivityHandlerService(
ILogger<ActivityHandlerService> logger,
NoteService noteSvc,
UserResolver userResolver,
DatabaseContext db,
HttpRequestService httpRqSvc,
QueueService queueService
QueueService queueService,
ActivityRenderer activityRenderer
) {
public Task PerformActivity(ASActivity activity, string? inboxUserId) {
logger.LogDebug("Processing activity: {activity}", activity.Id);
@ -29,7 +28,7 @@ public class APService(
switch (activity.Type) {
case "https://www.w3.org/ns/activitystreams#Create": {
if (activity.Object is ASNote note) return noteSvc.CreateNote(note, activity.Actor);
if (activity.Object is ASNote note) return noteSvc.ProcessNote(note, activity.Actor);
throw new NotImplementedException();
}
case "https://www.w3.org/ns/activitystreams#Like": {
@ -49,28 +48,6 @@ public class APService(
}
}
public async Task DeliverToFollowers(ASActivity activity, User actor) {
logger.LogDebug("Delivering activity {id} to followers", activity.Id);
if (activity.Actor == null) throw new Exception("Actor must not be null");
var inboxUrls = await db.Followings.Where(p => p.Followee == actor)
.Select(p => p.FollowerSharedInbox ?? p.FollowerInbox)
.Where(p => p != null)
.Select(p => p!)
.Distinct()
.ToListAsync();
if (inboxUrls.Count == 0) return;
var keypair = await db.UserKeypairs.FirstAsync(p => p.User == actor);
var payload = await activity.SignAndCompact(keypair);
foreach (var inboxUrl in inboxUrls) {
var request = await httpRqSvc.PostSigned(inboxUrl, payload, "application/activity+json", actor, keypair);
queueService.DeliverQueue.Enqueue(new DeliverJob(request));
}
}
private async Task Follow(ASObject followeeActor, ASObject followerActor, string requestId) {
var follower = await userResolver.Resolve(followerActor.Id);
var followee = await userResolver.Resolve(followeeActor.Id);
@ -79,19 +56,9 @@ public class APService(
//TODO: handle follow requests
var acceptActivity = new ASActivity {
Id = $"https://{config.Value.WebDomain}/activities/{new Guid().ToString().ToLowerInvariant()}",
Type = "https://www.w3.org/ns/activitystreams#Accept",
Actor = new ASActor {
Id = followeeActor.Id
},
Object = new ASObject {
Id = requestId
}
};
var keypair = await db.UserKeypairs.FirstAsync(p => p.User == followee);
var payload = await acceptActivity.SignAndCompact(keypair);
var acceptActivity = activityRenderer.RenderAccept(followeeActor, requestId);
var keypair = await db.UserKeypairs.FirstAsync(p => p.User == followee);
var payload = await acceptActivity.SignAndCompact(keypair);
var inboxUri = follower.SharedInbox ??
follower.Inbox ?? throw new Exception("Can't accept follow: user has no inbox");
var request = await httpRqSvc.PostSigned(inboxUri, payload, "application/activity+json", followee, keypair);

View file

@ -18,10 +18,10 @@ public class InboxQueue {
if (obj == null) throw new Exception("Failed to deserialize ASObject");
if (obj is not ASActivity activity) throw new NotImplementedException("Job data is not an ASActivity");
var apSvc = scope.GetRequiredService<APService>();
var logger = scope.GetRequiredService<ILogger<InboxQueue>>();
var apHandler = scope.GetRequiredService<ActivityHandlerService>();
var logger = scope.GetRequiredService<ILogger<InboxQueue>>();
logger.LogTrace("Preparation took {ms} ms", job.Duration);
await apSvc.PerformActivity(activity, job.InboxUserId);
await apHandler.PerformActivity(activity, job.InboxUserId);
}
}

View file

@ -1,3 +1,4 @@
using Iceshrimp.Backend.Controllers.Renderers.ActivityPub;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
@ -16,12 +17,41 @@ public class NoteService(
UserResolver userResolver,
IOptions<Config.InstanceSection> config,
UserService userSvc,
APFetchService fetchSvc
ActivityFetcherService fetchSvc,
ActivityDeliverService deliverSvc,
NoteRenderer noteRenderer,
UserRenderer userRenderer
) {
private readonly List<string> _resolverHistory = [];
private int _recursionLimit = 100;
public async Task<Note?> CreateNote(ASNote note, ASActor actor) {
public async Task<Note> CreateNote(User user, Note.NoteVisibility visibility, string? text = null,
string? cw = null, Note? reply = null, Note? renote = null) {
var actor = await userRenderer.Render(user);
var note = new Note {
Id = IdHelpers.GenerateSlowflakeId(),
Text = text,
Cw = cw,
Reply = reply,
Renote = renote,
UserId = user.Id,
CreatedAt = DateTime.UtcNow,
UserHost = null,
Visibility = visibility
};
await db.AddAsync(note);
await db.SaveChangesAsync();
var obj = noteRenderer.Render(note);
var activity = ActivityRenderer.RenderCreate(obj, actor);
await deliverSvc.DeliverToFollowers(activity, user);
return note;
}
public async Task<Note?> ProcessNote(ASNote note, ASActor actor) {
if (await db.Notes.AnyAsync(p => p.Uri == note.Id)) {
logger.LogDebug("Note '{id}' already exists, skipping", note.Id);
return null;
@ -100,7 +130,7 @@ public class NoteService(
var actor = await fetchSvc.FetchActor(attrTo.Id, instanceActor, instanceActorKeypair);
try {
return await CreateNote(fetchedNote, actor);
return await ProcessNote(fetchedNote, actor);
}
catch (Exception e) {
logger.LogDebug("Failed to create resolved note: {error}", e.Message);

View file

@ -16,7 +16,7 @@ public class UserService(
IOptions<Config.InstanceSection> config,
ILogger<UserService> logger,
DatabaseContext db,
APFetchService fetchSvc) {
ActivityFetcherService fetchSvc) {
private (string Username, string? Host) AcctToTuple(string acct) {
if (!acct.StartsWith("acct:")) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query");

View file

@ -38,4 +38,5 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Iceshrimp/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=misskeymarkdown/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=nodeinfo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=renote/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=webfinger/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>