diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs index cd323252..dec8a674 100644 --- a/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs @@ -4,11 +4,13 @@ using JR = System.Text.Json.Serialization.JsonRequiredAttribute; namespace Iceshrimp.Backend.Core.Federation.WebFinger; public sealed class Link { - [J("href"), JR] public string Href { get; set; } = null!; - [J("rel")] public string? Rel { get; set; } + [J("rel"), JR] public string Rel { get; set; } = null!; + [J("type")] public string? Type { get; set; } = null!; + [J("href")] public string? Href { get; set; } } public sealed class WebFingerResponse { - [J("links"), JR] public List Links { get; set; } = null!; - [J("subject"), JR] public string Subject { get; set; } = null!; + [J("links"), JR] public List Links { get; set; } = null!; + [J("subject"), JR] public string Subject { get; set; } = null!; + [J("aliases")] public List Aliases { get; set; } = []; } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/UserResolver.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/UserResolver.cs index 1607c5d2..cc278c4c 100644 --- a/Iceshrimp.Backend/Core/Federation/WebFinger/UserResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/UserResolver.cs @@ -1,31 +1,89 @@ +using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Federation.WebFinger; -public class UserResolver(ILogger logger) { +public class UserResolver(ILogger logger, UserService userSvc, DatabaseContext db) { private static string AcctToDomain(string acct) => acct.StartsWith("acct:") && acct.Contains('@') ? acct[5..].Split('@')[1] : throw new Exception("Invalid acct"); /* - * Split domain logic: - * 1. Get WebFinger response for query - * 2. [...] - * TODO: finish description and implement the rest + * The full web finger algorithm: + * + * 1. WebFinger(input_uri), find the rel=self type=application/activity+json entry, that's ap_uri + * 2. WebFinger(ap_uri), find the first acct: URI in [subject] + aliases, that's candidate_acct_uri + * 3. WebFinger(candidate_acct_uri), validate it also points to ap_uri. If so, you have acct_uri + * 4. Failing this, acct_uri = "acct:" + preferredUsername from AP actor + "@" + hostname from ap_uri (TODO: implement this) + * + * Avoid repeating WebFinger's with same URI for performance, TODO: optimize away validation checks when the domain matches */ - public async Task Resolve(string query) { + private async Task<(string Acct, string Uri)> WebFinger(string query) { + logger.LogDebug("Running WebFinger for query '{query}'", query); + var finger = new WebFinger(query); + var responses = new Dictionary(); var fingerRes = await finger.Resolve(); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{query}'"); - if (finger.Domain != AcctToDomain(fingerRes.Subject)) { - logger.LogInformation("possible split domain deployment detected, repeating webfinger"); + responses.Add(query, fingerRes); - finger = new WebFinger(fingerRes.Subject); + var apUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json") + ?.Href; + if (apUri == null) throw new Exception($"WebFinger response for '{query}' didn't contain a candidate link"); + + fingerRes = responses.GetValueOrDefault(apUri); + if (fingerRes == null) { + logger.LogDebug("AP uri didn't match query, re-running WebFinger for '{apUri}'", apUri); + + finger = new WebFinger(apUri); fingerRes = await finger.Resolve(); + + if (fingerRes == null) throw new Exception($"Failed to WebFinger '{apUri}'"); + responses.Add(apUri, fingerRes); } - throw new NotImplementedException("stub method return"); + var acctUri = fingerRes.Aliases.Prepend(fingerRes.Subject).FirstOrDefault(p => p.StartsWith("acct:")); + if (acctUri == null) throw new Exception($"WebFinger response for '{apUri}' didn't contain any acct uris"); + + fingerRes = responses.GetValueOrDefault(acctUri); + if (fingerRes == null) { + logger.LogDebug("Acct uri didn't match query, re-running WebFinger for '{acctUri}'", acctUri); + + finger = new WebFinger(acctUri); + fingerRes = await finger.Resolve(); + + if (fingerRes == null) throw new Exception($"Failed to WebFinger '{acctUri}'"); + responses.Add(acctUri, fingerRes); + } + + var finalAcct = fingerRes.Subject; + var finalUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json") + ?.Href ?? throw new Exception("Final AP URI was null"); + + return (finalAcct, finalUri); + } + + private static string NormalizeQuery(string query) => query.StartsWith("acct:") ? query[5..] : query; + + public async Task Resolve(string query) { + query = NormalizeQuery(query); + + // First, let's see if we already know the user + var user = await userSvc.GetUserFromQuery(query); + if (user != null) return user; + + // We don't, so we need to run WebFinger + var (acct, uri) = await WebFinger(query); + + // Check the database again with the new data + if (uri != query) user = await userSvc.GetUserFromQuery(uri); + if (user == null && acct != query) await userSvc.GetUserFromQuery(acct); + if (user != null) return user; + + // Pass the job on to userSvc, which will create the user + return await userSvc.CreateUser(acct, uri); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs index 656843e2..6b15b3bf 100644 --- a/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs @@ -1,10 +1,12 @@ using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Helpers; public static class ServiceExtensions { public static void AddServices(this IServiceCollection services) { + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index f86fc12a..d95d6842 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -1,10 +1,25 @@ using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Services; public class UserService(ILogger logger, DatabaseContext db) { + private static (string Username, string Host) AcctToTuple(string acct) { + if (!acct.StartsWith("acct:")) throw new Exception("Invalid query"); + throw new NotImplementedException(); //FIXME + } + + public Task GetUserFromQuery(string query) { + if (query.StartsWith("http://") || query.StartsWith("https://")) { + return db.Users.FirstOrDefaultAsync(p => p.Uri == query); + } - public async Task CreateUser() { - + 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) { + throw new NotImplementedException(); //FIXME } } \ No newline at end of file