♻️ Refactored publishers
This commit is contained in:
parent
2821beb1b7
commit
1f2e9b1de8
@ -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,17 +47,20 @@ 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
|
||||
.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);
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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,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<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(
|
||||
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<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));
|
||||
;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user