Refactor AP core
This commit is contained in:
parent
e4211fde56
commit
831a1ceeba
9 changed files with 120 additions and 53 deletions
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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>
|
Loading…
Add table
Reference in a new issue