From eace0cb0debeb4f7d84f290022ac825efccaec2b Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Fri, 12 Jan 2024 20:20:16 +0100 Subject: [PATCH] Implement basic UserService functions --- .../ActivityStreams/Types/ASActor.cs | 4 +-- Iceshrimp.Backend/Core/Federation/Fetch.cs | 4 --- .../Federation/Services/ActivityPubService.cs | 29 +++++++++++++++++++ .../{WebFinger => Services}/UserResolver.cs | 11 +++---- .../Core/Federation/WebFinger/Types.cs | 3 ++ .../Core/Helpers/HttpClientHelpers.cs | 3 +- .../Core/Helpers/ServiceExtensions.cs | 11 +++++++ .../Core/Services/HttpRequestService.cs | 29 +++++++++++++++++++ .../Core/Services/UserService.cs | 8 ++++- 9 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 Iceshrimp.Backend/Core/Federation/Services/ActivityPubService.cs rename Iceshrimp.Backend/Core/Federation/{WebFinger => Services}/UserResolver.cs (92%) create mode 100644 Iceshrimp.Backend/Core/Services/HttpRequestService.cs diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs index 6768f418..b12c8ce9 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs @@ -63,11 +63,11 @@ public class ASActor : ASObject { [J("https://www.w3.org/ns/activitystreams#followers")] [JC(typeof(ASCollectionConverter))] - public ASCollection? Followers { get; set; } + public ASCollection? Followers { get; set; } //FIXME: [J("https://www.w3.org/ns/activitystreams#following")] [JC(typeof(ASCollectionConverter))] - public ASCollection? Following { get; set; } + public ASCollection? Following { get; set; } //FIXME: [J("https://www.w3.org/ns/activitystreams#sharedInbox")] [JC(typeof(ASLinkConverter))] diff --git a/Iceshrimp.Backend/Core/Federation/Fetch.cs b/Iceshrimp.Backend/Core/Federation/Fetch.cs index 5155d6e4..2dcd257b 100644 --- a/Iceshrimp.Backend/Core/Federation/Fetch.cs +++ b/Iceshrimp.Backend/Core/Federation/Fetch.cs @@ -11,10 +11,6 @@ namespace Iceshrimp.Backend.Core.Federation; public class Fetch { private const string Accept = "application/activity+json"; - //TODO: required attribute doesn't work with Newtonsoft.Json it appears - //TODO: enforce @type values - //TODO: firstordefault -> singleordefault - public static void Test2() { var thing = FetchActivity("https://staging.e2net.social/users/9esresfwle/outbox?page=true"); var collection = thing.ToObject>(); diff --git a/Iceshrimp.Backend/Core/Federation/Services/ActivityPubService.cs b/Iceshrimp.Backend/Core/Federation/Services/ActivityPubService.cs new file mode 100644 index 00000000..0e0d29e3 --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/Services/ActivityPubService.cs @@ -0,0 +1,29 @@ +using Iceshrimp.Backend.Core.Federation.ActivityStreams; +using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; +using Iceshrimp.Backend.Core.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Iceshrimp.Backend.Core.Federation.Services; + +//TODO: required attribute doesn't work with Newtonsoft.Json it appears +//TODO: enforce @type values + +public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc) { + private async Task FetchActivity(string url) { + var request = httpRqSvc.Get(url, ["application/activity+json"]); + var response = await client.SendAsync(request); + var input = await response.Content.ReadAsStringAsync(); + var json = JsonConvert.DeserializeObject(input); + + var res = LDHelpers.Expand(json); + if (res == null) throw new Exception("Failed to expand JSON-LD object"); + return res; + } + + public async Task FetchActor(string uri) { + var activity = await FetchActivity(uri); + var actor = activity.ToObject>(); + return actor?.First() ?? throw new Exception("Failed to fetch actor"); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/UserResolver.cs b/Iceshrimp.Backend/Core/Federation/Services/UserResolver.cs similarity index 92% rename from Iceshrimp.Backend/Core/Federation/WebFinger/UserResolver.cs rename to Iceshrimp.Backend/Core/Federation/Services/UserResolver.cs index e4c3fa7f..8d9b1a9f 100644 --- a/Iceshrimp.Backend/Core/Federation/WebFinger/UserResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/Services/UserResolver.cs @@ -1,8 +1,9 @@ using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Services; -namespace Iceshrimp.Backend.Core.Federation.WebFinger; +namespace Iceshrimp.Backend.Core.Federation.Services; public class UserResolver(ILogger logger, UserService userSvc, DatabaseContext db) { private static string AcctToDomain(string acct) => @@ -24,7 +25,7 @@ public class UserResolver(ILogger logger, UserService userSvc, Dat private async Task<(string Acct, string Uri)> WebFinger(string query) { logger.LogDebug("Running WebFinger for query '{query}'", query); - var finger = new WebFinger(query); + var finger = new WebFinger.WebFinger(query); var responses = new Dictionary(); var fingerRes = await finger.Resolve(); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{query}'"); @@ -38,7 +39,7 @@ public class UserResolver(ILogger logger, UserService userSvc, Dat if (fingerRes == null) { logger.LogDebug("AP uri didn't match query, re-running WebFinger for '{apUri}'", apUri); - finger = new WebFinger(apUri); + finger = new WebFinger.WebFinger(apUri); fingerRes = await finger.Resolve(); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{apUri}'"); @@ -52,7 +53,7 @@ public class UserResolver(ILogger logger, UserService userSvc, Dat if (fingerRes == null) { logger.LogDebug("Acct uri didn't match query, re-running WebFinger for '{acctUri}'", acctUri); - finger = new WebFinger(acctUri); + finger = new WebFinger.WebFinger(acctUri); fingerRes = await finger.Resolve(); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{acctUri}'"); @@ -84,6 +85,6 @@ public class UserResolver(ILogger logger, UserService userSvc, Dat if (user != null) return user; // Pass the job on to userSvc, which will create the user - return await userSvc.CreateUser(acct, uri); + return await userSvc.CreateUser(uri, acct); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs index dec8a674..5b1f30af 100644 --- a/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs @@ -1,5 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; using JR = System.Text.Json.Serialization.JsonRequiredAttribute; +// ReSharper disable ClassNeverInstantiated.Global namespace Iceshrimp.Backend.Core.Federation.WebFinger; @@ -9,6 +11,7 @@ public sealed class Link { [J("href")] public string? Href { get; set; } } +[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")] public sealed class WebFingerResponse { [J("links"), JR] public List Links { get; set; } = null!; [J("subject"), JR] public string Subject { get; set; } = null!; diff --git a/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs b/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs index 617d6757..38682f83 100644 --- a/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs +++ b/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs @@ -3,9 +3,10 @@ using System.Net.Http.Headers; namespace Iceshrimp.Backend.Core.Helpers; public static class HttpClientHelpers { + //TODO: replace with HttpClient service 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/Helpers/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs index 6b15b3bf..c5594f8a 100644 --- a/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs @@ -1,4 +1,6 @@ using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Federation; +using Iceshrimp.Backend.Core.Federation.Services; using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Services; @@ -6,9 +8,18 @@ namespace Iceshrimp.Backend.Core.Helpers; public static class ServiceExtensions { public static void AddServices(this IServiceCollection services) { + // Transient = instantiated per request and class + //services.AddTransient(); + + // Scoped = instantiated per request services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // Singleton = instantiated once across application lifetime + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration) { diff --git a/Iceshrimp.Backend/Core/Services/HttpRequestService.cs b/Iceshrimp.Backend/Core/Services/HttpRequestService.cs new file mode 100644 index 00000000..6deece1e --- /dev/null +++ b/Iceshrimp.Backend/Core/Services/HttpRequestService.cs @@ -0,0 +1,29 @@ +using System.Net.Http.Headers; +using Iceshrimp.Backend.Core.Configuration; +using Microsoft.Extensions.Options; + +namespace Iceshrimp.Backend.Core.Services; + +public class HttpRequestService(IOptions options) { + private HttpRequestMessage GenerateRequest(string url, IEnumerable? accept, HttpMethod method) { + var message = new HttpRequestMessage { + RequestUri = new Uri(url), + Method = method, + //Headers = { UserAgent = { ProductInfoHeaderValue.Parse(options.Value.UserAgent) } } + }; + + //TODO: fix the user-agent so the commented out bit above works + message.Headers.TryAddWithoutValidation("User-Agent", options.Value.UserAgent); + + if (accept != null) { + foreach (var type in accept.Select(MediaTypeWithQualityHeaderValue.Parse)) + message.Headers.Accept.Add(type); + } + + return message; + } + + public HttpRequestMessage Get(string url, IEnumerable? accept) { + return GenerateRequest(url, accept, HttpMethod.Get); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index ebb94910..70d6f6c9 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -1,10 +1,12 @@ using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Federation; +using Iceshrimp.Backend.Core.Federation.Services; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Services; -public class UserService(ILogger logger, DatabaseContext db) { +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"); @@ -24,6 +26,10 @@ public class UserService(ILogger logger, DatabaseContext db) { } public async Task CreateUser(string uri, string acct) { + logger.LogInformation("Creating user {acct} with uri {uri}", acct, uri); + var actor = await apSvc.FetchActor(uri); + logger.LogInformation("Got actor: {inbox}", actor.Inbox); + throw new NotImplementedException(); //FIXME } } \ No newline at end of file