[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>();
|
||||
|
||||
// Scoped = instantiated per request
|
||||
services.AddScoped<ActivityPub.ActivityRenderer>();
|
||||
services.AddScoped<ActivityPub.UserRenderer>();
|
||||
services.AddScoped<ActivityPub.NoteRenderer>();
|
||||
services.AddScoped<ActivityPub.UserResolver>();
|
||||
services.AddScoped<UserService>();
|
||||
services.AddScoped<NoteService>();
|
||||
services.AddScoped<ActivityPub.ActivityDeliverService>();
|
||||
services.AddScoped<ActivityPub.ActivityHandlerService>();
|
||||
services.AddScoped<WebFingerService>();
|
||||
services.AddScoped<ActivityPub.FederationControlService>();
|
||||
services.AddScoped<AuthorizedFetchMiddleware>();
|
||||
services.AddScoped<AuthenticationMiddleware>();
|
||||
|
||||
//TODO: make this prettier
|
||||
services.AddScoped<UserRenderer>();
|
||||
services.AddScoped<NoteRenderer>();
|
||||
services
|
||||
.AddScoped<ActivityPub.ActivityRenderer>()
|
||||
.AddScoped<ActivityPub.UserRenderer>()
|
||||
.AddScoped<ActivityPub.NoteRenderer>()
|
||||
.AddScoped<ActivityPub.UserResolver>()
|
||||
.AddScoped<ActivityPub.ObjectResolver>()
|
||||
.AddScoped<ActivityPub.ActivityDeliverService>()
|
||||
.AddScoped<ActivityPub.FederationControlService>()
|
||||
.AddScoped<ActivityPub.ActivityHandlerService>()
|
||||
.AddScoped<ActivityPub.ActivityFetcherService>()
|
||||
.AddScoped<UserService>()
|
||||
.AddScoped<SystemUserService>()
|
||||
.AddScoped<NoteService>()
|
||||
.AddScoped<WebFingerService>()
|
||||
.AddScoped<AuthorizedFetchMiddleware>()
|
||||
.AddScoped<AuthenticationMiddleware>()
|
||||
.AddScoped<UserRenderer>()
|
||||
.AddScoped<NoteRenderer>();
|
||||
|
||||
// Singleton = instantiated once across application lifetime
|
||||
services.AddSingleton<HttpClient>();
|
||||
services.AddSingleton<HttpRequestService>();
|
||||
services.AddSingleton<ActivityPub.ActivityFetcherService>();
|
||||
services.AddSingleton<QueueService>();
|
||||
services.AddSingleton<ErrorHandlerMiddleware>();
|
||||
services.AddSingleton<RequestBufferingMiddleware>();
|
||||
services.AddSingleton<AuthorizationMiddleware>();
|
||||
services.AddSingleton<RequestVerificationMiddleware>();
|
||||
services.AddSingleton<RequestDurationMiddleware>();
|
||||
services
|
||||
.AddSingleton<HttpClient>()
|
||||
.AddSingleton<HttpRequestService>()
|
||||
.AddSingleton<QueueService>()
|
||||
.AddSingleton<ErrorHandlerMiddleware>()
|
||||
.AddSingleton<RequestBufferingMiddleware>()
|
||||
.AddSingleton<AuthorizationMiddleware>()
|
||||
.AddSingleton<RequestVerificationMiddleware>()
|
||||
.AddSingleton<RequestDurationMiddleware>();
|
||||
|
||||
// Hosted services = long running background tasks
|
||||
// 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;
|
||||
|
||||
//TODO: required attribute doesn't work with Newtonsoft.Json it appears
|
||||
//TODO: enforce @type values
|
||||
|
||||
public class ActivityFetcherService(HttpClient client, HttpRequestService httpRqSvc) {
|
||||
private static readonly JsonSerializerSettings JsonSerializerSettings =
|
||||
new();
|
||||
public class ActivityFetcherService(HttpClient client, HttpRequestService httpRqSvc, SystemUserService systemUserSvc) {
|
||||
private static readonly JsonSerializerSettings JsonSerializerSettings = new();
|
||||
//FIXME: not doing this breaks ld signatures, but doing this breaks mapping the object to datetime properties
|
||||
//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) {
|
||||
var request = httpRqSvc.GetSigned(url, ["application/activity+json"], actor, keypair);
|
||||
var response = await client.SendAsync(request);
|
||||
|
@ -42,8 +43,21 @@ public class ActivityFetcherService(HttpClient client, HttpRequestService httpRq
|
|||
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) {
|
||||
var activity = await FetchActivityAsync(uri, actor, keypair);
|
||||
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,
|
||||
ActivityRenderer activityRenderer,
|
||||
IOptions<Config.InstanceSection> config,
|
||||
FederationControlService federationCtrl
|
||||
FederationControlService federationCtrl,
|
||||
ObjectResolver resolver
|
||||
) {
|
||||
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId) {
|
||||
logger.LogDebug("Processing activity: {activity}", activity.Id);
|
||||
|
@ -28,6 +29,11 @@ public class ActivityHandlerService(
|
|||
throw GracefulException.UnprocessableEntity("Cannot perform activity as actor 'null'");
|
||||
if (await federationCtrl.ShouldBlockAsync(activity.Actor.Id))
|
||||
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
|
||||
|
||||
|
@ -40,12 +46,23 @@ public class ActivityHandlerService(
|
|||
return;
|
||||
}
|
||||
case ASDelete: {
|
||||
//TODO: handle user deletes
|
||||
if (activity.Object is not ASNote note)
|
||||
if (activity.Object is not ASTombstone tombstone)
|
||||
throw GracefulException.UnprocessableEntity("Delete activity object is invalid");
|
||||
await noteSvc.DeleteNoteAsync(note, activity.Actor);
|
||||
if (await db.Notes.AnyAsync(p => p.Uri == tombstone.Id)) {
|
||||
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: {
|
||||
if (activity.Object is not { } obj)
|
||||
throw GracefulException.UnprocessableEntity("Follow activity object is invalid");
|
||||
|
@ -73,7 +90,7 @@ public class ActivityHandlerService(
|
|||
case ASUndo: {
|
||||
//TODO: what other types of undo objects are there?
|
||||
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);
|
||||
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))]
|
||||
public ASObject? Object { get; set; }
|
||||
|
||||
public static class Types {
|
||||
public new static class Types {
|
||||
private const string Ns = Constants.ActivityStreamsNs;
|
||||
|
||||
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;
|
||||
|
||||
public const string Application = $"{Ns}#Application";
|
||||
|
|
|
@ -2,9 +2,8 @@ using J = Newtonsoft.Json.JsonPropertyAttribute;
|
|||
|
||||
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 ASIdObject(string id) : this() {
|
||||
public ASIdObject(string? id) : this() {
|
||||
Id = id;
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ public class ASNote : ASObject {
|
|||
return Note.NoteVisibility.Specified;
|
||||
}
|
||||
|
||||
public static class Types {
|
||||
public new static class Types {
|
||||
private const string Ns = Constants.ActivityStreamsNs;
|
||||
|
||||
public const string Note = $"{Ns}#Note";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||
|
@ -13,6 +14,8 @@ public class ASObject : ASIdObject {
|
|||
[JC(typeof(LDTypeConverter))]
|
||||
public string? Type { get; set; }
|
||||
|
||||
public bool IsUnresolved => GetType() == typeof(ASObject) && Type == null;
|
||||
|
||||
//FIXME: don't recurse creates and co
|
||||
public static ASObject? Deserialize(JToken token) {
|
||||
return token.Type switch {
|
||||
|
@ -23,6 +26,7 @@ public class ASObject : ASIdObject {
|
|||
ASActor.Types.Organization => token.ToObject<ASActor>(),
|
||||
ASActor.Types.Application => token.ToObject<ASActor>(),
|
||||
ASNote.Types.Note => token.ToObject<ASNote>(),
|
||||
Types.Tombstone => token.ToObject<ASTombstone>(),
|
||||
ASActivity.Types.Create => token.ToObject<ASCreate>(),
|
||||
ASActivity.Types.Delete => token.ToObject<ASDelete>(),
|
||||
ASActivity.Types.Follow => token.ToObject<ASFollow>(),
|
||||
|
@ -34,11 +38,22 @@ public class ASObject : ASIdObject {
|
|||
_ => token.ToObject<ASObject>()
|
||||
},
|
||||
JTokenType.Array => Deserialize(token.First()),
|
||||
JTokenType.String => new ASObject { Id = token.Value<string>() ?? "" },
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
JTokenType.String => new ASObject {
|
||||
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>;
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ public class AuthorizedFetchMiddleware(
|
|||
IOptionsSnapshot<Config.SecuritySection> config,
|
||||
DatabaseContext db,
|
||||
ActivityPub.UserResolver userResolver,
|
||||
UserService userSvc,
|
||||
SystemUserService systemUserSvc,
|
||||
ActivityPub.FederationControlService fedCtrlSvc,
|
||||
ILogger<AuthorizedFetchMiddleware> logger) : IMiddleware {
|
||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||
|
@ -24,7 +24,7 @@ public class AuthorizedFetchMiddleware(
|
|||
var request = ctx.Request;
|
||||
|
||||
//TODO: cache this somewhere
|
||||
var instanceActorUri = $"/users/{(await userSvc.GetInstanceActorAsync()).Id}";
|
||||
var instanceActorUri = $"/users/{(await systemUserSvc.GetInstanceActorAsync()).Id}";
|
||||
if (ctx.Request.Path.Value == instanceActorUri) {
|
||||
await next(ctx);
|
||||
return;
|
||||
|
|
|
@ -60,7 +60,7 @@ public class NoteService(
|
|||
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())
|
||||
var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
|
||||
if (dbNote == null) {
|
||||
|
@ -157,9 +157,7 @@ public class NoteService(
|
|||
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?
|
||||
var instanceActor = await userSvc.GetInstanceActorAsync();
|
||||
var instanceActorKeypair = await db.UserKeypairs.FirstAsync(p => p.User == instanceActor);
|
||||
var fetchedNote = await fetchSvc.FetchNoteAsync(uri, instanceActor, instanceActorKeypair);
|
||||
var fetchedNote = await fetchSvc.FetchNoteAsync(uri);
|
||||
if (fetchedNote?.AttributedTo is not [{ Id: not null } attrTo]) {
|
||||
logger.LogDebug("Invalid Note.AttributedTo, skipping");
|
||||
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
|
||||
var actor = await fetchSvc.FetchActorAsync(attrTo.Id, instanceActor, instanceActorKeypair);
|
||||
var actor = await fetchSvc.FetchActorAsync(attrTo.Id);
|
||||
|
||||
try {
|
||||
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) {
|
||||
logger.LogDebug("Creating user {acct} with uri {uri}", acct, uri);
|
||||
var instanceActor = await GetInstanceActorAsync();
|
||||
var instanceActorKeypair = await db.UserKeypairs.FirstAsync(p => p.User == instanceActor);
|
||||
var actor = await fetchSvc.FetchActorAsync(uri, instanceActor, instanceActorKeypair);
|
||||
var actor = await fetchSvc.FetchActorAsync(uri);
|
||||
logger.LogDebug("Got actor: {url}", actor.Url);
|
||||
|
||||
actor.Normalize(uri, acct);
|
||||
|
@ -153,62 +151,4 @@ public class UserService(
|
|||
|
||||
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