[backend/federation] Resolve incoming activity objects (ISH-20, ISH-12)
This commit is contained in:
parent
19d6251a2a
commit
02e003afa3
13 changed files with 223 additions and 115 deletions
|
@ -21,33 +21,35 @@ public static class ServiceExtensions {
|
||||||
//services.AddTransient<T>();
|
//services.AddTransient<T>();
|
||||||
|
|
||||||
// Scoped = instantiated per request
|
// Scoped = instantiated per request
|
||||||
services.AddScoped<ActivityPub.ActivityRenderer>();
|
services
|
||||||
services.AddScoped<ActivityPub.UserRenderer>();
|
.AddScoped<ActivityPub.ActivityRenderer>()
|
||||||
services.AddScoped<ActivityPub.NoteRenderer>();
|
.AddScoped<ActivityPub.UserRenderer>()
|
||||||
services.AddScoped<ActivityPub.UserResolver>();
|
.AddScoped<ActivityPub.NoteRenderer>()
|
||||||
services.AddScoped<UserService>();
|
.AddScoped<ActivityPub.UserResolver>()
|
||||||
services.AddScoped<NoteService>();
|
.AddScoped<ActivityPub.ObjectResolver>()
|
||||||
services.AddScoped<ActivityPub.ActivityDeliverService>();
|
.AddScoped<ActivityPub.ActivityDeliverService>()
|
||||||
services.AddScoped<ActivityPub.ActivityHandlerService>();
|
.AddScoped<ActivityPub.FederationControlService>()
|
||||||
services.AddScoped<WebFingerService>();
|
.AddScoped<ActivityPub.ActivityHandlerService>()
|
||||||
services.AddScoped<ActivityPub.FederationControlService>();
|
.AddScoped<ActivityPub.ActivityFetcherService>()
|
||||||
services.AddScoped<AuthorizedFetchMiddleware>();
|
.AddScoped<UserService>()
|
||||||
services.AddScoped<AuthenticationMiddleware>();
|
.AddScoped<SystemUserService>()
|
||||||
|
.AddScoped<NoteService>()
|
||||||
//TODO: make this prettier
|
.AddScoped<WebFingerService>()
|
||||||
services.AddScoped<UserRenderer>();
|
.AddScoped<AuthorizedFetchMiddleware>()
|
||||||
services.AddScoped<NoteRenderer>();
|
.AddScoped<AuthenticationMiddleware>()
|
||||||
|
.AddScoped<UserRenderer>()
|
||||||
|
.AddScoped<NoteRenderer>();
|
||||||
|
|
||||||
// Singleton = instantiated once across application lifetime
|
// Singleton = instantiated once across application lifetime
|
||||||
services.AddSingleton<HttpClient>();
|
services
|
||||||
services.AddSingleton<HttpRequestService>();
|
.AddSingleton<HttpClient>()
|
||||||
services.AddSingleton<ActivityPub.ActivityFetcherService>();
|
.AddSingleton<HttpRequestService>()
|
||||||
services.AddSingleton<QueueService>();
|
.AddSingleton<QueueService>()
|
||||||
services.AddSingleton<ErrorHandlerMiddleware>();
|
.AddSingleton<ErrorHandlerMiddleware>()
|
||||||
services.AddSingleton<RequestBufferingMiddleware>();
|
.AddSingleton<RequestBufferingMiddleware>()
|
||||||
services.AddSingleton<AuthorizationMiddleware>();
|
.AddSingleton<AuthorizationMiddleware>()
|
||||||
services.AddSingleton<RequestVerificationMiddleware>();
|
.AddSingleton<RequestVerificationMiddleware>()
|
||||||
services.AddSingleton<RequestDurationMiddleware>();
|
.AddSingleton<RequestDurationMiddleware>();
|
||||||
|
|
||||||
// Hosted services = long running background tasks
|
// Hosted services = long running background tasks
|
||||||
// Note: These need to be added as a singleton as well to ensure data consistency
|
// Note: These need to be added as a singleton as well to ensure data consistency
|
||||||
|
|
|
@ -8,15 +8,16 @@ using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||||
|
|
||||||
//TODO: required attribute doesn't work with Newtonsoft.Json it appears
|
public class ActivityFetcherService(HttpClient client, HttpRequestService httpRqSvc, SystemUserService systemUserSvc) {
|
||||||
//TODO: enforce @type values
|
private static readonly JsonSerializerSettings JsonSerializerSettings = new();
|
||||||
|
|
||||||
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
|
//FIXME: not doing this breaks ld signatures, but doing this breaks mapping the object to datetime properties
|
||||||
//new() { DateParseHandling = DateParseHandling.None };
|
//new() { DateParseHandling = DateParseHandling.None };
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ASObject>> FetchActivityAsync(string url) {
|
||||||
|
var (actor, keypair) = await systemUserSvc.GetInstanceActorWithKeypairAsync();
|
||||||
|
return await FetchActivityAsync(url, actor, keypair);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ASObject>> FetchActivityAsync(string url, User actor, UserKeypair keypair) {
|
public async Task<IEnumerable<ASObject>> FetchActivityAsync(string url, User actor, UserKeypair keypair) {
|
||||||
var request = httpRqSvc.GetSigned(url, ["application/activity+json"], actor, keypair);
|
var request = httpRqSvc.GetSigned(url, ["application/activity+json"], actor, keypair);
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
@ -42,8 +43,21 @@ public class ActivityFetcherService(HttpClient client, HttpRequestService httpRq
|
||||||
throw new GracefulException("Failed to fetch actor");
|
throw new GracefulException("Failed to fetch actor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ASActor> FetchActorAsync(string uri) {
|
||||||
|
var (actor, keypair) = await systemUserSvc.GetInstanceActorWithKeypairAsync();
|
||||||
|
var activity = await FetchActivityAsync(uri, actor, keypair);
|
||||||
|
return activity.OfType<ASActor>().FirstOrDefault() ??
|
||||||
|
throw new GracefulException("Failed to fetch actor");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ASNote?> FetchNoteAsync(string uri, User actor, UserKeypair keypair) {
|
public async Task<ASNote?> FetchNoteAsync(string uri, User actor, UserKeypair keypair) {
|
||||||
var activity = await FetchActivityAsync(uri, actor, keypair);
|
var activity = await FetchActivityAsync(uri, actor, keypair);
|
||||||
return activity.OfType<ASNote>().FirstOrDefault();
|
return activity.OfType<ASNote>().FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ASNote?> FetchNoteAsync(string uri) {
|
||||||
|
var (actor, keypair) = await systemUserSvc.GetInstanceActorWithKeypairAsync();
|
||||||
|
var activity = await FetchActivityAsync(uri, actor, keypair);
|
||||||
|
return activity.OfType<ASNote>().FirstOrDefault();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -20,7 +20,8 @@ public class ActivityHandlerService(
|
||||||
QueueService queueService,
|
QueueService queueService,
|
||||||
ActivityRenderer activityRenderer,
|
ActivityRenderer activityRenderer,
|
||||||
IOptions<Config.InstanceSection> config,
|
IOptions<Config.InstanceSection> config,
|
||||||
FederationControlService federationCtrl
|
FederationControlService federationCtrl,
|
||||||
|
ObjectResolver resolver
|
||||||
) {
|
) {
|
||||||
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId) {
|
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId) {
|
||||||
logger.LogDebug("Processing activity: {activity}", activity.Id);
|
logger.LogDebug("Processing activity: {activity}", activity.Id);
|
||||||
|
@ -28,6 +29,11 @@ public class ActivityHandlerService(
|
||||||
throw GracefulException.UnprocessableEntity("Cannot perform activity as actor 'null'");
|
throw GracefulException.UnprocessableEntity("Cannot perform activity as actor 'null'");
|
||||||
if (await federationCtrl.ShouldBlockAsync(activity.Actor.Id))
|
if (await federationCtrl.ShouldBlockAsync(activity.Actor.Id))
|
||||||
throw GracefulException.UnprocessableEntity("Instance is blocked");
|
throw GracefulException.UnprocessableEntity("Instance is blocked");
|
||||||
|
if (activity.Object == null)
|
||||||
|
throw GracefulException.UnprocessableEntity("Activity object is null");
|
||||||
|
if (activity.Object.IsUnresolved)
|
||||||
|
activity.Object = await resolver.ResolveObject(activity.Object) ??
|
||||||
|
throw GracefulException.UnprocessableEntity("Failed to resolve activity object");
|
||||||
|
|
||||||
//TODO: validate inboxUserId
|
//TODO: validate inboxUserId
|
||||||
|
|
||||||
|
@ -40,11 +46,22 @@ public class ActivityHandlerService(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case ASDelete: {
|
case ASDelete: {
|
||||||
//TODO: handle user deletes
|
if (activity.Object is not ASTombstone tombstone)
|
||||||
if (activity.Object is not ASNote note)
|
|
||||||
throw GracefulException.UnprocessableEntity("Delete activity object is invalid");
|
throw GracefulException.UnprocessableEntity("Delete activity object is invalid");
|
||||||
await noteSvc.DeleteNoteAsync(note, activity.Actor);
|
if (await db.Notes.AnyAsync(p => p.Uri == tombstone.Id)) {
|
||||||
return;
|
await noteSvc.DeleteNoteAsync(tombstone, activity.Actor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await db.Users.AnyAsync(p => p.Uri == tombstone.Id)) {
|
||||||
|
if (tombstone.Id != activity.Actor.Id)
|
||||||
|
throw GracefulException.UnprocessableEntity("Refusing to delete user: actor doesn't match");
|
||||||
|
|
||||||
|
//TODO: handle user deletes
|
||||||
|
throw new NotImplementedException("User deletes aren't supported yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw GracefulException.UnprocessableEntity("Delete activity object is unknown or invalid");
|
||||||
}
|
}
|
||||||
case ASFollow: {
|
case ASFollow: {
|
||||||
if (activity.Object is not { } obj)
|
if (activity.Object is not { } obj)
|
||||||
|
@ -73,7 +90,7 @@ public class ActivityHandlerService(
|
||||||
case ASUndo: {
|
case ASUndo: {
|
||||||
//TODO: what other types of undo objects are there?
|
//TODO: what other types of undo objects are there?
|
||||||
if (activity.Object is not ASActivity { Type: ASActivity.Types.Follow, Object: not null } undoActivity)
|
if (activity.Object is not ASActivity { Type: ASActivity.Types.Follow, Object: not null } undoActivity)
|
||||||
throw new NotImplementedException("Unsupported undo operation");
|
throw new NotImplementedException("Undo activity object is invalid");
|
||||||
await UnfollowAsync(undoActivity.Object, activity.Actor);
|
await UnfollowAsync(undoActivity.Object, activity.Actor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||||
|
|
||||||
|
public class ObjectResolver(
|
||||||
|
ILogger<ObjectResolver> logger,
|
||||||
|
ActivityFetcherService fetchSvc,
|
||||||
|
DatabaseContext db,
|
||||||
|
FederationControlService federationCtrl
|
||||||
|
) {
|
||||||
|
public async Task<ASObject?> ResolveObject(ASIdObject idObj) {
|
||||||
|
if (idObj is ASObject obj) return obj;
|
||||||
|
if (idObj.Id == null) {
|
||||||
|
logger.LogDebug("Refusing to resolve object with null id property");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await federationCtrl.ShouldBlockAsync(idObj.Id)) {
|
||||||
|
logger.LogDebug("Instance is blocked");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await db.Notes.AnyAsync(p => p.Uri == idObj.Id))
|
||||||
|
return new ASNote { Id = idObj.Id };
|
||||||
|
if (await db.Users.AnyAsync(p => p.Uri == idObj.Id))
|
||||||
|
return new ASActor { Id = idObj.Id };
|
||||||
|
|
||||||
|
try {
|
||||||
|
var result = await fetchSvc.FetchActivityAsync(idObj.Id);
|
||||||
|
return result.FirstOrDefault();
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
logger.LogDebug("Failed to resolve object {id}: {error}", idObj.Id, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ public class ASActivity : ASObject {
|
||||||
[JC(typeof(ASObjectConverter))]
|
[JC(typeof(ASObjectConverter))]
|
||||||
public ASObject? Object { get; set; }
|
public ASObject? Object { get; set; }
|
||||||
|
|
||||||
public static class Types {
|
public new static class Types {
|
||||||
private const string Ns = Constants.ActivityStreamsNs;
|
private const string Ns = Constants.ActivityStreamsNs;
|
||||||
|
|
||||||
public const string Create = $"{Ns}#Create";
|
public const string Create = $"{Ns}#Create";
|
||||||
|
|
|
@ -134,7 +134,7 @@ public class ASActor : ASObject {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Types {
|
public new static class Types {
|
||||||
private const string Ns = Constants.ActivityStreamsNs;
|
private const string Ns = Constants.ActivityStreamsNs;
|
||||||
|
|
||||||
public const string Application = $"{Ns}#Application";
|
public const string Application = $"{Ns}#Application";
|
||||||
|
|
|
@ -2,9 +2,8 @@ using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
|
|
||||||
//TODO: handle object | link correctly (we don't need to resolve it if it's already the full object)
|
|
||||||
public class ASIdObject() {
|
public class ASIdObject() {
|
||||||
public ASIdObject(string id) : this() {
|
public ASIdObject(string? id) : this() {
|
||||||
Id = id;
|
Id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ public class ASNote : ASObject {
|
||||||
return Note.NoteVisibility.Specified;
|
return Note.NoteVisibility.Specified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Types {
|
public new static class Types {
|
||||||
private const string Ns = Constants.ActivityStreamsNs;
|
private const string Ns = Constants.ActivityStreamsNs;
|
||||||
|
|
||||||
public const string Note = $"{Ns}#Note";
|
public const string Note = $"{Ns}#Note";
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||||
|
@ -8,11 +9,13 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
|
|
||||||
public class ASObject : ASIdObject {
|
public class ASObject : ASIdObject {
|
||||||
[J("@id")] [JR] public new required string Id { get; set; }
|
[J("@id")] [JR] public new required string Id { get; set; }
|
||||||
|
|
||||||
[J("@type")]
|
[J("@type")]
|
||||||
[JC(typeof(LDTypeConverter))]
|
[JC(typeof(LDTypeConverter))]
|
||||||
public string? Type { get; set; }
|
public string? Type { get; set; }
|
||||||
|
|
||||||
|
public bool IsUnresolved => GetType() == typeof(ASObject) && Type == null;
|
||||||
|
|
||||||
//FIXME: don't recurse creates and co
|
//FIXME: don't recurse creates and co
|
||||||
public static ASObject? Deserialize(JToken token) {
|
public static ASObject? Deserialize(JToken token) {
|
||||||
return token.Type switch {
|
return token.Type switch {
|
||||||
|
@ -23,6 +26,7 @@ public class ASObject : ASIdObject {
|
||||||
ASActor.Types.Organization => token.ToObject<ASActor>(),
|
ASActor.Types.Organization => token.ToObject<ASActor>(),
|
||||||
ASActor.Types.Application => token.ToObject<ASActor>(),
|
ASActor.Types.Application => token.ToObject<ASActor>(),
|
||||||
ASNote.Types.Note => token.ToObject<ASNote>(),
|
ASNote.Types.Note => token.ToObject<ASNote>(),
|
||||||
|
Types.Tombstone => token.ToObject<ASTombstone>(),
|
||||||
ASActivity.Types.Create => token.ToObject<ASCreate>(),
|
ASActivity.Types.Create => token.ToObject<ASCreate>(),
|
||||||
ASActivity.Types.Delete => token.ToObject<ASDelete>(),
|
ASActivity.Types.Delete => token.ToObject<ASDelete>(),
|
||||||
ASActivity.Types.Follow => token.ToObject<ASFollow>(),
|
ASActivity.Types.Follow => token.ToObject<ASFollow>(),
|
||||||
|
@ -33,13 +37,24 @@ public class ASObject : ASIdObject {
|
||||||
ASActivity.Types.Like => token.ToObject<ASActivity>(),
|
ASActivity.Types.Like => token.ToObject<ASActivity>(),
|
||||||
_ => token.ToObject<ASObject>()
|
_ => token.ToObject<ASObject>()
|
||||||
},
|
},
|
||||||
JTokenType.Array => Deserialize(token.First()),
|
JTokenType.Array => Deserialize(token.First()),
|
||||||
JTokenType.String => new ASObject { Id = token.Value<string>() ?? "" },
|
JTokenType.String => new ASObject {
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
Id = token.Value<string>() ??
|
||||||
|
throw new Exception("Encountered JTokenType.String with Value<string> null")
|
||||||
|
},
|
||||||
|
_ => throw new Exception($"Encountered JTokenType {token.Type}, which is not valid at this point")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Types {
|
||||||
|
private const string Ns = Constants.ActivityStreamsNs;
|
||||||
|
|
||||||
|
public const string Tombstone = $"{Ns}#Tombstone";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ASTombstone : ASObject;
|
||||||
|
|
||||||
public sealed class LDTypeConverter : ASSerializer.ListSingleObjectConverter<string>;
|
public sealed class LDTypeConverter : ASSerializer.ListSingleObjectConverter<string>;
|
||||||
|
|
||||||
internal sealed class ASObjectConverter : JsonConverter {
|
internal sealed class ASObjectConverter : JsonConverter {
|
||||||
|
|
|
@ -14,7 +14,7 @@ public class AuthorizedFetchMiddleware(
|
||||||
IOptionsSnapshot<Config.SecuritySection> config,
|
IOptionsSnapshot<Config.SecuritySection> config,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
ActivityPub.UserResolver userResolver,
|
ActivityPub.UserResolver userResolver,
|
||||||
UserService userSvc,
|
SystemUserService systemUserSvc,
|
||||||
ActivityPub.FederationControlService fedCtrlSvc,
|
ActivityPub.FederationControlService fedCtrlSvc,
|
||||||
ILogger<AuthorizedFetchMiddleware> logger) : IMiddleware {
|
ILogger<AuthorizedFetchMiddleware> logger) : IMiddleware {
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||||
|
@ -24,7 +24,7 @@ public class AuthorizedFetchMiddleware(
|
||||||
var request = ctx.Request;
|
var request = ctx.Request;
|
||||||
|
|
||||||
//TODO: cache this somewhere
|
//TODO: cache this somewhere
|
||||||
var instanceActorUri = $"/users/{(await userSvc.GetInstanceActorAsync()).Id}";
|
var instanceActorUri = $"/users/{(await systemUserSvc.GetInstanceActorAsync()).Id}";
|
||||||
if (ctx.Request.Path.Value == instanceActorUri) {
|
if (ctx.Request.Path.Value == instanceActorUri) {
|
||||||
await next(ctx);
|
await next(ctx);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -60,7 +60,7 @@ public class NoteService(
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteNoteAsync(ASNote note, ASActor actor) {
|
public async Task DeleteNoteAsync(ASTombstone note, ASActor actor) {
|
||||||
// ReSharper disable once EntityFramework.NPlusOne.IncompleteDataQuery (it doesn't know about IncludeCommonProperties())
|
// ReSharper disable once EntityFramework.NPlusOne.IncompleteDataQuery (it doesn't know about IncludeCommonProperties())
|
||||||
var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
|
var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
|
||||||
if (dbNote == null) {
|
if (dbNote == null) {
|
||||||
|
@ -157,9 +157,7 @@ public class NoteService(
|
||||||
if (note != null) return note;
|
if (note != null) return note;
|
||||||
|
|
||||||
//TODO: should we fall back to a regular user's keypair if fetching with instance actor fails & a local user is following the actor?
|
//TODO: should we fall back to a regular user's keypair if fetching with instance actor fails & a local user is following the actor?
|
||||||
var instanceActor = await userSvc.GetInstanceActorAsync();
|
var fetchedNote = await fetchSvc.FetchNoteAsync(uri);
|
||||||
var instanceActorKeypair = await db.UserKeypairs.FirstAsync(p => p.User == instanceActor);
|
|
||||||
var fetchedNote = await fetchSvc.FetchNoteAsync(uri, instanceActor, instanceActorKeypair);
|
|
||||||
if (fetchedNote?.AttributedTo is not [{ Id: not null } attrTo]) {
|
if (fetchedNote?.AttributedTo is not [{ Id: not null } attrTo]) {
|
||||||
logger.LogDebug("Invalid Note.AttributedTo, skipping");
|
logger.LogDebug("Invalid Note.AttributedTo, skipping");
|
||||||
return null;
|
return null;
|
||||||
|
@ -171,7 +169,7 @@ public class NoteService(
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: we don't need to fetch the actor every time, we can use userResolver here
|
//TODO: we don't need to fetch the actor every time, we can use userResolver here
|
||||||
var actor = await fetchSvc.FetchActorAsync(attrTo.Id, instanceActor, instanceActorKeypair);
|
var actor = await fetchSvc.FetchActorAsync(attrTo.Id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await ProcessNoteAsync(fetchedNote, actor);
|
return await ProcessNoteAsync(fetchedNote, actor);
|
||||||
|
|
84
Iceshrimp.Backend/Core/Services/SystemUserService.cs
Normal file
84
Iceshrimp.Backend/Core/Services/SystemUserService.cs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
|
public class SystemUserService(ILogger<SystemUserService> logger, DatabaseContext db, IDistributedCache cache) {
|
||||||
|
public async Task<User> GetInstanceActorAsync() {
|
||||||
|
return await GetOrCreateSystemUserAsync("instance.actor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> GetRelayActorAsync() {
|
||||||
|
return await GetOrCreateSystemUserAsync("relay.actor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(User user, UserKeypair keypair)> GetInstanceActorWithKeypairAsync() {
|
||||||
|
return await GetOrCreateSystemUserAndKeypairAsync("instance.actor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(User user, UserKeypair keypair)> GetRelayActorWithKeypairAsync() {
|
||||||
|
return await GetOrCreateSystemUserAndKeypairAsync("relay.actor");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(User user, UserKeypair keypair)> GetOrCreateSystemUserAndKeypairAsync(string username) {
|
||||||
|
var user = await GetOrCreateSystemUserAsync(username);
|
||||||
|
var keypair = await db.UserKeypairs.FirstAsync(p => p.User == user); //TODO: cache this in redis as well
|
||||||
|
|
||||||
|
return (user, keypair);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<User> GetOrCreateSystemUserAsync(string username) {
|
||||||
|
return await cache.FetchAsync($"systemUser:{username}", TimeSpan.FromHours(24), async () => {
|
||||||
|
logger.LogTrace("GetOrCreateSystemUser delegate method called for user {username}", username);
|
||||||
|
return await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username &&
|
||||||
|
p.Host == null) ??
|
||||||
|
await CreateSystemUserAsync(username);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<User> CreateSystemUserAsync(string username) {
|
||||||
|
if (await db.Users.AnyAsync(p => p.UsernameLower == username.ToLowerInvariant() && p.Host == null))
|
||||||
|
throw new GracefulException($"System user {username} already exists");
|
||||||
|
|
||||||
|
var keypair = RSA.Create(4096);
|
||||||
|
var user = new User {
|
||||||
|
Id = IdHelpers.GenerateSlowflakeId(),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Username = username,
|
||||||
|
UsernameLower = username.ToLowerInvariant(),
|
||||||
|
Host = null,
|
||||||
|
IsAdmin = false,
|
||||||
|
IsLocked = true,
|
||||||
|
IsExplorable = false,
|
||||||
|
IsBot = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var userKeypair = new UserKeypair {
|
||||||
|
UserId = user.Id,
|
||||||
|
PrivateKey = keypair.ExportPkcs8PrivateKeyPem(),
|
||||||
|
PublicKey = keypair.ExportSubjectPublicKeyInfoPem()
|
||||||
|
};
|
||||||
|
|
||||||
|
var userProfile = new UserProfile {
|
||||||
|
UserId = user.Id,
|
||||||
|
AutoAcceptFollowed = false,
|
||||||
|
Password = null
|
||||||
|
};
|
||||||
|
|
||||||
|
var usedUsername = new UsedUsername {
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Username = username.ToLowerInvariant()
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.AddRangeAsync(user, userKeypair, userProfile, usedUsername);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,9 +52,7 @@ public class UserService(
|
||||||
|
|
||||||
public async Task<User> CreateUserAsync(string uri, string acct) {
|
public async Task<User> CreateUserAsync(string uri, string acct) {
|
||||||
logger.LogDebug("Creating user {acct} with uri {uri}", acct, uri);
|
logger.LogDebug("Creating user {acct} with uri {uri}", acct, uri);
|
||||||
var instanceActor = await GetInstanceActorAsync();
|
var actor = await fetchSvc.FetchActorAsync(uri);
|
||||||
var instanceActorKeypair = await db.UserKeypairs.FirstAsync(p => p.User == instanceActor);
|
|
||||||
var actor = await fetchSvc.FetchActorAsync(uri, instanceActor, instanceActorKeypair);
|
|
||||||
logger.LogDebug("Got actor: {url}", actor.Url);
|
logger.LogDebug("Got actor: {url}", actor.Url);
|
||||||
|
|
||||||
actor.Normalize(uri, acct);
|
actor.Normalize(uri, acct);
|
||||||
|
@ -63,7 +61,7 @@ public class UserService(
|
||||||
Id = IdHelpers.GenerateSlowflakeId(),
|
Id = IdHelpers.GenerateSlowflakeId(),
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
LastFetchedAt = DateTime.UtcNow,
|
LastFetchedAt = DateTime.UtcNow,
|
||||||
DisplayName = actor.DisplayName,
|
DisplayName = actor.DisplayName,
|
||||||
IsLocked = actor.IsLocked ?? false,
|
IsLocked = actor.IsLocked ?? false,
|
||||||
IsBot = actor.IsBot,
|
IsBot = actor.IsBot,
|
||||||
Username = actor.Username!,
|
Username = actor.Username!,
|
||||||
|
@ -153,62 +151,4 @@ public class UserService(
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<User> GetInstanceActorAsync() {
|
|
||||||
return await GetOrCreateSystemUserAsync("instance.actor");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User> GetRelayActorAsync() {
|
|
||||||
return await GetOrCreateSystemUserAsync("relay.actor");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<User> GetOrCreateSystemUserAsync(string username) {
|
|
||||||
return await cache.FetchAsync($"systemUser:{username}", TimeSpan.FromHours(24), async () => {
|
|
||||||
logger.LogTrace("GetOrCreateSystemUser delegate method called for user {username}", username);
|
|
||||||
return await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username &&
|
|
||||||
p.Host == null) ??
|
|
||||||
await CreateSystemUserAsync(username);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<User> CreateSystemUserAsync(string username) {
|
|
||||||
if (await db.Users.AnyAsync(p => p.UsernameLower == username.ToLowerInvariant() && p.Host == null))
|
|
||||||
throw new GracefulException($"System user {username} already exists");
|
|
||||||
|
|
||||||
var keypair = RSA.Create(4096);
|
|
||||||
var user = new User {
|
|
||||||
Id = IdHelpers.GenerateSlowflakeId(),
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
Username = username,
|
|
||||||
UsernameLower = username.ToLowerInvariant(),
|
|
||||||
Host = null,
|
|
||||||
IsAdmin = false,
|
|
||||||
IsLocked = true,
|
|
||||||
IsExplorable = false,
|
|
||||||
IsBot = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var userKeypair = new UserKeypair {
|
|
||||||
UserId = user.Id,
|
|
||||||
PrivateKey = keypair.ExportPkcs8PrivateKeyPem(),
|
|
||||||
PublicKey = keypair.ExportSubjectPublicKeyInfoPem()
|
|
||||||
};
|
|
||||||
|
|
||||||
var userProfile = new UserProfile {
|
|
||||||
UserId = user.Id,
|
|
||||||
AutoAcceptFollowed = false,
|
|
||||||
Password = null
|
|
||||||
};
|
|
||||||
|
|
||||||
var usedUsername = new UsedUsername {
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
Username = username.ToLowerInvariant()
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.AddRangeAsync(user, userKeypair, userProfile, usedUsername);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue