🎨 Disassmeble the activity service parts
This commit is contained in:
@@ -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)
|
|
||||||
.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 is not null)
|
// Apply visibility filtering and execute
|
||||||
postsQuery = postsQuery.Where(p => filteredPublishersId.Contains(p.PublisherId));
|
postsQuery = postsQuery
|
||||||
|
.FilterWithVisibility(
|
||||||
|
currentUser,
|
||||||
|
userFriends,
|
||||||
|
filter is null ? userPublishers : [],
|
||||||
|
isListing: true)
|
||||||
|
.Take(take * 5);
|
||||||
|
|
||||||
// Complete the query with visibility filtering and execute
|
// Get, process and rank posts
|
||||||
var posts = await postsQuery
|
var posts = await GetAndProcessPosts(
|
||||||
.FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true)
|
postsQuery,
|
||||||
.Take(take * 5) // Fetch more posts to have a good pool for ranking
|
currentUser,
|
||||||
.ToListAsync();
|
userFriends,
|
||||||
|
userPublishers,
|
||||||
|
trackViews: true);
|
||||||
|
|
||||||
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
posts = RankPosts(posts, take);
|
||||||
|
|
||||||
var postsId = posts.Select(e => e.Id).ToList();
|
// Add posts to activities
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user