diff --git a/Iceshrimp.Backend/Controllers/Web/SettingsController.cs b/Iceshrimp.Backend/Controllers/Web/SettingsController.cs index e6b29219..d39f24dc 100644 --- a/Iceshrimp.Backend/Controllers/Web/SettingsController.cs +++ b/Iceshrimp.Backend/Controllers/Web/SettingsController.cs @@ -19,7 +19,7 @@ namespace Iceshrimp.Backend.Controllers.Web; [EnableRateLimiting("sliding")] [Route("/api/iceshrimp/settings")] [Produces(MediaTypeNames.Application.Json)] -public class SettingsController(DatabaseContext db, UserService userSvc) : ControllerBase +public class SettingsController(DatabaseContext db, ImportExportService importExportSvc) : ControllerBase { [HttpGet] [ProducesResults(HttpStatusCode.OK)] @@ -79,7 +79,7 @@ public class SettingsController(DatabaseContext db, UserService userSvc) : Contr if (followCount < 1) throw GracefulException.BadRequest("You do not follow any users"); - await userSvc.ExportFollowingAsync(user); + await importExportSvc.ExportFollowingAsync(user); return Accepted(); } @@ -100,7 +100,7 @@ public class SettingsController(DatabaseContext db, UserService userSvc) : Contr .Where(fqn => fqn.Contains('@')) .ToList(); - await userSvc.ImportFollowingAsync(user, fqns); + await importExportSvc.ImportFollowingAsync(user, fqns); return Accepted(); } diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index 926d7bc0..6ff2aa46 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -65,6 +65,7 @@ public static class ServiceExtensions .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() diff --git a/Iceshrimp.Backend/Core/Services/ImportExportService.cs b/Iceshrimp.Backend/Core/Services/ImportExportService.cs new file mode 100644 index 00000000..227b0ba9 --- /dev/null +++ b/Iceshrimp.Backend/Core/Services/ImportExportService.cs @@ -0,0 +1,64 @@ +using System.Text; +using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using static Iceshrimp.Backend.Core.Federation.ActivityPub.UserResolver; + +namespace Iceshrimp.Backend.Core.Services; + +public class ImportExportService( + DatabaseContext db, + ILogger logger, + IOptions instance, + CacheService cacheSvc, + DriveService driveSvc, + UserService userSvc, + ActivityPub.UserResolver userResolver +) +{ + public async Task ExportFollowingAsync(User user) + { + var followees = await db.Followings + .Include(p => p.Followee) + .Where(p => p.FollowerId == user.Id) + .Select(p => p.Followee) + .Where(p => !p.IsDeleted && !p.IsSystemUser && p.MovedToUri == null) + .OrderBy(p => p.Host) + .ThenBy(p => p.UsernameLower) + .Select(p => p.GetFqn(instance.Value.AccountDomain)) + .ToListAsync(); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes(string.Join("\n", followees))); + + await driveSvc.StoreFile(stream, user, + new DriveFileCreationRequest + { + Filename = $"following-{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}.csv", + IsSensitive = false, + MimeType = "text/csv" + }, true); + } + + public async Task ImportFollowingAsync(User user, List fqns) + { + foreach (var fqn in fqns) + { + var followee = await userResolver.ResolveAsync($"acct:{fqn}", ResolveFlags.Acct); + + try + { + await userSvc.FollowUserAsync(user, followee); + } + catch (Exception e) + { + logger.LogWarning("Failed to import follow {followee} for user {follower}: {error}", + followee.Id, user.Id, e); + } + } + + await QueryableTimelineExtensions.ResetHeuristic(user, cacheSvc); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index ff2e181c..3041dbbc 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using System.Security.Cryptography; -using System.Text; using System.Text.RegularExpressions; using AsyncKeyedLock; using EntityFramework.Exceptions.Common; @@ -40,7 +39,6 @@ public class UserService( QueueService queueSvc, EventService eventSvc, WebFingerService webFingerSvc, - CacheService cacheSvc, ActivityPub.FederationControlService fedCtrlSvc ) { @@ -947,54 +945,6 @@ public class UserService( await db.UserListMembers.Where(p => p.UserList.User == user && p.User == followee).ExecuteDeleteAsync(); } - public async Task ExportFollowingAsync(User user) - { - var followees = await db.Followings - .Include(p => p.Followee) - .Where(p => p.FollowerId == user.Id) - .Select(p => p.Followee) - .Where(p => !p.IsDeleted && !p.IsSystemUser && p.MovedToUri == null) - .OrderBy(p => p.Host) - .ThenBy(p => p.UsernameLower) - .Select(p => p.GetFqn(instance.Value.AccountDomain)) - .ToListAsync(); - - var stream = new MemoryStream(Encoding.UTF8.GetBytes(string.Join("\n", followees))); - - await driveSvc.StoreFile(stream, user, - new DriveFileCreationRequest - { - Filename = $"following-{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}.csv", - IsSensitive = false, - MimeType = "text/csv" - }, true); - } - - public async Task ImportFollowingAsync(User user, List fqns) - { - foreach (var fqn in fqns.Select(fqn => fqn.Split("@"))) - { - var followee = await db.Users - .IncludeCommonProperties() - .FirstOrDefaultAsync(p => fqn[0].ToLower() == p.UsernameLower && - fqn[1] == (p.Host ?? instance.Value.AccountDomain)); - - if (followee == null) continue; - - try - { - await FollowUserAsync(user, followee); - } - catch (Exception e) - { - logger.LogWarning("Failed to import follow {followee} for user {follower}: {error}", - followee.Id, user.Id, e); - } - } - - await QueryableTimelineExtensions.ResetHeuristic(user, cacheSvc); - } - [SuppressMessage("ReSharper", "SuggestBaseTypeForParameter", Justification = "Method only makes sense for users")] private void UpdateUserPinnedNotesInBackground(ASActor actor, User user, bool force = false) {