Implement basic UserService functions

This commit is contained in:
Laura Hausmann 2024-01-12 20:20:16 +01:00
parent bb1f4e6e27
commit eace0cb0de
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
9 changed files with 89 additions and 13 deletions

View file

@ -63,11 +63,11 @@ public class ASActor : ASObject {
[J("https://www.w3.org/ns/activitystreams#followers")] [J("https://www.w3.org/ns/activitystreams#followers")]
[JC(typeof(ASCollectionConverter))] [JC(typeof(ASCollectionConverter))]
public ASCollection<ASActor>? Followers { get; set; } public ASCollection? Followers { get; set; } //FIXME: <ASActor>
[J("https://www.w3.org/ns/activitystreams#following")] [J("https://www.w3.org/ns/activitystreams#following")]
[JC(typeof(ASCollectionConverter))] [JC(typeof(ASCollectionConverter))]
public ASCollection<ASActor>? Following { get; set; } public ASCollection? Following { get; set; } //FIXME: <ASActor>
[J("https://www.w3.org/ns/activitystreams#sharedInbox")] [J("https://www.w3.org/ns/activitystreams#sharedInbox")]
[JC(typeof(ASLinkConverter))] [JC(typeof(ASLinkConverter))]

View file

@ -11,10 +11,6 @@ namespace Iceshrimp.Backend.Core.Federation;
public class Fetch { public class Fetch {
private const string Accept = "application/activity+json"; 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() { public static void Test2() {
var thing = FetchActivity("https://staging.e2net.social/users/9esresfwle/outbox?page=true"); var thing = FetchActivity("https://staging.e2net.social/users/9esresfwle/outbox?page=true");
var collection = thing.ToObject<List<ASCollection>>(); var collection = thing.ToObject<List<ASCollection>>();

View file

@ -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<JArray> 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<JObject?>(input);
var res = LDHelpers.Expand(json);
if (res == null) throw new Exception("Failed to expand JSON-LD object");
return res;
}
public async Task<ASActor> FetchActor(string uri) {
var activity = await FetchActivity(uri);
var actor = activity.ToObject<List<ASActor>>();
return actor?.First() ?? throw new Exception("Failed to fetch actor");
}
}

View file

@ -1,8 +1,9 @@
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.WebFinger;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
namespace Iceshrimp.Backend.Core.Federation.WebFinger; namespace Iceshrimp.Backend.Core.Federation.Services;
public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, DatabaseContext db) { public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, DatabaseContext db) {
private static string AcctToDomain(string acct) => private static string AcctToDomain(string acct) =>
@ -24,7 +25,7 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Dat
private async Task<(string Acct, string Uri)> WebFinger(string query) { private async Task<(string Acct, string Uri)> WebFinger(string query) {
logger.LogDebug("Running WebFinger for query '{query}'", query); logger.LogDebug("Running WebFinger for query '{query}'", query);
var finger = new WebFinger(query); var finger = new WebFinger.WebFinger(query);
var responses = new Dictionary<string, WebFingerResponse>(); var responses = new Dictionary<string, WebFingerResponse>();
var fingerRes = await finger.Resolve(); var fingerRes = await finger.Resolve();
if (fingerRes == null) throw new Exception($"Failed to WebFinger '{query}'"); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{query}'");
@ -38,7 +39,7 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Dat
if (fingerRes == null) { if (fingerRes == null) {
logger.LogDebug("AP uri didn't match query, re-running WebFinger for '{apUri}'", apUri); 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(); fingerRes = await finger.Resolve();
if (fingerRes == null) throw new Exception($"Failed to WebFinger '{apUri}'"); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{apUri}'");
@ -52,7 +53,7 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Dat
if (fingerRes == null) { if (fingerRes == null) {
logger.LogDebug("Acct uri didn't match query, re-running WebFinger for '{acctUri}'", acctUri); 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(); fingerRes = await finger.Resolve();
if (fingerRes == null) throw new Exception($"Failed to WebFinger '{acctUri}'"); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{acctUri}'");
@ -84,6 +85,6 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Dat
if (user != null) return user; if (user != null) return user;
// Pass the job on to userSvc, which will create the user // Pass the job on to userSvc, which will create the user
return await userSvc.CreateUser(acct, uri); return await userSvc.CreateUser(uri, acct);
} }
} }

View file

@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
using JR = System.Text.Json.Serialization.JsonRequiredAttribute; using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
// ReSharper disable ClassNeverInstantiated.Global
namespace Iceshrimp.Backend.Core.Federation.WebFinger; namespace Iceshrimp.Backend.Core.Federation.WebFinger;
@ -9,6 +11,7 @@ public sealed class Link {
[J("href")] public string? Href { get; set; } [J("href")] public string? Href { get; set; }
} }
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")]
public sealed class WebFingerResponse { public sealed class WebFingerResponse {
[J("links"), JR] public List<Link> Links { get; set; } = null!; [J("links"), JR] public List<Link> Links { get; set; } = null!;
[J("subject"), JR] public string Subject { get; set; } = null!; [J("subject"), JR] public string Subject { get; set; } = null!;

View file

@ -3,9 +3,10 @@ using System.Net.Http.Headers;
namespace Iceshrimp.Backend.Core.Helpers; namespace Iceshrimp.Backend.Core.Helpers;
public static class HttpClientHelpers { public static class HttpClientHelpers {
//TODO: replace with HttpClient service
public static readonly HttpClient HttpClient = new() { public static readonly HttpClient HttpClient = new() {
DefaultRequestHeaders = { DefaultRequestHeaders = {
UserAgent = { ProductInfoHeaderValue.Parse("Iceshrimp.NET/0.0.1") } UserAgent = { ProductInfoHeaderValue.Parse("Iceshrimp.NET/0.0.1") }
} //FIXME (instance domain comment in parentheses doesn't work?) }
}; };
} }

View file

@ -1,4 +1,6 @@
using Iceshrimp.Backend.Core.Configuration; 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.Federation.WebFinger;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
@ -6,9 +8,18 @@ namespace Iceshrimp.Backend.Core.Helpers;
public static class ServiceExtensions { public static class ServiceExtensions {
public static void AddServices(this IServiceCollection services) { public static void AddServices(this IServiceCollection services) {
// Transient = instantiated per request and class
//services.AddTransient<T>();
// Scoped = instantiated per request
services.AddScoped<UserResolver>(); services.AddScoped<UserResolver>();
services.AddScoped<UserService>(); services.AddScoped<UserService>();
services.AddScoped<NoteService>(); services.AddScoped<NoteService>();
// Singleton = instantiated once across application lifetime
services.AddSingleton<HttpClient>();
services.AddSingleton<HttpRequestService>();
services.AddSingleton<ActivityPubService>();
} }
public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration) { public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration) {

View file

@ -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<Config.StaticSection> options) {
private HttpRequestMessage GenerateRequest(string url, IEnumerable<string>? 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<string>? accept) {
return GenerateRequest(url, accept, HttpMethod.Get);
}
}

View file

@ -1,10 +1,12 @@
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation;
using Iceshrimp.Backend.Core.Federation.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Services;
public class UserService(ILogger<UserService> logger, DatabaseContext db) { public class UserService(ILogger<UserService> logger, DatabaseContext db, HttpClient client, ActivityPubService apSvc) {
private static (string Username, string Host) AcctToTuple(string acct) { private static (string Username, string Host) AcctToTuple(string acct) {
if (!acct.StartsWith("acct:")) throw new Exception("Invalid query"); if (!acct.StartsWith("acct:")) throw new Exception("Invalid query");
@ -24,6 +26,10 @@ public class UserService(ILogger<UserService> logger, DatabaseContext db) {
} }
public async Task<User> CreateUser(string uri, string acct) { public async Task<User> 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 throw new NotImplementedException(); //FIXME
} }
} }