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