♻️ Refactored publishers

This commit is contained in:
LittleSheep 2025-06-14 16:19:45 +08:00
parent 2821beb1b7
commit 1f2e9b1de8
8 changed files with 213 additions and 44 deletions

View File

@ -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<List<Activity>> 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,6 +47,9 @@ public class ActivityService(AppDatabase db, RelationshipService rels, PostServi
{
var activities = new List<Activity>();
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
@ -53,10 +57,10 @@ public class ActivityService(AppDatabase db, RelationshipService rels, PostServi
.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);

View File

@ -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<PublisherSubscriptionService>();
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
);

View File

@ -11,10 +11,8 @@ namespace DysonNetwork.Sphere.Post;
public class PostService(
AppDatabase db,
FileService fs,
FileReferenceService fileRefService,
IStringLocalizer<NotificationResource> 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<Post> PostAsync(
Account.Account user,
Post post,
@ -111,9 +124,44 @@ public class PostService(
{
using var scope = factory.CreateScope();
var pubSub = scope.ServiceProvider.GetRequiredService<PublisherSubscriptionService>();
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<PublisherService>();
var nty = scope.ServiceProvider.GetRequiredService<NotificationService>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<PostService>>();
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,11 +303,21 @@ public class PostService(
await db.SaveChangesAsync();
if (!isSelfReact && op is not null)
if (!isSelfReact)
_ = Task.Run(async () =>
{
AccountService.SetCultureInfo(op);
using var scope = factory.CreateScope();
var pub = scope.ServiceProvider.GetRequiredService<PublisherService>();
var nty = scope.ServiceProvider.GetRequiredService<NotificationService>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<PostService>>();
try
{
var members = await pub.GetPublisherMembers(post.PublisherId);
foreach (var member in members)
{
AccountService.SetCultureInfo(member.Account);
await nty.SendNotification(
op,
member.Account,
"posts.reactions.new",
localizer["PostReactTitle", sender.Nick],
null,
@ -270,6 +327,12 @@ public class PostService(
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<Post> source,
Account.Account? currentUser,
List<Guid> userFriends,
List<Publisher.Publisher> 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));
;
}
}

View File

@ -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<List<Publisher>> GetUserPublishers(Guid userId)
{
var cacheKey = string.Format(UserPublishersCacheKey, userId);
// Try to get publishers from the cache first
var publishers = await cache.GetAsync<List<Publisher>>(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<List<PublisherMember>> GetPublisherMembers(Guid publisherId)
{
var cacheKey = string.Format(PublisherMembersCacheKey, publisherId);
// Try to get members from the cache first
var members = await cache.GetAsync<List<PublisherMember>>(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<Publisher> CreateIndividualPublisher(
Account.Account account,
string? name,

View File

@ -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<Notification> localizer)
{
/// <summary>
@ -41,7 +43,7 @@ public class PublisherSubscriptionService(
/// </summary>
/// <param name="post">The new post</param>
/// <returns>The number of subscribers notified</returns>
public async Task<int> NotifySubscribersPostAsync(Post.Post post)
public async Task<int> 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<string, object>
@ -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
);

View File

@ -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);

View File

@ -34,7 +34,7 @@
<value>You just got invited to join {0}</value>
</data>
<data name="PostSubscriptionTitle" xml:space="preserve">
<value>{0} just posted</value>
<value>{0} just posted {1}</value>
</data>
<data name="PostReactTitle" xml:space="preserve">
<value>{0} reacted your post</value>
@ -45,6 +45,18 @@
<data name="PostReactContentBody" xml:space="preserve">
<value>{0} added a reaction {1} to your post {2}</value>
</data>
<data name="PostReplyTitle" xml:space="preserve">
<value>{0} replied your post</value>
</data>
<data name="PostReplyBody">
<value>{0} replied: {1}</value>
</data>
<data name="PostReplyContentBody">
<value>{0} replied post {1}: {2}</value>
</data>
<data name="PostOnlyMedia" xml:space="preserve">
<value>shared media</value>
</data>
<data name="AuthCodeTitle" xml:space="preserve">
<value>Disposable Verification Code</value>
</data>

View File

@ -38,6 +38,18 @@
<data name="PostReactContentBody" xml:space="preserve">
<value>{0} 给你的帖子添加了一个 {1} 的反应 {2}</value>
</data>
<data name="PostReplyTitle" xml:space="preserve">
<value>{0} 回复了你的帖子</value>
</data>
<data name="PostReplyBody">
<value>{0}{1}</value>
</data>
<data name="PostReplyContentBody">
<value>{0} 回复了帖子 {1}: {2}</value>
</data>
<data name="PostOnlyMedia" xml:space="preserve">
<value>分享媒体</value>
</data>
<data name="AuthCodeTitle" xml:space="preserve">
<value>一次性验证码</value>
</data>