Compare commits
	
		
			3 Commits
		
	
	
		
			c1c17b5f4e
			...
			4b958a3c31
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b958a3c31 | |||
| 1f9021d459 | |||
| 7ad9deaf70 | 
| @@ -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; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -60,6 +60,7 @@ | |||||||
|         <PackageReference Include="StackExchange.Redis" Version="2.8.41"/> |         <PackageReference Include="StackExchange.Redis" Version="2.8.41"/> | ||||||
|         <PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0"/> |         <PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0"/> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/> |         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/> | ||||||
|  |         <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="9.0.3" /> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3"/> |         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3"/> | ||||||
|         <PackageReference Include="System.ServiceModel.Syndication" Version="9.0.7" /> |         <PackageReference Include="System.ServiceModel.Syndication" Version="9.0.7" /> | ||||||
|         <PackageReference Include="TencentCloudSDK.Tmt" Version="3.0.1276" /> |         <PackageReference Include="TencentCloudSDK.Tmt" Version="3.0.1276" /> | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization; | |||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using Swashbuckle.AspNetCore.Annotations; | ||||||
| using PublisherService = DysonNetwork.Sphere.Publisher.PublisherService; | using PublisherService = DysonNetwork.Sphere.Publisher.PublisherService; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Post; | namespace DysonNetwork.Sphere.Post; | ||||||
| @@ -37,8 +38,39 @@ public class PostController( | |||||||
|         return Ok(posts); |         return Ok(posts); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Retrieves a paginated list of posts with optional filtering and sorting. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="includeReplies">Whether to include reply posts in the results. If false, only root posts are returned.</param> | ||||||
|  |     /// <param name="offset">The number of posts to skip for pagination.</param> | ||||||
|  |     /// <param name="take">The maximum number of posts to return (default: 20).</param> | ||||||
|  |     /// <param name="pubName">Filter posts by publisher name.</param> | ||||||
|  |     /// <param name="realmName">Filter posts by realm slug.</param> | ||||||
|  |     /// <param name="type">Filter posts by post type (as integer).</param> | ||||||
|  |     /// <param name="categories">Filter posts by category slugs.</param> | ||||||
|  |     /// <param name="tags">Filter posts by tag slugs.</param> | ||||||
|  |     /// <param name="queryTerm">Search term to filter posts by title, description, or content.</param> | ||||||
|  |     /// <param name="queryVector">If true, uses vector search with the query term. If false, performs a simple ILIKE search.</param> | ||||||
|  |     /// <param name="onlyMedia">If true, only returns posts that have attachments.</param> | ||||||
|  |     /// <param name="shuffle">If true, returns posts in random order. If false, orders by published/created date (newest first).</param> | ||||||
|  |     /// <returns> | ||||||
|  |     /// Returns an ActionResult containing a list of Post objects that match the specified criteria. | ||||||
|  |     /// Includes an X-Total header with the total count of matching posts before pagination. | ||||||
|  |     /// </returns> | ||||||
|  |     /// <response code="200">Returns the list of posts matching the criteria.</response> | ||||||
|     [HttpGet] |     [HttpGet] | ||||||
|  |     [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<Post>))] | ||||||
|  |     [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |     [SwaggerOperation( | ||||||
|  |         Summary = "Retrieves a paginated list of posts", | ||||||
|  |         Description = "Gets posts with various filtering and sorting options. Supports pagination and advanced search capabilities.", | ||||||
|  |         OperationId = "ListPosts", | ||||||
|  |         Tags = ["Posts"] | ||||||
|  |     )] | ||||||
|  |     [SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved the list of posts", typeof(List<Post>))] | ||||||
|  |     [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request parameters")] | ||||||
|     public async Task<ActionResult<List<Post>>> ListPosts( |     public async Task<ActionResult<List<Post>>> ListPosts( | ||||||
|  |         [FromQuery(Name = "replies")] bool? includeReplies, | ||||||
|         [FromQuery] int offset = 0, |         [FromQuery] int offset = 0, | ||||||
|         [FromQuery] int take = 20, |         [FromQuery] int take = 20, | ||||||
|         [FromQuery(Name = "pub")] string? pubName = null, |         [FromQuery(Name = "pub")] string? pubName = null, | ||||||
| @@ -48,7 +80,6 @@ public class PostController( | |||||||
|         [FromQuery(Name = "tags")] List<string>? tags = null, |         [FromQuery(Name = "tags")] List<string>? tags = null, | ||||||
|         [FromQuery(Name = "query")] string? queryTerm = null, |         [FromQuery(Name = "query")] string? queryTerm = null, | ||||||
|         [FromQuery(Name = "vector")] bool queryVector = false, |         [FromQuery(Name = "vector")] bool queryVector = false, | ||||||
|         [FromQuery(Name = "replies")] bool includeReplies = false, |  | ||||||
|         [FromQuery(Name = "media")] bool onlyMedia = false, |         [FromQuery(Name = "media")] bool onlyMedia = false, | ||||||
|         [FromQuery(Name = "shuffle")] bool shuffle = false |         [FromQuery(Name = "shuffle")] bool shuffle = false | ||||||
|     ) |     ) | ||||||
| @@ -60,7 +91,7 @@ public class PostController( | |||||||
|         if (currentUser != null) |         if (currentUser != null) | ||||||
|         { |         { | ||||||
|             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest |             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||||
|             { AccountId = currentUser.Id }); |                 { AccountId = currentUser.Id }); | ||||||
|             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); |             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -83,11 +114,16 @@ public class PostController( | |||||||
|             query = query.Where(p => p.Categories.Any(c => categories.Contains(c.Slug))); |             query = query.Where(p => p.Categories.Any(c => categories.Contains(c.Slug))); | ||||||
|         if (tags is { Count: > 0 }) |         if (tags is { Count: > 0 }) | ||||||
|             query = query.Where(p => p.Tags.Any(c => tags.Contains(c.Slug))); |             query = query.Where(p => p.Tags.Any(c => tags.Contains(c.Slug))); | ||||||
|         if (!includeReplies) |  | ||||||
|             query = query.Where(e => e.RepliedPostId == null); |  | ||||||
|         if (onlyMedia) |         if (onlyMedia) | ||||||
|             query = query.Where(e => e.Attachments.Count > 0); |             query = query.Where(e => e.Attachments.Count > 0); | ||||||
|          |          | ||||||
|  |         query = includeReplies switch | ||||||
|  |         { | ||||||
|  |             false => query.Where(e => e.RepliedPostId == null), | ||||||
|  |             true => query.Where(e => e.RepliedPostId != null), | ||||||
|  |             _ => query | ||||||
|  |         }; | ||||||
|  |  | ||||||
|         if (!string.IsNullOrWhiteSpace(queryTerm)) |         if (!string.IsNullOrWhiteSpace(queryTerm)) | ||||||
|         { |         { | ||||||
|             if (queryVector) |             if (queryVector) | ||||||
| @@ -106,10 +142,9 @@ public class PostController( | |||||||
|         var totalCount = await query |         var totalCount = await query | ||||||
|             .CountAsync(); |             .CountAsync(); | ||||||
|  |  | ||||||
|         if (shuffle) |         query = shuffle | ||||||
|             query = query.OrderBy(e => EF.Functions.Random()); |             ? query.OrderBy(e => EF.Functions.Random()) | ||||||
|         else |             : query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt); | ||||||
|             query = query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt); |  | ||||||
|  |  | ||||||
|         var posts = await query |         var posts = await query | ||||||
|             .Include(e => e.RepliedPost) |             .Include(e => e.RepliedPost) | ||||||
| @@ -134,7 +169,7 @@ public class PostController( | |||||||
|         if (currentUser != null) |         if (currentUser != null) | ||||||
|         { |         { | ||||||
|             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest |             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||||
|             { AccountId = currentUser.Id }); |                 { AccountId = currentUser.Id }); | ||||||
|             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); |             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -168,7 +203,7 @@ public class PostController( | |||||||
|         if (currentUser != null) |         if (currentUser != null) | ||||||
|         { |         { | ||||||
|             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest |             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||||
|             { AccountId = currentUser.Id }); |                 { AccountId = currentUser.Id }); | ||||||
|             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); |             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -193,59 +228,6 @@ public class PostController( | |||||||
|         return Ok(post); |         return Ok(post); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     [HttpGet("search")] |  | ||||||
|     [Obsolete("Use the new ListPost API")] |  | ||||||
|     public async Task<ActionResult<List<Post>>> SearchPosts( |  | ||||||
|         [FromQuery] string query, |  | ||||||
|         [FromQuery] int offset = 0, |  | ||||||
|         [FromQuery] int take = 20, |  | ||||||
|         [FromQuery] bool useVector = true |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (string.IsNullOrWhiteSpace(query)) |  | ||||||
|             return BadRequest("Search query cannot be empty"); |  | ||||||
|  |  | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |  | ||||||
|         var currentUser = currentUserValue as Account; |  | ||||||
|         List<Guid> userFriends = []; |  | ||||||
|         if (currentUser != null) |  | ||||||
|         { |  | ||||||
|             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest |  | ||||||
|             { AccountId = currentUser.Id }); |  | ||||||
|             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); |  | ||||||
|  |  | ||||||
|         var queryable = db.Posts |  | ||||||
|             .FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true) |  | ||||||
|             .AsQueryable(); |  | ||||||
|         if (useVector) |  | ||||||
|             queryable = queryable.Where(p => p.SearchVector.Matches(EF.Functions.ToTsQuery(query))); |  | ||||||
|         else |  | ||||||
|             queryable = queryable.Where(p => |  | ||||||
|                 (p.Title != null && EF.Functions.ILike(p.Title, $"%{query}%")) || |  | ||||||
|                 (p.Description != null && EF.Functions.ILike(p.Description, $"%{query}%")) || |  | ||||||
|                 (p.Content != null && EF.Functions.ILike(p.Content, $"%{query}%")) |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|         var totalCount = await queryable.CountAsync(); |  | ||||||
|  |  | ||||||
|         var posts = await queryable |  | ||||||
|             .Include(e => e.RepliedPost) |  | ||||||
|             .Include(e => e.ForwardedPost) |  | ||||||
|             .Include(e => e.Categories) |  | ||||||
|             .Include(e => e.Tags) |  | ||||||
|             .OrderByDescending(e => e.PublishedAt ?? e.CreatedAt) |  | ||||||
|             .Skip(offset) |  | ||||||
|             .Take(take) |  | ||||||
|             .ToListAsync(); |  | ||||||
|         posts = await ps.LoadPostInfo(posts, currentUser, true); |  | ||||||
|  |  | ||||||
|         Response.Headers["X-Total"] = totalCount.ToString(); |  | ||||||
|         return Ok(posts); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{id:guid}/reactions")] |     [HttpGet("{id:guid}/reactions")] | ||||||
|     public async Task<ActionResult<List<PostReaction>>> GetReactions( |     public async Task<ActionResult<List<PostReaction>>> GetReactions( | ||||||
|         Guid id, |         Guid id, | ||||||
| @@ -279,7 +261,7 @@ public class PostController( | |||||||
|         if (currentUser != null) |         if (currentUser != null) | ||||||
|         { |         { | ||||||
|             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest |             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||||
|             { AccountId = currentUser.Id }); |                 { AccountId = currentUser.Id }); | ||||||
|             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); |             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -315,7 +297,7 @@ public class PostController( | |||||||
|         if (currentUser != null) |         if (currentUser != null) | ||||||
|         { |         { | ||||||
|             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest |             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||||
|             { AccountId = currentUser.Id }); |                 { AccountId = currentUser.Id }); | ||||||
|             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); |             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -502,7 +484,7 @@ public class PostController( | |||||||
|  |  | ||||||
|         var friendsResponse = |         var friendsResponse = | ||||||
|             await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest |             await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||||
|             { AccountId = currentUser.Id.ToString() }); |                 { AccountId = currentUser.Id.ToString() }); | ||||||
|         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)); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user