🎨 Disassmeble the activity service parts

This commit is contained in:
2025-08-22 16:56:21 +08:00
parent 7ad9deaf70
commit 1f9021d459

View File

@@ -1,7 +1,7 @@
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.WebReader;
using DysonNetwork.Sphere.Discovery; using DysonNetwork.Sphere.Discovery;
using DysonNetwork.Sphere.Post; using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
@@ -27,58 +27,28 @@ public class ActivityService(
public async Task<List<Activity>> GetActivitiesForAnyone( public async Task<List<Activity>> GetActivitiesForAnyone(
int take, int take,
Instant? cursor, Instant? cursor,
HashSet<string>? debugInclude = null HashSet<string>? debugInclude = null)
)
{ {
var activities = new List<Activity>(); var activities = new List<Activity>();
debugInclude ??= new HashSet<string>(); debugInclude ??= new HashSet<string>();
// Add realm discovery if needed
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2)) if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
{ {
var realms = await ds.GetCommunityRealmAsync(null, 5, 0, true); var realmActivity = await GetRealmDiscoveryActivity();
if (realms.Count > 0) if (realmActivity != null)
{ activities.Add(realmActivity);
activities.Add(new DiscoveryActivity(
realms.Select(x => new DiscoveryItem("realm", x)).ToList()
).ToActivity());
}
} }
// Add article discovery if needed
if (debugInclude.Contains("articles") || Random.Shared.NextDouble() < 0.2) if (debugInclude.Contains("articles") || Random.Shared.NextDouble() < 0.2)
{ {
var recentFeedIds = await db.WebArticles var articleActivity = await GetArticleDiscoveryActivity();
.GroupBy(a => a.FeedId) if (articleActivity != null)
.OrderByDescending(g => g.Max(a => a.PublishedAt)) activities.Add(articleActivity);
.Take(10) // Get recent 10 distinct feeds
.Select(g => g.Key)
.ToListAsync();
// For each feed, get one random article
var recentArticles = new List<WebArticle>();
var random = new Random();
foreach (var feedId in recentFeedIds.OrderBy(_ => random.Next()))
{
var article = await db.WebArticles
.Include(a => a.Feed)
.Where(a => a.FeedId == feedId)
.OrderBy(_ => EF.Functions.Random())
.FirstOrDefaultAsync();
if (article == null) continue;
recentArticles.Add(article);
if (recentArticles.Count >= 5) break; // Limit to 5 articles
}
if (recentArticles.Count > 0)
{
activities.Add(new DiscoveryActivity(
recentArticles.Select(x => new DiscoveryItem("article", x)).ToList()
).ToActivity());
}
} }
// Fetch a larger batch of recent posts to rank // Get and process posts
var postsQuery = db.Posts var postsQuery = db.Posts
.Include(e => e.RepliedPost) .Include(e => e.RepliedPost)
.Include(e => e.ForwardedPost) .Include(e => e.ForwardedPost)
@@ -89,31 +59,13 @@ public class ActivityService(
.Where(p => cursor == null || p.PublishedAt < cursor) .Where(p => cursor == null || p.PublishedAt < cursor)
.OrderByDescending(p => p.PublishedAt) .OrderByDescending(p => p.PublishedAt)
.FilterWithVisibility(null, [], [], isListing: true) .FilterWithVisibility(null, [], [], isListing: true)
.Take(take * 5); // Fetch more posts to have a good pool for ranking .Take(take * 5);
var posts = await postsQuery.ToListAsync(); var posts = await GetAndProcessPosts(postsQuery);
posts = await ps.LoadPostInfo(posts, null, true); posts = RankPosts(posts, take);
var postsId = posts.Select(e => e.Id).ToList(); // Add posts to activities
var reactionMaps = await ps.GetPostReactionMapBatch(postsId); activities.AddRange(posts.Select(post => post.ToActivity()));
foreach (var post in posts)
post.ReactionsCount =
reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary<string, int>();
// Rank and sort
// TODO: This feature is disabled for now
/*
var now = SystemClock.Instance.GetCurrentInstant();
var rankedPosts = posts
.Select(p => new { Post = p, Rank = CalculateHotRank(p, now) })
.OrderByDescending(x => x.Rank)
.Select(x => x.Post)
.Take(take)
.ToList(); */
// Formatting data
foreach (var post in posts)
activities.Add(post.ToActivity());
if (activities.Count == 0) if (activities.Count == 0)
activities.Add(Activity.Empty()); activities.Add(Activity.Empty());
@@ -126,135 +78,74 @@ public class ActivityService(
Instant? cursor, Instant? cursor,
Account currentUser, Account currentUser,
string? filter = null, string? filter = null,
HashSet<string>? debugInclude = null HashSet<string>? debugInclude = null)
)
{ {
var activities = new List<Activity>(); var activities = new List<Activity>();
debugInclude ??= [];
// Get user's friends and publishers
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
{ {
AccountId = currentUser.Id AccountId = currentUser.Id
}); });
var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id));
debugInclude ??= [];
// Add discovery activities if no specific filter is applied
if (string.IsNullOrEmpty(filter)) if (string.IsNullOrEmpty(filter))
{ {
// Add realm discovery if needed
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2)) if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
{ {
var realms = await ds.GetCommunityRealmAsync(null, 5, 0, true); var realmActivity = await GetRealmDiscoveryActivity();
if (realms.Count > 0) if (realmActivity != null)
{ activities.Add(realmActivity);
activities.Add(new DiscoveryActivity(
realms.Select(x => new DiscoveryItem("realm", x)).ToList()
).ToActivity());
}
} }
// Add publisher discovery if needed
if (cursor == null && (debugInclude.Contains("publishers") || Random.Shared.NextDouble() < 0.2)) if (cursor == null && (debugInclude.Contains("publishers") || Random.Shared.NextDouble() < 0.2))
{ {
var popularPublishers = await GetPopularPublishers(5); var publisherActivity = await GetPublisherDiscoveryActivity();
if (popularPublishers.Count > 0) if (publisherActivity != null)
{ activities.Add(publisherActivity);
activities.Add(new DiscoveryActivity(
popularPublishers.Select(x => new DiscoveryItem("publisher", x)).ToList()
).ToActivity());
}
} }
// Add article discovery if needed
if (debugInclude.Contains("articles") || Random.Shared.NextDouble() < 0.2) if (debugInclude.Contains("articles") || Random.Shared.NextDouble() < 0.2)
{ {
var recentFeedIds = await db.WebArticles var articleActivity = await GetArticleDiscoveryActivity();
.GroupBy(a => a.FeedId) if (articleActivity != null)
.OrderByDescending(g => g.Max(a => a.PublishedAt)) activities.Add(articleActivity);
.Take(10) // Get recent 10 distinct feeds
.Select(g => g.Key)
.ToListAsync();
// For each feed, get one random article
var recentArticles = new List<WebArticle>();
var random = new Random();
foreach (var feedId in recentFeedIds.OrderBy(_ => random.Next()))
{
var article = await db.WebArticles
.Include(a => a.Feed)
.Where(a => a.FeedId == feedId)
.OrderBy(_ => EF.Functions.Random())
.FirstOrDefaultAsync();
if (article == null) continue;
recentArticles.Add(article);
if (recentArticles.Count >= 5) break; // Limit to 5 articles
}
if (recentArticles.Count > 0)
{
activities.Add(new DiscoveryActivity(
recentArticles.Select(x => new DiscoveryItem("article", x)).ToList()
).ToActivity());
}
} }
} }
// Get publishers based on filter // Get publishers based on filter
var filteredPublishers = filter switch var filteredPublishers = await GetFilteredPublishers(filter, currentUser, userFriends);
{
"subscriptions" => await pub.GetSubscribedPublishers(Guid.Parse(currentUser.Id)),
"friends" => (await pub.GetUserPublishersBatch(userFriends)).SelectMany(x => x.Value)
.DistinctBy(x => x.Id)
.ToList(),
_ => null
};
var filteredPublishersId = filteredPublishers?.Select(e => e.Id).ToList(); var filteredPublishersId = filteredPublishers?.Select(e => e.Id).ToList();
// Build the query based on the filter // Build and execute the posts query
var postsQuery = db.Posts var postsQuery = BuildPostsQuery(cursor, filteredPublishersId);
.Include(e => e.RepliedPost)
.Include(e => e.ForwardedPost) // Apply visibility filtering and execute
.Include(e => e.Categories) postsQuery = postsQuery
.Include(e => e.Tags) .FilterWithVisibility(
.Include(e => e.Realm) currentUser,
.Where(e => e.RepliedPostId == null) userFriends,
.Where(p => cursor == null || p.PublishedAt < cursor) filter is null ? userPublishers : [],
.OrderByDescending(p => p.PublishedAt) isListing: true)
.AsQueryable(); .Take(take * 5);
if (filteredPublishersId is not null) // Get, process and rank posts
postsQuery = postsQuery.Where(p => filteredPublishersId.Contains(p.PublisherId)); var posts = await GetAndProcessPosts(
postsQuery,
// Complete the query with visibility filtering and execute currentUser,
var posts = await postsQuery userFriends,
.FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true) userPublishers,
.Take(take * 5) // Fetch more posts to have a good pool for ranking trackViews: true);
.ToListAsync();
posts = RankPosts(posts, take);
posts = await ps.LoadPostInfo(posts, currentUser, true);
// Add posts to activities
var postsId = posts.Select(e => e.Id).ToList();
var reactionMaps = await ps.GetPostReactionMapBatch(postsId);
foreach (var post in posts)
{
post.ReactionsCount =
reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary<string, int>();
// Track view for each post in the feed
await ps.IncreaseViewCount(post.Id, currentUser.Id.ToString());
}
// Rank and sort
// TODO: This feature is disabled for now
/*
var now = SystemClock.Instance.GetCurrentInstant();
var rankedPosts = posts
.Select(p => new { Post = p, Rank = CalculateHotRank(p, now) })
.OrderByDescending(x => x.Rank)
.Select(x => x.Post)
.Take(take)
.ToList(); */
// Formatting data
activities.AddRange(posts.Select(post => post.ToActivity())); activities.AddRange(posts.Select(post => post.ToActivity()));
if (activities.Count == 0) if (activities.Count == 0)
@@ -263,11 +154,20 @@ public class ActivityService(
return activities; return activities;
} }
private static double CalculatePopularity(List<Post.Post> posts) private static List<Post.Post> RankPosts(List<Post.Post> posts, int take)
{ {
var score = posts.Sum(p => p.Upvotes - p.Downvotes); // TODO: This feature is disabled for now
var postCount = posts.Count; // Uncomment and implement when ready
return score + postCount; /*
var now = SystemClock.Instance.GetCurrentInstant();
return posts
.Select(p => new { Post = p, Rank = CalculateHotRank(p, now) })
.OrderByDescending(x => x.Rank)
.Select(x => x.Post)
.Take(take)
.ToList();
*/
return posts.Take(take).ToList();
} }
private async Task<List<Publisher.Publisher>> GetPopularPublishers(int take) private async Task<List<Publisher.Publisher>> GetPopularPublishers(int take)
@@ -282,7 +182,7 @@ public class ActivityService(
var publisherIds = posts.Select(p => p.PublisherId).Distinct().ToList(); var publisherIds = posts.Select(p => p.PublisherId).Distinct().ToList();
var publishers = await db.Publishers.Where(p => publisherIds.Contains(p.Id)).ToListAsync(); var publishers = await db.Publishers.Where(p => publisherIds.Contains(p.Id)).ToListAsync();
var rankedPublishers = publishers return publishers
.Select(p => new .Select(p => new
{ {
Publisher = p, Publisher = p,
@@ -292,7 +192,121 @@ public class ActivityService(
.Select(x => x.Publisher) .Select(x => x.Publisher)
.Take(take) .Take(take)
.ToList(); .ToList();
}
return rankedPublishers; private async Task<Activity?> GetRealmDiscoveryActivity(int count = 5)
{
var realms = await ds.GetCommunityRealmAsync(null, count, 0, true);
return realms.Count > 0
? new DiscoveryActivity(realms.Select(x => new DiscoveryItem("realm", x)).ToList()).ToActivity()
: null;
}
private async Task<Activity?> GetPublisherDiscoveryActivity(int count = 5)
{
var popularPublishers = await GetPopularPublishers(count);
return popularPublishers.Count > 0
? new DiscoveryActivity(popularPublishers.Select(x => new DiscoveryItem("publisher", x)).ToList()).ToActivity()
: null;
}
private async Task<Activity?> GetArticleDiscoveryActivity(int count = 5, int feedSampleSize = 10)
{
var recentFeedIds = await db.WebArticles
.GroupBy(a => a.FeedId)
.OrderByDescending(g => g.Max(a => a.PublishedAt))
.Take(feedSampleSize)
.Select(g => g.Key)
.ToListAsync();
var recentArticles = new List<WebArticle>();
var random = new Random();
foreach (var feedId in recentFeedIds.OrderBy(_ => random.Next()))
{
var article = await db.WebArticles
.Include(a => a.Feed)
.Where(a => a.FeedId == feedId)
.OrderBy(_ => EF.Functions.Random())
.FirstOrDefaultAsync();
if (article == null) continue;
recentArticles.Add(article);
if (recentArticles.Count >= count) break;
}
return recentArticles.Count > 0
? new DiscoveryActivity(recentArticles.Select(x => new DiscoveryItem("article", x)).ToList()).ToActivity()
: null;
}
private async Task<List<Post.Post>> GetAndProcessPosts(
IQueryable<Post.Post> baseQuery,
Account? currentUser = null,
List<Guid>? userFriends = null,
List<Publisher.Publisher>? userPublishers = null,
bool trackViews = true)
{
var posts = await baseQuery.ToListAsync();
posts = await ps.LoadPostInfo(posts, currentUser, true);
var postsId = posts.Select(e => e.Id).ToList();
var reactionMaps = await ps.GetPostReactionMapBatch(postsId);
foreach (var post in posts)
{
post.ReactionsCount = reactionMaps.GetValueOrDefault(post.Id, new Dictionary<string, int>());
if (trackViews && currentUser != null)
{
await ps.IncreaseViewCount(post.Id, currentUser.Id.ToString());
}
}
return posts;
}
private IQueryable<Post.Post> BuildPostsQuery(Instant? cursor, List<Guid>? filteredPublishersId = null)
{
var query = db.Posts
.Include(e => e.RepliedPost)
.Include(e => e.ForwardedPost)
.Include(e => e.Categories)
.Include(e => e.Tags)
.Include(e => e.Realm)
.Where(e => e.RepliedPostId == null)
.Where(p => cursor == null || p.PublishedAt < cursor)
.OrderByDescending(p => p.PublishedAt)
.AsQueryable();
if (filteredPublishersId != null && filteredPublishersId.Any())
{
query = query.Where(p => filteredPublishersId.Contains(p.PublisherId));
}
return query;
}
private async Task<List<Publisher.Publisher>?> GetFilteredPublishers(
string? filter,
Account currentUser,
List<Guid> userFriends)
{
return filter?.ToLower() switch
{
"subscriptions" => await pub.GetSubscribedPublishers(Guid.Parse(currentUser.Id)),
"friends" => (await pub.GetUserPublishersBatch(userFriends))
.SelectMany(x => x.Value)
.DistinctBy(x => x.Id)
.ToList(),
_ => null
};
}
private static double CalculatePopularity(List<Post.Post> posts)
{
var score = posts.Sum(p => p.Upvotes - p.Downvotes);
var postCount = posts.Count;
return score + postCount;
} }
} }