diff --git a/Iceshrimp.Backend/Core/Configuration/Config.cs b/Iceshrimp.Backend/Core/Configuration/Config.cs
index 8b92f4a4..75c1bfe6 100644
--- a/Iceshrimp.Backend/Core/Configuration/Config.cs
+++ b/Iceshrimp.Backend/Core/Configuration/Config.cs
@@ -2,10 +2,16 @@ namespace Iceshrimp.Backend.Core.Configuration;
public sealed class Config {
// FIXME: This doesn't reflect config updates.
- public static Config StartupConfig { get; set; } = null!;
-
- public required InstanceSection Instance { get; set; }
- public required DatabaseSection Database { get; set; }
+ public static Config StartupConfig { get; set; } = null!;
+
+ public required InstanceSection Instance { get; set; }
+ public required DatabaseSection Database { get; set; }
+ public StaticSection Static = new();
+
+ public sealed class StaticSection {
+ public string Version = "0.0.1";
+ public string UserAgent => $"Iceshrimp.NET/{Version} (https://{StartupConfig.Instance.WebDomain})";
+ }
public sealed class InstanceSection {
public required int ListenPort { get; set; } = 3000;
diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs
new file mode 100644
index 00000000..cd323252
--- /dev/null
+++ b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs
@@ -0,0 +1,14 @@
+using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
+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; }
+}
+
+public sealed class WebFingerResponse {
+ [J("links"), JR] public List Links { get; set; } = null!;
+ [J("subject"), JR] public string Subject { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs
new file mode 100644
index 00000000..09051993
--- /dev/null
+++ b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs
@@ -0,0 +1,105 @@
+using System.Net.Http.Headers;
+using System.Text.Encodings.Web;
+using System.Xml;
+using Iceshrimp.Backend.Core.Configuration;
+using Iceshrimp.Backend.Core.Helpers;
+
+namespace Iceshrimp.Backend.Core.Federation.WebFinger;
+
+/*
+ * There's two different WebFinger implementations out there
+ * 1. Get /.well-known/host-meta, extract the WebFinger query url template from there
+ * 2. Get /.well-known/webfinger?resource={uri} directly
+ *
+ * We have to check for host-meta first, and only fall back to the second implementation if it
+ * - doesn't exist
+ * - doesn't have a query url template
+ */
+
+public class WebFinger {
+ private readonly string _query;
+ private readonly string _proto;
+ private readonly string _domain;
+ private readonly string? _username;
+ private string? _webFingerUrl;
+
+ private string HostMetaUrl => $"{_proto}://{_domain}/.well-known/host-meta";
+ private string DefaultWebFingerTemplate => $"{_proto}://{_domain}/.well-known/webfinger?resource={{uri}}";
+
+ public WebFinger(string query) {
+ _query = query;
+ if (_query.StartsWith("http://") || _query.StartsWith("https://")) {
+ var uri = new Uri(_query);
+ _domain = uri.Host;
+ _proto = _query.StartsWith("http://") ? "http" : "https";
+ }
+ else if (_query.StartsWith('@')) {
+ _proto = "https";
+
+ var split = _query.Split('@');
+ if (split.Length == 2) {
+ throw new Exception("Can't run WebFinger for local user");
+ }
+
+ if (split.Length == 3) {
+ _username = split[1];
+ _domain = split[2];
+ }
+ else {
+ throw new Exception("Invalid query");
+ }
+ }
+ else {
+ throw new Exception("Invalid query");
+ }
+ }
+
+ private string? GetWebFingerTemplateFromHostMeta() {
+ var client = HttpClientHelpers.HttpClient;
+ var request = new HttpRequestMessage {
+ RequestUri = new Uri(HostMetaUrl),
+ Method = HttpMethod.Get,
+ Headers = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/xrd+xml") } }
+ };
+ var res = client.SendAsync(request);
+ var xml = new XmlDocument();
+ xml.Load(res.Result.Content.ReadAsStreamAsync().Result);
+ var section = xml["XRD"]?.GetElementsByTagName("Link");
+ if (section == null) return null;
+
+ //TODO: implement https://stackoverflow.com/a/37322614/18402176 instead
+
+ for (var i = 0; i < section.Count; i++) {
+ if (section[i]?.Attributes?["rel"]?.InnerText == "lrdd") {
+ return section[i]?.Attributes?["template"]?.InnerText;
+ }
+ }
+
+ return null;
+ }
+
+ private string GetWebFingerUrl() {
+ var template = GetWebFingerTemplateFromHostMeta() ?? DefaultWebFingerTemplate;
+ var query = _query.StartsWith('@') ? $"acct:{_query.Substring(1)}" : _query;
+ var encoded = UrlEncoder.Default.Encode(query);
+ return template.Replace("{uri}", encoded);
+ }
+
+ public async Task Resolve() {
+ _webFingerUrl = GetWebFingerUrl();
+
+ var client = HttpClientHelpers.HttpClient;
+ var request = new HttpRequestMessage {
+ RequestUri = new Uri(_webFingerUrl),
+ Method = HttpMethod.Get,
+ Headers = {
+ Accept = {
+ MediaTypeWithQualityHeaderValue.Parse("application/jrd+json"),
+ MediaTypeWithQualityHeaderValue.Parse("application/json")
+ }
+ }
+ };
+ var res = await client.SendAsync(request);
+ return await res.Content.ReadFromJsonAsync();
+ }
+}
\ No newline at end of file
diff --git a/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs b/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs
new file mode 100644
index 00000000..617d6757
--- /dev/null
+++ b/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs
@@ -0,0 +1,11 @@
+using System.Net.Http.Headers;
+
+namespace Iceshrimp.Backend.Core.Helpers;
+
+public static class HttpClientHelpers {
+ public static readonly HttpClient HttpClient = new() {
+ DefaultRequestHeaders = {
+ UserAgent = { ProductInfoHeaderValue.Parse("Iceshrimp.NET/0.0.1") }
+ } //FIXME (instance domain comment in parentheses doesn't work?)
+ };
+}
\ No newline at end of file
diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs
new file mode 100644
index 00000000..8c27564f
--- /dev/null
+++ b/Iceshrimp.Backend/Core/Services/UserService.cs
@@ -0,0 +1,7 @@
+namespace Iceshrimp.Backend.Core.Services;
+
+public static class UserService {
+ public static async Task CreateUser() {
+
+ }
+}
\ No newline at end of file