diff --git a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs index 38cb0b03..d41a3692 100644 --- a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs @@ -88,10 +88,11 @@ public static class WebApplicationExtensions if (args.Contains("--recompute-counters")) { - app.Logger.LogInformation("Recomputing note & user counters, this will take a while..."); + app.Logger.LogInformation("Recomputing note, user & instance counters, this will take a while..."); var maintenanceSvc = provider.GetRequiredService(); await maintenanceSvc.RecomputeNoteCountersAsync(); await maintenanceSvc.RecomputeUserCountersAsync(); + await maintenanceSvc.RecomputeInstanceCountersAsync(); } app.Logger.LogInformation("Verifying redis connection..."); diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs index 62fa9f08..4f264593 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs @@ -335,6 +335,16 @@ public class ActivityHandlerService( follower.FollowingCount++; followee.FollowersCount++; + _ = followupTaskSvc.ExecuteTask("UpdateInstanceFollowingCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(follower.Host, + new Uri(follower.Uri!).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.FollowingCount, i => i.FollowingCount + 1)); + }); + await db.AddAsync(following); await db.SaveChangesAsync(); await notificationSvc.GenerateFollowNotification(follower, followee); @@ -403,6 +413,16 @@ public class ActivityHandlerService( actor.FollowersCount++; request.Follower.FollowingCount++; + _ = followupTaskSvc.ExecuteTask("UpdateInstanceFollowersCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(request.Followee.Host!, + new Uri(request.Followee.Uri!).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.FollowersCount, i => i.FollowersCount + 1)); + }); + db.Remove(request); await db.AddAsync(following); await db.SaveChangesAsync(); diff --git a/Iceshrimp.Backend/Core/Services/DatabaseMaintenanceService.cs b/Iceshrimp.Backend/Core/Services/DatabaseMaintenanceService.cs index e1540aff..12fd7fb8 100644 --- a/Iceshrimp.Backend/Core/Services/DatabaseMaintenanceService.cs +++ b/Iceshrimp.Backend/Core/Services/DatabaseMaintenanceService.cs @@ -23,4 +23,18 @@ public class DatabaseMaintenanceService(DatabaseContext db) .SetProperty(u => u.NotesCount, u => db.Notes.Count(n => n.User == u && !n.IsPureRenote))); } + + public async Task RecomputeInstanceCountersAsync() + { + await db.Instances.ExecuteUpdateAsync(p => p.SetProperty(i => i.NotesCount, + i => db.Notes.Count(n => n.UserHost == i.Host)) + .SetProperty(i => i.UsersCount, + i => db.Users.Count(u => u.Host == i.Host)) + .SetProperty(i => i.FollowersCount, + i => db.Followings + .Count(n => n.FolloweeHost == i.Host)) + .SetProperty(i => i.FollowingCount, + i => db.Followings + .Count(n => n.FollowerHost == i.Host))); + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/InstanceService.cs b/Iceshrimp.Backend/Core/Services/InstanceService.cs index 59f9f5e8..9b34af5b 100644 --- a/Iceshrimp.Backend/Core/Services/InstanceService.cs +++ b/Iceshrimp.Backend/Core/Services/InstanceService.cs @@ -14,7 +14,7 @@ public class InstanceService(DatabaseContext db, HttpClient httpClient) o.PoolInitialFill = 5; }); - private async Task GetUpdatedInstanceMetadataAsync(string host, string webDomain) + public async Task GetUpdatedInstanceMetadataAsync(string host, string webDomain) { host = host.ToLowerInvariant(); var instance = db.Instances.FirstOrDefault(p => p.Host == host); diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index 4dcfc14b..fbeabcf3 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -37,7 +37,8 @@ public class NoteService( NotificationService notificationSvc, EventService eventSvc, ActivityPub.ActivityRenderer activityRenderer, - EmojiService emojiSvc + EmojiService emojiSvc, + FollowupTaskService followupTaskSvc ) { private readonly List _resolverHistory = []; @@ -111,7 +112,23 @@ public class NoteService( await notificationSvc.GenerateReplyNotifications(note, mentionedLocalUserIds); await notificationSvc.GenerateRenoteNotification(note); - if (user.Host != null) return note; + if (user.Host != null) + { + if (user.Uri != null) + { + _ = followupTaskSvc.ExecuteTask("UpdateInstanceNoteCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = + await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(user.Host, new Uri(user.Uri).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.NotesCount, i => i.NotesCount + 1)); + }); + } + + return note; + } var actor = userRenderer.RenderLite(user); ASActivity activity = note is { IsPureRenote: true, Renote: not null } @@ -232,7 +249,22 @@ public class NoteService( await db.SaveChangesAsync(); if (note.UserHost != null) + { + if (note.User.Uri != null) + { + _ = followupTaskSvc.ExecuteTask("UpdateInstanceNoteCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = + await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(note.UserHost, new Uri(note.User.Uri).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.NotesCount, i => i.NotesCount - 1)); + }); + } + return; + } var recipients = await db.Users.Where(p => note.Mentions.Concat(note.VisibleUserIds).Distinct().Contains(p.Id)) .Select(p => new User { Host = p.Host, Inbox = p.Inbox }) @@ -279,6 +311,21 @@ public class NoteService( db.Remove(dbNote); eventSvc.RaiseNoteDeleted(this, dbNote); await db.SaveChangesAsync(); + + // ReSharper disable once EntityFramework.NPlusOne.IncompleteDataUsage (same reason as above) + if (dbNote.User.Uri != null && dbNote.UserHost != null) + { + _ = followupTaskSvc.ExecuteTask("UpdateInstanceNoteCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + // ReSharper disable once EntityFramework.NPlusOne.IncompleteDataUsage (same reason as above) + var dbInstance = + await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(dbNote.UserHost, new Uri(dbNote.User.Uri).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.NotesCount, i => i.NotesCount - 1)); + }); + } } public async Task UndoAnnounceAsync(ASNote note, User actor) diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 1a88a394..e76d37e9 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -185,6 +185,14 @@ public class UserService( await db.SaveChangesAsync(); await processPendingDeletes(); user = await UpdateProfileMentions(user, actor); + _ = followupTaskSvc.ExecuteTask("UpdateInstanceUserCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(host, new Uri(uri).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount + 1)); + }); return user; } catch (UniqueConstraintException) @@ -437,6 +445,15 @@ public class UserService( db.Remove(user); await db.SaveChangesAsync(); + _ = followupTaskSvc.ExecuteTask("UpdateInstanceUserCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(user.Host!, new Uri(user.Uri!).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount - 1)); + }); + if (user.Avatar != null) await driveSvc.RemoveFile(user.Avatar); if (user.Banner != null) @@ -511,6 +528,19 @@ public class UserService( await db.AddAsync(following); await db.SaveChangesAsync(); + if (request.Follower is { Host: not null }) + { + _ = followupTaskSvc.ExecuteTask("UpdateInstanceFollowingCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(request.Follower.Host, + new Uri(request.Follower.Uri!).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.FollowingCount, i => i.FollowingCount + 1)); + }); + } + await notificationSvc.GenerateFollowNotification(request.Follower, request.Followee); await notificationSvc.GenerateFollowRequestAcceptedNotification(request); @@ -627,6 +657,19 @@ public class UserService( db.RemoveRange(followings); await db.SaveChangesAsync(); + if (followee.Host != null) + { + _ = followupTaskSvc.ExecuteTask("UpdateInstanceFollowersCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = + await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(followee.Host, new Uri(followee.Uri!).Host); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.FollowersCount, i => i.FollowersCount - 1)); + }); + } + followee.PrecomputedIsFollowedBy = false; }