using System.Security.Cryptography; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Federation.ActivityPub; using Iceshrimp.Backend.Core.Helpers; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Services; public class UserService(ILogger logger, DatabaseContext db, HttpClient client, ActivityPubService apSvc) { private static (string Username, string Host) AcctToTuple(string acct) { if (!acct.StartsWith("acct:")) throw new Exception("Invalid query"); var split = acct[5..].Split('@'); if (split.Length != 2) throw new Exception("Invalid query"); return (split[0], split[1]); } public Task GetUserFromQuery(string query) { if (query.StartsWith("http://") || query.StartsWith("https://")) { return db.Users.FirstOrDefaultAsync(p => p.Uri == query); } var tuple = AcctToTuple(query); return db.Users.FirstOrDefaultAsync(p => p.Username == tuple.Username && p.Host == tuple.Host); } public async Task CreateUser(string uri, string acct) { logger.LogDebug("Creating user {acct} with uri {uri}", acct, uri); var instanceActor = await GetInstanceActor(); var instanceActorKeypair = await db.UserKeypairs.FirstAsync(p => p.UserId == instanceActor.Id); var actor = await apSvc.FetchActor(uri, instanceActor, instanceActorKeypair); logger.LogDebug("Got actor: {url}", actor.Url); actor.Normalize(uri, acct); var user = new User { Id = IdHelpers.GenerateSlowflakeId(), CreatedAt = DateTime.UtcNow, LastFetchedAt = DateTime.UtcNow, Name = actor.DisplayName, IsLocked = actor.IsLocked ?? false, IsBot = actor.IsBot, Username = actor.Username!, UsernameLower = actor.Username!.ToLowerInvariant(), Host = AcctToTuple(acct).Host, MovedToUri = actor.MovedTo?.Link, AlsoKnownAs = actor.AlsoKnownAs?.Link, IsExplorable = actor.IsDiscoverable ?? false, Inbox = actor.Inbox?.Link, SharedInbox = actor.SharedInbox?.Link, FollowersUri = actor.Followers?.Id, Uri = actor.Id, IsCat = actor.IsCat ?? false, Featured = actor.Featured?.Link, //FollowersCount //FollowingCount Emojis = [], //FIXME Tags = [], //FIXME }; //TODO: add UserProfile as well await db.Users.AddAsync(user); await db.SaveChangesAsync(); return user; } public async Task GetInstanceActor() => await GetOrCreateSystemUser("instance.actor"); public async Task GetRelayActor() => await GetOrCreateSystemUser("relay.actor"); //TODO: cache in redis private async Task GetOrCreateSystemUser(string username) => await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username && p.Host == null) ?? await CreateSystemUser(username); private async Task CreateSystemUser(string username) { if (await db.Users.AnyAsync(p => p.UsernameLower == username.ToLowerInvariant() && p.Host == null)) throw new Exception("User 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; } }