From 1f2e9b1de8438a84ac91804609e753dfe703486e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 14 Jun 2025 16:19:45 +0800 Subject: [PATCH] :recycle: Refactored publishers --- .../Activity/ActivityService.cs | 14 ++- DysonNetwork.Sphere/Post/PostController.cs | 19 +-- DysonNetwork.Sphere/Post/PostService.cs | 110 ++++++++++++++---- .../Publisher/PublisherService.cs | 50 ++++++++ .../Publisher/PublisherSubscriptionService.cs | 14 +-- .../NotificationResource.Designer.cs | 24 ++++ .../Localization/NotificationResource.resx | 14 ++- .../NotificationResource.zh-hans.resx | 12 ++ 8 files changed, 213 insertions(+), 44 deletions(-) diff --git a/DysonNetwork.Sphere/Activity/ActivityService.cs b/DysonNetwork.Sphere/Activity/ActivityService.cs index 50e1c5a..e3f856e 100644 --- a/DysonNetwork.Sphere/Activity/ActivityService.cs +++ b/DysonNetwork.Sphere/Activity/ActivityService.cs @@ -1,11 +1,12 @@ using DysonNetwork.Sphere.Account; using DysonNetwork.Sphere.Post; +using DysonNetwork.Sphere.Publisher; using Microsoft.EntityFrameworkCore; using NodaTime; namespace DysonNetwork.Sphere.Activity; -public class ActivityService(AppDatabase db, RelationshipService rels, PostService ps) +public class ActivityService(AppDatabase db, PublisherService pub, RelationshipService rels, PostService ps) { public async Task> GetActivitiesForAnyone(int take, Instant? cursor) { @@ -20,7 +21,7 @@ public class ActivityService(AppDatabase db, RelationshipService rels, PostServi .Where(e => e.RepliedPostId == null) .Where(p => cursor == null || p.PublishedAt < cursor) .OrderByDescending(p => p.PublishedAt) - .FilterWithVisibility(null, [], isListing: true) + .FilterWithVisibility(null, [], [], isListing: true) .Take(take) .ToListAsync(); posts = PostService.TruncatePostContent(posts); @@ -46,17 +47,20 @@ public class ActivityService(AppDatabase db, RelationshipService rels, PostServi { var activities = new List(); var userFriends = await rels.ListAccountFriends(currentUser); - + var userPublishers = await pub.GetUserPublishers(currentUser.Id); + + var publishersId = userPublishers.Select(e => e.Id).ToList(); + // Crunching data var posts = await db.Posts .Include(e => e.RepliedPost) .Include(e => e.ForwardedPost) .Include(e => e.Categories) .Include(e => e.Tags) - .Where(e => e.RepliedPostId == null) + .Where(e => e.RepliedPostId == null || publishersId.Contains(e.RepliedPost!.PublisherId)) .Where(p => cursor == null || p.PublishedAt < cursor) .OrderByDescending(p => p.PublishedAt) - .FilterWithVisibility(currentUser, userFriends, isListing: true) + .FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true) .Take(take) .ToListAsync(); posts = PostService.TruncatePostContent(posts); diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 6c8c7c8..109cdcd 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -30,15 +30,17 @@ public class PostController( HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); var currentUser = currentUserValue as Account.Account; var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser); + var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); var publisher = pubName == null ? null : await db.Publishers.FirstOrDefaultAsync(p => p.Name == pubName); var query = db.Posts.AsQueryable(); if (publisher != null) query = query.Where(p => p.Publisher.Id == publisher.Id); + query = query + .FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true); var totalCount = await query - .FilterWithVisibility(currentUser, userFriends, isListing: true) .CountAsync(); var posts = await query .Include(e => e.RepliedPost) @@ -46,7 +48,6 @@ public class PostController( .Include(e => e.Categories) .Include(e => e.Tags) .Where(e => e.RepliedPostId == null) - .FilterWithVisibility(currentUser, userFriends, isListing: true) .OrderByDescending(e => e.PublishedAt ?? e.CreatedAt) .Skip(offset) .Take(take) @@ -71,13 +72,14 @@ public class PostController( HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); var currentUser = currentUserValue as Account.Account; var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser); + var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); var post = await db.Posts .Where(e => e.Id == id) .Include(e => e.Publisher) .Include(e => e.Tags) .Include(e => e.Categories) - .FilterWithVisibility(currentUser, userFriends) + .FilterWithVisibility(currentUser, userFriends, userPublishers) .FirstOrDefaultAsync(); if (post is null) return NotFound(); @@ -93,6 +95,7 @@ public class PostController( HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); var currentUser = currentUserValue as Account.Account; var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser); + var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); var parent = await db.Posts .Where(e => e.Id == id) @@ -101,14 +104,14 @@ public class PostController( var totalCount = await db.Posts .Where(e => e.RepliedPostId == parent.Id) - .FilterWithVisibility(currentUser, userFriends, isListing: true) + .FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true) .CountAsync(); var posts = await db.Posts .Where(e => e.RepliedPostId == id) .Include(e => e.ForwardedPost) .Include(e => e.Categories) .Include(e => e.Tags) - .FilterWithVisibility(currentUser, userFriends, isListing: true) + .FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true) .OrderByDescending(e => e.PublishedAt ?? e.CreatedAt) .Skip(offset) .Take(take) @@ -223,7 +226,7 @@ public class PostController( { using var scope = factory.CreateScope(); var subs = scope.ServiceProvider.GetRequiredService(); - await subs.NotifySubscribersPostAsync(post); + await subs.NotifySubscriberPost(post); }); als.CreateActionLogFromRequest( @@ -248,12 +251,13 @@ public class PostController( HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); if (currentUserValue is not Account.Account currentUser) return Unauthorized(); var userFriends = await rels.ListAccountFriends(currentUser); + var userPublishers = await pub.GetUserPublishers(currentUser.Id); var post = await db.Posts .Where(e => e.Id == id) .Include(e => e.Publisher) .ThenInclude(e => e.Account) - .FilterWithVisibility(currentUser, userFriends) + .FilterWithVisibility(currentUser, userFriends, userPublishers) .FirstOrDefaultAsync(); if (post is null) return NotFound(); @@ -274,7 +278,6 @@ public class PostController( post, reaction, currentUser, - post.Publisher.Account, isExistingReaction, isSelfReact ); diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index bdde6dd..030d1f2 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -11,10 +11,8 @@ namespace DysonNetwork.Sphere.Post; public class PostService( AppDatabase db, - FileService fs, FileReferenceService fileRefService, IStringLocalizer localizer, - NotificationService nty, IServiceScopeFactory factory ) { @@ -33,6 +31,21 @@ public class PostService( return input; } + public (string title, string content) ChopPostForNotification(Post post) + { + var content = !string.IsNullOrEmpty(post.Description) + ? post.Description?.Length >= 40 ? post.Description[..37] + "..." : post.Description + : post.Content?.Length >= 100 + ? string.Concat(post.Content.AsSpan(0, 97), "...") + : post.Content; + var title = post.Title ?? (post.Content?.Length >= 10 ? post.Content[..10] + "..." : post.Content); + if (content is null) + content = localizer["PostOnlyMedia"]; + if (title is null) + title = localizer["PostOnlyMedia"]; + return (title, content); + } + public async Task PostAsync( Account.Account user, Post post, @@ -111,9 +124,44 @@ public class PostService( { using var scope = factory.CreateScope(); var pubSub = scope.ServiceProvider.GetRequiredService(); - await pubSub.NotifySubscribersPostAsync(post); + await pubSub.NotifySubscriberPost(post); }); + if (post.PublishedAt is not null && post.PublishedAt.Value.ToDateTimeUtc() <= DateTime.UtcNow && + post.RepliedPost is not null) + { + _ = Task.Run(async () => + { + var sender = post.Publisher; + using var scope = factory.CreateScope(); + var pub = scope.ServiceProvider.GetRequiredService(); + var nty = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService>(); + try + { + var members = await pub.GetPublisherMembers(post.RepliedPost.PublisherId); + foreach (var member in members) + { + AccountService.SetCultureInfo(member.Account); + var (_, content) = ChopPostForNotification(post); + await nty.SendNotification( + member.Account, + "post.replies", + localizer["PostReplyTitle", sender.Nick], + null, + string.IsNullOrWhiteSpace(post.Title) + ? localizer["PostReplyBody", sender.Nick, content] + : localizer["PostReplyContentBody", sender.Nick, post.Title, content] + ); + } + } + catch (Exception err) + { + logger.LogError($"Error when sending post reactions notification: {err.Message} {err.StackTrace}"); + } + }); + } + return post; } @@ -214,7 +262,6 @@ public class PostService( Post post, PostReaction reaction, Account.Account sender, - Account.Account? op, bool isRemoving, bool isSelfReact ) @@ -256,20 +303,36 @@ public class PostService( await db.SaveChangesAsync(); - if (!isSelfReact && op is not null) - { - AccountService.SetCultureInfo(op); - await nty.SendNotification( - op, - "posts.reactions.new", - localizer["PostReactTitle", sender.Nick], - null, - string.IsNullOrWhiteSpace(post.Title) - ? localizer["PostReactBody", sender.Nick, reaction.Symbol] - : localizer["PostReactContentBody", sender.Nick, reaction.Symbol, - post.Title] - ); - } + if (!isSelfReact) + _ = Task.Run(async () => + { + using var scope = factory.CreateScope(); + var pub = scope.ServiceProvider.GetRequiredService(); + var nty = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService>(); + try + { + var members = await pub.GetPublisherMembers(post.PublisherId); + foreach (var member in members) + { + AccountService.SetCultureInfo(member.Account); + await nty.SendNotification( + member.Account, + "posts.reactions.new", + localizer["PostReactTitle", sender.Nick], + null, + string.IsNullOrWhiteSpace(post.Title) + ? localizer["PostReactBody", sender.Nick, reaction.Symbol] + : localizer["PostReactContentBody", sender.Nick, reaction.Symbol, + post.Title] + ); + } + } + catch (Exception ex) + { + logger.LogError($"Error when sending post reactions notification: {ex.Message} {ex.StackTrace}"); + } + }); return isRemoving; } @@ -342,15 +405,17 @@ public static class PostQueryExtensions this IQueryable source, Account.Account? currentUser, List userFriends, + List publishers, bool isListing = false ) { var now = Instant.FromDateTimeUtc(DateTime.UtcNow); + var publishersId = publishers.Select(e => e.Id).ToList(); source = isListing switch { true when currentUser is not null => source.Where(e => - e.Visibility != PostVisibility.Unlisted || e.Publisher.AccountId == currentUser.Id), + e.Visibility != PostVisibility.Unlisted || publishersId.Contains(e.PublisherId)), true => source.Where(e => e.Visibility != PostVisibility.Unlisted), _ => source }; @@ -361,10 +426,11 @@ public static class PostQueryExtensions .Where(e => e.Visibility == PostVisibility.Public); return source - .Where(e => (e.PublishedAt != null && now >= e.PublishedAt) || e.Publisher.AccountId == currentUser.Id) - .Where(e => e.Visibility != PostVisibility.Private || e.Publisher.AccountId == currentUser.Id) + .Where(e => (e.PublishedAt != null && now >= e.PublishedAt) || publishersId.Contains(e.PublisherId)) + .Where(e => e.Visibility != PostVisibility.Private || publishersId.Contains(e.PublisherId)) .Where(e => e.Visibility != PostVisibility.Friends || (e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) || - e.Publisher.AccountId == currentUser.Id); + publishersId.Contains(e.PublisherId)); + ; } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Publisher/PublisherService.cs b/DysonNetwork.Sphere/Publisher/PublisherService.cs index b5d6070..65119ef 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherService.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherService.cs @@ -8,6 +8,56 @@ namespace DysonNetwork.Sphere.Publisher; public class PublisherService(AppDatabase db, FileService fs, FileReferenceService fileRefService, ICacheService cache) { + private const string UserPublishersCacheKey = "accounts:{0}:publishers"; + + public async Task> GetUserPublishers(Guid userId) + { + var cacheKey = string.Format(UserPublishersCacheKey, userId); + + // Try to get publishers from the cache first + var publishers = await cache.GetAsync>(cacheKey); + if (publishers is not null) + return publishers; + + // If not in cache, fetch from a database + var publishersId = await db.PublisherMembers + .Where(p => p.AccountId == userId) + .Select(p => p.PublisherId) + .ToListAsync(); + publishers = await db.Publishers + .Where(p => publishersId.Contains(p.Id)) + .ToListAsync(); + + // Store in a cache for 5 minutes + await cache.SetAsync(cacheKey, publishers, TimeSpan.FromMinutes(5)); + + return publishers; + } + + private const string PublisherMembersCacheKey = "publishers:{0}:members"; + + public async Task> GetPublisherMembers(Guid publisherId) + { + var cacheKey = string.Format(PublisherMembersCacheKey, publisherId); + + // Try to get members from the cache first + var members = await cache.GetAsync>(cacheKey); + if (members is not null) + return members; + + // If not in cache, fetch from a database + members = await db.PublisherMembers + .Where(p => p.PublisherId == publisherId) + .Include(p => p.Account) + .ThenInclude(p => p.Profile) + .ToListAsync(); + + // Store in cache for 5 minutes (consistent with other cache durations in the class) + await cache.SetAsync(cacheKey, members, TimeSpan.FromMinutes(5)); + + return members; + } + public async Task CreateIndividualPublisher( Account.Account account, string? name, diff --git a/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs b/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs index 725c719..c1743cc 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs @@ -1,4 +1,5 @@ using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Post; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; @@ -7,6 +8,7 @@ namespace DysonNetwork.Sphere.Publisher; public class PublisherSubscriptionService( AppDatabase db, NotificationService nty, + PostService ps, IStringLocalizer localizer) { /// @@ -41,7 +43,7 @@ public class PublisherSubscriptionService( /// /// The new post /// The number of subscribers notified - public async Task NotifySubscribersPostAsync(Post.Post post) + public async Task NotifySubscriberPost(Post.Post post) { var subscribers = await db.PublisherSubscriptions .Include(ps => ps.Account) @@ -52,11 +54,7 @@ public class PublisherSubscriptionService( return 0; // Create notification data - var message = !string.IsNullOrEmpty(post.Description) - ? post.Description?.Length > 40 ? post.Description[..37] + "..." : post.Description - : post.Content?.Length > 100 - ? string.Concat(post.Content.AsSpan(0, 97), "...") - : post.Content; + var (title, message) = ps.ChopPostForNotification(post); // Data to include with the notification var data = new Dictionary @@ -75,8 +73,8 @@ public class PublisherSubscriptionService( await nty.SendNotification( subscription.Account, "posts.new", - localizer["New post from {0}", post.Publisher.Name], - string.IsNullOrWhiteSpace(post.Title) ? null : post.Title, + localizer["PostSubscriptionTitle", post.Publisher.Name, title], + null, message, data ); diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs index 545ae65..c207cff 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs @@ -99,6 +99,30 @@ namespace DysonNetwork.Sphere.Resources.Localization { } } + internal static string PostReplyTitle { + get { + return ResourceManager.GetString("PostReplyTitle", resourceCulture); + } + } + + internal static string PostReplyBody { + get { + return ResourceManager.GetString("PostReplyBody", resourceCulture); + } + } + + internal static string PostReplyContentBody { + get { + return ResourceManager.GetString("PostReplyContentBody", resourceCulture); + } + } + + internal static string PostOnlyMedia { + get { + return ResourceManager.GetString("PostOnlyMedia", resourceCulture); + } + } + internal static string AuthCodeTitle { get { return ResourceManager.GetString("AuthCodeTitle", resourceCulture); diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx index 63e663c..57e3a6c 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx @@ -34,7 +34,7 @@ You just got invited to join {0} - {0} just posted + {0} just posted {1} {0} reacted your post @@ -45,6 +45,18 @@ {0} added a reaction {1} to your post {2} + + {0} replied your post + + + {0} replied: {1} + + + {0} replied post {1}: {2} + + + shared media + Disposable Verification Code diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx index 234481c..0ab6d81 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx @@ -38,6 +38,18 @@ {0} 给你的帖子添加了一个 {1} 的反应 {2} + + {0} 回复了你的帖子 + + + {0}:{1} + + + {0} 回复了帖子 {1}: {2} + + + 分享媒体 + 一次性验证码