From 605f9e68c5372d6ed6ef895ce8c14d5c2b636998 Mon Sep 17 00:00:00 2001 From: Slendy Date: Fri, 14 Jul 2023 15:27:33 -0500 Subject: [PATCH 01/40] Initial framework for recent activity --- ProjectLighthouse/Database/DatabaseContext.cs | 15 +++++++++++ ProjectLighthouse/Types/Activity/EventType.cs | 26 +++++++++++++++++++ .../Types/Entities/Activity/ActivityEntity.cs | 21 +++++++++++++++ .../Entities/Activity/CommentActivityEntry.cs | 9 +++++++ .../Entities/Activity/LevelActivityEntity.cs | 17 ++++++++++++ .../Entities/Activity/NewsActivityEntity.cs | 11 ++++++++ .../Entities/Activity/PhotoActivityEntity.cs | 16 ++++++++++++ .../Activity/PlaylistActivityEntity.cs | 12 +++++++++ .../Entities/Activity/ScoreActivityEntity.cs | 15 +++++++++++ .../Entities/Activity/UserActivityEntity.cs | 9 +++++++ 10 files changed, 151 insertions(+) create mode 100644 ProjectLighthouse/Types/Activity/EventType.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/CommentActivityEntry.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs diff --git a/ProjectLighthouse/Database/DatabaseContext.cs b/ProjectLighthouse/Database/DatabaseContext.cs index 0a2e09e15..699fc2f7f 100644 --- a/ProjectLighthouse/Database/DatabaseContext.cs +++ b/ProjectLighthouse/Database/DatabaseContext.cs @@ -1,4 +1,5 @@ using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Maintenance; @@ -26,6 +27,10 @@ public partial class DatabaseContext : DbContext public DbSet WebTokens { get; set; } #endregion + #region Activity + public DbSet Activities { get; set; } + #endregion + #region Users public DbSet Comments { get; set; } public DbSet LastContacts { get; set; } @@ -81,6 +86,16 @@ protected internal DatabaseContext() public DatabaseContext(DbContextOptions options) : base(options) { } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().UseTpcMappingStrategy(); + base.OnModelCreating(modelBuilder); + } + public static DatabaseContext CreateNewInstance() { DbContextOptionsBuilder builder = new(); diff --git a/ProjectLighthouse/Types/Activity/EventType.cs b/ProjectLighthouse/Types/Activity/EventType.cs new file mode 100644 index 000000000..7e0c74d75 --- /dev/null +++ b/ProjectLighthouse/Types/Activity/EventType.cs @@ -0,0 +1,26 @@ +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +public enum EventType +{ + HeartLevel, + UnheartLevel, + HeartUser, + UnheartUser, + PlayLevel, + RateLevel, + TagLevel, + CommentOnLevel, + DeleteLevelComment, + UploadPhoto, + PublishLevel, + UnpublishLevel, + Score, + NewsPost, + MMPickLevel, + DpadRateLevel, + ReviewLevel, + CommentOnUser, + CreatePlaylist, + HeartPlaylist, + AddLevelToPlaylist, +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs new file mode 100644 index 000000000..cdb81944f --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +public class ActivityEntity +{ + [Key] + public int ActivityId { get; set; } + + public long Timestamp { get; set; } + + public int UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public UserEntity User { get; set; } + + public EventType Type { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntry.cs b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntry.cs new file mode 100644 index 000000000..4aa02e0d3 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntry.cs @@ -0,0 +1,9 @@ +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: CommentOnUser, CommentOnLevel, DeleteLevelComment +/// +public class CommentActivityEntry +{ + +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs new file mode 100644 index 000000000..e0dac1772 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: play_level, heart_level, publish_level, +/// +public class LevelActivityEntity : ActivityEntity +{ + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } + + +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs new file mode 100644 index 000000000..647db324e --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs @@ -0,0 +1,11 @@ +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: NewsPost +/// +public class NewsActivityEntity : ActivityEntity +{ + public string Title { get; set; } = ""; + + public string Body { get; set; } = ""; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs new file mode 100644 index 000000000..6db40d59e --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: UploadPhoto +/// +public class PhotoActivityEntity : ActivityEntity +{ + public int PhotoId { get; set; } + + [ForeignKey(nameof(PhotoId))] + public PhotoEntity Photo { get; set; } + +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs new file mode 100644 index 000000000..502baf4b2 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +public class PlaylistActivityEntity : ActivityEntity +{ + public int PlaylistId { get; set; } + + [ForeignKey(nameof(PlaylistId))] + public PlaylistEntity Playlist { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs new file mode 100644 index 000000000..17c500ac0 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: Score +/// +public class ScoreActivityEntity : ActivityEntity +{ + public int ScoreId { get; set; } + + [ForeignKey(nameof(ScoreId))] + public ScoreEntity Score { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs new file mode 100644 index 000000000..748563679 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs @@ -0,0 +1,9 @@ +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: HeartUser, UnheartUser +/// +public class UserActivityEntity : ActivityEntity +{ + +} \ No newline at end of file From 23cb1bef1ca7efc5785f1af91324b90a210c7a94 Mon Sep 17 00:00:00 2001 From: Slendy Date: Thu, 20 Jul 2023 19:38:37 -0500 Subject: [PATCH 02/40] Initial implementation of recent activity DB migrations intentionally left out since they aren't finalized --- .../Controllers/ActivityController.cs | 286 ++++++++++++++++++ .../Controllers/CommentController.cs | 16 +- .../Controllers/FriendsController.cs | 2 +- .../Controllers/Resources/PhotosController.cs | 2 +- .../Controllers/Slots/CategoryController.cs | 3 + .../Controllers/Slots/ListController.cs | 4 +- .../Controllers/Slots/PlaylistController.cs | 3 +- .../Controllers/Slots/PublishController.cs | 2 +- .../Controllers/Slots/ReviewController.cs | 2 +- .../Controllers/Slots/ScoreController.cs | 2 +- .../Controllers/Slots/SearchController.cs | 2 +- .../Controllers/Slots/SlotsController.cs | 2 +- .../Controllers/StatisticsController.cs | 2 +- .../Controllers/UserController.cs | 2 +- .../Extensions/DatabaseContextExtensions.cs | 2 +- .../Types/Categories/PlaylistCategory.cs | 2 + .../Types/Categories/SlotCategory.cs | 1 + .../Types/Categories/UserCategory.cs | 2 + .../Moderator/ModerationRemovalController.cs | 2 +- .../Pages/Partials/PhotoPartial.cshtml | 2 +- .../Pages/Partials/ReviewPartial.cshtml | 2 +- .../Integration/SlotFilterTests.cs | 2 +- .../Unit/Controllers/SlotControllerTests.cs | 2 +- .../Controllers/StatisticsControllerTests.cs | 2 +- .../Unit/Controllers/UserControllerTests.cs | 2 +- ProjectLighthouse.Tests/Unit/LocationTests.cs | 2 +- .../Unit/PaginationTests.cs | 2 +- .../Database/ActivityInterceptor.cs | 140 +++++++++ ProjectLighthouse/Database/DatabaseContext.cs | 33 +- .../Extensions/DateTimeExtensions.cs | 12 + .../Activity/ActivityEntityEventHandler.cs | 179 +++++++++++ .../Types/Activity/ActivityGroup.cs | 41 +++ ProjectLighthouse/Types/Activity/EventType.cs | 45 ++- .../Types/Activity/IEntityEventHandler.cs | 10 + .../Types/Entities/Activity/ActivityEntity.cs | 5 +- .../Activity/CommentActivityEntity.cs | 15 + .../Entities/Activity/CommentActivityEntry.cs | 9 - .../Activity/PlaylistActivityEntity.cs | 3 + .../Entities/Activity/ReviewActivityEntity.cs | 14 + .../Entities/Activity/UserActivityEntity.cs | 10 +- .../Types/Entities/Level/ReviewEntity.cs | 2 +- ProjectLighthouse/Types/Levels/Category.cs | 1 + .../Activity/Events/GameCommentEvent.cs | 55 ++++ .../Activity/Events/GameEvent.cs | 130 ++++++++ .../Activity/Events/GameHeartEvent.cs | 43 +++ .../Activity/Events/GamePhotoUploadEvent.cs | 45 +++ .../Activity/Events/GamePlayLevelEvent.cs | 26 ++ .../Activity/Events/GamePublishLevelEvent.cs | 31 ++ .../Activity/Events/GameReviewEvent.cs | 32 ++ .../Activity/Events/GameScoreEvent.cs | 39 +++ .../Activity/GameSlotStreamGroup.cs | 21 ++ .../Serialization/Activity/GameStream.cs | 127 ++++++++ .../Serialization/Activity/GameStreamGroup.cs | 80 +++++ .../Activity/GameUserStreamGroup.cs | 29 ++ .../{ => Comment}/CommentListResponse.cs | 2 +- .../{ => Comment}/GameComment.cs | 2 +- .../Serialization/{ => Photo}/GamePhoto.cs | 2 +- .../{ => Photo}/GamePhotoSubject.cs | 2 +- .../{ => Photo}/PhotoListResponse.cs | 2 +- .../Serialization/{ => Photo}/PhotoSlot.cs | 2 +- .../Serialization/{ => Playlist}/Author.cs | 2 +- .../{ => Playlist}/GamePlaylist.cs | 2 +- .../{ => Playlist}/GenericPlaylistResponse.cs | 2 +- .../Serialization/{ => Playlist}/IconList.cs | 2 +- .../{ => Playlist}/PlaylistResponse.cs | 2 +- .../Serialization/{ => Review}/GameReview.cs | 2 +- .../{ => Review}/ReviewResponse.cs | 2 +- .../Types/Serialization/Review/ReviewSlot.cs | 22 ++ .../Types/Serialization/ReviewSlot.cs | 14 - .../Serialization/{ => Score}/GameScore.cs | 2 +- .../{ => Score}/MultiScoreboardResponse.cs | 2 +- .../{ => Score}/ScoreboardResponse.cs | 2 +- .../{ => Slot}/CategoryListResponse.cs | 2 +- .../Serialization/{ => Slot}/GameCategory.cs | 2 +- .../{ => Slot}/GameDeveloperSlot.cs | 2 +- .../Serialization/{ => Slot}/GameUserSlot.cs | 4 +- .../{ => Slot}/GenericSlotResponse.cs | 2 +- .../{ => Slot}/PlanetStatsResponse.cs | 2 +- .../Serialization/{ => Slot}/SlotBase.cs | 5 +- .../{ => Slot}/SlotResourceResponse.cs | 2 +- .../{ => User}/FriendResponse.cs | 2 +- .../Serialization/{ => User}/GameUser.cs | 2 +- .../{ => User}/GenericUserResponse.cs | 2 +- .../{ => User}/MinimalUserListResponse.cs | 2 +- .../Serialization/{ => User}/NpHandle.cs | 2 +- .../{ => User}/UserListResponse.cs | 4 +- 86 files changed, 1542 insertions(+), 93 deletions(-) create mode 100644 ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs create mode 100644 ProjectLighthouse/Database/ActivityInterceptor.cs create mode 100644 ProjectLighthouse/Extensions/DateTimeExtensions.cs create mode 100644 ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs create mode 100644 ProjectLighthouse/Types/Activity/ActivityGroup.cs create mode 100644 ProjectLighthouse/Types/Activity/IEntityEventHandler.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs delete mode 100644 ProjectLighthouse/Types/Entities/Activity/CommentActivityEntry.cs create mode 100644 ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GamePlayLevelEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/GameSlotStreamGroup.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/GameStream.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/GameUserStreamGroup.cs rename ProjectLighthouse/Types/Serialization/{ => Comment}/CommentListResponse.cs (83%) rename ProjectLighthouse/Types/Serialization/{ => Comment}/GameComment.cs (97%) rename ProjectLighthouse/Types/Serialization/{ => Photo}/GamePhoto.cs (98%) rename ProjectLighthouse/Types/Serialization/{ => Photo}/GamePhotoSubject.cs (91%) rename ProjectLighthouse/Types/Serialization/{ => Photo}/PhotoListResponse.cs (83%) rename ProjectLighthouse/Types/Serialization/{ => Photo}/PhotoSlot.cs (88%) rename ProjectLighthouse/Types/Serialization/{ => Playlist}/Author.cs (80%) rename ProjectLighthouse/Types/Serialization/{ => Playlist}/GamePlaylist.cs (97%) rename ProjectLighthouse/Types/Serialization/{ => Playlist}/GenericPlaylistResponse.cs (93%) rename ProjectLighthouse/Types/Serialization/{ => Playlist}/IconList.cs (82%) rename ProjectLighthouse/Types/Serialization/{ => Playlist}/PlaylistResponse.cs (85%) rename ProjectLighthouse/Types/Serialization/{ => Review}/GameReview.cs (98%) rename ProjectLighthouse/Types/Serialization/{ => Review}/ReviewResponse.cs (90%) create mode 100644 ProjectLighthouse/Types/Serialization/Review/ReviewSlot.cs delete mode 100644 ProjectLighthouse/Types/Serialization/ReviewSlot.cs rename ProjectLighthouse/Types/Serialization/{ => Score}/GameScore.cs (95%) rename ProjectLighthouse/Types/Serialization/{ => Score}/MultiScoreboardResponse.cs (93%) rename ProjectLighthouse/Types/Serialization/{ => Score}/ScoreboardResponse.cs (94%) rename ProjectLighthouse/Types/Serialization/{ => Slot}/CategoryListResponse.cs (93%) rename ProjectLighthouse/Types/Serialization/{ => Slot}/GameCategory.cs (95%) rename ProjectLighthouse/Types/Serialization/{ => Slot}/GameDeveloperSlot.cs (96%) rename ProjectLighthouse/Types/Serialization/{ => Slot}/GameUserSlot.cs (98%) rename ProjectLighthouse/Types/Serialization/{ => Slot}/GenericSlotResponse.cs (95%) rename ProjectLighthouse/Types/Serialization/{ => Slot}/PlanetStatsResponse.cs (88%) rename ProjectLighthouse/Types/Serialization/{ => Slot}/SlotBase.cs (95%) rename ProjectLighthouse/Types/Serialization/{ => Slot}/SlotResourceResponse.cs (86%) rename ProjectLighthouse/Types/Serialization/{ => User}/FriendResponse.cs (85%) rename ProjectLighthouse/Types/Serialization/{ => User}/GameUser.cs (99%) rename ProjectLighthouse/Types/Serialization/{ => User}/GenericUserResponse.cs (95%) rename ProjectLighthouse/Types/Serialization/{ => User}/MinimalUserListResponse.cs (89%) rename ProjectLighthouse/Types/Serialization/{ => User}/NpHandle.cs (86%) rename ProjectLighthouse/Types/Serialization/{ => User}/UserListResponse.cs (89%) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs new file mode 100644 index 000000000..f78fda687 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -0,0 +1,286 @@ +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.StorableLists.Stores; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization.Activity; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; + +[ApiController] +[Authorize] +[Route("LITTLEBIGPLANETPS3_XML/stream")] +[Produces("text/xml")] +public class ActivityController : ControllerBase +{ + private readonly DatabaseContext database; + + public ActivityController(DatabaseContext database) + { + this.database = database; + } + + public class ActivityDto + { + public required ActivityEntity Activity { get; set; } + public int? TargetSlotId { get; set; } + public int? TargetUserId { get; set; } + public int? TargetPlaylistId { get; set; } + public int? SlotCreatorId { get; set; } + } + //TODO refactor this mess into a separate db file or something + + private static Expression> ActivityToDto() + { + return a => new ActivityDto + { + Activity = a, + TargetSlotId = a is LevelActivityEntity + ? ((LevelActivityEntity)a).SlotId + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.PhotoId != 0 + ? ((PhotoActivityEntity)a).Photo.SlotId + : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level + ? ((CommentActivityEntity)a).Comment.TargetId + : a is ScoreActivityEntity + ? ((ScoreActivityEntity)a).Score.SlotId + : 0, + + TargetUserId = a is UserActivityEntity + ? ((UserActivityEntity)a).TargetUserId + : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile + ? ((CommentActivityEntity)a).Comment.TargetId + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 + ? ((PhotoActivityEntity)a).Photo.CreatorId + : 0, + TargetPlaylistId = a is PlaylistActivityEntity ? ((PlaylistActivityEntity)a).PlaylistId : 0, + }; + } + + private static IQueryable> GroupActivities + (IQueryable activityQuery) + { + return activityQuery.Select(ActivityToDto()) + .GroupBy(dto => new ActivityGroup + { + Timestamp = dto.Activity.Timestamp.Date, + UserId = dto.Activity.UserId, + TargetUserId = dto.TargetUserId, + TargetSlotId = dto.TargetSlotId, + TargetPlaylistId = dto.TargetPlaylistId, + }, + dto => dto.Activity); + } + + private static IQueryable> GroupActivities + (IQueryable activityQuery) + { + return activityQuery.GroupBy(dto => new ActivityGroup + { + Timestamp = dto.Activity.Timestamp.Date, + UserId = dto.Activity.UserId, + TargetUserId = dto.TargetUserId, + TargetSlotId = dto.TargetSlotId, + TargetPlaylistId = dto.TargetPlaylistId, + }, + dto => dto.Activity); + } + + // TODO this is kinda ass, can maybe improve once comment migration is merged + private async Task> GetFilters + ( + GameTokenEntity token, + bool excludeNews, + bool excludeMyLevels, + bool excludeFriends, + bool excludeFavouriteUsers, + bool excludeMyself + ) + { + IQueryable query = this.database.Activities.AsQueryable(); + if (excludeNews) query = query.Where(a => a.Type != EventType.NewsPost); + + IQueryable dtoQuery = query.Select(a => new ActivityDto + { + Activity = a, + SlotCreatorId = a is LevelActivityEntity + ? ((LevelActivityEntity)a).Slot.CreatorId + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 + ? ((PhotoActivityEntity)a).Photo.Slot!.CreatorId + : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level + ? ((CommentActivityEntity)a).Comment.TargetId + : a is ScoreActivityEntity + ? ((ScoreActivityEntity)a).Score.Slot.CreatorId + : 0, + }); + + Expression> predicate = PredicateExtensions.False(); + + predicate = predicate.Or(a => a.SlotCreatorId == 0 || excludeMyLevels + ? a.SlotCreatorId != token.UserId + : a.SlotCreatorId == token.UserId); + + List? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds; + if (friendIds != null) + { + predicate = excludeFriends + ? predicate.Or(a => !friendIds.Contains(a.Activity.UserId)) + : predicate.Or(a => friendIds.Contains(a.Activity.UserId)); + } + + List favouriteUsers = await this.database.HeartedProfiles.Where(hp => hp.UserId == token.UserId) + .Select(hp => hp.HeartedUserId) + .ToListAsync(); + + predicate = excludeFavouriteUsers + ? predicate.Or(a => !favouriteUsers.Contains(a.Activity.UserId)) + : predicate.Or(a => favouriteUsers.Contains(a.Activity.UserId)); + + predicate = excludeMyself + ? predicate.Or(a => a.Activity.UserId != token.UserId) + : predicate.Or(a => a.Activity.UserId == token.UserId); + + query = dtoQuery.Where(predicate).Select(dto => dto.Activity); + + return query.OrderByDescending(a => a.Timestamp); + } + + public Task GetMostRecentEventTime(GameTokenEntity token, DateTime upperBound) + { + return this.database.Activities.Where(a => a.UserId == token.UserId) + .Where(a => a.Timestamp < upperBound) + .OrderByDescending(a => a.Timestamp) + .Select(a => a.Timestamp) + .FirstOrDefaultAsync(); + } + + [HttpGet] + public async Task GlobalActivity + ( + long timestamp, + bool excludeNews, + bool excludeMyLevels, + bool excludeFriends, + bool excludeFavouriteUsers, + bool excludeMyself + ) + { + GameTokenEntity token = this.GetToken(); + + if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest(); + + if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis; + + DateTime start = DateTimeExtensions.FromUnixTimeMilliseconds(timestamp); + + DateTime soonestTime = await this.GetMostRecentEventTime(token, start); + Console.WriteLine(@"Most recent event occurred at " + soonestTime); + soonestTime = soonestTime.Subtract(TimeSpan.FromDays(1)); + + long soonestTimestamp = soonestTime.ToUnixTimeMilliseconds(); + + long endTimestamp = soonestTimestamp - 86_400_000; + + Console.WriteLine(@$"soonestTime: {soonestTimestamp}, endTime: {endTimestamp}"); + + IQueryable activityEvents = await this.GetFilters(token, + excludeNews, + excludeMyLevels, + excludeFriends, + excludeFavouriteUsers, + excludeMyself); + + DateTime end = DateTimeExtensions.FromUnixTimeMilliseconds(endTimestamp); + + activityEvents = activityEvents.Where(a => a.Timestamp < start && a.Timestamp > end); + + Console.WriteLine($@"start: {start}, end: {end}"); + + List> groups = await GroupActivities(activityEvents).ToListAsync(); + + foreach (IGrouping group in groups) + { + ActivityGroup key = group.Key; + Console.WriteLine( + $@"{key.GroupType}: Timestamp: {key.Timestamp}, UserId: {key.UserId}, TargetSlotId: {key.TargetSlotId}, " + + @$"TargetUserId: {key.TargetUserId}, TargetPlaylistId: {key.TargetPlaylistId}"); + foreach (ActivityEntity activity in group) + { + Console.WriteLine($@" {activity.Type}: Timestamp: {activity.Timestamp}"); + } + } + + DateTime oldestTime = groups.Any() ? groups.Min(g => g.Any() ? g.Min(a => a.Timestamp) : end) : end; + long oldestTimestamp = oldestTime.ToUnixTimeMilliseconds(); + + return this.Ok(await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp)); + } + + [HttpGet("slot/{slotType}/{slotId:int}")] + public async Task SlotActivity(string slotType, int slotId, long timestamp) + { + GameTokenEntity token = this.GetToken(); + + if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest(); + + if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis; + + long endTimestamp = timestamp - 864_000; + + if (slotType is not ("developer" or "user")) return this.BadRequest(); + + if (slotType == "developer") + slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); + + IQueryable slotActivity = this.database.Activities.Select(ActivityToDto()) + .Where(a => a.TargetSlotId == slotId); + + DateTime start = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime; + DateTime end = DateTimeOffset.FromUnixTimeMilliseconds(endTimestamp).DateTime; + + slotActivity = slotActivity.Where(a => a.Activity.Timestamp < start && a.Activity.Timestamp > end); + + List> groups = await GroupActivities(slotActivity).ToListAsync(); + + DateTime oldestTime = groups.Max(g => g.Max(a => a.Timestamp)); + long oldestTimestamp = new DateTimeOffset(oldestTime).ToUnixTimeMilliseconds(); + + return this.Ok(await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp)); + } + + [HttpGet("user2/{userId:int}/")] + public async Task UserActivity(int userId, long timestamp) + { + GameTokenEntity token = this.GetToken(); + + if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest(); + + if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis; + + long endTimestamp = timestamp - 864_000; + + IQueryable userActivity = this.database.Activities.Select(ActivityToDto()) + .Where(a => a.TargetUserId == userId); + + DateTime start = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime; + DateTime end = DateTimeOffset.FromUnixTimeMilliseconds(endTimestamp).DateTime; + + userActivity = userActivity.Where(a => a.Activity.Timestamp < start && a.Activity.Timestamp > end); + + List> groups = await GroupActivities(userActivity).ToListAsync(); + + DateTime oldestTime = groups.Max(g => g.Max(a => a.Timestamp)); + long oldestTimestamp = new DateTimeOffset(oldestTime).ToUnixTimeMilliseconds(); + + return this.Ok( + await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs index 76f2b715b..ae1704996 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Comment; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -44,6 +44,20 @@ public async Task RateComment([FromQuery] int commentId, [FromQue return this.Ok(); } + [HttpGet("userComment/{username}")] + [HttpGet("comment/{slotType}/{slotId:int}")] + public async Task GetSingleComment(string? username, string? slotType, int? slotId, int commentId) + { + GameTokenEntity token = this.GetToken(); + + if (username == null == (SlotHelper.IsTypeInvalid(slotType) || slotId == null)) return this.BadRequest(); + + CommentEntity? comment = await this.database.Comments.FindAsync(commentId); + if (comment == null) return this.NotFound(); + + return this.Ok(GameComment.CreateFromEntity(comment, token.UserId)); + } + [HttpGet("comments/{slotType}/{slotId:int}")] [HttpGet("userComments/{username}")] public async Task GetComments(string? username, string? slotType, int slotId) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs index 83e198d8e..a0381f465 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.StorableLists.Stores; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index e79626439..07a80775a 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -12,7 +12,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Photo; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs index 60ac1c580..b9a36852d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs @@ -13,6 +13,9 @@ using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Misc; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs index 67fff1804..3db2016b1 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs @@ -9,7 +9,9 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs index 4763708a5..46e8b90e7 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs @@ -4,7 +4,8 @@ using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs index e09a06e65..9a013a9e1 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Resources; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index aa4e1c5b9..c32df3384 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index 79ae31632..841cef389 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Score; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs index a4f129b5f..064af5ac9 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs @@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs index 13f9afa8a..d0930e0af 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -13,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs index 3eb828e47..d45c1ff0d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Filter.Filters; using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs index fb5b6128f..8caa0e141 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs @@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs index bef6943ee..ee09930ba 100644 --- a/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs +++ b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs @@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs index 00ed79ce2..a72123068 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs @@ -5,6 +5,8 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs index 39d6986dd..5ed58a06b 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs @@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs index d9826a923..bd188d7aa 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs @@ -5,6 +5,8 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs b/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs index 782b39919..cb2356554 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs @@ -1,7 +1,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml index 70b98e64a..66fea3d9e 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml @@ -3,7 +3,7 @@ @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @using LBPUnion.ProjectLighthouse.Types.Entities.Profile @using LBPUnion.ProjectLighthouse.Types.Levels -@using LBPUnion.ProjectLighthouse.Types.Serialization +@using LBPUnion.ProjectLighthouse.Types.Serialization.Photo @model LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity @{ diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml index 610342658..18c3d0607 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml @@ -3,7 +3,7 @@ @using LBPUnion.ProjectLighthouse.Files @using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Types.Entities.Level -@using LBPUnion.ProjectLighthouse.Types.Serialization +@using LBPUnion.ProjectLighthouse.Types.Serialization.Review @{ bool isMobile = (bool?)ViewData["IsMobile"] ?? false; diff --git a/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs index eacce2332..e448f2db8 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Tests.Integration; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Xunit; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs index 7408fcf59..66e5f6d6d 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs index 5335b2cf8..42049a31c 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs index e7228b2cf..8c76f0b01 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/ProjectLighthouse.Tests/Unit/LocationTests.cs b/ProjectLighthouse.Tests/Unit/LocationTests.cs index a97b80af1..ec9c80ca5 100644 --- a/ProjectLighthouse.Tests/Unit/LocationTests.cs +++ b/ProjectLighthouse.Tests/Unit/LocationTests.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Xunit; namespace LBPUnion.ProjectLighthouse.Tests.Unit; diff --git a/ProjectLighthouse.Tests/Unit/PaginationTests.cs b/ProjectLighthouse.Tests/Unit/PaginationTests.cs index de591d94d..ecf4eb9a4 100644 --- a/ProjectLighthouse.Tests/Unit/PaginationTests.cs +++ b/ProjectLighthouse.Tests/Unit/PaginationTests.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Filter; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Xunit; diff --git a/ProjectLighthouse/Database/ActivityInterceptor.cs b/ProjectLighthouse/Database/ActivityInterceptor.cs new file mode 100644 index 000000000..ca5d66f53 --- /dev/null +++ b/ProjectLighthouse/Database/ActivityInterceptor.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace LBPUnion.ProjectLighthouse.Database; + +public class ActivityInterceptor : SaveChangesInterceptor +{ + private class CustomTrackedEntity + { + public required EntityState State { get; init; } + public required object Entity { get; init; } + public required object OldEntity { get; init; } + } + + private readonly ConcurrentDictionary<(Type Type, int HashCode), CustomTrackedEntity> unsavedEntities; + private readonly IEntityEventHandler eventHandler; + + public ActivityInterceptor(IEntityEventHandler eventHandler) + { + this.eventHandler = eventHandler; + this.unsavedEntities = new ConcurrentDictionary<(Type Type, int HashCode), CustomTrackedEntity>(); + } + + #region Hooking stuff + + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + this.SaveNewEntities(eventData); + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync + (DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = new()) + { + this.SaveNewEntities(eventData); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) + { + this.ParseInsertedEntities(eventData); + return base.SavedChanges(eventData, result); + } + + public override ValueTask SavedChangesAsync + (SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = new()) + { + this.ParseInsertedEntities(eventData); + return base.SavedChangesAsync(eventData, result, cancellationToken); + } + + #endregion + + private void SaveNewEntities(DbContextEventData eventData) + { + if (eventData.Context == null) return; + + DbContext context = eventData.Context; + + this.unsavedEntities.Clear(); + + foreach (EntityEntry entry in context.ChangeTracker.Entries()) + { + // Ignore activities + if (entry.Metadata.BaseType?.ClrType == typeof(ActivityEntity) || entry.Metadata.ClrType == typeof(LastContactEntity)) continue; + + // Ignore tokens + if (entry.Metadata.Name.Contains("Token")) continue; + + if (entry.State is not (EntityState.Added or EntityState.Deleted or EntityState.Modified)) continue; + + this.unsavedEntities.TryAdd((entry.Entity.GetType(), entry.Entity.GetHashCode()), + new CustomTrackedEntity + { + State = entry.State, + Entity = entry.Entity, + OldEntity = entry.OriginalValues.ToObject(), + }); + } + } + + private void ParseInsertedEntities(DbContextEventData eventData) + { + if (eventData.Context is not DatabaseContext context) return; + + HashSet entities = new(); + + List entries = context.ChangeTracker.Entries().ToList(); + + foreach (KeyValuePair<(Type Type, int HashCode), CustomTrackedEntity> kvp in this.unsavedEntities) + { + EntityEntry entry = entries.FirstOrDefault(e => + e.Metadata.ClrType == kvp.Key.Type && e.Entity.GetHashCode() == kvp.Key.HashCode); + switch (kvp.Value.State) + { + case EntityState.Added: + case EntityState.Modified: + if (entry != null) entities.Add(kvp.Value); + break; + case EntityState.Deleted: + if (entry == null) entities.Add(kvp.Value); + break; + case EntityState.Detached: + case EntityState.Unchanged: + default: + break; + } + } + + foreach (CustomTrackedEntity entity in entities) + { + switch (entity.State) + { + case EntityState.Added: + this.eventHandler.OnEntityInserted(context, entity.Entity); + break; + case EntityState.Deleted: + this.eventHandler.OnEntityDeleted(context, entity.Entity); + break; + case EntityState.Modified: + this.eventHandler.OnEntityChanged(context, entity.OldEntity, entity.Entity); + break; + case EntityState.Detached: + case EntityState.Unchanged: + default: + continue; + } + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Database/DatabaseContext.cs b/ProjectLighthouse/Database/DatabaseContext.cs index 699fc2f7f..463945296 100644 --- a/ProjectLighthouse/Database/DatabaseContext.cs +++ b/ProjectLighthouse/Database/DatabaseContext.cs @@ -1,4 +1,5 @@ using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; @@ -86,16 +87,6 @@ protected internal DatabaseContext() public DatabaseContext(DbContextOptions options) : base(options) { } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().UseTpcMappingStrategy(); - modelBuilder.Entity().UseTpcMappingStrategy(); - modelBuilder.Entity().UseTpcMappingStrategy(); - modelBuilder.Entity().UseTpcMappingStrategy(); - modelBuilder.Entity().UseTpcMappingStrategy(); - base.OnModelCreating(modelBuilder); - } - public static DatabaseContext CreateNewInstance() { DbContextOptionsBuilder builder = new(); @@ -103,4 +94,26 @@ public static DatabaseContext CreateNewInstance() MySqlServerVersion.LatestSupportedServerVersion); return new DatabaseContext(builder.Options); } + + #region Activity + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + //TODO implement reviews + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + base.OnModelCreating(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.AddInterceptors(new ActivityInterceptor(new ActivityEntityEventHandler())); + base.OnConfiguring(optionsBuilder); + } + #endregion } \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/DateTimeExtensions.cs b/ProjectLighthouse/Extensions/DateTimeExtensions.cs new file mode 100644 index 000000000..6cb037596 --- /dev/null +++ b/ProjectLighthouse/Extensions/DateTimeExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace LBPUnion.ProjectLighthouse.Extensions; + +public static class DateTimeExtensions +{ + public static long ToUnixTimeMilliseconds(this DateTime dateTime) => + new DateTimeOffset(dateTime).ToUniversalTime().ToUnixTimeMilliseconds(); + + public static DateTime FromUnixTimeMilliseconds(long timestamp) => + DateTimeOffset.FromUnixTimeMilliseconds(timestamp).ToUniversalTime().DateTime; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs new file mode 100644 index 000000000..60caca08d --- /dev/null +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -0,0 +1,179 @@ +#nullable enable +using System; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Reflection; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Levels; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +//TODO implement missing event triggers +public class ActivityEntityEventHandler : IEntityEventHandler +{ + public void OnEntityInserted(DatabaseContext database, T entity) where T : class + { + Console.WriteLine($@"OnEntityInserted: {entity.GetType().Name}"); + ActivityEntity? activity = entity switch + { + SlotEntity slot => new LevelActivityEntity + { + Type = EventType.PublishLevel, + SlotId = slot.SlotId, + UserId = slot.CreatorId, + }, + CommentEntity comment => new CommentActivityEntity + { + Type = comment.Type == CommentType.Level ? EventType.CommentOnLevel : EventType.CommentOnUser, + CommentId = comment.CommentId, + UserId = comment.PosterUserId, + }, + PhotoEntity photo => new PhotoActivityEntity + { + Type = EventType.UploadPhoto, + PhotoId = photo.PhotoId, + UserId = photo.CreatorId, + }, + ScoreEntity score => new ScoreActivityEntity + { + Type = EventType.Score, + ScoreId = score.ScoreId, + //TODO merge score migration + // UserId = int.Parse(score.PlayerIds[0]), + }, + HeartedLevelEntity heartedLevel => new LevelActivityEntity + { + Type = EventType.HeartLevel, + SlotId = heartedLevel.SlotId, + UserId = heartedLevel.UserId, + }, + HeartedProfileEntity heartedProfile => new UserActivityEntity + { + Type = EventType.HeartUser, + TargetUserId = heartedProfile.HeartedUserId, + UserId = heartedProfile.UserId, + }, + VisitedLevelEntity visitedLevel => new LevelActivityEntity + { + Type = EventType.PlayLevel, + SlotId = visitedLevel.SlotId, + UserId = visitedLevel.UserId, + }, + _ => null, + }; + InsertActivity(database, activity); + } + + private static void InsertActivity(DatabaseContext database, ActivityEntity? activity) + { + if (activity == null) return; + + Console.WriteLine("Inserting activity: " + activity.GetType().Name); + + activity.Timestamp = DateTime.UtcNow; + database.Activities.Add(activity); + database.SaveChanges(); + } + + public void OnEntityChanged(DatabaseContext database, T origEntity, T currentEntity) where T : class + { + foreach (PropertyInfo propInfo in currentEntity.GetType().GetProperties()) + { + if (!propInfo.CanRead || !propInfo.CanWrite) continue; + + if (propInfo.CustomAttributes.Any(c => c.AttributeType == typeof(NotMappedAttribute))) continue; + + object? origVal = propInfo.GetValue(origEntity); + object? newVal = propInfo.GetValue(currentEntity); + if ((origVal == null && newVal == null) || (origVal != null && newVal != null && origVal.Equals(newVal))) + continue; + + Console.WriteLine($@"Value for {propInfo.Name} changed"); + Console.WriteLine($@"Orig val: {origVal?.ToString() ?? "null"}"); + Console.WriteLine($@"New val: {newVal?.ToString() ?? "null"}"); + } + + Console.WriteLine($@"OnEntityChanged: {currentEntity.GetType().Name}"); + ActivityEntity? activity = null; + switch (currentEntity) + { + case VisitedLevelEntity visitedLevel: + { + if (origEntity is not VisitedLevelEntity) break; + + activity = new LevelActivityEntity + { + Type = EventType.PlayLevel, + SlotId = visitedLevel.SlotId, + UserId = visitedLevel.UserId, + }; + break; + } + case SlotEntity slotEntity: + { + if (origEntity is not SlotEntity oldSlotEntity) break; + + if (!oldSlotEntity.TeamPick && slotEntity.TeamPick) + { + activity = new LevelActivityEntity + { + Type = EventType.MMPickLevel, + SlotId = slotEntity.SlotId, + UserId = SlotHelper.GetPlaceholderUserId(database).Result, + }; + } + else if (oldSlotEntity.SlotId == slotEntity.SlotId && slotEntity.Type == SlotType.User) + { + activity = new LevelActivityEntity + { + Type = EventType.PublishLevel, + SlotId = slotEntity.SlotId, + UserId = slotEntity.CreatorId, + }; + } + + break; + } + } + + InsertActivity(database, activity); + } + + public void OnEntityDeleted(DatabaseContext database, T entity) where T : class + { + Console.WriteLine($@"OnEntityDeleted: {entity.GetType().Name}"); + ActivityEntity? activity = entity switch + { + //TODO move this to EntityModified and use CommentEntity.Deleted + CommentEntity comment => comment.Type switch + { + CommentType.Level => new CommentActivityEntity + { + Type = EventType.DeleteLevelComment, + CommentId = comment.CommentId, + UserId = comment.PosterUserId, + }, + _ => null, + }, + HeartedLevelEntity heartedLevel => new LevelActivityEntity + { + Type = EventType.UnheartLevel, + SlotId = heartedLevel.SlotId, + UserId = heartedLevel.UserId, + }, + HeartedProfileEntity heartedProfile => new UserActivityEntity + { + Type = EventType.UnheartUser, + TargetUserId = heartedProfile.HeartedUserId, + UserId = heartedProfile.UserId, + }, + _ => null, + }; + InsertActivity(database, activity); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityGroup.cs b/ProjectLighthouse/Types/Activity/ActivityGroup.cs new file mode 100644 index 000000000..61ae381c3 --- /dev/null +++ b/ProjectLighthouse/Types/Activity/ActivityGroup.cs @@ -0,0 +1,41 @@ +using System; +using System.Xml.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +public class ActivityGroup +{ + public DateTime Timestamp { get; set; } + public int UserId { get; set; } + public int? TargetSlotId { get; set; } + public int? TargetUserId { get; set; } + public int? TargetPlaylistId { get; set; } + + public int TargetId => + this.GroupType switch + { + ActivityGroupType.User => this.TargetUserId ?? 0, + ActivityGroupType.Level => this.TargetSlotId ?? 0, + ActivityGroupType.Playlist => this.TargetPlaylistId ?? 0, + _ => this.UserId, + }; + + public ActivityGroupType GroupType => + this.TargetSlotId != 0 + ? ActivityGroupType.Level + : this.TargetUserId != 0 + ? ActivityGroupType.User + : ActivityGroupType.Playlist; +} + +public enum ActivityGroupType +{ + [XmlEnum("user")] + User, + + [XmlEnum("slot")] + Level, + + [XmlEnum("playlist")] + Playlist, +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/EventType.cs b/ProjectLighthouse/Types/Activity/EventType.cs index 7e0c74d75..e4ec2e9ac 100644 --- a/ProjectLighthouse/Types/Activity/EventType.cs +++ b/ProjectLighthouse/Types/Activity/EventType.cs @@ -1,26 +1,69 @@ -namespace LBPUnion.ProjectLighthouse.Types.Activity; +using System.Xml.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; public enum EventType { + [XmlEnum("heart_level")] HeartLevel, + + [XmlEnum("unheart_level")] UnheartLevel, + + [XmlEnum("heart_user")] HeartUser, + + [XmlEnum("unheart_user")] UnheartUser, + + [XmlEnum("play_level")] PlayLevel, + + [XmlEnum("rate_level")] RateLevel, + + [XmlEnum("tag_level")] TagLevel, + + [XmlEnum("comment_on_level")] CommentOnLevel, + + [XmlEnum("delete_level_comment")] DeleteLevelComment, + + [XmlEnum("upload_photo")] UploadPhoto, + + [XmlEnum("publish_level")] PublishLevel, + + [XmlEnum("unpublish_level")] UnpublishLevel, + + [XmlEnum("score")] Score, + + [XmlEnum("news_post")] NewsPost, + + [XmlEnum("mm_pick_level")] MMPickLevel, + + [XmlEnum("dpad_rate_level")] DpadRateLevel, + + [XmlEnum("review_level")] ReviewLevel, + + [XmlEnum("comment_on_user")] CommentOnUser, + + [XmlEnum("create_playlist")] CreatePlaylist, + + [XmlEnum("heart_playlist")] HeartPlaylist, + + [XmlEnum("add_level_to_playlist")] AddLevelToPlaylist, } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/IEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/IEntityEventHandler.cs new file mode 100644 index 000000000..982857ce1 --- /dev/null +++ b/ProjectLighthouse/Types/Activity/IEntityEventHandler.cs @@ -0,0 +1,10 @@ +using LBPUnion.ProjectLighthouse.Database; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +public interface IEntityEventHandler +{ + public void OnEntityInserted(DatabaseContext database, T entity) where T : class; + public void OnEntityChanged(DatabaseContext database, T origEntity, T currentEntity) where T : class; + public void OnEntityDeleted(DatabaseContext database, T entity) where T : class; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs index cdb81944f..7a1c5bf19 100644 --- a/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; @@ -10,7 +11,7 @@ public class ActivityEntity [Key] public int ActivityId { get; set; } - public long Timestamp { get; set; } + public DateTime Timestamp { get; set; } public int UserId { get; set; } diff --git a/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs new file mode 100644 index 000000000..0c20175e7 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: CommentOnUser, CommentOnLevel, DeleteLevelComment +/// +public class CommentActivityEntity : ActivityEntity +{ + public int CommentId { get; set; } + + [ForeignKey(nameof(CommentId))] + public CommentEntity Comment { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntry.cs b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntry.cs deleted file mode 100644 index 4aa02e0d3..000000000 --- a/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntry.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; - -/// -/// Supported event types: CommentOnUser, CommentOnLevel, DeleteLevelComment -/// -public class CommentActivityEntry -{ - -} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs index 502baf4b2..fecf6b803 100644 --- a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs @@ -3,6 +3,9 @@ namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; +/// +/// Supported event types: CreatePlaylist, HeartPlaylist, AddLevelToPlaylist +/// public class PlaylistActivityEntity : ActivityEntity { public int PlaylistId { get; set; } diff --git a/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs new file mode 100644 index 000000000..5c2950331 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +public class ReviewActivityEntity : ActivityEntity +{ + public int ReviewId { get; set; } + + [ForeignKey(nameof(ReviewId))] + public ReviewEntity Review { get; set; } + + // TODO review_modified? +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs index 748563679..89d27e8b2 100644 --- a/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs @@ -1,9 +1,15 @@ -namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// /// Supported event types: HeartUser, UnheartUser /// public class UserActivityEntity : ActivityEntity { - + public int TargetUserId { get; set; } + + [ForeignKey(nameof(TargetUserId))] + public UserEntity TargetUser { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs b/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs index 59091953c..c6f18240b 100644 --- a/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs +++ b/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; namespace LBPUnion.ProjectLighthouse.Types.Entities.Level; diff --git a/ProjectLighthouse/Types/Levels/Category.cs b/ProjectLighthouse/Types/Levels/Category.cs index c18a4a00a..1b3c08de5 100644 --- a/ProjectLighthouse/Types/Levels/Category.cs +++ b/ProjectLighthouse/Types/Levels/Category.cs @@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; namespace LBPUnion.ProjectLighthouse.Types.Levels; diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs new file mode 100644 index 000000000..93d382576 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +[XmlInclude(typeof(GameUserCommentEvent))] +[XmlInclude(typeof(GameSlotCommentEvent))] +public class GameCommentEvent : GameEvent +{ + [XmlElement("comment_id")] + public int CommentId { get; set; } +} + +public class GameUserCommentEvent : GameCommentEvent +{ + [XmlElement("object_user")] + public string TargetUsername { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + CommentEntity comment = await database.Comments.FindAsync(this.CommentId); + if (comment == null) return; + + UserEntity user = await database.Users.FindAsync(comment.TargetId); + if (user == null) return; + + this.TargetUsername = user.Username; + } +} + +public class GameSlotCommentEvent : GameCommentEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot TargetSlot { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + CommentEntity comment = await database.Comments.FindAsync(this.CommentId); + if (comment == null) return; + + SlotEntity slot = await database.Slots.FindAsync(comment.TargetId); + + if (slot == null) return; + + this.TargetSlot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs new file mode 100644 index 000000000..db9d6dcf5 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +[XmlInclude(typeof(GameCommentEvent))] +[XmlInclude(typeof(GamePhotoUploadEvent))] +[XmlInclude(typeof(GamePlayLevelEvent))] +[XmlInclude(typeof(GameReviewEvent))] +[XmlInclude(typeof(GameScoreEvent))] +[XmlInclude(typeof(GameHeartLevelEvent))] +[XmlInclude(typeof(GameHeartUserEvent))] +public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization +{ + [XmlIgnore] + private int UserId { get; set; } + + [XmlAttribute("type")] + public EventType Type { get; set; } + + [XmlElement("timestamp")] + public long Timestamp { get; set; } + + [XmlElement("actor")] + public string Username { get; set; } + + protected async Task PrepareSerialization(DatabaseContext database) + { + Console.WriteLine($@"SERIALIZATION!! {this.UserId} - {this.GetHashCode()}"); + UserEntity user = await database.Users.FindAsync(this.UserId); + if (user == null) return; + this.Username = user.Username; + } + + public static IEnumerable CreateFromActivityGroups(IGrouping group) + { + List events = new(); + + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + // Events with Count need special treatment + switch (group.Key) + { + case EventType.PlayLevel: + { + if (group.First() is not LevelActivityEntity levelActivity) break; + + events.Add(new GamePlayLevelEvent + { + Slot = new ReviewSlot + { + SlotId = levelActivity.SlotId, + }, + Count = group.Count(), + UserId = levelActivity.UserId, + Timestamp = levelActivity.Timestamp.ToUnixTimeMilliseconds(), + Type = levelActivity.Type, + }); + break; + } + case EventType.PublishLevel: + { + if (group.First() is not LevelActivityEntity levelActivity) break; + + events.Add(new GamePublishLevelEvent + { + Slot = new ReviewSlot + { + SlotId = levelActivity.SlotId, + }, + Count = group.Count(), + UserId = levelActivity.UserId, + Timestamp = levelActivity.Timestamp.ToUnixTimeMilliseconds(), + Type = levelActivity.Type, + }); + break; + } + // Everything else can be handled as normal + default: events.AddRange(group.Select(CreateFromActivity)); + break; + } + return events.AsEnumerable(); + } + + private static GameEvent CreateFromActivity(ActivityEntity activity) + { + GameEvent gameEvent = activity.Type switch + { + EventType.PlayLevel => new GamePlayLevelEvent + { + Slot = new ReviewSlot + { + SlotId = ((LevelActivityEntity)activity).SlotId, + }, + }, + EventType.CommentOnLevel => new GameSlotCommentEvent + { + CommentId = ((CommentActivityEntity)activity).CommentId, + }, + EventType.CommentOnUser => new GameUserCommentEvent + { + CommentId = ((CommentActivityEntity)activity).CommentId, + }, + EventType.HeartUser or EventType.UnheartUser => new GameHeartUserEvent + { + TargetUserId = ((UserActivityEntity)activity).TargetUserId, + }, + EventType.HeartLevel or EventType.UnheartLevel => new GameHeartLevelEvent + { + TargetSlot = new ReviewSlot + { + SlotId = ((LevelActivityEntity)activity).SlotId, + }, + }, + _ => new GameEvent(), + }; + gameEvent.UserId = activity.UserId; + gameEvent.Type = activity.Type; + gameEvent.Timestamp = activity.Timestamp.ToUnixTimeMilliseconds(); + return gameEvent; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs new file mode 100644 index 000000000..251d3f920 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameHeartUserEvent : GameEvent +{ + [XmlIgnore] + public int TargetUserId { get; set; } + + [XmlElement("object_user")] + public string TargetUsername { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + UserEntity targetUser = await database.Users.FindAsync(this.TargetUserId); + if (targetUser == null) return; + + this.TargetUsername = targetUser.Username; + } +} + +public class GameHeartLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot TargetSlot { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.TargetSlot.SlotId); + if (slot == null) return; + + this.TargetSlot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs new file mode 100644 index 000000000..527c9c7bf --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GamePhotoUploadEvent : GameEvent +{ + [XmlElement("photo_id")] + public int PhotoId { get; set; } + + [XmlElement("object_slot_id")] + [DefaultValue(null)] + public ReviewSlot SlotId { get; set; } + + [XmlElement("user_in_photo")] + public List PhotoParticipants { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + PhotoEntity photo = await database.Photos.Where(p => p.PhotoId == this.PhotoId) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User) + .FirstOrDefaultAsync(); + if (photo == null) return; + + this.PhotoParticipants = photo.PhotoSubjects.Select(ps => ps.User.Username).ToList(); + + if (photo.SlotId == null) return; + + SlotEntity slot = await database.Slots.FindAsync(photo.SlotId); + if (slot == null) return; + + this.SlotId = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePlayLevelEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePlayLevelEvent.cs new file mode 100644 index 000000000..af00048c5 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePlayLevelEvent.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GamePlayLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("count")] + public int Count { get; set; } = 1; + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs new file mode 100644 index 000000000..74b35b7fe --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GamePublishLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("republish")] + public bool IsRepublish { get; set; } + + [XmlElement("count")] + public int Count { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + // TODO does this work? + this.IsRepublish = slot.LastUpdated == slot.FirstUploaded; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs new file mode 100644 index 000000000..e089b9875 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs @@ -0,0 +1,32 @@ +using System.ComponentModel; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameReviewEvent : GameEvent +{ + [XmlElement("slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("review_id")] + public int ReviewId { get; set; } + + [XmlElement("review_modified")] + [DefaultValue(0)] + public long ReviewTimestamp { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + ReviewEntity review = await database.Reviews.FindAsync(this.ReviewId); + if (review == null) return; + + SlotEntity slot = await database.Slots.FindAsync(review.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs new file mode 100644 index 000000000..bed5c1acf --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameScoreEvent : GameEvent +{ + [XmlIgnore] + public int ScoreId { get; set; } + + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("score")] + public int Score { get; set; } + + [XmlElement("user_count")] + public int UserCount { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + ScoreEntity score = await database.Scores.FindAsync(this.ScoreId); + if (score == null) return; + + SlotEntity slot = await database.Slots.FindAsync(score.SlotId); + if (slot == null) return; + + this.Score = score.Points; + //TODO is this correct? + this.UserCount = score.Type; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameSlotStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GameSlotStreamGroup.cs new file mode 100644 index 000000000..9c145fb50 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameSlotStreamGroup.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +public class GameSlotStreamGroup : GameStreamGroup, INeedsPreparationForSerialization +{ + [XmlElement("slot_id")] + public ReviewSlot Slot { get; set; } + + public async Task PrepareSerialization(DatabaseContext database) + { + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs new file mode 100644 index 000000000..95dd7929d --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +/// +/// The global stream object, contains all +/// +[XmlRoot("stream")] +public class GameStream : ILbpSerializable, INeedsPreparationForSerialization +{ + [XmlIgnore] + private List SlotIds { get; set; } + + [XmlIgnore] + private List UserIds { get; set; } + + [XmlIgnore] + private int TargetUserId { get; set; } + + [XmlIgnore] + private GameVersion TargetGame { get; set; } + + [XmlElement("start_timestamp")] + public long StartTimestamp { get; set; } + + [XmlElement("end_timestamp")] + public long EndTimestamp { get; set; } + + [XmlArray("groups")] + [XmlArrayItem("group")] + public List Groups { get; set; } + + [XmlArray("slots")] + [XmlArrayItem("slot")] + public List Slots { get; set; } + + [XmlArray("users")] + [XmlArrayItem("user")] + public List Users { get; set; } + + [XmlArray("news")] + [XmlArrayItem("item")] + public List News { get; set; } + //TODO implement lbp1 and lbp2 news objects + + public async Task PrepareSerialization(DatabaseContext database) + { + if (this.SlotIds.Count > 0) + { + this.Slots = new List(); + foreach (int slotId in this.SlotIds) + { + SlotEntity slot = await database.Slots.FindAsync(slotId); + if (slot == null) continue; + + this.Slots.Add(SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId)); + } + } + + if (this.UserIds.Count > 0) + { + this.Users = new List(); + foreach (int userId in this.UserIds) + { + UserEntity user = await database.Users.FindAsync(userId); + if (user == null) continue; + + this.Users.Add(GameUser.CreateFromEntity(user, this.TargetGame)); + } + } + } + + public static async Task CreateFromEntityResult + ( + DatabaseContext database, + GameTokenEntity token, + List> results, + long startTimestamp, + long endTimestamp + ) + { + List slotIds = results.Where(g => g.Key.TargetSlotId != null && g.Key.TargetSlotId.Value != 0) + .Select(g => g.Key.TargetSlotId.Value) + .ToList(); + Console.WriteLine($@"slotIds: {string.Join(",", slotIds)}"); + List userIds = results.Where(g => g.Key.TargetUserId != null && g.Key.TargetUserId.Value != 0) + .Select(g => g.Key.TargetUserId.Value) + .Distinct() + .Union(results.Select(g => g.Key.UserId)) + .ToList(); + // Cache target levels and users within DbContext + await database.Slots.Where(s => slotIds.Contains(s.SlotId)).LoadAsync(); + await database.Users.Where(u => userIds.Contains(u.UserId)).LoadAsync(); + Console.WriteLine($@"userIds: {string.Join(",", userIds)}"); + Console.WriteLine($@"Stream contains {slotIds.Count} slots and {userIds.Count} users"); + GameStream gameStream = new() + { + TargetUserId = token.UserId, + TargetGame = token.GameVersion, + StartTimestamp = startTimestamp, + EndTimestamp = endTimestamp, + SlotIds = slotIds, + UserIds = userIds, + Groups = new List(), + }; + foreach (IGrouping group in results) + { + gameStream.Groups.Add(GameStreamGroup.CreateFromGrouping(group)); + } + + return gameStream; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs new file mode 100644 index 000000000..ef783f541 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +/// +/// Top level groups generally contain all events for a given level or user +/// +/// The sub-groups are always and contain all activities from a single user +/// for the top level group entity +/// +/// +[XmlInclude(typeof(GameUserStreamGroup))] +[XmlInclude(typeof(GameSlotStreamGroup))] +public class GameStreamGroup : ILbpSerializable +{ + [XmlAttribute("type")] + public ActivityGroupType Type { get; set; } + + [XmlElement("timestamp")] + public long Timestamp { get; set; } + + [XmlArray("subgroups")] + [XmlArrayItem("group")] + [DefaultValue(null)] + public List Groups { get; set; } + + [XmlArray("events")] + [XmlArrayItem("event")] + [DefaultValue(null)] + public List Events { get; set; } + + public static GameStreamGroup CreateFromGrouping(IGrouping group) + { + ActivityGroupType type = group.Key.GroupType; + GameStreamGroup gameGroup = type switch + { + ActivityGroupType.Level => new GameSlotStreamGroup + { + Slot = new ReviewSlot + { + SlotId = group.Key.TargetId, + }, + }, + ActivityGroupType.User => new GameUserStreamGroup + { + UserId = group.Key.TargetId, + }, + _ => new GameStreamGroup(), + }; + gameGroup.Timestamp = new DateTimeOffset(group.Select(a => a.Timestamp).MaxBy(a => a)).ToUnixTimeMilliseconds(); + gameGroup.Type = type; + + List> eventGroups = group.OrderByDescending(a => a.Timestamp).GroupBy(g => g.Type).ToList(); + //TODO removeme debug + foreach (IGrouping bruh in eventGroups) + { + Console.WriteLine($@"group key: {bruh.Key}, count={bruh.Count()}"); + } + gameGroup.Groups = new List + { + new GameUserStreamGroup + { + UserId = group.Key.UserId, + Type = ActivityGroupType.User, + Timestamp = gameGroup.Timestamp, + Events = eventGroups.SelectMany(GameEvent.CreateFromActivityGroups).ToList(), + }, + }; + + return gameGroup; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameUserStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GameUserStreamGroup.cs new file mode 100644 index 000000000..bc7cf53b7 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameUserStreamGroup.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +public class GameUserStreamGroup : GameStreamGroup, INeedsPreparationForSerialization +{ + [XmlIgnore] + public int UserId { get; set; } + + [XmlElement("user_id")] + public string Username { get; set; } + + public async Task PrepareSerialization(DatabaseContext database) + { + UserEntity user = await database.Users.FindAsync(this.UserId); + if (user == null) return; + + this.Username = user.Username; + } + + public static GameUserStreamGroup Create(int userId) => + new() + { + UserId = userId, + }; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/CommentListResponse.cs b/ProjectLighthouse/Types/Serialization/Comment/CommentListResponse.cs similarity index 83% rename from ProjectLighthouse/Types/Serialization/CommentListResponse.cs rename to ProjectLighthouse/Types/Serialization/Comment/CommentListResponse.cs index 35d9023ef..b9b11d0c5 100644 --- a/ProjectLighthouse/Types/Serialization/CommentListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Comment/CommentListResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Comment; [XmlRoot("comments")] public struct CommentListResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameComment.cs b/ProjectLighthouse/Types/Serialization/Comment/GameComment.cs similarity index 97% rename from ProjectLighthouse/Types/Serialization/GameComment.cs rename to ProjectLighthouse/Types/Serialization/Comment/GameComment.cs index 07919f5ef..16597d13c 100644 --- a/ProjectLighthouse/Types/Serialization/GameComment.cs +++ b/ProjectLighthouse/Types/Serialization/Comment/GameComment.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Comment; [XmlRoot("comment")] [XmlType("comment")] diff --git a/ProjectLighthouse/Types/Serialization/GamePhoto.cs b/ProjectLighthouse/Types/Serialization/Photo/GamePhoto.cs similarity index 98% rename from ProjectLighthouse/Types/Serialization/GamePhoto.cs rename to ProjectLighthouse/Types/Serialization/Photo/GamePhoto.cs index 67bb2dfb9..3c0cdd9e1 100644 --- a/ProjectLighthouse/Types/Serialization/GamePhoto.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/GamePhoto.cs @@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Levels; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo; [XmlRoot("photo")] [XmlType("photo")] diff --git a/ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs b/ProjectLighthouse/Types/Serialization/Photo/GamePhotoSubject.cs similarity index 91% rename from ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs rename to ProjectLighthouse/Types/Serialization/Photo/GamePhotoSubject.cs index 94cbd50cb..914b82272 100644 --- a/ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/GamePhotoSubject.cs @@ -1,7 +1,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo; [XmlType("subject")] [XmlRoot("subject")] diff --git a/ProjectLighthouse/Types/Serialization/PhotoListResponse.cs b/ProjectLighthouse/Types/Serialization/Photo/PhotoListResponse.cs similarity index 83% rename from ProjectLighthouse/Types/Serialization/PhotoListResponse.cs rename to ProjectLighthouse/Types/Serialization/Photo/PhotoListResponse.cs index a8d640289..d4f5fbfdb 100644 --- a/ProjectLighthouse/Types/Serialization/PhotoListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/PhotoListResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo; [XmlRoot("photos")] public struct PhotoListResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/PhotoSlot.cs b/ProjectLighthouse/Types/Serialization/Photo/PhotoSlot.cs similarity index 88% rename from ProjectLighthouse/Types/Serialization/PhotoSlot.cs rename to ProjectLighthouse/Types/Serialization/Photo/PhotoSlot.cs index c4699720a..82dea9826 100644 --- a/ProjectLighthouse/Types/Serialization/PhotoSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/PhotoSlot.cs @@ -3,7 +3,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Levels; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo; [XmlRoot("slot")] public class PhotoSlot : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/Author.cs b/ProjectLighthouse/Types/Serialization/Playlist/Author.cs similarity index 80% rename from ProjectLighthouse/Types/Serialization/Author.cs rename to ProjectLighthouse/Types/Serialization/Playlist/Author.cs index 7de0599a1..cfd577b3d 100644 --- a/ProjectLighthouse/Types/Serialization/Author.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/Author.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; [XmlRoot("author")] public struct Author : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GamePlaylist.cs b/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs similarity index 97% rename from ProjectLighthouse/Types/Serialization/GamePlaylist.cs rename to ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs index 5ced447c9..90d893293 100644 --- a/ProjectLighthouse/Types/Serialization/GamePlaylist.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs @@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; [XmlRoot("playlist")] public class GamePlaylist : ILbpSerializable, INeedsPreparationForSerialization diff --git a/ProjectLighthouse/Types/Serialization/GenericPlaylistResponse.cs b/ProjectLighthouse/Types/Serialization/Playlist/GenericPlaylistResponse.cs similarity index 93% rename from ProjectLighthouse/Types/Serialization/GenericPlaylistResponse.cs rename to ProjectLighthouse/Types/Serialization/Playlist/GenericPlaylistResponse.cs index ba85d8c87..f0f1f0f78 100644 --- a/ProjectLighthouse/Types/Serialization/GenericPlaylistResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/GenericPlaylistResponse.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; public struct GenericPlaylistResponse : ILbpSerializable, IHasCustomRoot where T : ILbpSerializable { diff --git a/ProjectLighthouse/Types/Serialization/IconList.cs b/ProjectLighthouse/Types/Serialization/Playlist/IconList.cs similarity index 82% rename from ProjectLighthouse/Types/Serialization/IconList.cs rename to ProjectLighthouse/Types/Serialization/Playlist/IconList.cs index 87a6f6cbc..9d0e7f1cb 100644 --- a/ProjectLighthouse/Types/Serialization/IconList.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/IconList.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; public struct IconList : ILbpSerializable { diff --git a/ProjectLighthouse/Types/Serialization/PlaylistResponse.cs b/ProjectLighthouse/Types/Serialization/Playlist/PlaylistResponse.cs similarity index 85% rename from ProjectLighthouse/Types/Serialization/PlaylistResponse.cs rename to ProjectLighthouse/Types/Serialization/Playlist/PlaylistResponse.cs index 81a460035..a5b309ab7 100644 --- a/ProjectLighthouse/Types/Serialization/PlaylistResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/PlaylistResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; [XmlRoot("playlists")] public struct PlaylistResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameReview.cs b/ProjectLighthouse/Types/Serialization/Review/GameReview.cs similarity index 98% rename from ProjectLighthouse/Types/Serialization/GameReview.cs rename to ProjectLighthouse/Types/Serialization/Review/GameReview.cs index 7dba385e7..1da3b77b5 100644 --- a/ProjectLighthouse/Types/Serialization/GameReview.cs +++ b/ProjectLighthouse/Types/Serialization/Review/GameReview.cs @@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types.Levels; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review; [XmlRoot("deleted_by")] public enum DeletedBy diff --git a/ProjectLighthouse/Types/Serialization/ReviewResponse.cs b/ProjectLighthouse/Types/Serialization/Review/ReviewResponse.cs similarity index 90% rename from ProjectLighthouse/Types/Serialization/ReviewResponse.cs rename to ProjectLighthouse/Types/Serialization/Review/ReviewResponse.cs index a15915dd7..e13e99bbc 100644 --- a/ProjectLighthouse/Types/Serialization/ReviewResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Review/ReviewResponse.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review; [XmlRoot("reviews")] public struct ReviewResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/Review/ReviewSlot.cs b/ProjectLighthouse/Types/Serialization/Review/ReviewSlot.cs new file mode 100644 index 000000000..11715e170 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Review/ReviewSlot.cs @@ -0,0 +1,22 @@ +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Levels; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +[XmlRoot("slot")] +public class ReviewSlot : ILbpSerializable +{ + [XmlAttribute("type")] + public SlotType SlotType { get; set; } + + [XmlText] + public int SlotId { get; set; } + + public static ReviewSlot CreateFromEntity(SlotEntity slot) => + new() + { + SlotType = slot.Type, + SlotId = slot.Type == SlotType.User ? slot.SlotId : slot.InternalSlotId, + }; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/ReviewSlot.cs b/ProjectLighthouse/Types/Serialization/ReviewSlot.cs deleted file mode 100644 index f4148adfd..000000000 --- a/ProjectLighthouse/Types/Serialization/ReviewSlot.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Xml.Serialization; -using LBPUnion.ProjectLighthouse.Types.Levels; - -namespace LBPUnion.ProjectLighthouse.Types.Serialization; - -[XmlRoot("slot")] -public class ReviewSlot : ILbpSerializable -{ - [XmlAttribute("type")] - public SlotType SlotType { get; set; } - - [XmlText] - public int SlotId { get; set; } -} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/GameScore.cs b/ProjectLighthouse/Types/Serialization/Score/GameScore.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GameScore.cs rename to ProjectLighthouse/Types/Serialization/Score/GameScore.cs index 1195ce96f..9c10ae736 100644 --- a/ProjectLighthouse/Types/Serialization/GameScore.cs +++ b/ProjectLighthouse/Types/Serialization/Score/GameScore.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score; [XmlRoot("playRecord")] [XmlType("playRecord")] diff --git a/ProjectLighthouse/Types/Serialization/MultiScoreboardResponse.cs b/ProjectLighthouse/Types/Serialization/Score/MultiScoreboardResponse.cs similarity index 93% rename from ProjectLighthouse/Types/Serialization/MultiScoreboardResponse.cs rename to ProjectLighthouse/Types/Serialization/Score/MultiScoreboardResponse.cs index 831e5efed..3d00f1412 100644 --- a/ProjectLighthouse/Types/Serialization/MultiScoreboardResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Score/MultiScoreboardResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score; [XmlRoot("scoreboards")] public class MultiScoreboardResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/ScoreboardResponse.cs b/ProjectLighthouse/Types/Serialization/Score/ScoreboardResponse.cs similarity index 94% rename from ProjectLighthouse/Types/Serialization/ScoreboardResponse.cs rename to ProjectLighthouse/Types/Serialization/Score/ScoreboardResponse.cs index f9c457633..2ee27c0ba 100644 --- a/ProjectLighthouse/Types/Serialization/ScoreboardResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Score/ScoreboardResponse.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score; public struct ScoreboardResponse: ILbpSerializable, IHasCustomRoot { diff --git a/ProjectLighthouse/Types/Serialization/CategoryListResponse.cs b/ProjectLighthouse/Types/Serialization/Slot/CategoryListResponse.cs similarity index 93% rename from ProjectLighthouse/Types/Serialization/CategoryListResponse.cs rename to ProjectLighthouse/Types/Serialization/Slot/CategoryListResponse.cs index 904fb2909..faa245081 100644 --- a/ProjectLighthouse/Types/Serialization/CategoryListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/CategoryListResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("categories")] public class CategoryListResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameCategory.cs b/ProjectLighthouse/Types/Serialization/Slot/GameCategory.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GameCategory.cs rename to ProjectLighthouse/Types/Serialization/Slot/GameCategory.cs index e097de974..2f0a2069d 100644 --- a/ProjectLighthouse/Types/Serialization/GameCategory.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GameCategory.cs @@ -2,7 +2,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Levels; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("category")] public class GameCategory : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs b/ProjectLighthouse/Types/Serialization/Slot/GameDeveloperSlot.cs similarity index 96% rename from ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs rename to ProjectLighthouse/Types/Serialization/Slot/GameDeveloperSlot.cs index 3de55ee46..d7a67c6c3 100644 --- a/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GameDeveloperSlot.cs @@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Levels; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("slot")] public class GameDeveloperSlot : SlotBase, INeedsPreparationForSerialization diff --git a/ProjectLighthouse/Types/Serialization/GameUserSlot.cs b/ProjectLighthouse/Types/Serialization/Slot/GameUserSlot.cs similarity index 98% rename from ProjectLighthouse/Types/Serialization/GameUserSlot.cs rename to ProjectLighthouse/Types/Serialization/Slot/GameUserSlot.cs index 978818be4..11890cce9 100644 --- a/ProjectLighthouse/Types/Serialization/GameUserSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GameUserSlot.cs @@ -13,10 +13,12 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Misc; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("slot")] public class GameUserSlot : SlotBase, INeedsPreparationForSerialization diff --git a/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs b/ProjectLighthouse/Types/Serialization/Slot/GenericSlotResponse.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs rename to ProjectLighthouse/Types/Serialization/Slot/GenericSlotResponse.cs index b00e8575e..036c02c54 100644 --- a/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GenericSlotResponse.cs @@ -3,7 +3,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; public struct GenericSlotResponse : ILbpSerializable, IHasCustomRoot { diff --git a/ProjectLighthouse/Types/Serialization/PlanetStatsResponse.cs b/ProjectLighthouse/Types/Serialization/Slot/PlanetStatsResponse.cs similarity index 88% rename from ProjectLighthouse/Types/Serialization/PlanetStatsResponse.cs rename to ProjectLighthouse/Types/Serialization/Slot/PlanetStatsResponse.cs index 0457d282a..9dbcd2e5d 100644 --- a/ProjectLighthouse/Types/Serialization/PlanetStatsResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/PlanetStatsResponse.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("planetStats")] public class PlanetStatsResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/SlotBase.cs b/ProjectLighthouse/Types/Serialization/Slot/SlotBase.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/SlotBase.cs rename to ProjectLighthouse/Types/Serialization/Slot/SlotBase.cs index 49d3ce60d..9778a941e 100644 --- a/ProjectLighthouse/Types/Serialization/SlotBase.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/SlotBase.cs @@ -3,9 +3,10 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlInclude(typeof(GameUserSlot))] [XmlInclude(typeof(GameDeveloperSlot))] @@ -49,7 +50,7 @@ public static SlotEntity ConvertToEntity(GameUserSlot slot) => public static SlotBase CreateFromEntity(SlotEntity slot, GameTokenEntity token) => CreateFromEntity(slot, token.GameVersion, token.UserId); - private static SlotBase CreateFromEntity(SlotEntity slot, GameVersion targetGame, int targetUserId) + public static SlotBase CreateFromEntity(SlotEntity slot, GameVersion targetGame, int targetUserId) { if (slot == null) { diff --git a/ProjectLighthouse/Types/Serialization/SlotResourceResponse.cs b/ProjectLighthouse/Types/Serialization/Slot/SlotResourceResponse.cs similarity index 86% rename from ProjectLighthouse/Types/Serialization/SlotResourceResponse.cs rename to ProjectLighthouse/Types/Serialization/Slot/SlotResourceResponse.cs index 7f9954db9..585f4a94d 100644 --- a/ProjectLighthouse/Types/Serialization/SlotResourceResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/SlotResourceResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("slot")] public struct SlotResourceResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/FriendResponse.cs b/ProjectLighthouse/Types/Serialization/User/FriendResponse.cs similarity index 85% rename from ProjectLighthouse/Types/Serialization/FriendResponse.cs rename to ProjectLighthouse/Types/Serialization/User/FriendResponse.cs index bf12e68ce..ed8abca86 100644 --- a/ProjectLighthouse/Types/Serialization/FriendResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/FriendResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; [XmlRoot("npdata")] public struct FriendResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameUser.cs b/ProjectLighthouse/Types/Serialization/User/GameUser.cs similarity index 99% rename from ProjectLighthouse/Types/Serialization/GameUser.cs rename to ProjectLighthouse/Types/Serialization/User/GameUser.cs index cb365ed17..054e3b6d1 100644 --- a/ProjectLighthouse/Types/Serialization/GameUser.cs +++ b/ProjectLighthouse/Types/Serialization/User/GameUser.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; [XmlRoot("user")] public class GameUser : ILbpSerializable, INeedsPreparationForSerialization diff --git a/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs b/ProjectLighthouse/Types/Serialization/User/GenericUserResponse.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GenericUserResponse.cs rename to ProjectLighthouse/Types/Serialization/User/GenericUserResponse.cs index ef97d42ca..430841657 100644 --- a/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/GenericUserResponse.cs @@ -3,7 +3,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; public struct GenericUserResponse : ILbpSerializable, IHasCustomRoot where T : ILbpSerializable { diff --git a/ProjectLighthouse/Types/Serialization/MinimalUserListResponse.cs b/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs similarity index 89% rename from ProjectLighthouse/Types/Serialization/MinimalUserListResponse.cs rename to ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs index c289bdc00..be99ba4cc 100644 --- a/ProjectLighthouse/Types/Serialization/MinimalUserListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; [XmlRoot("users")] public struct MinimalUserListResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/NpHandle.cs b/ProjectLighthouse/Types/Serialization/User/NpHandle.cs similarity index 86% rename from ProjectLighthouse/Types/Serialization/NpHandle.cs rename to ProjectLighthouse/Types/Serialization/User/NpHandle.cs index c79529362..8138f0ae0 100644 --- a/ProjectLighthouse/Types/Serialization/NpHandle.cs +++ b/ProjectLighthouse/Types/Serialization/User/NpHandle.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; public class NpHandle : ILbpSerializable { diff --git a/ProjectLighthouse/Types/Serialization/UserListResponse.cs b/ProjectLighthouse/Types/Serialization/User/UserListResponse.cs similarity index 89% rename from ProjectLighthouse/Types/Serialization/UserListResponse.cs rename to ProjectLighthouse/Types/Serialization/User/UserListResponse.cs index e5a156306..32b8731d6 100644 --- a/ProjectLighthouse/Types/Serialization/UserListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/UserListResponse.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; public struct UserListResponse : ILbpSerializable, IHasCustomRoot { @@ -23,7 +23,7 @@ public UserListResponse(string rootElement, List users) } [XmlIgnore] - public string RootTag { get; set; } + private string RootTag { get; set; } [XmlElement("user")] public List Users { get; set; } From 1737a16f380b6222d0cdc822dfbdfc171706455c Mon Sep 17 00:00:00 2001 From: Slendy Date: Thu, 27 Jul 2023 17:30:42 -0500 Subject: [PATCH 03/40] Use SQLite in-memory in lieu of EF In-Memory for testing Also replaces usages of DateTime.Now with DateTime.UtcNow for internal time storage --- .../Startup/ApiStartup.cs | 6 +- .../Startup/GameServerStartup.cs | 6 +- .../CompleteEmailVerificationPage.cshtml.cs | 2 +- .../Pages/Login/LoginForm.cshtml | 2 +- .../Pages/Login/LoginForm.cshtml.cs | 2 +- .../Startup/WebsiteStartup.cs | 7 +- .../Unit/Activity/ActivityInterceptorTests.cs | 83 +++++++++++++++++++ .../Extensions/DateTimeExtensions.cs | 4 +- .../StorableLists/Stores/RoomStore.cs | 3 +- .../User/MinimalUserListResponse.cs | 1 - 10 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityInterceptorTests.cs diff --git a/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs b/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs index a070862c2..ff85fc0a5 100644 --- a/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs +++ b/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs @@ -28,11 +28,7 @@ public void ConfigureServices(IServiceCollection services) } ); - services.AddDbContext(builder => - { - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); - }); + services.AddDbContext(DatabaseContext.ConfigureBuilder()); services.AddSwaggerGen ( diff --git a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs index 0d372e8f1..e35d95e66 100644 --- a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs +++ b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs @@ -54,11 +54,7 @@ public void ConfigureServices(IServiceCollection services) } ); - services.AddDbContext(builder => - { - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); - }); + services.AddDbContext(DatabaseContext.ConfigureBuilder()); IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled ? new MailQueueService(new SmtpMailSender()) diff --git a/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs index a67e667f6..e83c60b96 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs @@ -64,7 +64,7 @@ public async Task OnGet(string token) webToken.UserToken, new CookieOptions { - Expires = DateTimeOffset.Now.AddDays(7), + Expires = DateTimeOffset.UtcNow.AddDays(7), }); return this.Redirect("/passwordReset"); } diff --git a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml index 8c8834623..b3c6f8f4e 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml @@ -38,7 +38,7 @@

- Instance logo diff --git a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs index 0af0f304d..c2ac92918 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs @@ -100,7 +100,7 @@ public async Task OnPost(string username, string password, string webToken.UserToken, new CookieOptions { - Expires = DateTimeOffset.Now.AddDays(7), + Expires = DateTimeOffset.UtcNow.AddDays(7), } ); diff --git a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs index bb89c6bff..614601024 100644 --- a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs +++ b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs @@ -13,7 +13,6 @@ using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Localization; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; #if !DEBUG @@ -39,11 +38,7 @@ public void ConfigureServices(IServiceCollection services) services.AddControllers(); services.AddRazorPages().WithRazorPagesAtContentRoot(); - services.AddDbContext(builder => - { - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); - }); + services.AddDbContext(DatabaseContext.ConfigureBuilder()); IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled ? new MailQueueService(new SmtpMailSender()) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityInterceptorTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityInterceptorTests.cs new file mode 100644 index 000000000..6d84cbf6a --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityInterceptorTests.cs @@ -0,0 +1,83 @@ +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using Microsoft.EntityFrameworkCore; +using Moq; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity; + +[Trait("Category", "Unit")] +public class ActivityInterceptorTests +{ + private static async Task GetTestDatabase(IMock eventHandlerMock) + { + DbContextOptionsBuilder optionsBuilder = await MockHelper.GetInMemoryDbOptions(); + + optionsBuilder.AddInterceptors(new ActivityInterceptor(eventHandlerMock.Object)); + DatabaseContext database = new(optionsBuilder.Options); + await database.Database.EnsureCreatedAsync(); + return database; + } + + [Fact] + public async Task SaveChangesWithNewEntity_ShouldCallEntityInserted() + { + Mock eventHandlerMock = new(); + DatabaseContext database = await GetTestDatabase(eventHandlerMock); + + database.Users.Add(new UserEntity + { + UserId = 1, + Username = "test", + }); + await database.SaveChangesAsync(); + + eventHandlerMock.Verify(x => x.OnEntityInserted(It.IsAny(), It.Is(user => user is UserEntity)), Times.Once); + } + + [Fact] + public async Task SaveChangesWithModifiedEntity_ShouldCallEntityChanged() + { + Mock eventHandlerMock = new(); + DatabaseContext database = await GetTestDatabase(eventHandlerMock); + + UserEntity user = new() + { + Username = "test", + }; + + database.Users.Add(user); + await database.SaveChangesAsync(); + + user.Username = "test2"; + await database.SaveChangesAsync(); + + eventHandlerMock.Verify(x => x.OnEntityChanged(It.IsAny(), + It.Is(u => u is UserEntity && ((UserEntity)u).Username == "test"), + It.Is(u => u is UserEntity && ((UserEntity)u).Username == "test2")), + Times.Once); + } + + [Fact] + public async Task SaveChangesWithModifiedEntity_ShouldCallEntityDeleted() + { + Mock eventHandlerMock = new(); + DatabaseContext database = await GetTestDatabase(eventHandlerMock); + + UserEntity user = new() + { + Username = "test", + }; + + database.Users.Add(user); + await database.SaveChangesAsync(); + + database.Users.Remove(user); + await database.SaveChangesAsync(); + + eventHandlerMock.Verify(x => x.OnEntityDeleted(It.IsAny(), It.Is(u => u is UserEntity)), Times.Once); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/DateTimeExtensions.cs b/ProjectLighthouse/Extensions/DateTimeExtensions.cs index 6cb037596..3fdb67689 100644 --- a/ProjectLighthouse/Extensions/DateTimeExtensions.cs +++ b/ProjectLighthouse/Extensions/DateTimeExtensions.cs @@ -5,8 +5,8 @@ namespace LBPUnion.ProjectLighthouse.Extensions; public static class DateTimeExtensions { public static long ToUnixTimeMilliseconds(this DateTime dateTime) => - new DateTimeOffset(dateTime).ToUniversalTime().ToUnixTimeMilliseconds(); + ((DateTimeOffset)DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)).ToUnixTimeMilliseconds(); public static DateTime FromUnixTimeMilliseconds(long timestamp) => - DateTimeOffset.FromUnixTimeMilliseconds(timestamp).ToUniversalTime().DateTime; + DateTimeOffset.FromUnixTimeMilliseconds(timestamp).UtcDateTime; } \ No newline at end of file diff --git a/ProjectLighthouse/StorableLists/Stores/RoomStore.cs b/ProjectLighthouse/StorableLists/Stores/RoomStore.cs index 6bf1754d9..3adb5e4d0 100644 --- a/ProjectLighthouse/StorableLists/Stores/RoomStore.cs +++ b/ProjectLighthouse/StorableLists/Stores/RoomStore.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms; namespace LBPUnion.ProjectLighthouse.StorableLists.Stores; @@ -10,7 +11,7 @@ public static class RoomStore public static StorableList GetRooms() { - if (RedisDatabase.Initialized) + if (!ServerStatics.IsUnitTesting && RedisDatabase.Initialized) { return new RedisStorableList(RedisDatabase.GetRooms()); } diff --git a/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs b/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs index be99ba4cc..64f06c297 100644 --- a/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs @@ -19,7 +19,6 @@ public MinimalUserListResponse(List users) public class MinimalUserProfile : ILbpSerializable { - [XmlElement("npHandle")] public NpHandle UserHandle { get; set; } = new(); } \ No newline at end of file From 60d851fb153d3f52ad7ed57714484831b93e432d Mon Sep 17 00:00:00 2001 From: Slendy Date: Fri, 28 Jul 2023 17:45:28 -0500 Subject: [PATCH 04/40] Finish most of Recent Activity --- .../Controllers/StatisticsEndpoints.cs | 2 +- .../Controllers/ActivityController.cs | 427 +++++----- .../Controllers/NewsController.cs | 31 + .../Controllers/Slots/ReviewController.cs | 16 + .../Controllers/Slots/SearchController.cs | 2 +- .../Controllers/Slots/SlotsController.cs | 2 +- .../Controllers/StatisticsController.cs | 2 +- .../Extensions/ControllerExtensions.cs | 2 +- .../Types/Categories/CustomCategory.cs | 2 +- .../Types/Categories/TeamPicksCategory.cs | 2 +- .../Activity/ActivityEventHandlerTests.cs | 727 ++++++++++++++++++ .../Controllers/ActivityControllerTests.cs | 6 + .../Controllers/ControllerExtensionTests.cs | 2 +- ProjectLighthouse.Tests/Unit/FilterTests.cs | 2 +- ProjectLighthouse/Database/DatabaseContext.cs | 23 +- .../Extensions/ActivityQueryExtensions.cs | 121 +++ .../Extensions/ControllerExtensions.cs | 1 + .../Filter/ActivityQueryBuilder.cs | 55 ++ .../Filters/Activity/EventTypeFilter.cs | 27 + .../Filters/Activity/ExcludeNewsFilter.cs | 12 + .../Filters/Activity/IncludeNewsFilter.cs | 14 + .../Filters/Activity/IncludeUserIdFilter.cs | 29 + .../Filters/Activity/MyLevelActivityFilter.cs | 27 + .../Activity/PlaylistActivityFilter.cs | 30 + .../Filters/{ => Slot}/AdventureFilter.cs | 2 +- .../Filters/{ => Slot}/AuthorLabelFilter.cs | 4 +- .../Filters/{ => Slot}/CreatorFilter.cs | 2 +- .../Filters/{ => Slot}/CrossControlFilter.cs | 2 +- .../{ => Slot}/ExcludeAdventureFilter.cs | 2 +- .../{ => Slot}/ExcludeCrossControlFilter.cs | 2 +- .../{ => Slot}/ExcludeLBP1OnlyFilter.cs | 2 +- .../{ => Slot}/ExcludeMovePackFilter.cs | 2 +- .../Filters/{ => Slot}/FirstUploadedFilter.cs | 2 +- .../Filters/{ => Slot}/GameVersionFilter.cs | 2 +- .../{ => Slot}/GameVersionListFilter.cs | 4 +- .../Filters/{ => Slot}/HiddenSlotFilter.cs | 2 +- .../Filters/{ => Slot}/MovePackFilter.cs | 2 +- .../Filters/{ => Slot}/PlayerCountFilter.cs | 2 +- .../Filters/{ => Slot}/ResultTypeFilter.cs | 2 +- .../Filter/Filters/{ => Slot}/SlotIdFilter.cs | 4 +- .../Filters/{ => Slot}/SlotTypeFilter.cs | 2 +- .../Filters/{ => Slot}/SubLevelFilter.cs | 2 +- .../Filters/{ => Slot}/TeamPickFilter.cs | 2 +- .../Filter/Filters/{ => Slot}/TextFilter.cs | 2 +- .../20230725013522_InitialActivity.cs | 149 ++++ .../DatabaseContextModelSnapshot.cs | 259 +++++++ .../Types/Activity/ActivityDto.cs | 33 + .../Activity/ActivityEntityEventHandler.cs | 143 +++- .../Types/Activity/ActivityGroup.cs | 46 +- ProjectLighthouse/Types/Activity/EventType.cs | 45 +- .../Entities/Activity/LevelActivityEntity.cs | 5 +- .../Entities/Activity/NewsActivityEntity.cs | 10 +- .../Activity/PlaylistActivityEntity.cs | 27 +- .../Entities/Activity/ReviewActivityEntity.cs | 5 +- .../Types/Filter/IActivityFilter.cs | 6 + .../Events/GameAddLevelToPlaylistEvent.cs | 26 + .../Events/GameCreatePlaylistEvent.cs | 16 + .../Activity/Events/GameDpadRateLevelEvent.cs | 32 + .../Activity/Events/GameEvent.cs | 202 +++-- .../Activity/Events/GameHeartEvent.cs | 11 + .../Activity/Events/GameNewsEvent.cs | 9 + .../Activity/Events/GamePhotoUploadEvent.cs | 4 +- .../Activity/Events/GamePublishLevelEvent.cs | 8 +- .../Activity/Events/GameRateLevelEvent.cs | 32 + .../Activity/Events/GameReviewEvent.cs | 9 +- .../Activity/Events/GameTeamPickLevelEvent.cs | 24 + .../Activity/GameNewsStreamGroup.cs | 9 + .../Activity/GamePlaylistStreamGroup.cs | 9 + .../Serialization/Activity/GameStream.cs | 110 +-- .../Activity/GameStreamFilter.cs | 26 + .../Serialization/Activity/GameStreamGroup.cs | 68 +- .../Types/Serialization/News/GameNews.cs | 63 ++ .../Types/Serialization/News/GameNewsFrame.cs | 40 + .../Serialization/News/GameNewsObject.cs | 54 ++ .../Serialization/Playlist/GamePlaylist.cs | 2 +- .../Types/Serialization/Slot/MinimalSlot.cs | 22 + .../Types/Serialization/User/GameUser.cs | 26 +- 77 files changed, 2711 insertions(+), 429 deletions(-) create mode 100644 ProjectLighthouse.Servers.GameServer/Controllers/NewsController.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs create mode 100644 ProjectLighthouse/Extensions/ActivityQueryExtensions.cs create mode 100644 ProjectLighthouse/Filter/ActivityQueryBuilder.cs create mode 100644 ProjectLighthouse/Filter/Filters/Activity/EventTypeFilter.cs create mode 100644 ProjectLighthouse/Filter/Filters/Activity/ExcludeNewsFilter.cs create mode 100644 ProjectLighthouse/Filter/Filters/Activity/IncludeNewsFilter.cs create mode 100644 ProjectLighthouse/Filter/Filters/Activity/IncludeUserIdFilter.cs create mode 100644 ProjectLighthouse/Filter/Filters/Activity/MyLevelActivityFilter.cs create mode 100644 ProjectLighthouse/Filter/Filters/Activity/PlaylistActivityFilter.cs rename ProjectLighthouse/Filter/Filters/{ => Slot}/AdventureFilter.cs (83%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/AuthorLabelFilter.cs (78%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/CreatorFilter.cs (87%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/CrossControlFilter.cs (83%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/ExcludeAdventureFilter.cs (83%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/ExcludeCrossControlFilter.cs (84%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/ExcludeLBP1OnlyFilter.cs (92%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/ExcludeMovePackFilter.cs (83%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/FirstUploadedFilter.cs (93%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/GameVersionFilter.cs (93%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/GameVersionListFilter.cs (78%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/HiddenSlotFilter.cs (82%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/MovePackFilter.cs (82%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/PlayerCountFilter.cs (93%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/ResultTypeFilter.cs (89%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/SlotIdFilter.cs (82%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/SlotTypeFilter.cs (89%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/SubLevelFilter.cs (87%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/TeamPickFilter.cs (83%) rename ProjectLighthouse/Filter/Filters/{ => Slot}/TextFilter.cs (94%) create mode 100644 ProjectLighthouse/Migrations/20230725013522_InitialActivity.cs create mode 100644 ProjectLighthouse/Types/Activity/ActivityDto.cs create mode 100644 ProjectLighthouse/Types/Filter/IActivityFilter.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameAddLevelToPlaylistEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameCreatePlaylistEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameDpadRateLevelEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameNewsEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameRateLevelEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/Events/GameTeamPickLevelEvent.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/GameNewsStreamGroup.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/GamePlaylistStreamGroup.cs create mode 100644 ProjectLighthouse/Types/Serialization/Activity/GameStreamFilter.cs create mode 100644 ProjectLighthouse/Types/Serialization/News/GameNews.cs create mode 100644 ProjectLighthouse/Types/Serialization/News/GameNewsFrame.cs create mode 100644 ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs create mode 100644 ProjectLighthouse/Types/Serialization/Slot/MinimalSlot.cs diff --git a/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs b/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs index dcd9a5c29..736e47669 100644 --- a/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs +++ b/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs @@ -1,6 +1,6 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.API.Responses; using LBPUnion.ProjectLighthouse.Types.Users; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index f78fda687..107c3ebb3 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -1,11 +1,10 @@ using System.Linq.Expressions; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter.Filters.Activity; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.StorableLists.Stores; using LBPUnion.ProjectLighthouse.Types.Activity; -using LBPUnion.ProjectLighthouse.Types.Entities.Activity; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization.Activity; @@ -29,143 +28,210 @@ public ActivityController(DatabaseContext database) this.database = database; } - public class ActivityDto - { - public required ActivityEntity Activity { get; set; } - public int? TargetSlotId { get; set; } - public int? TargetUserId { get; set; } - public int? TargetPlaylistId { get; set; } - public int? SlotCreatorId { get; set; } - } - //TODO refactor this mess into a separate db file or something - - private static Expression> ActivityToDto() - { - return a => new ActivityDto - { - Activity = a, - TargetSlotId = a is LevelActivityEntity - ? ((LevelActivityEntity)a).SlotId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.PhotoId != 0 - ? ((PhotoActivityEntity)a).Photo.SlotId - : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level - ? ((CommentActivityEntity)a).Comment.TargetId - : a is ScoreActivityEntity - ? ((ScoreActivityEntity)a).Score.SlotId - : 0, - - TargetUserId = a is UserActivityEntity - ? ((UserActivityEntity)a).TargetUserId - : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile - ? ((CommentActivityEntity)a).Comment.TargetId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 - ? ((PhotoActivityEntity)a).Photo.CreatorId - : 0, - TargetPlaylistId = a is PlaylistActivityEntity ? ((PlaylistActivityEntity)a).PlaylistId : 0, - }; - } - - private static IQueryable> GroupActivities - (IQueryable activityQuery) - { - return activityQuery.Select(ActivityToDto()) - .GroupBy(dto => new ActivityGroup - { - Timestamp = dto.Activity.Timestamp.Date, - UserId = dto.Activity.UserId, - TargetUserId = dto.TargetUserId, - TargetSlotId = dto.TargetSlotId, - TargetPlaylistId = dto.TargetPlaylistId, - }, - dto => dto.Activity); - } - - private static IQueryable> GroupActivities - (IQueryable activityQuery) - { - return activityQuery.GroupBy(dto => new ActivityGroup - { - Timestamp = dto.Activity.Timestamp.Date, - UserId = dto.Activity.UserId, - TargetUserId = dto.TargetUserId, - TargetSlotId = dto.TargetSlotId, - TargetPlaylistId = dto.TargetPlaylistId, - }, - dto => dto.Activity); - } - - // TODO this is kinda ass, can maybe improve once comment migration is merged - private async Task> GetFilters + /// + /// This method is only used for LBP2 so we exclude playlists + /// + private async Task> GetFilters ( + IQueryable dtoQuery, GameTokenEntity token, bool excludeNews, bool excludeMyLevels, bool excludeFriends, bool excludeFavouriteUsers, - bool excludeMyself + bool excludeMyself, + bool excludeMyPlaylists = true ) { - IQueryable query = this.database.Activities.AsQueryable(); - if (excludeNews) query = query.Where(a => a.Type != EventType.NewsPost); + Expression> predicate = PredicateExtensions.False(); + + List favouriteUsers = await this.database.HeartedProfiles.Where(hp => hp.UserId == token.UserId) + .Select(hp => hp.HeartedUserId) + .ToListAsync(); + + List? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds; + friendIds ??= new List(); + + // This is how lbp3 does its filtering + GameStreamFilter? filter = await this.DeserializeBody(); + if (filter?.Sources != null) + { + foreach (GameStreamFilterEventSource filterSource in filter.Sources.Where(filterSource => + filterSource.SourceType != null && filterSource.Types?.Count != 0)) + { + EventType[] types = filterSource.Types?.ToArray() ?? Array.Empty(); + EventTypeFilter eventFilter = new(types); + predicate = filterSource.SourceType switch + { + "MyLevels" => predicate.Or(new MyLevelActivityFilter(token.UserId, eventFilter).GetPredicate()), + "FavouriteUsers" => predicate.Or( + new IncludeUserIdFilter(favouriteUsers, eventFilter).GetPredicate()), + "Friends" => predicate.Or(new IncludeUserIdFilter(friendIds, eventFilter).GetPredicate()), + _ => predicate, + }; + } + } - IQueryable dtoQuery = query.Select(a => new ActivityDto + Expression> newsPredicate = !excludeNews + ? new IncludeNewsFilter().GetPredicate() + : new ExcludeNewsFilter().GetPredicate(); + + predicate = predicate.Or(newsPredicate); + + if (!excludeMyLevels) { - Activity = a, - SlotCreatorId = a is LevelActivityEntity - ? ((LevelActivityEntity)a).Slot.CreatorId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 - ? ((PhotoActivityEntity)a).Photo.Slot!.CreatorId - : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level - ? ((CommentActivityEntity)a).Comment.TargetId - : a is ScoreActivityEntity - ? ((ScoreActivityEntity)a).Score.Slot.CreatorId - : 0, - }); + predicate = predicate.Or(dto => dto.TargetSlotCreatorId == token.UserId); + } - Expression> predicate = PredicateExtensions.False(); + List includedUserIds = new(); - predicate = predicate.Or(a => a.SlotCreatorId == 0 || excludeMyLevels - ? a.SlotCreatorId != token.UserId - : a.SlotCreatorId == token.UserId); + if (!excludeFriends) + { + includedUserIds.AddRange(friendIds); + } - List? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds; - if (friendIds != null) + if (!excludeFavouriteUsers) { - predicate = excludeFriends - ? predicate.Or(a => !friendIds.Contains(a.Activity.UserId)) - : predicate.Or(a => friendIds.Contains(a.Activity.UserId)); + includedUserIds.AddRange(favouriteUsers); } - List favouriteUsers = await this.database.HeartedProfiles.Where(hp => hp.UserId == token.UserId) - .Select(hp => hp.HeartedUserId) - .ToListAsync(); + if (!excludeMyself) + { + includedUserIds.Add(token.UserId); + } - predicate = excludeFavouriteUsers - ? predicate.Or(a => !favouriteUsers.Contains(a.Activity.UserId)) - : predicate.Or(a => favouriteUsers.Contains(a.Activity.UserId)); + predicate = predicate.Or(dto => includedUserIds.Contains(dto.Activity.UserId)); + + if (!excludeMyPlaylists) + { + List creatorPlaylists = await this.database.Playlists.Where(p => p.CreatorId == token.UserId) + .Select(p => p.PlaylistId) + .ToListAsync(); + predicate = predicate.Or(new PlaylistActivityFilter(creatorPlaylists).GetPredicate()); + } + else + { + predicate = predicate.And(dto => + dto.Activity.Type != EventType.CreatePlaylist && + dto.Activity.Type != EventType.HeartPlaylist && + dto.Activity.Type != EventType.AddLevelToPlaylist); + } - predicate = excludeMyself - ? predicate.Or(a => a.Activity.UserId != token.UserId) - : predicate.Or(a => a.Activity.UserId == token.UserId); + Console.WriteLine(predicate); - query = dtoQuery.Where(predicate).Select(dto => dto.Activity); + dtoQuery = dtoQuery.Where(predicate); - return query.OrderByDescending(a => a.Timestamp); + return dtoQuery; } - public Task GetMostRecentEventTime(GameTokenEntity token, DateTime upperBound) + public Task GetMostRecentEventTime(IQueryable activity, DateTime upperBound) { - return this.database.Activities.Where(a => a.UserId == token.UserId) - .Where(a => a.Timestamp < upperBound) - .OrderByDescending(a => a.Timestamp) - .Select(a => a.Timestamp) + return activity.OrderByDescending(a => a.Activity.Timestamp) + .Where(a => a.Activity.Timestamp < upperBound) + .Select(a => a.Activity.Timestamp) .FirstOrDefaultAsync(); } + private async Task<(DateTime Start, DateTime End)> GetTimeBounds + (IQueryable activityQuery, long? startTime, long? endTime) + { + if (startTime is null or 0) startTime = TimeHelper.TimestampMillis; + + DateTime start = DateTimeExtensions.FromUnixTimeMilliseconds(startTime.Value); + DateTime end; + + if (endTime == null) + { + end = await this.GetMostRecentEventTime(activityQuery, start); + // If there is no recent event then set it to the the start + if (end == DateTime.MinValue) end = start; + end = end.Subtract(TimeSpan.FromDays(7)); + } + else + { + end = DateTimeExtensions.FromUnixTimeMilliseconds(endTime.Value); + // Don't allow more than 7 days worth of activity in a single page + if (start.Subtract(end).TotalDays > 7) + { + end = start.Subtract(TimeSpan.FromDays(7)); + } + } + + return (start, end); + } + + private static DateTime GetOldestTime + (IReadOnlyCollection> groups, DateTime defaultTimestamp) => + groups.Any() + ? groups.Min(g => g.MinBy(a => a.Activity.Timestamp)?.Activity.Timestamp ?? defaultTimestamp) + : defaultTimestamp; + + /// + /// Speeds up serialization because many nested entities need to find Slots by id + /// and since they use the Find() method they can benefit from having the entities + /// already tracked by the context + /// + private async Task CacheEntities(IReadOnlyCollection groups) + { + List slotIds = groups.GetIds(ActivityGroupType.Level); + List userIds = groups.GetIds(ActivityGroupType.User); + List playlistIds = groups.GetIds(ActivityGroupType.Playlist); + List newsIds = groups.GetIds(ActivityGroupType.News); + + // Cache target levels and users within DbContext + if (slotIds.Count > 0) await this.database.Slots.Where(s => slotIds.Contains(s.SlotId)).LoadAsync(); + if (userIds.Count > 0) await this.database.Users.Where(u => userIds.Contains(u.UserId)).LoadAsync(); + if (playlistIds.Count > 0) + await this.database.Playlists.Where(p => playlistIds.Contains(p.PlaylistId)).LoadAsync(); + if (newsIds.Count > 0) + await this.database.WebsiteAnnouncements.Where(a => newsIds.Contains(a.AnnouncementId)).LoadAsync(); + } + + /// + /// LBP3 uses a different grouping format that wants the actor to be the top level group and the events should be the subgroups + /// + [HttpPost] + public async Task GlobalActivityLBP3 + (long timestamp, bool excludeMyPlaylists, bool excludeNews, bool excludeMyself) + { + GameTokenEntity token = this.GetToken(); + + if (token.GameVersion != GameVersion.LittleBigPlanet3) return this.NotFound(); + + IQueryable activityEvents = await this.GetFilters( + this.database.Activities.ToActivityDto(true, true), + token, + excludeNews, + true, + true, + true, + excludeMyself, + excludeMyPlaylists); + + (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityEvents, timestamp, null); + + // LBP3 is grouped by actorThenObject meaning it wants all events by a user grouped together rather than + // all user events for a level or profile grouped together + List> groups = await activityEvents + .Where(dto => dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End) + .ToActivityGroups(true) + .ToListAsync(); + + List outerGroups = groups.ToOuterActivityGroups(true); + + long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); + + return this.Ok(GameStream.CreateFromGroups(token, + outerGroups, + times.Start.ToUnixTimeMilliseconds(), + oldestTimestamp)); + } + [HttpGet] public async Task GlobalActivity ( long timestamp, + long endTimestamp, bool excludeNews, bool excludeMyLevels, bool excludeFriends, @@ -175,112 +241,109 @@ bool excludeMyself { GameTokenEntity token = this.GetToken(); - if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest(); - - if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis; - - DateTime start = DateTimeExtensions.FromUnixTimeMilliseconds(timestamp); - - DateTime soonestTime = await this.GetMostRecentEventTime(token, start); - Console.WriteLine(@"Most recent event occurred at " + soonestTime); - soonestTime = soonestTime.Subtract(TimeSpan.FromDays(1)); + if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.NotFound(); - long soonestTimestamp = soonestTime.ToUnixTimeMilliseconds(); - - long endTimestamp = soonestTimestamp - 86_400_000; - - Console.WriteLine(@$"soonestTime: {soonestTimestamp}, endTime: {endTimestamp}"); - - IQueryable activityEvents = await this.GetFilters(token, + IQueryable activityEvents = await this.GetFilters(this.database.Activities.ToActivityDto(true), + token, excludeNews, excludeMyLevels, excludeFriends, excludeFavouriteUsers, excludeMyself); - DateTime end = DateTimeExtensions.FromUnixTimeMilliseconds(endTimestamp); + (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityEvents, timestamp, endTimestamp); + + List> groups = await activityEvents + .Where(dto => dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End) + .ToActivityGroups() + .ToListAsync(); - activityEvents = activityEvents.Where(a => a.Timestamp < start && a.Timestamp > end); + List outerGroups = groups.ToOuterActivityGroups(); - Console.WriteLine($@"start: {start}, end: {end}"); + long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); - List> groups = await GroupActivities(activityEvents).ToListAsync(); + await this.CacheEntities(outerGroups); + + GameStream? gameStream = GameStream.CreateFromGroups(token, + outerGroups, + times.Start.ToUnixTimeMilliseconds(), + oldestTimestamp); + + return this.Ok(gameStream); + } - foreach (IGrouping group in groups) + #if DEBUG + private static void PrintOuterGroups(List outerGroups) + { + foreach (OuterActivityGroup outer in outerGroups) { - ActivityGroup key = group.Key; - Console.WriteLine( - $@"{key.GroupType}: Timestamp: {key.Timestamp}, UserId: {key.UserId}, TargetSlotId: {key.TargetSlotId}, " + - @$"TargetUserId: {key.TargetUserId}, TargetPlaylistId: {key.TargetPlaylistId}"); - foreach (ActivityEntity activity in group) + Console.WriteLine(@$"Outer group key: {outer.Key}"); + List> itemGroup = outer.Groups; + foreach (IGrouping item in itemGroup) { - Console.WriteLine($@" {activity.Type}: Timestamp: {activity.Timestamp}"); + Console.WriteLine( + @$" Inner group key: TargetId={item.Key.TargetId}, UserId={item.Key.UserId}, Type={item.Key.Type}"); + foreach (ActivityDto activity in item) + { + Console.WriteLine( + @$" Activity: {activity.GroupType}, Timestamp: {activity.Activity.Timestamp}, UserId: {activity.Activity.UserId}, EventType: {activity.Activity.Type}, TargetId: {activity.TargetId}"); + } } } - - DateTime oldestTime = groups.Any() ? groups.Min(g => g.Any() ? g.Min(a => a.Timestamp) : end) : end; - long oldestTimestamp = oldestTime.ToUnixTimeMilliseconds(); - - return this.Ok(await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp)); } + #endif [HttpGet("slot/{slotType}/{slotId:int}")] - public async Task SlotActivity(string slotType, int slotId, long timestamp) + [HttpGet("user2/{username}")] + public async Task SlotActivity(string? slotType, int slotId, string? username, long? timestamp) { GameTokenEntity token = this.GetToken(); - if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest(); - - if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis; - - long endTimestamp = timestamp - 864_000; + if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.NotFound(); - if (slotType is not ("developer" or "user")) return this.BadRequest(); + if ((SlotHelper.IsTypeInvalid(slotType) || slotId == 0) == (username == null)) return this.BadRequest(); - if (slotType == "developer") - slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); + IQueryable activityQuery = this.database.Activities.ToActivityDto() + .Where(a => a.Activity.Type != EventType.NewsPost && a.Activity.Type != EventType.MMPickLevel); - IQueryable slotActivity = this.database.Activities.Select(ActivityToDto()) - .Where(a => a.TargetSlotId == slotId); + bool isLevelActivity = username == null; - DateTime start = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime; - DateTime end = DateTimeOffset.FromUnixTimeMilliseconds(endTimestamp).DateTime; - - slotActivity = slotActivity.Where(a => a.Activity.Timestamp < start && a.Activity.Timestamp > end); - - List> groups = await GroupActivities(slotActivity).ToListAsync(); - - DateTime oldestTime = groups.Max(g => g.Max(a => a.Timestamp)); - long oldestTimestamp = new DateTimeOffset(oldestTime).ToUnixTimeMilliseconds(); - - return this.Ok(await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp)); - } - - [HttpGet("user2/{userId:int}/")] - public async Task UserActivity(int userId, long timestamp) - { - GameTokenEntity token = this.GetToken(); + // Slot activity + if (isLevelActivity) + { + if (slotType == "developer") + slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); - if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest(); + if (!await this.database.Slots.AnyAsync(s => s.SlotId == slotId)) return this.NotFound(); - if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis; + activityQuery = activityQuery.Where(dto => dto.TargetSlotId == slotId); + } + // User activity + else + { + int userId = await this.database.Users.Where(u => u.Username == username) + .Select(u => u.UserId) + .FirstOrDefaultAsync(); + if (userId == 0) return this.NotFound(); + activityQuery = activityQuery.Where(dto => dto.Activity.UserId == userId); + } - long endTimestamp = timestamp - 864_000; + (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityQuery, timestamp, null); - IQueryable userActivity = this.database.Activities.Select(ActivityToDto()) - .Where(a => a.TargetUserId == userId); + activityQuery = activityQuery.Where(dto => + dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End); - DateTime start = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime; - DateTime end = DateTimeOffset.FromUnixTimeMilliseconds(endTimestamp).DateTime; + List> groups = await activityQuery.ToActivityGroups().ToListAsync(); - userActivity = userActivity.Where(a => a.Activity.Timestamp < start && a.Activity.Timestamp > end); + List outerGroups = groups.ToOuterActivityGroups(); - List> groups = await GroupActivities(userActivity).ToListAsync(); + long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); - DateTime oldestTime = groups.Max(g => g.Max(a => a.Timestamp)); - long oldestTimestamp = new DateTimeOffset(oldestTime).ToUnixTimeMilliseconds(); + await this.CacheEntities(outerGroups); - return this.Ok( - await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp)); + return this.Ok(GameStream.CreateFromGroups(token, + outerGroups, + times.Start.ToUnixTimeMilliseconds(), + oldestTimestamp)); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/NewsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/NewsController.cs new file mode 100644 index 000000000..9fb7df7c9 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Controllers/NewsController.cs @@ -0,0 +1,31 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Serialization.News; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; + +[ApiController] +[Authorize] +[Route("LITTLEBIGPLANETPS3_XML/")] +[Produces("text/xml")] +public class NewsController : ControllerBase +{ + private readonly DatabaseContext database; + + public NewsController(DatabaseContext database) + { + this.database = database; + } + + [HttpGet("news")] + public async Task GetNews() + { + List websiteAnnouncements = + await this.database.WebsiteAnnouncements.OrderByDescending(a => a.AnnouncementId).ToListAsync(); + + return this.Ok(GameNews.CreateFromEntity(websiteAnnouncements)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index c32df3384..8e0772461 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -141,6 +141,22 @@ public async Task PostReview(int slotId) return this.Ok(); } + [HttpGet("review/user/{slotId:int}/{reviewerName}")] + public async Task GetReview(int slotId, string reviewerName) + { + GameTokenEntity token = this.GetToken(); + + int reviewerId = await this.database.Users.Where(u => u.Username == reviewerName) + .Select(s => s.UserId) + .FirstOrDefaultAsync(); + if (reviewerId == 0) return this.NotFound(); + + ReviewEntity? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.ReviewerId == reviewerId && r.SlotId == slotId); + if (review == null) return this.NotFound(); + + return this.Ok(GameReview.CreateFromEntity(review, token)); + } + [HttpGet("reviewsFor/user/{slotId:int}")] public async Task ReviewsFor(int slotId) { diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs index 064af5ac9..367335f97 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs @@ -2,7 +2,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Level; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs index d0930e0af..ff35092bc 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata; using LBPUnion.ProjectLighthouse.Helpers; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs index d45c1ff0d..f6417ab35 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; diff --git a/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs b/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs index 7c654a6f2..5f8d6a20c 100644 --- a/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs +++ b/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs @@ -1,5 +1,5 @@ using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Users; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs index b483f546f..fb6003bfd 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs @@ -1,7 +1,7 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs index 606557a77..ee8396d01 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs @@ -2,7 +2,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs new file mode 100644 index 000000000..169ccd421 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -0,0 +1,727 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity; + +[Trait("Category", "Unit")] +public class ActivityEventHandlerTests +{ + #region Entity Inserts + [Fact] + public async Task Level_Insert_ShouldCreatePublishActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + CreatorId = 1, + SlotId = 1, + }; + database.Slots.Add(slot); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, slot); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.PublishLevel && a.SlotId == 1)); + } + + [Fact] + public async Task LevelComment_Insert_ShouldCreateCommentActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + CommentEntity comment = new() + { + CommentId = 1, + PosterUserId = 1, + TargetId = 1, + Type = CommentType.Level, + }; + database.Comments.Add(comment); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, comment); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.CommentOnLevel && a.CommentId == 1)); + } + + [Fact] + public async Task ProfileComment_Insert_ShouldCreateCommentActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + CommentEntity comment = new() + { + CommentId = 1, + PosterUserId = 1, + TargetId = 1, + Type = CommentType.Profile, + }; + database.Comments.Add(comment); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, comment); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.CommentOnUser && a.CommentId == 1)); + } + + [Fact] + public async Task Photo_Insert_ShouldCreatePhotoActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + PhotoEntity photo = new() + { + PhotoId = 1, + CreatorId = 1, + }; + database.Photos.Add(photo); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, photo); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UploadPhoto && a.PhotoId == 1)); + } + + [Fact] + public async Task Score_Insert_ShouldCreateScoreActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + ScoreEntity score = new() + { + ScoreId = 1, + SlotId = 1, + PlayerIdCollection = "test", + }; + database.Scores.Add(score); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, score); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.Score && a.ScoreId == 1)); + } + + [Fact] + public async Task HeartedLevel_Insert_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + await database.SaveChangesAsync(); + + HeartedLevelEntity heartedLevel = new() + { + HeartedLevelId = 1, + UserId = 1, + SlotId = 1, + }; + + eventHandler.OnEntityInserted(database, heartedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1)); + } + + [Fact] + public async Task HeartedProfile_Insert_ShouldCreateUserActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + HeartedProfileEntity heartedProfile = new() + { + HeartedProfileId = 1, + UserId = 1, + HeartedUserId = 1, + }; + + eventHandler.OnEntityInserted(database, heartedProfile); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1)); + } + + [Fact] + public async Task HeartedPlaylist_Insert_ShouldCreatePlaylistActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + PlaylistEntity playlist = new() + { + PlaylistId = 1, + CreatorId = 1, + }; + database.Playlists.Add(playlist); + await database.SaveChangesAsync(); + + HeartedPlaylistEntity heartedPlaylist = new() + { + HeartedPlaylistId = 1, + UserId = 1, + PlaylistId = 1, + }; + + eventHandler.OnEntityInserted(database, heartedPlaylist); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartPlaylist && a.PlaylistId == 1)); + } + + [Fact] + public async Task VisitedLevel_Insert_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + await database.SaveChangesAsync(); + + VisitedLevelEntity visitedLevel = new() + { + VisitedLevelId = 1, + UserId = 1, + SlotId = 1, + }; + + eventHandler.OnEntityInserted(database, visitedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.PlayLevel && a.SlotId == 1)); + } + + [Fact] + public async Task Review_Insert_ShouldCreateReviewActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + ReviewEntity review = new() + { + ReviewId = 1, + ReviewerId = 1, + SlotId = 1, + }; + database.Reviews.Add(review); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, review); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.ReviewLevel && a.ReviewId == 1)); + } + + [Fact] + public async Task RatedLevel_WithRatingInsert_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + await database.SaveChangesAsync(); + + RatedLevelEntity ratedLevel = new() + { + RatedLevelId = 1, + UserId = 1, + SlotId = 1, + Rating = 1, + }; + + eventHandler.OnEntityInserted(database, ratedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.DpadRateLevel && a.SlotId == 1)); + } + + [Fact] + public async Task RatedLevel_WithLBP1RatingInsert_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + await database.SaveChangesAsync(); + + RatedLevelEntity ratedLevel = new() + { + RatedLevelId = 1, + UserId = 1, + SlotId = 1, + RatingLBP1 = 5, + }; + + eventHandler.OnEntityInserted(database, ratedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.RateLevel && a.SlotId == 1)); + } + + [Fact] + public async Task Playlist_Insert_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + PlaylistEntity playlist = new() + { + PlaylistId = 1, + CreatorId = 1, + }; + database.Playlists.Add(playlist); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, playlist); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.CreatePlaylist && a.PlaylistId == 1)); + } + #endregion + + #region Entity changes + [Fact] + public async Task VisitedLevel_WithNoChange_ShouldNotCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + await database.SaveChangesAsync(); + + VisitedLevelEntity visitedLevel = new() + { + VisitedLevelId = 1, + UserId = 1, + SlotId = 1, + }; + + eventHandler.OnEntityChanged(database, visitedLevel, visitedLevel); + + Assert.Null(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.PlayLevel && a.SlotId == 1)); + } + + [Fact] + public async Task VisitedLevel_WithChange_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + await database.SaveChangesAsync(); + + VisitedLevelEntity oldVisitedLevel = new() + { + VisitedLevelId = 1, + UserId = 1, + SlotId = 1, + PlaysLBP2 = 1, + }; + + VisitedLevelEntity newVisitedLevel = new() + { + VisitedLevelId = 1, + UserId = 1, + SlotId = 1, + PlaysLBP2 = 2, + }; + + eventHandler.OnEntityChanged(database, oldVisitedLevel, newVisitedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.PlayLevel && a.SlotId == 1)); + } + + [Fact] + public async Task Slot_WithTeamPickChange_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity oldSlot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(oldSlot); + await database.SaveChangesAsync(); + + SlotEntity newSlot = new() + { + SlotId = 1, + CreatorId = 1, + TeamPick = true, + }; + + eventHandler.OnEntityChanged(database, oldSlot, newSlot); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.MMPickLevel && a.SlotId == 1)); + } + + [Fact] + public async Task Slot_WithRepublish_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity oldSlot = new() + { + SlotId = 1, + CreatorId = 1, + }; + + database.Slots.Add(oldSlot); + await database.SaveChangesAsync(); + + SlotEntity newSlot = new() + { + SlotId = 1, + CreatorId = 1, + LastUpdated = 1, + }; + + eventHandler.OnEntityChanged(database, oldSlot, newSlot); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.PublishLevel && a.SlotId == 1)); + } + + [Fact] + public async Task Comment_WithDeletion_ShouldCreateCommentActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + CommentEntity oldComment = new() + { + CommentId = 1, + PosterUserId = 1, + Type = CommentType.Level, + }; + + database.Comments.Add(oldComment); + await database.SaveChangesAsync(); + + CommentEntity newComment = new() + { + CommentId = 1, + PosterUserId = 1, + Type = CommentType.Level, + Deleted = true, + }; + + eventHandler.OnEntityChanged(database, oldComment, newComment); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.DeleteLevelComment && a.CommentId == 1)); + } + + [Fact] + public async Task Playlist_WithSlotsChanged_ShouldCreatePlaylistWithSlotActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + PlaylistEntity oldPlaylist = new() + { + PlaylistId = 1, + CreatorId = 1, + }; + + database.Playlists.Add(oldPlaylist); + await database.SaveChangesAsync(); + + PlaylistEntity newPlaylist = new() + { + PlaylistId = 1, + CreatorId = 1, + SlotCollection = "1", + }; + + eventHandler.OnEntityChanged(database, oldPlaylist, newPlaylist); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.AddLevelToPlaylist && a.PlaylistId == 1 && a.SlotId == 1)); + } + #endregion + + #region Entity deletion + [Fact] + public async Task HeartedLevel_Delete_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + HeartedLevelEntity heartedLevel = new() + { + HeartedLevelId = 1, + UserId = 1, + SlotId = 1, + }; + + database.HeartedLevels.Add(heartedLevel); + await database.SaveChangesAsync(); + + eventHandler.OnEntityDeleted(database, heartedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UnheartLevel && a.SlotId == 1)); + } + + [Fact] + public async Task HeartedProfile_Delete_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + HeartedProfileEntity heartedProfile = new() + { + HeartedProfileId = 1, + UserId = 1, + HeartedUserId = 1, + }; + + database.HeartedProfiles.Add(heartedProfile); + await database.SaveChangesAsync(); + + eventHandler.OnEntityDeleted(database, heartedProfile); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1)); + } + #endregion +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs new file mode 100644 index 000000000..1aa1d38d3 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs @@ -0,0 +1,6 @@ +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +public class ActivityControllerTests +{ + //TODO write activity controller tests +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ControllerExtensionTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ControllerExtensionTests.cs index 4a00ab680..00d986938 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ControllerExtensionTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ControllerExtensionTests.cs @@ -1,5 +1,5 @@ using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; using LBPUnion.ProjectLighthouse.Tests.Helpers; diff --git a/ProjectLighthouse.Tests/Unit/FilterTests.cs b/ProjectLighthouse.Tests/Unit/FilterTests.cs index a3eae2e90..0be1bdbb5 100644 --- a/ProjectLighthouse.Tests/Unit/FilterTests.cs +++ b/ProjectLighthouse.Tests/Unit/FilterTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Levels; diff --git a/ProjectLighthouse/Database/DatabaseContext.cs b/ProjectLighthouse/Database/DatabaseContext.cs index 463945296..6fc43ccfa 100644 --- a/ProjectLighthouse/Database/DatabaseContext.cs +++ b/ProjectLighthouse/Database/DatabaseContext.cs @@ -1,3 +1,4 @@ +using System; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; @@ -90,30 +91,34 @@ public DatabaseContext(DbContextOptions options) : base(options public static DatabaseContext CreateNewInstance() { DbContextOptionsBuilder builder = new(); - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); + ConfigureBuilder()(builder); return new DatabaseContext(builder.Options); } + public static Action ConfigureBuilder() + { + return builder => + { + builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, + MySqlServerVersion.LatestSupportedServerVersion); + builder.AddInterceptors(new ActivityInterceptor(new ActivityEntityEventHandler())); + }; + } + #region Activity protected override void OnModelCreating(ModelBuilder modelBuilder) { - //TODO implement reviews modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); base.OnModelCreating(modelBuilder); } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.AddInterceptors(new ActivityInterceptor(new ActivityEntityEventHandler())); - base.OnConfiguring(optionsBuilder); - } #endregion } \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs new file mode 100644 index 000000000..e2b1363df --- /dev/null +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Linq; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Extensions; + +public static class ActivityQueryExtensions +{ + public static List GetIds(this IReadOnlyCollection groups, ActivityGroupType type) + { + List ids = new(); + // Add outer group ids + ids.AddRange(groups.Where(g => g.Key.GroupType == type) + .Where(g => g.Key.TargetId != 0) + .Select(g => g.Key.TargetId) + .ToList()); + + // Add specific event ids + ids.AddRange(groups.SelectMany(g => + g.Groups.SelectMany(gr => gr.Where(a => a.GroupType == type).Select(a => a.TargetId)))); + if (type == ActivityGroupType.User) + { + ids.AddRange(groups.Where(g => g.Key.GroupType is not ActivityGroupType.News) + .SelectMany(g => g.Groups.Select(a => a.Key.UserId))); + } + + return ids.Distinct().ToList(); + } + + public static IQueryable> ToActivityGroups + (this IQueryable activityQuery, bool groupByActor = false) => + groupByActor + ? activityQuery.GroupBy(dto => new ActivityGroup + { + Timestamp = dto.Activity.Timestamp.Date, + UserId = dto.Activity.UserId, + TargetNewsId = dto.TargetNewsId ?? 0, + TargetTeamPickSlotId = dto.TargetTeamPickId ?? 0, + }) + : activityQuery.GroupBy(dto => new ActivityGroup + { + Timestamp = dto.Activity.Timestamp.Date, + TargetUserId = dto.TargetUserId ?? 0, + TargetSlotId = dto.TargetSlotId ?? 0, + TargetPlaylistId = dto.TargetPlaylistId ?? 0, + TargetNewsId = dto.TargetNewsId ?? 0, + }); + + public static List ToOuterActivityGroups + (this IEnumerable> activityGroups, bool groupByActor = false) => + // Pin news posts to the top + activityGroups.OrderByDescending(g => g.Key.GroupType == ActivityGroupType.News ? 1 : 0) + .ThenByDescending(g => g.MaxBy(a => a.Activity.Timestamp)?.Activity.Timestamp ?? g.Key.Timestamp) + .Select(g => new OuterActivityGroup + { + Key = g.Key, + Groups = g.OrderByDescending(a => a.Activity.Timestamp) + .GroupBy(gr => new InnerActivityGroup + { + Type = groupByActor ? gr.GroupType : gr.GroupType != ActivityGroupType.News ? ActivityGroupType.User : ActivityGroupType.News, + UserId = gr.Activity.UserId, + TargetId = groupByActor ? gr.TargetId : gr.Activity.UserId, + }) + .ToList(), + }) + .ToList(); + + // WARNING - To the next person who tries to improve this code: As of writing this, it's not possible + // to build a pattern matching switch statement with expression trees. so the only other option + // is to basically rewrite this nested ternary mess with expression trees which isn't much better + // The resulting SQL generated by EntityFramework uses a CASE statement which is probably fine + public static IQueryable ToActivityDto + (this IQueryable activityQuery, bool includeSlotCreator = false, bool includeTeamPick = false) + { + return activityQuery.Select(a => new ActivityDto + { + Activity = a, + TargetSlotId = a is LevelActivityEntity + ? ((LevelActivityEntity)a).SlotId + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.PhotoId != 0 + ? ((PhotoActivityEntity)a).Photo.SlotId + : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level + ? ((CommentActivityEntity)a).Comment.TargetId + : a is ScoreActivityEntity + ? ((ScoreActivityEntity)a).Score.SlotId + : a is ReviewActivityEntity + ? ((ReviewActivityEntity)a).Review.SlotId + : 0, + + TargetUserId = a is UserActivityEntity + ? ((UserActivityEntity)a).TargetUserId + : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile + ? ((CommentActivityEntity)a).Comment.TargetId + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 + ? ((PhotoActivityEntity)a).Photo.CreatorId + : 0, + TargetPlaylistId = a is PlaylistActivityEntity || a is PlaylistWithSlotActivityEntity + ? ((PlaylistActivityEntity)a).PlaylistId + : 0, + TargetNewsId = a is NewsActivityEntity ? ((NewsActivityEntity)a).NewsId : 0, + TargetTeamPickId = includeTeamPick + ? a.Type == EventType.MMPickLevel && a is LevelActivityEntity ? ((LevelActivityEntity)a).SlotId : 0 + : 0, + TargetSlotCreatorId = includeSlotCreator + ? a is LevelActivityEntity + ? ((LevelActivityEntity)a).Slot.CreatorId + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 + ? ((PhotoActivityEntity)a).Photo.Slot!.CreatorId + : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level + ? ((CommentActivityEntity)a).Comment.TargetId + : a is ScoreActivityEntity + ? ((ScoreActivityEntity)a).Score.Slot.CreatorId + : a is ReviewActivityEntity + ? ((ReviewActivityEntity)a).Review.Slot!.CreatorId + : 0 + : 0, + }); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/ControllerExtensions.cs b/ProjectLighthouse/Extensions/ControllerExtensions.cs index db779f21b..cb955398b 100644 --- a/ProjectLighthouse/Extensions/ControllerExtensions.cs +++ b/ProjectLighthouse/Extensions/ControllerExtensions.cs @@ -44,6 +44,7 @@ public static async Task ReadBodyAsync(this ControllerBase controller) public static async Task DeserializeBody(this ControllerBase controller, params string[] rootElements) { string bodyString = await controller.ReadBodyAsync(); + if (bodyString.Length == 0) return default; try { // Prevent unescaped ampersands from causing deserialization to fail diff --git a/ProjectLighthouse/Filter/ActivityQueryBuilder.cs b/ProjectLighthouse/Filter/ActivityQueryBuilder.cs new file mode 100644 index 000000000..0ff56fbeb --- /dev/null +++ b/ProjectLighthouse/Filter/ActivityQueryBuilder.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter; + +public class ActivityQueryBuilder : IQueryBuilder +{ + private readonly List filters; + + public ActivityQueryBuilder() + { + this.filters = new List(); + } + + public Expression> Build() + { + Expression> predicate = PredicateExtensions.True(); + predicate = this.filters.Aggregate(predicate, (current, filter) => current.And(filter.GetPredicate())); + return predicate; + } + + public ActivityQueryBuilder RemoveFilter(Type type) + { + this.filters.RemoveAll(f => f.GetType() == type); + return this; + } + + #nullable enable + public IEnumerable GetFilters(Type type) => this.filters.Where(f => f.GetType() == type).ToList(); + #nullable disable + + public ActivityQueryBuilder AddFilter(int index, IActivityFilter filter) + { + this.filters.Insert(index, filter); + return this; + } + + public ActivityQueryBuilder Clone() + { + ActivityQueryBuilder clone = new(); + clone.filters.AddRange(this.filters); + return clone; + } + + public ActivityQueryBuilder AddFilter(IActivityFilter filter) + { + this.filters.Add(filter); + return this; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/Activity/EventTypeFilter.cs b/ProjectLighthouse/Filter/Filters/Activity/EventTypeFilter.cs new file mode 100644 index 000000000..80d981139 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/Activity/EventTypeFilter.cs @@ -0,0 +1,27 @@ +#nullable enable +using System; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity; + +public class EventTypeFilter : IActivityFilter +{ + private readonly EventType[] events; + + public EventTypeFilter(params EventType[] events) + { + this.events = events; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.False(); + predicate = this.events.Aggregate(predicate, + (current, eventType) => current.Or(a => a.Activity.Type == eventType)); + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/Activity/ExcludeNewsFilter.cs b/ProjectLighthouse/Filter/Filters/Activity/ExcludeNewsFilter.cs new file mode 100644 index 000000000..36d545c8d --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/Activity/ExcludeNewsFilter.cs @@ -0,0 +1,12 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity; + +public class ExcludeNewsFilter : IActivityFilter +{ + public Expression> GetPredicate() => a => a.Activity is NewsActivityEntity && a.Activity.Type != EventType.NewsPost; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/Activity/IncludeNewsFilter.cs b/ProjectLighthouse/Filter/Filters/Activity/IncludeNewsFilter.cs new file mode 100644 index 000000000..ddd489a51 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/Activity/IncludeNewsFilter.cs @@ -0,0 +1,14 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity; + +public class IncludeNewsFilter : IActivityFilter +{ + public Expression> GetPredicate() => + a => (a.Activity is NewsActivityEntity && a.Activity.Type == EventType.NewsPost) || + a.Activity.Type == EventType.MMPickLevel; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/Activity/IncludeUserIdFilter.cs b/ProjectLighthouse/Filter/Filters/Activity/IncludeUserIdFilter.cs new file mode 100644 index 000000000..c158a24df --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/Activity/IncludeUserIdFilter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity; + +public class IncludeUserIdFilter : IActivityFilter +{ + private readonly IEnumerable userIds; + private readonly EventTypeFilter eventFilter; + + public IncludeUserIdFilter(IEnumerable userIds, EventTypeFilter eventFilter = null) + { + this.userIds = userIds; + this.eventFilter = eventFilter; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.False(); + predicate = this.userIds.Aggregate(predicate, (current, friendId) => current.Or(a => a.Activity.UserId == friendId)); + if (this.eventFilter != null) predicate = predicate.And(this.eventFilter.GetPredicate()); + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/Activity/MyLevelActivityFilter.cs b/ProjectLighthouse/Filter/Filters/Activity/MyLevelActivityFilter.cs new file mode 100644 index 000000000..5ab3ccac9 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/Activity/MyLevelActivityFilter.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity; + +public class MyLevelActivityFilter : IActivityFilter +{ + private readonly int userId; + private readonly EventTypeFilter eventFilter; + + public MyLevelActivityFilter(int userId, EventTypeFilter eventFilter = null) + { + this.userId = userId; + this.eventFilter = eventFilter; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.False(); + predicate = predicate.Or(a => a.TargetSlotCreatorId == this.userId); + if (this.eventFilter != null) predicate = predicate.And(this.eventFilter.GetPredicate()); + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/Activity/PlaylistActivityFilter.cs b/ProjectLighthouse/Filter/Filters/Activity/PlaylistActivityFilter.cs new file mode 100644 index 000000000..c35c00eee --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/Activity/PlaylistActivityFilter.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity; + +public class PlaylistActivityFilter : IActivityFilter +{ + private readonly List playlistIds; + private readonly EventTypeFilter eventFilter; + + public PlaylistActivityFilter(List playlistIds, EventTypeFilter eventFilter = null) + { + this.playlistIds = playlistIds; + this.eventFilter = eventFilter; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.False(); + predicate = this.playlistIds.Aggregate(predicate, (current, playlistId) => current.Or(a => (a.Activity is PlaylistActivityEntity || a.Activity is PlaylistWithSlotActivityEntity) && a.TargetPlaylistId == playlistId)); + if (this.eventFilter != null) predicate = predicate.And(this.eventFilter.GetPredicate()); + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/AdventureFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/AdventureFilter.cs similarity index 83% rename from ProjectLighthouse/Filter/Filters/AdventureFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/AdventureFilter.cs index c77563611..3dfdebbca 100644 --- a/ProjectLighthouse/Filter/Filters/AdventureFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/AdventureFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class AdventureFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/AuthorLabelFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/AuthorLabelFilter.cs similarity index 78% rename from ProjectLighthouse/Filter/Filters/AuthorLabelFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/AuthorLabelFilter.cs index e203cf672..8d9674477 100644 --- a/ProjectLighthouse/Filter/Filters/AuthorLabelFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/AuthorLabelFilter.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class AuthorLabelFilter : ISlotFilter { @@ -20,7 +20,7 @@ public Expression> GetPredicate() { Expression> predicate = PredicateExtensions.True(); predicate = this.labels.Aggregate(predicate, - (current, label) => current.And(s => s.AuthorLabels.Contains(label))); + (current, label) => PredicateExtensions.And(current, s => s.AuthorLabels.Contains(label))); return predicate; } } \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/CreatorFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/CreatorFilter.cs similarity index 87% rename from ProjectLighthouse/Filter/Filters/CreatorFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/CreatorFilter.cs index a3283b54c..22e26a20c 100644 --- a/ProjectLighthouse/Filter/Filters/CreatorFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/CreatorFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class CreatorFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/CrossControlFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/CrossControlFilter.cs similarity index 83% rename from ProjectLighthouse/Filter/Filters/CrossControlFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/CrossControlFilter.cs index 37a8bd81b..395a6efd3 100644 --- a/ProjectLighthouse/Filter/Filters/CrossControlFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/CrossControlFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class CrossControlFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/ExcludeAdventureFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/ExcludeAdventureFilter.cs similarity index 83% rename from ProjectLighthouse/Filter/Filters/ExcludeAdventureFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/ExcludeAdventureFilter.cs index 6d6510d6f..c518d4014 100644 --- a/ProjectLighthouse/Filter/Filters/ExcludeAdventureFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ExcludeAdventureFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class ExcludeAdventureFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/ExcludeCrossControlFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/ExcludeCrossControlFilter.cs similarity index 84% rename from ProjectLighthouse/Filter/Filters/ExcludeCrossControlFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/ExcludeCrossControlFilter.cs index 069eb3459..15cd1d9ee 100644 --- a/ProjectLighthouse/Filter/Filters/ExcludeCrossControlFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ExcludeCrossControlFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class ExcludeCrossControlFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/ExcludeLBP1OnlyFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/ExcludeLBP1OnlyFilter.cs similarity index 92% rename from ProjectLighthouse/Filter/Filters/ExcludeLBP1OnlyFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/ExcludeLBP1OnlyFilter.cs index 5d8130081..3a2758852 100644 --- a/ProjectLighthouse/Filter/Filters/ExcludeLBP1OnlyFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ExcludeLBP1OnlyFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Users; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class ExcludeLBP1OnlyFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/ExcludeMovePackFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/ExcludeMovePackFilter.cs similarity index 83% rename from ProjectLighthouse/Filter/Filters/ExcludeMovePackFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/ExcludeMovePackFilter.cs index 2b03fab7f..33e157085 100644 --- a/ProjectLighthouse/Filter/Filters/ExcludeMovePackFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ExcludeMovePackFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class ExcludeMovePackFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/FirstUploadedFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/FirstUploadedFilter.cs similarity index 93% rename from ProjectLighthouse/Filter/Filters/FirstUploadedFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/FirstUploadedFilter.cs index 07e4d8659..818585a0e 100644 --- a/ProjectLighthouse/Filter/Filters/FirstUploadedFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/FirstUploadedFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class FirstUploadedFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/GameVersionFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/GameVersionFilter.cs similarity index 93% rename from ProjectLighthouse/Filter/Filters/GameVersionFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/GameVersionFilter.cs index b34447390..425e72aed 100644 --- a/ProjectLighthouse/Filter/Filters/GameVersionFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/GameVersionFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Users; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class GameVersionFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/GameVersionListFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/GameVersionListFilter.cs similarity index 78% rename from ProjectLighthouse/Filter/Filters/GameVersionListFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/GameVersionListFilter.cs index 813b2e757..6e64b2638 100644 --- a/ProjectLighthouse/Filter/Filters/GameVersionListFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/GameVersionListFilter.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Users; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class GameVersionListFilter : ISlotFilter { @@ -19,5 +19,5 @@ public GameVersionListFilter(params GameVersion[] versions) public Expression> GetPredicate() => this.versions.Aggregate(PredicateExtensions.False(), - (current, version) => current.Or(s => s.GameVersion == version)); + (current, version) => PredicateExtensions.Or(current, s => s.GameVersion == version)); } \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/HiddenSlotFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/HiddenSlotFilter.cs similarity index 82% rename from ProjectLighthouse/Filter/Filters/HiddenSlotFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/HiddenSlotFilter.cs index eab9820b6..38999a1d2 100644 --- a/ProjectLighthouse/Filter/Filters/HiddenSlotFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/HiddenSlotFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class HiddenSlotFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/MovePackFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/MovePackFilter.cs similarity index 82% rename from ProjectLighthouse/Filter/Filters/MovePackFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/MovePackFilter.cs index 5c29834ae..c7170ce8b 100644 --- a/ProjectLighthouse/Filter/Filters/MovePackFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/MovePackFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class MovePackFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/PlayerCountFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/PlayerCountFilter.cs similarity index 93% rename from ProjectLighthouse/Filter/Filters/PlayerCountFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/PlayerCountFilter.cs index 71a647334..fd68f4a22 100644 --- a/ProjectLighthouse/Filter/Filters/PlayerCountFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/PlayerCountFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class PlayerCountFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/ResultTypeFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/ResultTypeFilter.cs similarity index 89% rename from ProjectLighthouse/Filter/Filters/ResultTypeFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/ResultTypeFilter.cs index 3460fb57f..427e2a149 100644 --- a/ProjectLighthouse/Filter/Filters/ResultTypeFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ResultTypeFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class ResultTypeFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/SlotIdFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/SlotIdFilter.cs similarity index 82% rename from ProjectLighthouse/Filter/Filters/SlotIdFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/SlotIdFilter.cs index 174127122..0d9005492 100644 --- a/ProjectLighthouse/Filter/Filters/SlotIdFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/SlotIdFilter.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class SlotIdFilter : ISlotFilter { @@ -20,7 +20,7 @@ public SlotIdFilter(List slotIds) public Expression> GetPredicate() { Expression> predicate = PredicateExtensions.False(); - predicate = this.slotIds.Aggregate(predicate, (current, slotId) => current.Or(s => s.SlotId == slotId)); + predicate = this.slotIds.Aggregate(predicate, (current, slotId) => PredicateExtensions.Or(current, s => s.SlotId == slotId)); return predicate; } } \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/SlotTypeFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/SlotTypeFilter.cs similarity index 89% rename from ProjectLighthouse/Filter/Filters/SlotTypeFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/SlotTypeFilter.cs index 96ea578d1..0b1c1a79d 100644 --- a/ProjectLighthouse/Filter/Filters/SlotTypeFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/SlotTypeFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class SlotTypeFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/SubLevelFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/SubLevelFilter.cs similarity index 87% rename from ProjectLighthouse/Filter/Filters/SubLevelFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/SubLevelFilter.cs index 2116ae661..b7365c0d4 100644 --- a/ProjectLighthouse/Filter/Filters/SubLevelFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/SubLevelFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class SubLevelFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/TeamPickFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/TeamPickFilter.cs similarity index 83% rename from ProjectLighthouse/Filter/Filters/TeamPickFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/TeamPickFilter.cs index eb77c5ea0..ad9088c8a 100644 --- a/ProjectLighthouse/Filter/Filters/TeamPickFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/TeamPickFilter.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class TeamPickFilter : ISlotFilter { diff --git a/ProjectLighthouse/Filter/Filters/TextFilter.cs b/ProjectLighthouse/Filter/Filters/Slot/TextFilter.cs similarity index 94% rename from ProjectLighthouse/Filter/Filters/TextFilter.cs rename to ProjectLighthouse/Filter/Filters/Slot/TextFilter.cs index 7566ca444..007785170 100644 --- a/ProjectLighthouse/Filter/Filters/TextFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/TextFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Filter.Filters; +namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot; public class TextFilter : ISlotFilter { diff --git a/ProjectLighthouse/Migrations/20230725013522_InitialActivity.cs b/ProjectLighthouse/Migrations/20230725013522_InitialActivity.cs new file mode 100644 index 000000000..da18f6cf6 --- /dev/null +++ b/ProjectLighthouse/Migrations/20230725013522_InitialActivity.cs @@ -0,0 +1,149 @@ +using System; +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20230725013522_InitialActivity")] + public partial class InitialActivity : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Activities", + columns: table => new + { + ActivityId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Timestamp = table.Column(type: "datetime(6)", nullable: false), + UserId = table.Column(type: "int", nullable: false), + Type = table.Column(type: "int", nullable: false), + Discriminator = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CommentId = table.Column(type: "int", nullable: true), + SlotId = table.Column(type: "int", nullable: true), + NewsId = table.Column(type: "int", nullable: true), + PhotoId = table.Column(type: "int", nullable: true), + PlaylistId = table.Column(type: "int", nullable: true), + ReviewId = table.Column(type: "int", nullable: true), + ScoreId = table.Column(type: "int", nullable: true), + TargetUserId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Activities", x => x.ActivityId); + table.ForeignKey( + name: "FK_Activities_Comments_CommentId", + column: x => x.CommentId, + principalTable: "Comments", + principalColumn: "CommentId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Photos_PhotoId", + column: x => x.PhotoId, + principalTable: "Photos", + principalColumn: "PhotoId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Playlists_PlaylistId", + column: x => x.PlaylistId, + principalTable: "Playlists", + principalColumn: "PlaylistId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Reviews_ReviewId", + column: x => x.ReviewId, + principalTable: "Reviews", + principalColumn: "ReviewId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Scores_ScoreId", + column: x => x.ScoreId, + principalTable: "Scores", + principalColumn: "ScoreId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Slots_SlotId", + column: x => x.SlotId, + principalTable: "Slots", + principalColumn: "SlotId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Users_TargetUserId", + column: x => x.TargetUserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_WebsiteAnnouncements_NewsId", + column: x => x.NewsId, + principalTable: "WebsiteAnnouncements", + principalColumn: "AnnouncementId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_CommentId", + table: "Activities", + column: "CommentId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_NewsId", + table: "Activities", + column: "NewsId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_PhotoId", + table: "Activities", + column: "PhotoId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_PlaylistId", + table: "Activities", + column: "PlaylistId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_ReviewId", + table: "Activities", + column: "ReviewId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_ScoreId", + table: "Activities", + column: "ScoreId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_SlotId", + table: "Activities", + column: "SlotId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_TargetUserId", + table: "Activities", + column: "TargetUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_UserId", + table: "Activities", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Activities"); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs index 34e58686d..af5224fce 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -19,6 +19,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasAnnotation("ProductVersion", "8.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 64); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity", b => + { + b.Property("ActivityId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("ActivityId"); + + b.HasIndex("UserId"); + + b.ToTable("Activities"); + + b.HasDiscriminator("Discriminator").HasValue("ActivityEntity"); + + b.UseTphMappingStrategy(); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedLevelEntity", b => { b.Property("HeartedLevelId") @@ -1090,6 +1120,136 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("WebsiteAnnouncements"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.CommentActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.HasIndex("CommentId"); + + b.HasDiscriminator().HasValue("CommentActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("SlotId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("SlotId"); + + b.HasIndex("SlotId"); + + b.HasDiscriminator().HasValue("LevelActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("NewsId") + .HasColumnType("int"); + + b.HasIndex("NewsId"); + + b.HasDiscriminator().HasValue("NewsActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PhotoActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("PhotoId") + .HasColumnType("int"); + + b.HasIndex("PhotoId"); + + b.HasDiscriminator().HasValue("PhotoActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PlaylistActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("PlaylistId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("PlaylistId"); + + b.HasIndex("PlaylistId"); + + b.HasDiscriminator().HasValue("PlaylistActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PlaylistWithSlotActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("PlaylistId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("PlaylistId"); + + b.Property("SlotId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("SlotId"); + + b.HasIndex("PlaylistId"); + + b.HasDiscriminator().HasValue("PlaylistWithSlotActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ReviewActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("ReviewId") + .HasColumnType("int"); + + b.HasIndex("ReviewId"); + + b.HasDiscriminator().HasValue("ReviewActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ScoreActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("ScoreId") + .HasColumnType("int"); + + b.HasIndex("ScoreId"); + + b.HasDiscriminator().HasValue("ScoreActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("TargetUserId") + .HasColumnType("int"); + + b.HasIndex("TargetUserId"); + + b.HasDiscriminator().HasValue("UserActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedLevelEntity", b => { b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") @@ -1483,6 +1643,105 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Publisher"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.CommentActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", "Comment") + .WithMany() + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Comment"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Website.WebsiteAnnouncementEntity", "News") + .WithMany() + .HasForeignKey("NewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("News"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PhotoActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo") + .WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Photo"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PlaylistActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.PlaylistEntity", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playlist"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PlaylistWithSlotActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.PlaylistEntity", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playlist"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ReviewActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.ReviewEntity", "Review") + .WithMany() + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ScoreActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.ScoreEntity", "Score") + .WithMany() + .HasForeignKey("ScoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Score"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "TargetUser") + .WithMany() + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", b => { b.Navigation("PhotoSubjects"); diff --git a/ProjectLighthouse/Types/Activity/ActivityDto.cs b/ProjectLighthouse/Types/Activity/ActivityDto.cs new file mode 100644 index 000000000..b6f12d444 --- /dev/null +++ b/ProjectLighthouse/Types/Activity/ActivityDto.cs @@ -0,0 +1,33 @@ +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +public class ActivityDto +{ + public required ActivityEntity Activity { get; set; } + public int? TargetSlotId { get; set; } + public int? TargetSlotCreatorId { get; set; } + public int? TargetUserId { get; set; } + public int? TargetPlaylistId { get; set; } + public int? TargetNewsId { get; set; } + public int? TargetTeamPickId { get; set; } + + public int TargetId => + this.GroupType switch + { + ActivityGroupType.User => this.TargetUserId ?? 0, + ActivityGroupType.Level => this.TargetSlotId ?? 0, + ActivityGroupType.Playlist => this.TargetPlaylistId ?? 0, + ActivityGroupType.News => this.TargetNewsId ?? 0, + _ => this.Activity.UserId, + }; + + public ActivityGroupType GroupType => + this.TargetSlotId != 0 + ? ActivityGroupType.Level + : this.TargetUserId != 0 + ? ActivityGroupType.User + : this.TargetPlaylistId != 0 + ? ActivityGroupType.Playlist + : ActivityGroupType.News; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 60caca08d..76d84e992 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -4,12 +4,13 @@ using System.Linq; using System.Reflection; using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; using LBPUnion.ProjectLighthouse.Types.Levels; +using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Types.Activity; @@ -44,7 +45,7 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl Type = EventType.Score, ScoreId = score.ScoreId, //TODO merge score migration - // UserId = int.Parse(score.PlayerIds[0]), + UserId = database.Users.Where(u => u.Username == score.PlayerIds[0]).Select(u => u.UserId).First(), }, HeartedLevelEntity heartedLevel => new LevelActivityEntity { @@ -58,12 +59,42 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl TargetUserId = heartedProfile.HeartedUserId, UserId = heartedProfile.UserId, }, + HeartedPlaylistEntity heartedPlaylist => new PlaylistActivityEntity + { + Type = EventType.HeartPlaylist, + PlaylistId = heartedPlaylist.PlaylistId, + UserId = heartedPlaylist.UserId, + }, VisitedLevelEntity visitedLevel => new LevelActivityEntity { Type = EventType.PlayLevel, SlotId = visitedLevel.SlotId, UserId = visitedLevel.UserId, }, + ReviewEntity review => new ReviewActivityEntity + { + Type = EventType.ReviewLevel, + ReviewId = review.ReviewId, + UserId = review.ReviewerId, + }, + RatedLevelEntity ratedLevel => new LevelActivityEntity + { + Type = ratedLevel.Rating != 0 ? EventType.DpadRateLevel : EventType.RateLevel, + SlotId = ratedLevel.SlotId, + UserId = ratedLevel.UserId, + }, + PlaylistEntity playlist => new PlaylistActivityEntity + { + Type = EventType.CreatePlaylist, + PlaylistId = playlist.PlaylistId, + UserId = playlist.CreatorId, + }, + WebsiteAnnouncementEntity announcement => new NewsActivityEntity + { + Type = EventType.NewsPost, + UserId = announcement.PublisherId ?? 0, + NewsId = announcement.AnnouncementId, + }, _ => null, }; InsertActivity(database, activity); @@ -82,6 +113,7 @@ private static void InsertActivity(DatabaseContext database, ActivityEntity? act public void OnEntityChanged(DatabaseContext database, T origEntity, T currentEntity) where T : class { + #if DEBUG foreach (PropertyInfo propInfo in currentEntity.GetType().GetProperties()) { if (!propInfo.CanRead || !propInfo.CanWrite) continue; @@ -97,14 +129,19 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current Console.WriteLine($@"Orig val: {origVal?.ToString() ?? "null"}"); Console.WriteLine($@"New val: {newVal?.ToString() ?? "null"}"); } - Console.WriteLine($@"OnEntityChanged: {currentEntity.GetType().Name}"); + #endif + ActivityEntity? activity = null; switch (currentEntity) { case VisitedLevelEntity visitedLevel: { - if (origEntity is not VisitedLevelEntity) break; + if (origEntity is not VisitedLevelEntity oldVisitedLevel) break; + + int Plays(VisitedLevelEntity entity) => entity.PlaysLBP1 + entity.PlaysLBP2 + entity.PlaysLBP3; + + if (Plays(oldVisitedLevel) >= Plays(visitedLevel)) break; activity = new LevelActivityEntity { @@ -118,25 +155,88 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current { if (origEntity is not SlotEntity oldSlotEntity) break; - if (!oldSlotEntity.TeamPick && slotEntity.TeamPick) + switch (oldSlotEntity.TeamPick) { - activity = new LevelActivityEntity + // When a level is team picked + case false when slotEntity.TeamPick: + activity = new LevelActivityEntity + { + Type = EventType.MMPickLevel, + SlotId = slotEntity.SlotId, + UserId = slotEntity.CreatorId, + }; + break; + // When a level has its team pick removed then remove the corresponding activity + case true when !slotEntity.TeamPick: + database.Activities.OfType() + .Where(a => a.Type == EventType.MMPickLevel) + .Where(a => a.SlotId == slotEntity.SlotId) + .ExecuteDelete(); + break; + default: { - Type = EventType.MMPickLevel, - SlotId = slotEntity.SlotId, - UserId = SlotHelper.GetPlaceholderUserId(database).Result, - }; + if (oldSlotEntity.SlotId == slotEntity.SlotId && + slotEntity.Type == SlotType.User && + oldSlotEntity.LastUpdated != slotEntity.LastUpdated) + { + activity = new LevelActivityEntity + { + Type = EventType.PublishLevel, + SlotId = slotEntity.SlotId, + UserId = slotEntity.CreatorId, + }; + } + + break; + } } - else if (oldSlotEntity.SlotId == slotEntity.SlotId && slotEntity.Type == SlotType.User) + break; + } + case CommentEntity comment: + { + if (origEntity is not CommentEntity oldComment) break; + + if (oldComment.Deleted || !comment.Deleted) break; + + if (comment.Type != CommentType.Level) break; + + activity = new CommentActivityEntity { - activity = new LevelActivityEntity + Type = EventType.DeleteLevelComment, + CommentId = comment.CommentId, + UserId = comment.PosterUserId, + }; + break; + } + case PlaylistEntity playlist: + { + if (origEntity is not PlaylistEntity oldPlaylist) break; + + int[] newSlots = playlist.SlotIds; + int[] oldSlots = oldPlaylist.SlotIds; + Console.WriteLine($@"Old playlist slots: {string.Join(",", oldSlots)}"); + Console.WriteLine($@"New playlist slots: {string.Join(",", newSlots)}"); + + int[] addedSlots = newSlots.Except(oldSlots).ToArray(); + + Console.WriteLine($@"Added playlist slots: {string.Join(",", addedSlots)}"); + + // If no new level have been added + if (addedSlots.Length == 0) break; + + // Normally events only need 1 resulting ActivityEntity but here + // we need multiple, so we have to do the inserting ourselves. + foreach (int slotId in addedSlots) + { + ActivityEntity entity = new PlaylistWithSlotActivityEntity { - Type = EventType.PublishLevel, - SlotId = slotEntity.SlotId, - UserId = slotEntity.CreatorId, + Type = EventType.AddLevelToPlaylist, + PlaylistId = playlist.PlaylistId, + SlotId = slotId, + UserId = playlist.CreatorId, }; + InsertActivity(database, entity); } - break; } } @@ -149,17 +249,6 @@ public void OnEntityDeleted(DatabaseContext database, T entity) where T : cla Console.WriteLine($@"OnEntityDeleted: {entity.GetType().Name}"); ActivityEntity? activity = entity switch { - //TODO move this to EntityModified and use CommentEntity.Deleted - CommentEntity comment => comment.Type switch - { - CommentType.Level => new CommentActivityEntity - { - Type = EventType.DeleteLevelComment, - CommentId = comment.CommentId, - UserId = comment.PosterUserId, - }, - _ => null, - }, HeartedLevelEntity heartedLevel => new LevelActivityEntity { Type = EventType.UnheartLevel, diff --git a/ProjectLighthouse/Types/Activity/ActivityGroup.cs b/ProjectLighthouse/Types/Activity/ActivityGroup.cs index 61ae381c3..51f981b0b 100644 --- a/ProjectLighthouse/Types/Activity/ActivityGroup.cs +++ b/ProjectLighthouse/Types/Activity/ActivityGroup.cs @@ -1,31 +1,59 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Xml.Serialization; namespace LBPUnion.ProjectLighthouse.Types.Activity; -public class ActivityGroup +public struct ActivityGroup { public DateTime Timestamp { get; set; } public int UserId { get; set; } public int? TargetSlotId { get; set; } public int? TargetUserId { get; set; } public int? TargetPlaylistId { get; set; } + public int? TargetNewsId { get; set; } + public int? TargetTeamPickSlotId { get; set; } public int TargetId => this.GroupType switch { - ActivityGroupType.User => this.TargetUserId ?? 0, - ActivityGroupType.Level => this.TargetSlotId ?? 0, + ActivityGroupType.User => this.TargetUserId ?? this.UserId, + ActivityGroupType.Level => this.TargetSlotId?? 0, + ActivityGroupType.TeamPick => this.TargetTeamPickSlotId ?? 0, ActivityGroupType.Playlist => this.TargetPlaylistId ?? 0, + ActivityGroupType.News => this.TargetNewsId ?? 0, _ => this.UserId, }; public ActivityGroupType GroupType => - this.TargetSlotId != 0 + (this.TargetSlotId ?? 0) != 0 ? ActivityGroupType.Level - : this.TargetUserId != 0 + : (this.TargetUserId ?? 0) != 0 ? ActivityGroupType.User - : ActivityGroupType.Playlist; + : (this.TargetPlaylistId ?? 0) != 0 + ? ActivityGroupType.Playlist + : (this.TargetNewsId ?? 0) != 0 + ? ActivityGroupType.News + : (this.TargetTeamPickSlotId ?? 0) != 0 + ? ActivityGroupType.TeamPick + : ActivityGroupType.User; + + public override string ToString() => + $@"{this.GroupType} Group: Timestamp: {this.Timestamp}, UserId: {this.UserId}, TargetId: {this.TargetId}"; +} + +public struct OuterActivityGroup +{ + public ActivityGroup Key { get; set; } + public List> Groups { get; set; } +} + +public struct InnerActivityGroup +{ + public ActivityGroupType Type { get; set; } + public int UserId { get; set; } + public int TargetId { get; set; } } public enum ActivityGroupType @@ -38,4 +66,10 @@ public enum ActivityGroupType [XmlEnum("playlist")] Playlist, + + [XmlEnum("news")] + News, + + [XmlEnum("slot")] + TeamPick, } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/EventType.cs b/ProjectLighthouse/Types/Activity/EventType.cs index e4ec2e9ac..f70100cdb 100644 --- a/ProjectLighthouse/Types/Activity/EventType.cs +++ b/ProjectLighthouse/Types/Activity/EventType.cs @@ -2,68 +2,71 @@ namespace LBPUnion.ProjectLighthouse.Types.Activity; +/// +/// UnheartLevel, UnheartUser, DeleteLevelComment, and UnpublishLevel don't actually do anything +/// public enum EventType { [XmlEnum("heart_level")] - HeartLevel, + HeartLevel = 0, [XmlEnum("unheart_level")] - UnheartLevel, + UnheartLevel = 1, [XmlEnum("heart_user")] - HeartUser, + HeartUser = 2, [XmlEnum("unheart_user")] - UnheartUser, + UnheartUser = 3, [XmlEnum("play_level")] - PlayLevel, + PlayLevel = 4, [XmlEnum("rate_level")] - RateLevel, + RateLevel = 5, [XmlEnum("tag_level")] - TagLevel, + TagLevel = 6, [XmlEnum("comment_on_level")] - CommentOnLevel, + CommentOnLevel = 7, [XmlEnum("delete_level_comment")] - DeleteLevelComment, + DeleteLevelComment = 8, [XmlEnum("upload_photo")] - UploadPhoto, + UploadPhoto = 9, [XmlEnum("publish_level")] - PublishLevel, + PublishLevel = 10, [XmlEnum("unpublish_level")] - UnpublishLevel, + UnpublishLevel = 11, [XmlEnum("score")] - Score, + Score = 12, [XmlEnum("news_post")] - NewsPost, + NewsPost = 13, [XmlEnum("mm_pick_level")] - MMPickLevel, + MMPickLevel = 14, [XmlEnum("dpad_rate_level")] - DpadRateLevel, + DpadRateLevel = 15, [XmlEnum("review_level")] - ReviewLevel, + ReviewLevel = 16, [XmlEnum("comment_on_user")] - CommentOnUser, + CommentOnUser = 17, [XmlEnum("create_playlist")] - CreatePlaylist, + CreatePlaylist = 18, [XmlEnum("heart_playlist")] - HeartPlaylist, + HeartPlaylist = 19, [XmlEnum("add_level_to_playlist")] - AddLevelToPlaylist, + AddLevelToPlaylist = 20, } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs index e0dac1772..699a93f5b 100644 --- a/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs @@ -4,14 +4,13 @@ namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: play_level, heart_level, publish_level, +/// Supported event types: play_level, heart_level, publish_level, unheart_level, dpad_rate_level, rate_level, tag_level, mm_pick_level /// public class LevelActivityEntity : ActivityEntity { + [Column("SlotId")] public int SlotId { get; set; } [ForeignKey(nameof(SlotId))] public SlotEntity Slot { get; set; } - - } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs index 647db324e..32a768ca7 100644 --- a/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs @@ -1,11 +1,15 @@ -namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// /// Supported event types: NewsPost /// public class NewsActivityEntity : ActivityEntity { - public string Title { get; set; } = ""; + public int NewsId { get; set; } - public string Body { get; set; } = ""; + [ForeignKey(nameof(NewsId))] + public WebsiteAnnouncementEntity News { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs index fecf6b803..4d5354594 100644 --- a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs @@ -4,12 +4,37 @@ namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: CreatePlaylist, HeartPlaylist, AddLevelToPlaylist +/// Supported event types: CreatePlaylist, HeartPlaylist /// public class PlaylistActivityEntity : ActivityEntity { + [Column("PlaylistId")] public int PlaylistId { get; set; } [ForeignKey(nameof(PlaylistId))] public PlaylistEntity Playlist { get; set; } +} + +/// +/// Supported event types: AddLevelToPlaylist +/// +/// The relationship between and +/// is slightly hacky but it allows conditional reuse of columns from other ActivityEntity's +/// +/// +/// +public class PlaylistWithSlotActivityEntity : ActivityEntity +{ + [Column("PlaylistId")] + public int PlaylistId { get; set; } + + [ForeignKey(nameof(PlaylistId))] + public PlaylistEntity Playlist { get; set; } + + /// + /// This reuses the SlotId column of but has no ForeignKey definition so that it can be null + /// It effectively serves as extra storage for PlaylistActivityEntity to use for the AddLevelToPlaylistEvent + /// + [Column("SlotId")] + public int SlotId { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs index 5c2950331..9a722601f 100644 --- a/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs @@ -3,12 +3,13 @@ namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; +/// +/// Supported event types: DpadRateLevel, ReviewLevel, RateLevel, TagLevel +/// public class ReviewActivityEntity : ActivityEntity { public int ReviewId { get; set; } [ForeignKey(nameof(ReviewId))] public ReviewEntity Review { get; set; } - - // TODO review_modified? } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Filter/IActivityFilter.cs b/ProjectLighthouse/Types/Filter/IActivityFilter.cs new file mode 100644 index 000000000..659e2af14 --- /dev/null +++ b/ProjectLighthouse/Types/Filter/IActivityFilter.cs @@ -0,0 +1,6 @@ +using LBPUnion.ProjectLighthouse.Types.Activity; + +namespace LBPUnion.ProjectLighthouse.Types.Filter; + +public interface IActivityFilter : IFilter +{ } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameAddLevelToPlaylistEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameAddLevelToPlaylistEvent.cs new file mode 100644 index 000000000..384194575 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameAddLevelToPlaylistEvent.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameAddLevelToPlaylistEvent : GameEvent +{ + [XmlElement("object_playlist_id")] + public int TargetPlaylistId { get; set; } + + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameCreatePlaylistEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameCreatePlaylistEvent.cs new file mode 100644 index 000000000..94744c247 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameCreatePlaylistEvent.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameCreatePlaylistEvent : GameEvent +{ + [XmlElement("object_playlist_id")] + public int TargetPlaylistId { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameDpadRateLevelEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameDpadRateLevelEvent.cs new file mode 100644 index 000000000..d61de1c04 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameDpadRateLevelEvent.cs @@ -0,0 +1,32 @@ +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameDpadRateLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("dpad_rating")] + public int Rating { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + + this.Rating = await database.RatedLevels.Where(r => r.SlotId == slot.SlotId && r.UserId == this.UserId) + .Select(r => r.Rating) + .FirstOrDefaultAsync(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs index db9d6dcf5..c9b8079f3 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using System.Xml.Serialization; @@ -19,10 +20,19 @@ namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; [XmlInclude(typeof(GameScoreEvent))] [XmlInclude(typeof(GameHeartLevelEvent))] [XmlInclude(typeof(GameHeartUserEvent))] +[XmlInclude(typeof(GameHeartPlaylistEvent))] +[XmlInclude(typeof(GameReviewEvent))] +[XmlInclude(typeof(GamePublishLevelEvent))] +[XmlInclude(typeof(GameRateLevelEvent))] +[XmlInclude(typeof(GameDpadRateLevelEvent))] +[XmlInclude(typeof(GameTeamPickLevelEvent))] +[XmlInclude(typeof(GameNewsEvent))] +[XmlInclude(typeof(GameCreatePlaylistEvent))] +[XmlInclude(typeof(GameAddLevelToPlaylistEvent))] public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization { [XmlIgnore] - private int UserId { get; set; } + protected int UserId { get; set; } [XmlAttribute("type")] public EventType Type { get; set; } @@ -31,100 +41,190 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization public long Timestamp { get; set; } [XmlElement("actor")] + [DefaultValue(null)] public string Username { get; set; } protected async Task PrepareSerialization(DatabaseContext database) { - Console.WriteLine($@"SERIALIZATION!! {this.UserId} - {this.GetHashCode()}"); + Console.WriteLine($@"EVENT SERIALIZATION!! {this.UserId} - {this.GetHashCode()}"); UserEntity user = await database.Users.FindAsync(this.UserId); if (user == null) return; this.Username = user.Username; } - public static IEnumerable CreateFromActivityGroups(IGrouping group) + public static IEnumerable CreateFromActivities(IEnumerable activities) { List events = new(); - - // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - // Events with Count need special treatment - switch (group.Key) + List> typeGroups = activities.GroupBy(g => g.Activity.Type).ToList(); + foreach (IGrouping typeGroup in typeGroups) { - case EventType.PlayLevel: + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + // Events with Count need special treatment + switch (typeGroup.Key) { - if (group.First() is not LevelActivityEntity levelActivity) break; - - events.Add(new GamePlayLevelEvent + case EventType.PlayLevel: { - Slot = new ReviewSlot - { - SlotId = levelActivity.SlotId, - }, - Count = group.Count(), - UserId = levelActivity.UserId, - Timestamp = levelActivity.Timestamp.ToUnixTimeMilliseconds(), - Type = levelActivity.Type, - }); - break; - } - case EventType.PublishLevel: - { - if (group.First() is not LevelActivityEntity levelActivity) break; + if (typeGroup.First().Activity is not LevelActivityEntity levelActivity) break; - events.Add(new GamePublishLevelEvent - { - Slot = new ReviewSlot + events.Add(new GamePlayLevelEvent { - SlotId = levelActivity.SlotId, - }, - Count = group.Count(), - UserId = levelActivity.UserId, - Timestamp = levelActivity.Timestamp.ToUnixTimeMilliseconds(), - Type = levelActivity.Type, - }); - break; + Slot = new ReviewSlot + { + SlotId = levelActivity.SlotId, + }, + Count = typeGroup.Count(), + UserId = levelActivity.UserId, + Timestamp = levelActivity.Timestamp.ToUnixTimeMilliseconds(), + Type = levelActivity.Type, + }); + break; + } + // Everything else can be handled as normal + default: + events.AddRange(typeGroup.Select(CreateFromActivity).Where(a => a != null)); + break; } - // Everything else can be handled as normal - default: events.AddRange(group.Select(CreateFromActivity)); - break; } + return events.AsEnumerable(); } - private static GameEvent CreateFromActivity(ActivityEntity activity) + private static bool IsValidActivity(ActivityEntity activity) { - GameEvent gameEvent = activity.Type switch + return activity switch + { + CommentActivityEntity => activity.Type is EventType.CommentOnLevel or EventType.CommentOnUser + or EventType.DeleteLevelComment, + LevelActivityEntity => activity.Type is EventType.PlayLevel or EventType.HeartLevel + or EventType.UnheartLevel or EventType.DpadRateLevel or EventType.RateLevel or EventType.MMPickLevel + or EventType.PublishLevel or EventType.TagLevel, + NewsActivityEntity => activity.Type is EventType.NewsPost, + PhotoActivityEntity => activity.Type is EventType.UploadPhoto, + PlaylistActivityEntity => activity.Type is EventType.CreatePlaylist or EventType.HeartPlaylist, + PlaylistWithSlotActivityEntity => activity.Type is EventType.AddLevelToPlaylist, + ReviewActivityEntity => activity.Type is EventType.ReviewLevel, + ScoreActivityEntity => activity.Type is EventType.Score, + UserActivityEntity => activity.Type is EventType.HeartUser or EventType.UnheartUser + or EventType.CommentOnUser, + _ => false, + }; + } + + private static GameEvent CreateFromActivity(ActivityDto activity) + { + if (!IsValidActivity(activity.Activity)) + { + Console.WriteLine(@"Invalid Activity: " + activity.Activity.ActivityId); + return null; + } + + int targetId = activity.TargetId; + + GameEvent gameEvent = activity.Activity.Type switch { EventType.PlayLevel => new GamePlayLevelEvent { Slot = new ReviewSlot { - SlotId = ((LevelActivityEntity)activity).SlotId, + SlotId = targetId, + }, + }, + EventType.HeartLevel or EventType.UnheartLevel => new GameHeartLevelEvent + { + TargetSlot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.DpadRateLevel => new GameDpadRateLevelEvent + { + Slot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.Score => new GameScoreEvent + { + ScoreId = ((ScoreActivityEntity)activity.Activity).ScoreId, + Slot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.RateLevel => new GameRateLevelEvent + { + Slot = new ReviewSlot + { + SlotId = targetId }, }, EventType.CommentOnLevel => new GameSlotCommentEvent { - CommentId = ((CommentActivityEntity)activity).CommentId, + CommentId = ((CommentActivityEntity)activity.Activity).CommentId, }, EventType.CommentOnUser => new GameUserCommentEvent { - CommentId = ((CommentActivityEntity)activity).CommentId, + CommentId = ((CommentActivityEntity)activity.Activity).CommentId, }, EventType.HeartUser or EventType.UnheartUser => new GameHeartUserEvent { - TargetUserId = ((UserActivityEntity)activity).TargetUserId, + TargetUserId = targetId, }, - EventType.HeartLevel or EventType.UnheartLevel => new GameHeartLevelEvent + EventType.ReviewLevel => new GameReviewEvent { - TargetSlot = new ReviewSlot + ReviewId = ((ReviewActivityEntity)activity.Activity).ReviewId, + Slot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.UploadPhoto => new GamePhotoUploadEvent + { + Slot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.MMPickLevel => new GameTeamPickLevelEvent + { + Slot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.PublishLevel => new GamePublishLevelEvent + { + Slot = new ReviewSlot + { + SlotId = targetId, + }, + Count = 1, + }, + EventType.NewsPost => new GameNewsEvent + { + NewsId = targetId, + }, + EventType.CreatePlaylist => new GameCreatePlaylistEvent + { + TargetPlaylistId = targetId, + }, + EventType.HeartPlaylist => new GameHeartPlaylistEvent + { + TargetPlaylistId = targetId, + }, + EventType.AddLevelToPlaylist => new GameAddLevelToPlaylistEvent + { + TargetPlaylistId = targetId, + Slot = new ReviewSlot { - SlotId = ((LevelActivityEntity)activity).SlotId, + SlotId = ((PlaylistWithSlotActivityEntity)activity.Activity).SlotId, }, }, _ => new GameEvent(), }; - gameEvent.UserId = activity.UserId; - gameEvent.Type = activity.Type; - gameEvent.Timestamp = activity.Timestamp.ToUnixTimeMilliseconds(); + gameEvent.UserId = activity.Activity.UserId; + gameEvent.Type = activity.Activity.Type; + gameEvent.Timestamp = activity.Activity.Timestamp.ToUnixTimeMilliseconds(); return gameEvent; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs index 251d3f920..815834598 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs @@ -40,4 +40,15 @@ public class GameHeartLevelEvent : GameEvent this.TargetSlot = ReviewSlot.CreateFromEntity(slot); } +} + +public class GameHeartPlaylistEvent : GameEvent +{ + [XmlElement("object_playlist_id")] + public int TargetPlaylistId { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameNewsEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameNewsEvent.cs new file mode 100644 index 000000000..818f46cc8 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameNewsEvent.cs @@ -0,0 +1,9 @@ +using System.Xml.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameNewsEvent : GameEvent +{ + [XmlElement("news_id")] + public int NewsId { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs index 527c9c7bf..fa8395555 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs @@ -18,7 +18,7 @@ public class GamePhotoUploadEvent : GameEvent [XmlElement("object_slot_id")] [DefaultValue(null)] - public ReviewSlot SlotId { get; set; } + public ReviewSlot Slot { get; set; } [XmlElement("user_in_photo")] public List PhotoParticipants { get; set; } @@ -40,6 +40,6 @@ public class GamePhotoUploadEvent : GameEvent SlotEntity slot = await database.Slots.FindAsync(photo.SlotId); if (slot == null) return; - this.SlotId = ReviewSlot.CreateFromEntity(slot); + this.Slot = ReviewSlot.CreateFromEntity(slot); } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs index 74b35b7fe..4d802f2aa 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Level; @@ -12,7 +13,7 @@ public class GamePublishLevelEvent : GameEvent public ReviewSlot Slot { get; set; } [XmlElement("republish")] - public bool IsRepublish { get; set; } + public int IsRepublish { get; set; } [XmlElement("count")] public int Count { get; set; } @@ -26,6 +27,7 @@ public class GamePublishLevelEvent : GameEvent this.Slot = ReviewSlot.CreateFromEntity(slot); // TODO does this work? - this.IsRepublish = slot.LastUpdated == slot.FirstUploaded; + bool republish = Math.Abs(this.Timestamp - slot.FirstUploaded) > 5000; + this.IsRepublish = Convert.ToInt32(republish); } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameRateLevelEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameRateLevelEvent.cs new file mode 100644 index 000000000..c3fd2d56a --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameRateLevelEvent.cs @@ -0,0 +1,32 @@ +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameRateLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("rating")] + public double Rating { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + + this.Rating = await database.RatedLevels.Where(r => r.SlotId == slot.SlotId && r.UserId == this.UserId) + .Select(r => r.RatingLBP1) + .FirstOrDefaultAsync(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs index e089b9875..af8c5b824 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.Threading.Tasks; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Database; @@ -9,7 +10,7 @@ namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; public class GameReviewEvent : GameEvent { - [XmlElement("slot_id")] + [XmlElement("object_slot_id")] public ReviewSlot Slot { get; set; } [XmlElement("review_id")] @@ -21,9 +22,13 @@ public class GameReviewEvent : GameEvent public new async Task PrepareSerialization(DatabaseContext database) { + await base.PrepareSerialization(database); + ReviewEntity review = await database.Reviews.FindAsync(this.ReviewId); if (review == null) return; + this.ReviewTimestamp = this.Timestamp; + SlotEntity slot = await database.Slots.FindAsync(review.SlotId); if (slot == null) return; diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameTeamPickLevelEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameTeamPickLevelEvent.cs new file mode 100644 index 000000000..9f502dcea --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameTeamPickLevelEvent.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameTeamPickLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + + // Don't serialize usernames for team picks + this.Username = null; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameNewsStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GameNewsStreamGroup.cs new file mode 100644 index 000000000..f5030206d --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameNewsStreamGroup.cs @@ -0,0 +1,9 @@ +using System.Xml.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +public class GameNewsStreamGroup : GameStreamGroup +{ + [XmlElement("news_id")] + public int NewsId { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GamePlaylistStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GamePlaylistStreamGroup.cs new file mode 100644 index 000000000..337cd9349 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GamePlaylistStreamGroup.cs @@ -0,0 +1,9 @@ +using System.Xml.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +public class GamePlaylistStreamGroup : GameStreamGroup +{ + [XmlElement("playlist_id")] + public int PlaylistId { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs index 95dd7929d..df8fdfebc 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -1,18 +1,21 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Activity; -using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Serialization.News; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; -using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; @@ -23,10 +26,16 @@ namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; public class GameStream : ILbpSerializable, INeedsPreparationForSerialization { [XmlIgnore] - private List SlotIds { get; set; } + public List SlotIds { get; set; } [XmlIgnore] - private List UserIds { get; set; } + public List UserIds { get; set; } + + [XmlIgnore] + public List PlaylistIds { get; set; } + + [XmlIgnore] + public List NewsIds { get; set; } [XmlIgnore] private int TargetUserId { get; set; } @@ -42,84 +51,85 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization [XmlArray("groups")] [XmlArrayItem("group")] + [DefaultValue(null)] public List Groups { get; set; } [XmlArray("slots")] [XmlArrayItem("slot")] + [DefaultValue(null)] public List Slots { get; set; } [XmlArray("users")] [XmlArrayItem("user")] + [DefaultValue(null)] public List Users { get; set; } + [XmlArray("playlists")] + [XmlArrayItem("playlist")] + [DefaultValue(null)] + public List Playlists { get; set; } + [XmlArray("news")] [XmlArrayItem("item")] - public List News { get; set; } - //TODO implement lbp1 and lbp2 news objects + [DefaultValue(null)] + public List News { get; set; } public async Task PrepareSerialization(DatabaseContext database) { - if (this.SlotIds.Count > 0) + async Task> LoadEntities(List ids, Func transformation) + where TFrom : class { - this.Slots = new List(); - foreach (int slotId in this.SlotIds) + List results = new(); + if (ids.Count <= 0) return null; + foreach (int id in ids) { - SlotEntity slot = await database.Slots.FindAsync(slotId); - if (slot == null) continue; + TFrom entity = await database.Set().FindAsync(id); + if (entity == null) continue; - this.Slots.Add(SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId)); + results.Add(transformation(entity)); } - } - - if (this.UserIds.Count > 0) - { - this.Users = new List(); - foreach (int userId in this.UserIds) - { - UserEntity user = await database.Users.FindAsync(userId); - if (user == null) continue; - this.Users.Add(GameUser.CreateFromEntity(user, this.TargetGame)); - } + return results; } + + this.Slots = await LoadEntities(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId)); + this.Users = await LoadEntities(this.UserIds, user => GameUser.CreateFromEntity(user, this.TargetGame)); + this.Playlists = await LoadEntities(this.PlaylistIds, GamePlaylist.CreateFromEntity); + this.News = await LoadEntities(this.NewsIds, GameNewsObject.CreateFromEntity); } - public static async Task CreateFromEntityResult - ( - DatabaseContext database, - GameTokenEntity token, - List> results, - long startTimestamp, - long endTimestamp - ) + public static GameStream CreateFromGroups + (GameTokenEntity token, List groups, long startTimestamp, long endTimestamp) { - List slotIds = results.Where(g => g.Key.TargetSlotId != null && g.Key.TargetSlotId.Value != 0) - .Select(g => g.Key.TargetSlotId.Value) - .ToList(); - Console.WriteLine($@"slotIds: {string.Join(",", slotIds)}"); - List userIds = results.Where(g => g.Key.TargetUserId != null && g.Key.TargetUserId.Value != 0) - .Select(g => g.Key.TargetUserId.Value) - .Distinct() - .Union(results.Select(g => g.Key.UserId)) - .ToList(); - // Cache target levels and users within DbContext - await database.Slots.Where(s => slotIds.Contains(s.SlotId)).LoadAsync(); - await database.Users.Where(u => userIds.Contains(u.UserId)).LoadAsync(); - Console.WriteLine($@"userIds: {string.Join(",", userIds)}"); - Console.WriteLine($@"Stream contains {slotIds.Count} slots and {userIds.Count} users"); GameStream gameStream = new() { TargetUserId = token.UserId, TargetGame = token.GameVersion, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, - SlotIds = slotIds, - UserIds = userIds, - Groups = new List(), + SlotIds = groups.GetIds(ActivityGroupType.Level), + UserIds = groups.GetIds(ActivityGroupType.User), + PlaylistIds = groups.GetIds(ActivityGroupType.Playlist), + NewsIds = groups.GetIds(ActivityGroupType.News), }; - foreach (IGrouping group in results) + if (groups.Count == 0) return gameStream; + + gameStream.Groups = groups.Select(GameStreamGroup.CreateFromGroup).ToList(); + + // Workaround for level activity because it shouldn't contain nested activity groups + if (gameStream.Groups.Count == 1 && groups.First().Key.GroupType == ActivityGroupType.Level) { - gameStream.Groups.Add(GameStreamGroup.CreateFromGrouping(group)); + gameStream.Groups = gameStream.Groups.First().Groups; + } + + // Workaround to turn a single subgroup into the primary group for news and team picks + for (int i = 0; i < gameStream.Groups.Count; i++) + { + GameStreamGroup group = gameStream.Groups[i]; + if (group.Type is not (ActivityGroupType.TeamPick or ActivityGroupType.News)) continue; + if (group.Groups.Count > 1) continue; + + gameStream.Groups[i] = group.Groups.First(); } return gameStream; diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStreamFilter.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStreamFilter.cs new file mode 100644 index 000000000..d235b7187 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStreamFilter.cs @@ -0,0 +1,26 @@ +#nullable enable +using System.Collections.Generic; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Activity; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +[XmlRoot("stream")] +// This class should only be deserialized +public class GameStreamFilter +{ + [XmlArray("sources")] + [XmlArrayItem("source")] + public List? Sources { get; set; } +} + +[XmlRoot("source")] +public class GameStreamFilterEventSource +{ + [XmlAttribute("type")] + public string? SourceType { get; set; } + + [XmlArray("event_filters")] + [XmlArrayItem("event_filter")] + public List? Types { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs index ef783f541..4afc678b2 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs @@ -3,8 +3,8 @@ using System.ComponentModel; using System.Linq; using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Activity; -using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; using LBPUnion.ProjectLighthouse.Types.Serialization.Review; @@ -19,6 +19,8 @@ namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; /// [XmlInclude(typeof(GameUserStreamGroup))] [XmlInclude(typeof(GameSlotStreamGroup))] +[XmlInclude(typeof(GamePlaylistStreamGroup))] +[XmlInclude(typeof(GameNewsStreamGroup))] public class GameStreamGroup : ILbpSerializable { [XmlAttribute("type")] @@ -35,46 +37,62 @@ public class GameStreamGroup : ILbpSerializable [XmlArray("events")] [XmlArrayItem("event")] [DefaultValue(null)] + // ReSharper disable once MemberCanBePrivate.Global + // (the serializer can't see this if it's private) public List Events { get; set; } - public static GameStreamGroup CreateFromGrouping(IGrouping group) + public static GameStreamGroup CreateFromGroup(OuterActivityGroup group) + { + GameStreamGroup gameGroup = CreateGroup(group.Key.GroupType, + group.Key.TargetId, + streamGroup => + { + streamGroup.Timestamp = group.Groups + .Max(g => g.MaxBy(a => a.Activity.Timestamp)?.Activity.Timestamp ?? group.Key.Timestamp) + .ToUnixTimeMilliseconds(); + }); + + gameGroup.Groups = new List(group.Groups.Select(g => CreateGroup(g.Key.Type, + g.Key.TargetId, + streamGroup => + { + streamGroup.Timestamp = + g.MaxBy(a => a.Activity.Timestamp).Activity.Timestamp.ToUnixTimeMilliseconds(); + streamGroup.Events = GameEvent.CreateFromActivities(g).ToList(); + })) + .ToList()); + + return gameGroup; + } + + private static GameStreamGroup CreateGroup + (ActivityGroupType type, int targetId, Action groupAction) { - ActivityGroupType type = group.Key.GroupType; GameStreamGroup gameGroup = type switch { - ActivityGroupType.Level => new GameSlotStreamGroup + ActivityGroupType.Level or ActivityGroupType.TeamPick => new GameSlotStreamGroup { Slot = new ReviewSlot { - SlotId = group.Key.TargetId, + SlotId = targetId, }, }, ActivityGroupType.User => new GameUserStreamGroup { - UserId = group.Key.TargetId, + UserId = targetId, }, - _ => new GameStreamGroup(), - }; - gameGroup.Timestamp = new DateTimeOffset(group.Select(a => a.Timestamp).MaxBy(a => a)).ToUnixTimeMilliseconds(); - gameGroup.Type = type; - - List> eventGroups = group.OrderByDescending(a => a.Timestamp).GroupBy(g => g.Type).ToList(); - //TODO removeme debug - foreach (IGrouping bruh in eventGroups) - { - Console.WriteLine($@"group key: {bruh.Key}, count={bruh.Count()}"); - } - gameGroup.Groups = new List - { - new GameUserStreamGroup + ActivityGroupType.Playlist => new GamePlaylistStreamGroup { - UserId = group.Key.UserId, - Type = ActivityGroupType.User, - Timestamp = gameGroup.Timestamp, - Events = eventGroups.SelectMany(GameEvent.CreateFromActivityGroups).ToList(), + PlaylistId = targetId, }, + ActivityGroupType.News => new GameNewsStreamGroup + { + NewsId = targetId, + }, + _ => new GameStreamGroup(), }; - + gameGroup.Type = type; + groupAction(gameGroup); return gameGroup; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/News/GameNews.cs b/ProjectLighthouse/Types/Serialization/News/GameNews.cs new file mode 100644 index 000000000..51bfa9cdf --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/News/GameNews.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.News; + +/// +/// Used in LBP1 only +/// +[XmlRoot("news")] +public class GameNews : ILbpSerializable +{ + [XmlElement("subcategory")] + public List Entries { get; set; } + + public static GameNews CreateFromEntity(List entities) => + new() + { + Entries = entities.Select(entity => new GameNewsSubcategory + { + Item = new GameNewsItem + { + Content = new GameNewsContent + { + Frame = new GameNewsFrame + { + Title = entity.Title, + Width = 512, + Container = new List + { + new() + { + Content = entity.Content, + Width = 512, + }, + }, + }, + }, + }, + }) + .ToList(), + }; +} + +[XmlRoot("subcategory")] +public class GameNewsSubcategory : ILbpSerializable +{ + [XmlElement("item")] + public GameNewsItem Item { get; set; } +} + +public class GameNewsItem : ILbpSerializable +{ + [XmlElement("content")] + public GameNewsContent Content { get; set; } +} + +public class GameNewsContent : ILbpSerializable +{ + [XmlElement("frame")] + public GameNewsFrame Frame { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/News/GameNewsFrame.cs b/ProjectLighthouse/Types/Serialization/News/GameNewsFrame.cs new file mode 100644 index 000000000..ac4956e88 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/News/GameNewsFrame.cs @@ -0,0 +1,40 @@ +#nullable enable +using System.Collections.Generic; +using System.ComponentModel; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.News; + +[XmlRoot("frame")] +public class GameNewsFrame : ILbpSerializable +{ + [XmlAttribute("width")] + public int Width { get; set; } + + [XmlElement("title")] + public string Title { get; set; } = ""; + + [XmlElement("item")] + [DefaultValue(null)] + public List? Container { get; set; } +} + +public class GameNewsFrameContainer : ILbpSerializable +{ + [XmlAttribute("width")] + public int Width { get; set; } + + [XmlElement("content")] + [DefaultValue(null)] + public string Content { get; set; } = ""; + + [XmlElement("npHandle")] + [DefaultValue(null)] + public MinimalUserProfile? User { get; set; } + + [XmlElement("slot")] + [DefaultValue(null)] + public MinimalSlot? Slot { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs b/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs new file mode 100644 index 000000000..90d15bac1 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs @@ -0,0 +1,54 @@ +using System.ComponentModel; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.News; + +/// +/// Used in LBP2 and beyond +/// +[XmlRoot("item")] +public class GameNewsObject : ILbpSerializable +{ + [XmlElement("id")] + public int Id { get; set; } + + [XmlElement("title")] + public string Title { get; set; } + + [XmlElement("summary")] + public string Summary { get; set; } + + [XmlElement("text")] + public string Text { get; set; } + + [XmlElement("date")] + public long Timestamp { get; set; } + + [XmlElement("image")] + [DefaultValue(null)] + public GameNewsImage Image { get; set; } + + [XmlElement("category")] + public string Category { get; set; } + + public static GameNewsObject CreateFromEntity(WebsiteAnnouncementEntity entity) => + new() + { + Id = entity.AnnouncementId, + Title = entity.Title, + Summary = "there's an extra spot for summary here", + Text = entity.Content, + Category = "no_category", + }; +} + +[XmlRoot("image")] +public class GameNewsImage : ILbpSerializable +{ + [XmlElement("hash")] + public string Hash { get; set; } + + [XmlElement("alignment")] + public string Alignment { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs b/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs index 90d893293..80a9d6a14 100644 --- a/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs @@ -30,7 +30,6 @@ public class GamePlaylist : ILbpSerializable, INeedsPreparationForSerialization [XmlElement("name")] public string Name { get; set; } = ""; - [DefaultValue("")] [XmlElement("description")] public string Description { get; set; } = ""; @@ -62,6 +61,7 @@ public async Task PrepareSerialization(DatabaseContext database) Username = authorUsername, }; + this.LevelCount = this.SlotIds.Length; this.Hearts = await database.HeartedPlaylists.CountAsync(h => h.HeartedPlaylistId == this.PlaylistId); this.PlaylistQuota = ServerConfiguration.Instance.UserGeneratedContentLimits.ListsQuota; List iconList = this.SlotIds.Select(id => database.Slots.FirstOrDefault(s => s.SlotId == id)) diff --git a/ProjectLighthouse/Types/Serialization/Slot/MinimalSlot.cs b/ProjectLighthouse/Types/Serialization/Slot/MinimalSlot.cs new file mode 100644 index 000000000..c59dfccd8 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Slot/MinimalSlot.cs @@ -0,0 +1,22 @@ +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Levels; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; + +[XmlRoot("slot")] +public class MinimalSlot : ILbpSerializable +{ + [XmlElement("type")] + public SlotType Type { get; set; } + + [XmlElement("id")] + public int SlotId { get; set; } + + public MinimalSlot CreateFromEntity(SlotEntity slot) => + new() + { + Type = slot.Type, + SlotId = slot.SlotId, + }; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/User/GameUser.cs b/ProjectLighthouse/Types/Serialization/User/GameUser.cs index 054e3b6d1..ec4fdc42f 100644 --- a/ProjectLighthouse/Types/Serialization/User/GameUser.cs +++ b/ProjectLighthouse/Types/Serialization/User/GameUser.cs @@ -1,4 +1,6 @@ -using System.ComponentModel; +using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using System.Xml.Serialization; @@ -185,15 +187,21 @@ public async Task PrepareSerialization(DatabaseContext database) int entitledSlots = ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots + stats.BonusSlots; - IQueryable SlotCount(GameVersion version) - { - return database.Slots.Where(s => s.CreatorId == this.UserId && s.GameVersion == version); - } + Dictionary slotsByGame = await database.Slots.Where(s => s.CreatorId == this.UserId && !s.CrossControllerRequired) + .GroupBy(s => s.GameVersion) + .Select(g => new + { + Game = g.Key, + Count = g.Count(), + }) + .ToDictionaryAsync(k => k.Game, k => k.Count); + + int GetSlotCount(GameVersion version) => slotsByGame.TryGetValue(version, out int count) ? count : 0; if (this.TargetGame == GameVersion.LittleBigPlanetVita) { this.Lbp2EntitledSlots = entitledSlots; - this.Lbp2UsedSlots = await SlotCount(GameVersion.LittleBigPlanetVita).CountAsync(); + this.Lbp2UsedSlots = GetSlotCount(GameVersion.LittleBigPlanetVita); } else { @@ -201,9 +209,9 @@ IQueryable SlotCount(GameVersion version) this.Lbp2EntitledSlots = entitledSlots; this.CrossControlEntitledSlots = entitledSlots; this.Lbp3EntitledSlots = entitledSlots; - this.Lbp1UsedSlots = await SlotCount(GameVersion.LittleBigPlanet1).CountAsync(); - this.Lbp2UsedSlots = await SlotCount(GameVersion.LittleBigPlanet2).CountAsync(s => !s.CrossControllerRequired); - this.Lbp3UsedSlots = await SlotCount(GameVersion.LittleBigPlanet3).CountAsync(); + this.Lbp1UsedSlots = GetSlotCount(GameVersion.LittleBigPlanet1); + this.Lbp2UsedSlots = GetSlotCount(GameVersion.LittleBigPlanet2); + this.Lbp3UsedSlots = GetSlotCount(GameVersion.LittleBigPlanet3); this.Lbp1FreeSlots = this.Lbp1EntitledSlots - this.Lbp1UsedSlots; From 29e3f8661d3e12129a8e44586f5298e66b447df1 Mon Sep 17 00:00:00 2001 From: Slendy Date: Fri, 28 Jul 2023 17:59:06 -0500 Subject: [PATCH 05/40] Prevent nesting workaround for level activity from messing with global activity --- .../Controllers/ActivityController.cs | 4 ++-- ProjectLighthouse/Types/Serialization/Activity/GameStream.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index 107c3ebb3..03252a69e 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -295,7 +295,7 @@ private static void PrintOuterGroups(List outerGroups) [HttpGet("slot/{slotType}/{slotId:int}")] [HttpGet("user2/{username}")] - public async Task SlotActivity(string? slotType, int slotId, string? username, long? timestamp) + public async Task LocalActivity(string? slotType, int slotId, string? username, long? timestamp) { GameTokenEntity token = this.GetToken(); @@ -344,6 +344,6 @@ public async Task SlotActivity(string? slotType, int slotId, stri return this.Ok(GameStream.CreateFromGroups(token, outerGroups, times.Start.ToUnixTimeMilliseconds(), - oldestTimestamp)); + oldestTimestamp, isLevelActivity)); } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs index df8fdfebc..c803d7d01 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -99,7 +99,7 @@ async Task> LoadEntities(List ids, Func groups, long startTimestamp, long endTimestamp) + (GameTokenEntity token, List groups, long startTimestamp, long endTimestamp, bool dontNest = false) { GameStream gameStream = new() { @@ -117,7 +117,7 @@ public static GameStream CreateFromGroups gameStream.Groups = groups.Select(GameStreamGroup.CreateFromGroup).ToList(); // Workaround for level activity because it shouldn't contain nested activity groups - if (gameStream.Groups.Count == 1 && groups.First().Key.GroupType == ActivityGroupType.Level) + if (gameStream.Groups.Count == 1 && groups.First().Key.GroupType == ActivityGroupType.Level && !dontNest) { gameStream.Groups = gameStream.Groups.First().Groups; } From d14a0497413b4952a0ab0a6b05720429ed3a687e Mon Sep 17 00:00:00 2001 From: Slendy Date: Fri, 28 Jul 2023 18:01:34 -0500 Subject: [PATCH 06/40] Fix weird naming convention --- ProjectLighthouse/Types/Serialization/Activity/GameStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs index c803d7d01..158f166ef 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -99,7 +99,7 @@ async Task> LoadEntities(List ids, Func groups, long startTimestamp, long endTimestamp, bool dontNest = false) + (GameTokenEntity token, List groups, long startTimestamp, long endTimestamp, bool removeNesting = false) { GameStream gameStream = new() { @@ -117,7 +117,7 @@ public static GameStream CreateFromGroups gameStream.Groups = groups.Select(GameStreamGroup.CreateFromGroup).ToList(); // Workaround for level activity because it shouldn't contain nested activity groups - if (gameStream.Groups.Count == 1 && groups.First().Key.GroupType == ActivityGroupType.Level && !dontNest) + if (gameStream.Groups.Count == 1 && groups.First().Key.GroupType == ActivityGroupType.Level && removeNesting) { gameStream.Groups = gameStream.Groups.First().Groups; } From c6f79da0527422f1cfd94412a72bbbe30227d0bc Mon Sep 17 00:00:00 2001 From: Slendy Date: Sun, 13 Aug 2023 22:29:22 -0500 Subject: [PATCH 07/40] Add timestamp to WebAnnouncements --- .../Types/Entities/Website/WebsiteAnnouncementEntity.cs | 5 ++++- ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ProjectLighthouse/Types/Entities/Website/WebsiteAnnouncementEntity.cs b/ProjectLighthouse/Types/Entities/Website/WebsiteAnnouncementEntity.cs index 07b6859e4..d66bda25d 100644 --- a/ProjectLighthouse/Types/Entities/Website/WebsiteAnnouncementEntity.cs +++ b/ProjectLighthouse/Types/Entities/Website/WebsiteAnnouncementEntity.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; @@ -13,6 +14,8 @@ public class WebsiteAnnouncementEntity public string Content { get; set; } + public DateTime PublishedAt { get; set; } + #nullable enable public int? PublisherId { get; set; } diff --git a/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs b/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs index 90d15bac1..cc4b4659b 100644 --- a/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs +++ b/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Website; namespace LBPUnion.ProjectLighthouse.Types.Serialization.News; @@ -37,9 +38,10 @@ public static GameNewsObject CreateFromEntity(WebsiteAnnouncementEntity entity) { Id = entity.AnnouncementId, Title = entity.Title, - Summary = "there's an extra spot for summary here", + Summary = "", Text = entity.Content, Category = "no_category", + Timestamp = entity.PublishedAt.ToUnixTimeMilliseconds(), }; } From d20b21e98ad02e3058342e28aac3e5594aa2efe7 Mon Sep 17 00:00:00 2001 From: Slendy Date: Sat, 19 Aug 2023 02:15:55 -0500 Subject: [PATCH 08/40] Actually commit migration file for announcement timestamp --- ...814031609_AddPublishedAtToAnnouncements.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ProjectLighthouse/Migrations/20230814031609_AddPublishedAtToAnnouncements.cs diff --git a/ProjectLighthouse/Migrations/20230814031609_AddPublishedAtToAnnouncements.cs b/ProjectLighthouse/Migrations/20230814031609_AddPublishedAtToAnnouncements.cs new file mode 100644 index 000000000..97840f9c5 --- /dev/null +++ b/ProjectLighthouse/Migrations/20230814031609_AddPublishedAtToAnnouncements.cs @@ -0,0 +1,31 @@ +using System; +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20230814031609_AddPublishedAtToAnnouncements")] + public partial class AddPublishedAtToAnnouncements : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PublishedAt", + table: "WebsiteAnnouncements", + type: "datetime(6)", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PublishedAt", + table: "WebsiteAnnouncements"); + } + } +} From 7533ae5e89123e76440c9c6103680dab205473cb Mon Sep 17 00:00:00 2001 From: Slendy Date: Sun, 27 Aug 2023 00:30:21 -0500 Subject: [PATCH 09/40] Fix broken merge --- ProjectLighthouse/Extensions/ActivityQueryExtensions.cs | 6 +++--- .../Types/Activity/ActivityEntityEventHandler.cs | 5 +---- .../Types/Serialization/Activity/Events/GameCommentEvent.cs | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index e2b1363df..132ad0dc1 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -82,7 +82,7 @@ public static IQueryable ToActivityDto : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.PhotoId != 0 ? ((PhotoActivityEntity)a).Photo.SlotId : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level - ? ((CommentActivityEntity)a).Comment.TargetId + ? ((CommentActivityEntity)a).Comment.TargetSlotId : a is ScoreActivityEntity ? ((ScoreActivityEntity)a).Score.SlotId : a is ReviewActivityEntity @@ -92,7 +92,7 @@ public static IQueryable ToActivityDto TargetUserId = a is UserActivityEntity ? ((UserActivityEntity)a).TargetUserId : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile - ? ((CommentActivityEntity)a).Comment.TargetId + ? ((CommentActivityEntity)a).Comment.TargetUserId : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 ? ((PhotoActivityEntity)a).Photo.CreatorId : 0, @@ -109,7 +109,7 @@ public static IQueryable ToActivityDto : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 ? ((PhotoActivityEntity)a).Photo.Slot!.CreatorId : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level - ? ((CommentActivityEntity)a).Comment.TargetId + ? ((CommentActivityEntity)a).Comment.TargetSlot.CreatorId : a is ScoreActivityEntity ? ((ScoreActivityEntity)a).Score.Slot.CreatorId : a is ReviewActivityEntity diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 76d84e992..aa3bb5abd 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -1,8 +1,6 @@ #nullable enable using System; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Reflection; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; @@ -44,8 +42,7 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl { Type = EventType.Score, ScoreId = score.ScoreId, - //TODO merge score migration - UserId = database.Users.Where(u => u.Username == score.PlayerIds[0]).Select(u => u.UserId).First(), + UserId = score.UserId, }, HeartedLevelEntity heartedLevel => new LevelActivityEntity { diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs index 93d382576..1b2977857 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs @@ -27,7 +27,7 @@ public class GameUserCommentEvent : GameCommentEvent CommentEntity comment = await database.Comments.FindAsync(this.CommentId); if (comment == null) return; - UserEntity user = await database.Users.FindAsync(comment.TargetId); + UserEntity user = await database.Users.FindAsync(comment.TargetUserId); if (user == null) return; this.TargetUsername = user.Username; @@ -46,7 +46,7 @@ public class GameSlotCommentEvent : GameCommentEvent CommentEntity comment = await database.Comments.FindAsync(this.CommentId); if (comment == null) return; - SlotEntity slot = await database.Slots.FindAsync(comment.TargetId); + SlotEntity slot = await database.Slots.FindAsync(comment.TargetSlotId); if (slot == null) return; From dd5b1b8f0851633441f07715e890a597b1dc3c47 Mon Sep 17 00:00:00 2001 From: Slendy Date: Sun, 27 Aug 2023 00:33:22 -0500 Subject: [PATCH 10/40] Fix broken tests --- .../Unit/Activity/ActivityEventHandlerTests.cs | 6 +++--- .../Types/Activity/ActivityEntityEventHandler.cs | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs index 169ccd421..ba86e0e36 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -60,7 +60,7 @@ public async Task LevelComment_Insert_ShouldCreateCommentActivity() { CommentId = 1, PosterUserId = 1, - TargetId = 1, + TargetSlotId = 1, Type = CommentType.Level, }; database.Comments.Add(comment); @@ -89,7 +89,7 @@ public async Task ProfileComment_Insert_ShouldCreateCommentActivity() { CommentId = 1, PosterUserId = 1, - TargetId = 1, + TargetUserId = 1, Type = CommentType.Profile, }; database.Comments.Add(comment); @@ -152,7 +152,7 @@ public async Task Score_Insert_ShouldCreateScoreActivity() { ScoreId = 1, SlotId = 1, - PlayerIdCollection = "test", + UserId = 1, }; database.Scores.Add(score); await database.SaveChangesAsync(); diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index aa3bb5abd..dd8447f14 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -9,6 +9,10 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Website; using LBPUnion.ProjectLighthouse.Types.Levels; using Microsoft.EntityFrameworkCore; +#if DEBUG +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; +#endif namespace LBPUnion.ProjectLighthouse.Types.Activity; From a6aa12fbd9b17945df5a97b01d6baa39879b1190 Mon Sep 17 00:00:00 2001 From: Slendy Date: Sun, 27 Aug 2023 00:37:16 -0500 Subject: [PATCH 11/40] Fix foreign key constraint on comment activity test --- .../Unit/Activity/ActivityEventHandlerTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs index ba86e0e36..36f60b229 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -56,6 +56,12 @@ public async Task LevelComment_Insert_ShouldCreateCommentActivity() }, }); + SlotEntity slot = new() + { + SlotId = 1, + }; + database.Slots.Add(slot); + CommentEntity comment = new() { CommentId = 1, From 8d034db465508d55f690b27c4419ebbb6c190bbb Mon Sep 17 00:00:00 2001 From: Slendy Date: Sun, 27 Aug 2023 00:40:44 -0500 Subject: [PATCH 12/40] I forgor the creator id --- .../Unit/Activity/ActivityEventHandlerTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs index 36f60b229..ccfcc134a 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -59,6 +59,7 @@ public async Task LevelComment_Insert_ShouldCreateCommentActivity() SlotEntity slot = new() { SlotId = 1, + CreatorId = 1, }; database.Slots.Add(slot); From 2949e83e01d0e3ad1c3b9a0a1e3850fa5aff17cf Mon Sep 17 00:00:00 2001 From: Slendy Date: Wed, 30 Aug 2023 17:48:07 -0500 Subject: [PATCH 13/40] Remove 7 player mode and show your playlists in LBP3 --- .../Controllers/ActivityController.cs | 2 +- .../Controllers/ActivityControllerTests.cs | 35 ++++++++++++++++++- .../Activity/ActivityEntityEventHandler.cs | 4 +-- .../Activity/Events/GameScoreEvent.cs | 6 ++-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index 03252a69e..f023f443d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -102,7 +102,7 @@ private async Task> GetFilters predicate = predicate.Or(dto => includedUserIds.Contains(dto.Activity.UserId)); - if (!excludeMyPlaylists) + if (!excludeMyPlaylists && !excludeMyself && token.GameVersion == GameVersion.LittleBigPlanet3) { List creatorPlaylists = await this.database.Playlists.Where(p => p.CreatorId == token.UserId) .Select(p => p.PlaylistId) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs index 1aa1d38d3..b37ed5956 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs @@ -1,6 +1,39 @@ -namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Serialization.Activity; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Xunit; +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] public class ActivityControllerTests { + private static void SetupToken(ControllerBase controller, GameVersion version) + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = version; + controller.SetupTestController(token); + } + + [Fact] + public async Task LBP2GlobalActivity_ShouldReturnNothing_WhenEmpty() + { + DatabaseContext database = await MockHelper.GetTestDatabase(); + ActivityController activityController = new(database); + SetupToken(activityController, GameVersion.LittleBigPlanet2); + + long timestamp = TimeHelper.TimestampMillis; + + IActionResult response = await activityController.GlobalActivity(timestamp, 0, false, false, false, false, false); + GameStream stream = response.CastTo(); + Assert.Null(stream.Groups); + Assert.Equal(timestamp, stream.StartTimestamp); + } //TODO write activity controller tests } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index dd8447f14..58b766fb5 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -140,8 +140,6 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current { if (origEntity is not VisitedLevelEntity oldVisitedLevel) break; - int Plays(VisitedLevelEntity entity) => entity.PlaysLBP1 + entity.PlaysLBP2 + entity.PlaysLBP3; - if (Plays(oldVisitedLevel) >= Plays(visitedLevel)) break; activity = new LevelActivityEntity @@ -151,6 +149,8 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current UserId = visitedLevel.UserId, }; break; + + int Plays(VisitedLevelEntity entity) => entity.PlaysLBP1 + entity.PlaysLBP2 + entity.PlaysLBP3; } case SlotEntity slotEntity: { diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs index bed5c1acf..a6fd19d68 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.ComponentModel; +using System.Threading.Tasks; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Level; @@ -17,6 +18,7 @@ public class GameScoreEvent : GameEvent [XmlElement("score")] public int Score { get; set; } + [DefaultValue(0)] [XmlElement("user_count")] public int UserCount { get; set; } @@ -32,7 +34,7 @@ public class GameScoreEvent : GameEvent this.Score = score.Points; //TODO is this correct? - this.UserCount = score.Type; + this.UserCount = score.Type == 7 ? 0 : score.Type; this.Slot = ReviewSlot.CreateFromEntity(slot); } From 24fa301182a63e1cd6779db8a1e7fd7c8bf07138 Mon Sep 17 00:00:00 2001 From: Slendy Date: Thu, 31 Aug 2023 18:20:53 -0500 Subject: [PATCH 14/40] Don't create activities for story levels Also no longer shows you activities from incompatible levels (someone plays an LBP3 level but you won't be shown it from LBP2) Also gets rid of versus scores --- .../Controllers/ActivityController.cs | 4 + .../Extensions/ActivityQueryExtensions.cs | 51 ++++++----- .../Types/Activity/ActivityDto.cs | 2 + .../Activity/ActivityEntityEventHandler.cs | 86 +++++++++++++------ 4 files changed, 96 insertions(+), 47 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index f023f443d..a87594a9a 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -43,6 +43,10 @@ private async Task> GetFilters bool excludeMyPlaylists = true ) { + dtoQuery = token.GameVersion == GameVersion.LittleBigPlanetVita + ? dtoQuery.Where(dto => dto.TargetSlotGameVersion == null || dto.TargetSlotGameVersion == token.GameVersion) + : dtoQuery.Where(dto => dto.TargetSlotGameVersion == null || dto.TargetSlotGameVersion <= token.GameVersion); + Expression> predicate = PredicateExtensions.False(); List favouriteUsers = await this.database.HeartedProfiles.Where(hp => hp.UserId == token.UserId) diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index 132ad0dc1..4ae5cc228 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -71,6 +71,7 @@ public static List ToOuterActivityGroups // to build a pattern matching switch statement with expression trees. so the only other option // is to basically rewrite this nested ternary mess with expression trees which isn't much better // The resulting SQL generated by EntityFramework uses a CASE statement which is probably fine + // TOTAL HOURS WASTED: 3 public static IQueryable ToActivityDto (this IQueryable activityQuery, bool includeSlotCreator = false, bool includeTeamPick = false) { @@ -79,7 +80,7 @@ public static IQueryable ToActivityDto Activity = a, TargetSlotId = a is LevelActivityEntity ? ((LevelActivityEntity)a).SlotId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.PhotoId != 0 + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 ? ((PhotoActivityEntity)a).Photo.SlotId : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level ? ((CommentActivityEntity)a).Comment.TargetSlotId @@ -87,22 +88,18 @@ public static IQueryable ToActivityDto ? ((ScoreActivityEntity)a).Score.SlotId : a is ReviewActivityEntity ? ((ReviewActivityEntity)a).Review.SlotId - : 0, - - TargetUserId = a is UserActivityEntity - ? ((UserActivityEntity)a).TargetUserId - : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile - ? ((CommentActivityEntity)a).Comment.TargetUserId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 - ? ((PhotoActivityEntity)a).Photo.CreatorId - : 0, - TargetPlaylistId = a is PlaylistActivityEntity || a is PlaylistWithSlotActivityEntity - ? ((PlaylistActivityEntity)a).PlaylistId - : 0, - TargetNewsId = a is NewsActivityEntity ? ((NewsActivityEntity)a).NewsId : 0, - TargetTeamPickId = includeTeamPick - ? a.Type == EventType.MMPickLevel && a is LevelActivityEntity ? ((LevelActivityEntity)a).SlotId : 0 - : 0, + : null, + TargetSlotGameVersion = a is LevelActivityEntity + ? ((LevelActivityEntity)a).Slot.GameVersion + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 + ? ((PhotoActivityEntity)a).Photo.Slot.GameVersion + : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level + ? ((CommentActivityEntity)a).Comment.TargetSlot.GameVersion + : a is ScoreActivityEntity + ? ((ScoreActivityEntity)a).Score.Slot.GameVersion + : a is ReviewActivityEntity + ? ((ReviewActivityEntity)a).Review.Slot.GameVersion + : null, TargetSlotCreatorId = includeSlotCreator ? a is LevelActivityEntity ? ((LevelActivityEntity)a).Slot.CreatorId @@ -114,8 +111,22 @@ public static IQueryable ToActivityDto ? ((ScoreActivityEntity)a).Score.Slot.CreatorId : a is ReviewActivityEntity ? ((ReviewActivityEntity)a).Review.Slot!.CreatorId - : 0 - : 0, - }); + : null + : null, + + TargetUserId = a is UserActivityEntity + ? ((UserActivityEntity)a).TargetUserId + : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile + ? ((CommentActivityEntity)a).Comment.TargetUserId + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 + ? ((PhotoActivityEntity)a).Photo.CreatorId + : null, + TargetPlaylistId = a is PlaylistActivityEntity || a is PlaylistWithSlotActivityEntity + ? ((PlaylistActivityEntity)a).PlaylistId + : null, + TargetNewsId = a is NewsActivityEntity ? ((NewsActivityEntity)a).NewsId : null, + TargetTeamPickId = includeTeamPick + ? a.Type == EventType.MMPickLevel && a is LevelActivityEntity ? ((LevelActivityEntity)a).SlotId : null + : null, }); } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityDto.cs b/ProjectLighthouse/Types/Activity/ActivityDto.cs index b6f12d444..d813f76d9 100644 --- a/ProjectLighthouse/Types/Activity/ActivityDto.cs +++ b/ProjectLighthouse/Types/Activity/ActivityDto.cs @@ -1,4 +1,5 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Users; namespace LBPUnion.ProjectLighthouse.Types.Activity; @@ -7,6 +8,7 @@ public class ActivityDto public required ActivityEntity Activity { get; set; } public int? TargetSlotId { get; set; } public int? TargetSlotCreatorId { get; set; } + public GameVersion? TargetSlotGameVersion { get; set; } public int? TargetUserId { get; set; } public int? TargetPlaylistId { get; set; } public int? TargetNewsId { get; set; } diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 58b766fb5..741f8149b 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -16,7 +16,6 @@ namespace LBPUnion.ProjectLighthouse.Types.Activity; -//TODO implement missing event triggers public class ActivityEntityEventHandler : IEntityEventHandler { public void OnEntityInserted(DatabaseContext database, T entity) where T : class @@ -24,35 +23,56 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl Console.WriteLine($@"OnEntityInserted: {entity.GetType().Name}"); ActivityEntity? activity = entity switch { - SlotEntity slot => new LevelActivityEntity + SlotEntity slot => slot.Type switch { - Type = EventType.PublishLevel, - SlotId = slot.SlotId, - UserId = slot.CreatorId, + SlotType.User => new LevelActivityEntity + { + Type = EventType.PublishLevel, + SlotId = slot.SlotId, + UserId = slot.CreatorId, + }, + _ => null, }, - CommentEntity comment => new CommentActivityEntity + CommentEntity comment => comment.TargetSlot?.Type switch { - Type = comment.Type == CommentType.Level ? EventType.CommentOnLevel : EventType.CommentOnUser, - CommentId = comment.CommentId, - UserId = comment.PosterUserId, + SlotType.User => new CommentActivityEntity + { + Type = comment.Type == CommentType.Level ? EventType.CommentOnLevel : EventType.CommentOnUser, + CommentId = comment.CommentId, + UserId = comment.PosterUserId, + }, + _ => null, }, - PhotoEntity photo => new PhotoActivityEntity + PhotoEntity photo => photo.Slot?.Type switch { - Type = EventType.UploadPhoto, - PhotoId = photo.PhotoId, - UserId = photo.CreatorId, + SlotType.User => new PhotoActivityEntity + { + Type = EventType.UploadPhoto, + PhotoId = photo.PhotoId, + UserId = photo.CreatorId, + }, + _ => null, }, - ScoreEntity score => new ScoreActivityEntity + ScoreEntity score => score.Slot.Type switch { - Type = EventType.Score, - ScoreId = score.ScoreId, - UserId = score.UserId, + // Don't add story scores or versus scores + SlotType.User when score.Type != 7 => new ScoreActivityEntity + { + Type = EventType.Score, + ScoreId = score.ScoreId, + UserId = score.UserId, + }, + _ => null, }, - HeartedLevelEntity heartedLevel => new LevelActivityEntity + HeartedLevelEntity heartedLevel => heartedLevel.Slot.Type switch { - Type = EventType.HeartLevel, - SlotId = heartedLevel.SlotId, - UserId = heartedLevel.UserId, + SlotType.User => new LevelActivityEntity + { + Type = EventType.HeartLevel, + SlotId = heartedLevel.SlotId, + UserId = heartedLevel.UserId, + }, + _ => null, }, HeartedProfileEntity heartedProfile => new UserActivityEntity { @@ -123,8 +143,7 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current object? origVal = propInfo.GetValue(origEntity); object? newVal = propInfo.GetValue(currentEntity); - if ((origVal == null && newVal == null) || (origVal != null && newVal != null && origVal.Equals(newVal))) - continue; + if ((origVal == null && newVal == null) || (origVal != null && newVal != null && origVal.Equals(newVal))) continue; Console.WriteLine($@"Value for {propInfo.Name} changed"); Console.WriteLine($@"Orig val: {origVal?.ToString() ?? "null"}"); @@ -197,6 +216,14 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current { if (origEntity is not CommentEntity oldComment) break; + if (comment.TargetSlotId != null) + { + SlotType slotType = database.Slots.Where(s => s.SlotId == comment.TargetSlotId) + .Select(s => s.Type) + .FirstOrDefault(); + if (slotType != SlotType.User) break; + } + if (oldComment.Deleted || !comment.Deleted) break; if (comment.Type != CommentType.Level) break; @@ -238,6 +265,7 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current }; InsertActivity(database, entity); } + break; } } @@ -250,11 +278,15 @@ public void OnEntityDeleted(DatabaseContext database, T entity) where T : cla Console.WriteLine($@"OnEntityDeleted: {entity.GetType().Name}"); ActivityEntity? activity = entity switch { - HeartedLevelEntity heartedLevel => new LevelActivityEntity + HeartedLevelEntity heartedLevel => heartedLevel.Slot.Type switch { - Type = EventType.UnheartLevel, - SlotId = heartedLevel.SlotId, - UserId = heartedLevel.UserId, + SlotType.User => new LevelActivityEntity + { + Type = EventType.UnheartLevel, + SlotId = heartedLevel.SlotId, + UserId = heartedLevel.UserId, + }, + _ => null, }, HeartedProfileEntity heartedProfile => new UserActivityEntity { From 966f6198c64f6b3603fb21dd9245e90ed735b839 Mon Sep 17 00:00:00 2001 From: Slendy Date: Thu, 31 Aug 2023 18:27:02 -0500 Subject: [PATCH 15/40] Revert GameScoreEvent user count workaround --- .../Types/Serialization/Activity/Events/GameScoreEvent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs index a6fd19d68..5f26cc90d 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs @@ -34,7 +34,7 @@ public class GameScoreEvent : GameEvent this.Score = score.Points; //TODO is this correct? - this.UserCount = score.Type == 7 ? 0 : score.Type; + this.UserCount = score.Type; this.Slot = ReviewSlot.CreateFromEntity(slot); } From 15dbf562aaa68e6860818dbc6032cd330a6affa2 Mon Sep 17 00:00:00 2001 From: Slendy Date: Thu, 31 Aug 2023 18:32:24 -0500 Subject: [PATCH 16/40] Remove/replace console writes with debug logging --- .../Controllers/ActivityController.cs | 14 +++++++----- .../Activity/ActivityEntityEventHandler.cs | 22 ++++++++++--------- ProjectLighthouse/Types/Logging/LogArea.cs | 1 + .../Activity/Events/GameEvent.cs | 8 +++++-- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index a87594a9a..792c67117 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -3,10 +3,12 @@ using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Filter.Filters.Activity; using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.StorableLists.Stores; using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Serialization.Activity; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; @@ -121,7 +123,7 @@ private async Task> GetFilters dto.Activity.Type != EventType.AddLevelToPlaylist); } - Console.WriteLine(predicate); + Logger.Debug(predicate.ToString(), LogArea.Activity); dtoQuery = dtoQuery.Where(predicate); @@ -281,16 +283,16 @@ private static void PrintOuterGroups(List outerGroups) { foreach (OuterActivityGroup outer in outerGroups) { - Console.WriteLine(@$"Outer group key: {outer.Key}"); + Logger.Debug(@$"Outer group key: {outer.Key}", LogArea.Activity); List> itemGroup = outer.Groups; foreach (IGrouping item in itemGroup) { - Console.WriteLine( - @$" Inner group key: TargetId={item.Key.TargetId}, UserId={item.Key.UserId}, Type={item.Key.Type}"); + Logger.Debug( + @$" Inner group key: TargetId={item.Key.TargetId}, UserId={item.Key.UserId}, Type={item.Key.Type}", LogArea.Activity); foreach (ActivityDto activity in item) { - Console.WriteLine( - @$" Activity: {activity.GroupType}, Timestamp: {activity.Activity.Timestamp}, UserId: {activity.Activity.UserId}, EventType: {activity.Activity.Type}, TargetId: {activity.TargetId}"); + Logger.Debug( + @$" Activity: {activity.GroupType}, Timestamp: {activity.Activity.Timestamp}, UserId: {activity.Activity.UserId}, EventType: {activity.Activity.Type}, TargetId: {activity.TargetId}", LogArea.Activity); } } } diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 741f8149b..333903752 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -2,12 +2,14 @@ using System; using System.Linq; using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Website; using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Logging; using Microsoft.EntityFrameworkCore; #if DEBUG using System.ComponentModel.DataAnnotations.Schema; @@ -20,7 +22,7 @@ public class ActivityEntityEventHandler : IEntityEventHandler { public void OnEntityInserted(DatabaseContext database, T entity) where T : class { - Console.WriteLine($@"OnEntityInserted: {entity.GetType().Name}"); + Logger.Debug($@"OnEntityInserted: {entity.GetType().Name}", LogArea.Activity); ActivityEntity? activity = entity switch { SlotEntity slot => slot.Type switch @@ -125,7 +127,7 @@ private static void InsertActivity(DatabaseContext database, ActivityEntity? act { if (activity == null) return; - Console.WriteLine("Inserting activity: " + activity.GetType().Name); + Logger.Debug("Inserting activity: " + activity.GetType().Name, LogArea.Activity); activity.Timestamp = DateTime.UtcNow; database.Activities.Add(activity); @@ -145,11 +147,11 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current object? newVal = propInfo.GetValue(currentEntity); if ((origVal == null && newVal == null) || (origVal != null && newVal != null && origVal.Equals(newVal))) continue; - Console.WriteLine($@"Value for {propInfo.Name} changed"); - Console.WriteLine($@"Orig val: {origVal?.ToString() ?? "null"}"); - Console.WriteLine($@"New val: {newVal?.ToString() ?? "null"}"); + Logger.Debug($@"Value for {propInfo.Name} changed", LogArea.Activity); + Logger.Debug($@"Orig val: {origVal?.ToString() ?? "null"}", LogArea.Activity); + Logger.Debug($@"New val: {newVal?.ToString() ?? "null"}", LogArea.Activity); } - Console.WriteLine($@"OnEntityChanged: {currentEntity.GetType().Name}"); + Logger.Debug($@"OnEntityChanged: {currentEntity.GetType().Name}", LogArea.Activity); #endif ActivityEntity? activity = null; @@ -242,12 +244,12 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current int[] newSlots = playlist.SlotIds; int[] oldSlots = oldPlaylist.SlotIds; - Console.WriteLine($@"Old playlist slots: {string.Join(",", oldSlots)}"); - Console.WriteLine($@"New playlist slots: {string.Join(",", newSlots)}"); + Logger.Debug($@"Old playlist slots: {string.Join(",", oldSlots)}", LogArea.Activity); + Logger.Debug($@"New playlist slots: {string.Join(",", newSlots)}", LogArea.Activity); int[] addedSlots = newSlots.Except(oldSlots).ToArray(); - Console.WriteLine($@"Added playlist slots: {string.Join(",", addedSlots)}"); + Logger.Debug($@"Added playlist slots: {string.Join(",", addedSlots)}", LogArea.Activity); // If no new level have been added if (addedSlots.Length == 0) break; @@ -275,7 +277,7 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current public void OnEntityDeleted(DatabaseContext database, T entity) where T : class { - Console.WriteLine($@"OnEntityDeleted: {entity.GetType().Name}"); + Logger.Debug($@"OnEntityDeleted: {entity.GetType().Name}", LogArea.Activity); ActivityEntity? activity = entity switch { HeartedLevelEntity heartedLevel => heartedLevel.Slot.Type switch diff --git a/ProjectLighthouse/Types/Logging/LogArea.cs b/ProjectLighthouse/Types/Logging/LogArea.cs index 10146e225..9a8cc178c 100644 --- a/ProjectLighthouse/Types/Logging/LogArea.cs +++ b/ProjectLighthouse/Types/Logging/LogArea.cs @@ -28,4 +28,5 @@ public enum LogArea Email, Serialization, Synchronization, + Activity, } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs index c9b8079f3..d69d626f6 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -6,9 +6,11 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Serialization.Review; namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; @@ -46,7 +48,9 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization protected async Task PrepareSerialization(DatabaseContext database) { - Console.WriteLine($@"EVENT SERIALIZATION!! {this.UserId} - {this.GetHashCode()}"); + #if DEBUG + Logger.Debug($@"EVENT SERIALIZATION!! userId: {this.UserId} - hashCode: {this.GetHashCode()}", LogArea.Activity); + #endif UserEntity user = await database.Users.FindAsync(this.UserId); if (user == null) return; this.Username = user.Username; @@ -114,7 +118,7 @@ private static GameEvent CreateFromActivity(ActivityDto activity) { if (!IsValidActivity(activity.Activity)) { - Console.WriteLine(@"Invalid Activity: " + activity.Activity.ActivityId); + Logger.Error(@"Invalid Activity: " + activity.Activity.ActivityId, LogArea.Activity); return null; } From 0c9c8fd7b2e6926417b1128d278e2dc79b1c3185 Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 5 Sep 2023 22:51:01 -0500 Subject: [PATCH 17/40] Fix score, photo, and comment activities --- .../Activity/ActivityEventHandlerTests.cs | 1 + .../Activity/ActivityEntityEventHandler.cs | 33 +++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs index ccfcc134a..6990c9108 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -196,6 +196,7 @@ public async Task HeartedLevel_Insert_ShouldCreateLevelActivity() HeartedLevelId = 1, UserId = 1, SlotId = 1, + Slot = slot, }; eventHandler.OnEntityInserted(database, heartedLevel); diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 333903752..f338f9f4f 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -35,25 +35,44 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, _ => null, }, - CommentEntity comment => comment.TargetSlot?.Type switch + CommentEntity comment => comment.Type switch { - SlotType.User => new CommentActivityEntity + CommentType.Level => comment.TargetSlot?.Type switch { - Type = comment.Type == CommentType.Level ? EventType.CommentOnLevel : EventType.CommentOnUser, + SlotType.User => new CommentActivityEntity + { + Type = EventType.CommentOnLevel, + CommentId = comment.CommentId, + UserId = comment.PosterUserId, + }, + _ => null, + }, + CommentType.Profile => new CommentActivityEntity + { + Type = EventType.CommentOnUser, CommentId = comment.CommentId, UserId = comment.PosterUserId, - }, + }, _ => null, }, - PhotoEntity photo => photo.Slot?.Type switch + PhotoEntity photo => photo.SlotId switch { - SlotType.User => new PhotoActivityEntity + null => new PhotoActivityEntity { Type = EventType.UploadPhoto, PhotoId = photo.PhotoId, UserId = photo.CreatorId, }, - _ => null, + _ => photo.Slot?.Type switch + { + SlotType.User => new PhotoActivityEntity + { + Type = EventType.UploadPhoto, + PhotoId = photo.PhotoId, + UserId = photo.CreatorId, + }, + _ => null, + }, }, ScoreEntity score => score.Slot.Type switch { From d440a26476e90949e590fc8252861904daa7638b Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 5 Sep 2023 22:59:20 -0500 Subject: [PATCH 18/40] Go back to using 0 as empty value instead of null --- .../Extensions/ActivityQueryExtensions.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index 4ae5cc228..c92cc60d8 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -88,7 +88,7 @@ public static IQueryable ToActivityDto ? ((ScoreActivityEntity)a).Score.SlotId : a is ReviewActivityEntity ? ((ReviewActivityEntity)a).Review.SlotId - : null, + : 0, TargetSlotGameVersion = a is LevelActivityEntity ? ((LevelActivityEntity)a).Slot.GameVersion : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 @@ -99,7 +99,7 @@ public static IQueryable ToActivityDto ? ((ScoreActivityEntity)a).Score.Slot.GameVersion : a is ReviewActivityEntity ? ((ReviewActivityEntity)a).Review.Slot.GameVersion - : null, + : 0, TargetSlotCreatorId = includeSlotCreator ? a is LevelActivityEntity ? ((LevelActivityEntity)a).Slot.CreatorId @@ -111,8 +111,8 @@ public static IQueryable ToActivityDto ? ((ScoreActivityEntity)a).Score.Slot.CreatorId : a is ReviewActivityEntity ? ((ReviewActivityEntity)a).Review.Slot!.CreatorId - : null - : null, + : 0 + : 0, TargetUserId = a is UserActivityEntity ? ((UserActivityEntity)a).TargetUserId @@ -120,13 +120,13 @@ public static IQueryable ToActivityDto ? ((CommentActivityEntity)a).Comment.TargetUserId : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 ? ((PhotoActivityEntity)a).Photo.CreatorId - : null, + : 0, TargetPlaylistId = a is PlaylistActivityEntity || a is PlaylistWithSlotActivityEntity ? ((PlaylistActivityEntity)a).PlaylistId - : null, - TargetNewsId = a is NewsActivityEntity ? ((NewsActivityEntity)a).NewsId : null, + : 0, + TargetNewsId = a is NewsActivityEntity ? ((NewsActivityEntity)a).NewsId : 0, TargetTeamPickId = includeTeamPick - ? a.Type == EventType.MMPickLevel && a is LevelActivityEntity ? ((LevelActivityEntity)a).SlotId : null - : null, }); + ? a.Type == EventType.MMPickLevel && a is LevelActivityEntity ? ((LevelActivityEntity)a).SlotId : 0 + : 0, }); } } \ No newline at end of file From c1d932d4de2a6f2e4745fb7a515ea0038cec5c91 Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 5 Sep 2023 23:27:03 -0500 Subject: [PATCH 19/40] Fix group news IDs being poster user ID instead of post ID --- ProjectLighthouse/Extensions/ActivityQueryExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index c92cc60d8..92fbee97a 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -61,7 +61,7 @@ public static List ToOuterActivityGroups { Type = groupByActor ? gr.GroupType : gr.GroupType != ActivityGroupType.News ? ActivityGroupType.User : ActivityGroupType.News, UserId = gr.Activity.UserId, - TargetId = groupByActor ? gr.TargetId : gr.Activity.UserId, + TargetId = groupByActor ? gr.TargetId : gr.GroupType != ActivityGroupType.News ? gr.Activity.UserId : gr.TargetNewsId ?? 0, }) .ToList(), }) From 7b6786ce877708a5bfe53e37b8cf0f285b695fda Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 5 Sep 2023 23:34:44 -0500 Subject: [PATCH 20/40] Fix news summary on LBP2 --- .../Types/Serialization/Activity/GameStream.cs | 11 ++++++----- .../Types/Serialization/News/GameNewsObject.cs | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs index 158f166ef..3081f2104 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -76,6 +76,12 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization public async Task PrepareSerialization(DatabaseContext database) { + this.Slots = await LoadEntities(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId)); + this.Users = await LoadEntities(this.UserIds, user => GameUser.CreateFromEntity(user, this.TargetGame)); + this.Playlists = await LoadEntities(this.PlaylistIds, GamePlaylist.CreateFromEntity); + this.News = await LoadEntities(this.NewsIds, a => GameNewsObject.CreateFromEntity(a, this.TargetGame)); + return; + async Task> LoadEntities(List ids, Func transformation) where TFrom : class { @@ -91,11 +97,6 @@ async Task> LoadEntities(List ids, Func(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId)); - this.Users = await LoadEntities(this.UserIds, user => GameUser.CreateFromEntity(user, this.TargetGame)); - this.Playlists = await LoadEntities(this.PlaylistIds, GamePlaylist.CreateFromEntity); - this.News = await LoadEntities(this.NewsIds, GameNewsObject.CreateFromEntity); } public static GameStream CreateFromGroups diff --git a/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs b/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs index cc4b4659b..ec1eeee07 100644 --- a/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs +++ b/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs @@ -2,6 +2,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Users; namespace LBPUnion.ProjectLighthouse.Types.Serialization.News; @@ -33,12 +34,12 @@ public class GameNewsObject : ILbpSerializable [XmlElement("category")] public string Category { get; set; } - public static GameNewsObject CreateFromEntity(WebsiteAnnouncementEntity entity) => + public static GameNewsObject CreateFromEntity(WebsiteAnnouncementEntity entity, GameVersion gameVersion) => new() { Id = entity.AnnouncementId, Title = entity.Title, - Summary = "", + Summary = gameVersion == GameVersion.LittleBigPlanet2 ? entity.Content : "", Text = entity.Content, Category = "no_category", Timestamp = entity.PublishedAt.ToUnixTimeMilliseconds(), From 7c07742090c4fe2032afee15bb785a96ae7de623 Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 5 Sep 2023 23:43:13 -0500 Subject: [PATCH 21/40] Allow recent activity photos from moon and pod --- .../Types/Activity/ActivityEntityEventHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index f338f9f4f..29d7f9a8b 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -57,6 +57,7 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, PhotoEntity photo => photo.SlotId switch { + // Photos without levels null => new PhotoActivityEntity { Type = EventType.UploadPhoto, @@ -65,13 +66,14 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, _ => photo.Slot?.Type switch { - SlotType.User => new PhotoActivityEntity + SlotType.Developer => null, + // Non-story levels (moon, pod, etc) + _ => new PhotoActivityEntity { Type = EventType.UploadPhoto, PhotoId = photo.PhotoId, UserId = photo.CreatorId, }, - _ => null, }, }, ScoreEntity score => score.Slot.Type switch From 991b3f7af9335fddd4cae823754f254938f8507c Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 5 Sep 2023 23:46:19 -0500 Subject: [PATCH 22/40] Set PhotoId in serialized event response --- .../Serialization/Activity/Events/GamePhotoUploadEvent.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs index fa8395555..c2da06ed8 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs @@ -6,6 +6,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization.Review; using Microsoft.EntityFrameworkCore; @@ -34,12 +35,13 @@ public class GamePhotoUploadEvent : GameEvent if (photo == null) return; this.PhotoParticipants = photo.PhotoSubjects.Select(ps => ps.User.Username).ToList(); + this.PhotoId = photo.PhotoId; if (photo.SlotId == null) return; SlotEntity slot = await database.Slots.FindAsync(photo.SlotId); if (slot == null) return; - this.Slot = ReviewSlot.CreateFromEntity(slot); + if (slot.Type == SlotType.User) this.Slot = ReviewSlot.CreateFromEntity(slot); } } \ No newline at end of file From 4d2645b7c3dda05eb67abdca54efac1e4580baaa Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 5 Sep 2023 23:52:19 -0500 Subject: [PATCH 23/40] Only serialize user slots and set photo ID in event object --- .../Types/Serialization/Activity/Events/GameEvent.cs | 1 + .../Serialization/Activity/Events/GamePhotoUploadEvent.cs | 1 - .../Types/Serialization/Activity/GameStream.cs | 8 ++++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs index d69d626f6..649907d5c 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -184,6 +184,7 @@ private static GameEvent CreateFromActivity(ActivityDto activity) }, EventType.UploadPhoto => new GamePhotoUploadEvent { + PhotoId = ((PhotoActivityEntity)activity.Activity).PhotoId, Slot = new ReviewSlot { SlotId = targetId, diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs index c2da06ed8..62b9a9489 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs @@ -35,7 +35,6 @@ public class GamePhotoUploadEvent : GameEvent if (photo == null) return; this.PhotoParticipants = photo.PhotoSubjects.Select(ps => ps.User.Username).ToList(); - this.PhotoId = photo.PhotoId; if (photo.SlotId == null) return; diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs index 3081f2104..3f41b0250 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -11,6 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization.News; using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; @@ -76,13 +77,13 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization public async Task PrepareSerialization(DatabaseContext database) { - this.Slots = await LoadEntities(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId)); + this.Slots = await LoadEntities(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId), s => s.Type == SlotType.User); this.Users = await LoadEntities(this.UserIds, user => GameUser.CreateFromEntity(user, this.TargetGame)); this.Playlists = await LoadEntities(this.PlaylistIds, GamePlaylist.CreateFromEntity); this.News = await LoadEntities(this.NewsIds, a => GameNewsObject.CreateFromEntity(a, this.TargetGame)); return; - async Task> LoadEntities(List ids, Func transformation) + async Task> LoadEntities(List ids, Func transformation, Func predicate = null) where TFrom : class { List results = new(); @@ -90,6 +91,9 @@ async Task> LoadEntities(List ids, Func().FindAsync(id); + + if (predicate != null && !predicate(entity)) continue; + if (entity == null) continue; results.Add(transformation(entity)); From 41d2b5be7dacdeeaef4c0a90884fbd854e1ca5b0 Mon Sep 17 00:00:00 2001 From: Slendy Date: Thu, 7 Sep 2023 01:25:01 -0500 Subject: [PATCH 24/40] Prevent heart activity spam and fix photo grouping --- .../Activity/ActivityEventHandlerTests.cs | 227 +++++++++++++++++- .../Extensions/ActivityQueryExtensions.cs | 2 +- .../Activity/ActivityEntityEventHandler.cs | 47 ++++ 3 files changed, 274 insertions(+), 2 deletions(-) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs index 6990c9108..6f08b8b4f 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; @@ -205,6 +206,55 @@ public async Task HeartedLevel_Insert_ShouldCreateLevelActivity() .FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1)); } + [Fact] + public async Task HeartedLevel_InsertDuplicate_ShouldRemoveOldActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + LevelActivityEntity levelActivity = new() + { + UserId = 1, + SlotId = 1, + Type = EventType.HeartLevel, + Timestamp = DateTime.MinValue, + }; + + database.Activities.Add(levelActivity); + + await database.SaveChangesAsync(); + + HeartedLevelEntity heartedLevel = new() + { + HeartedLevelId = 1, + UserId = 1, + SlotId = 1, + Slot = slot, + }; + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1 && a.Timestamp == DateTime.MinValue)); + + eventHandler.OnEntityInserted(database, heartedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1 && a.Timestamp != DateTime.MinValue)); + } + [Fact] public async Task HeartedProfile_Insert_ShouldCreateUserActivity() { @@ -231,6 +281,46 @@ public async Task HeartedProfile_Insert_ShouldCreateUserActivity() .FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1)); } + [Fact] + public async Task HeartedProfile_InsertDuplicate_ShouldRemoveOldActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + UserActivityEntity userActivity = new() + { + UserId = 1, + TargetUserId = 1, + Type = EventType.HeartUser, + Timestamp = DateTime.MinValue, + }; + + database.Activities.Add(userActivity); + await database.SaveChangesAsync(); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1 && a.Timestamp == DateTime.MinValue)); + + HeartedProfileEntity heartedProfile = new() + { + HeartedProfileId = 1, + UserId = 1, + HeartedUserId = 1, + }; + + eventHandler.OnEntityInserted(database, heartedProfile); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1 && a.Timestamp != DateTime.MinValue)); + } + [Fact] public async Task HeartedPlaylist_Insert_ShouldCreatePlaylistActivity() { @@ -265,6 +355,54 @@ public async Task HeartedPlaylist_Insert_ShouldCreatePlaylistActivity() .FirstOrDefault(a => a.Type == EventType.HeartPlaylist && a.PlaylistId == 1)); } + [Fact] + public async Task HeartedPlaylist_InsertDuplicate_ShouldCreatePlaylistActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + PlaylistEntity playlist = new() + { + PlaylistId = 1, + CreatorId = 1, + }; + database.Playlists.Add(playlist); + + PlaylistActivityEntity playlistActivity = new() + { + UserId = 1, + PlaylistId = 1, + Type = EventType.HeartPlaylist, + Timestamp = DateTime.MinValue, + }; + database.Activities.Add(playlistActivity); + + await database.SaveChangesAsync(); + + HeartedPlaylistEntity heartedPlaylist = new() + { + HeartedPlaylistId = 1, + UserId = 1, + PlaylistId = 1, + }; + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => + a.Type == EventType.HeartPlaylist && a.PlaylistId == 1 && a.Timestamp == DateTime.MinValue)); + + eventHandler.OnEntityInserted(database, heartedPlaylist); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartPlaylist && a.PlaylistId == 1 && a.Timestamp != DateTime.MinValue)); + } + [Fact] public async Task VisitedLevel_Insert_ShouldCreateLevelActivity() { @@ -696,6 +834,52 @@ public async Task HeartedLevel_Delete_ShouldCreateLevelActivity() .FirstOrDefault(a => a.Type == EventType.UnheartLevel && a.SlotId == 1)); } + [Fact] + public async Task HeartedLevel_DeleteDuplicate_ShouldRemoveOldActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + LevelActivityEntity levelActivity = new() + { + UserId = 1, + SlotId = 1, + Type = EventType.UnheartLevel, + Timestamp = DateTime.MinValue, + }; + + database.Activities.Add(levelActivity); + + HeartedLevelEntity heartedLevel = new() + { + HeartedLevelId = 1, + UserId = 1, + SlotId = 1, + }; + + database.HeartedLevels.Add(heartedLevel); + await database.SaveChangesAsync(); + + eventHandler.OnEntityDeleted(database, heartedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UnheartLevel && a.SlotId == 1 && a.Timestamp != DateTime.MinValue)); + } + [Fact] public async Task HeartedProfile_Delete_ShouldCreateLevelActivity() { @@ -731,5 +915,46 @@ public async Task HeartedProfile_Delete_ShouldCreateLevelActivity() Assert.NotNull(database.Activities.OfType() .FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1)); } + + [Fact] + public async Task HeartedProfile_DeleteDuplicate_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + UserActivityEntity userActivity = new() + { + UserId = 1, + TargetUserId = 1, + Type = EventType.UnheartUser, + Timestamp = DateTime.MinValue, + }; + database.Activities.Add(userActivity); + + HeartedProfileEntity heartedProfile = new() + { + HeartedProfileId = 1, + UserId = 1, + HeartedUserId = 1, + }; + + database.HeartedProfiles.Add(heartedProfile); + await database.SaveChangesAsync(); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1 && a.Timestamp == DateTime.MinValue)); + + eventHandler.OnEntityDeleted(database, heartedProfile); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1 && a.Timestamp != DateTime.MinValue)); + } #endregion } \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index 92fbee97a..67394c8c8 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -118,7 +118,7 @@ public static IQueryable ToActivityDto ? ((UserActivityEntity)a).TargetUserId : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile ? ((CommentActivityEntity)a).Comment.TargetUserId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 + : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId == 0 ? ((PhotoActivityEntity)a).Photo.CreatorId : 0, TargetPlaylistId = a is PlaylistActivityEntity || a is PlaylistWithSlotActivityEntity diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 29d7f9a8b..2342c566b 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -1,7 +1,9 @@ #nullable enable using System; using System.Linq; +using System.Linq.Expressions; using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; @@ -144,12 +146,57 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl InsertActivity(database, activity); } + private static void RemoveDuplicateEvents(DatabaseContext database, ActivityEntity activity) + { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (activity.Type) + { + case EventType.HeartLevel: + case EventType.UnheartLevel: + { + if (activity is not LevelActivityEntity levelActivity) break; + + DeleteActivity(a => a.TargetSlotId == levelActivity.SlotId); + break; + } + case EventType.HeartUser: + case EventType.UnheartUser: + { + if (activity is not UserActivityEntity userActivity) break; + + DeleteActivity(a => a.TargetUserId == userActivity.TargetUserId); + break; + } + case EventType.HeartPlaylist: + { + if (activity is not PlaylistActivityEntity playlistActivity) break; + + DeleteActivity(a => a.TargetPlaylistId == playlistActivity.PlaylistId); + break; + } + } + + return; + + void DeleteActivity(Expression> predicate) + { + database.Activities.ToActivityDto() + .Where(a => a.Activity.UserId == activity.UserId) + .Where(a => a.Activity.Type == activity.Type) + .Where(predicate) + .Select(a => a.Activity) + .ExecuteDelete(); + } + } + private static void InsertActivity(DatabaseContext database, ActivityEntity? activity) { if (activity == null) return; Logger.Debug("Inserting activity: " + activity.GetType().Name, LogArea.Activity); + RemoveDuplicateEvents(database, activity); + activity.Timestamp = DateTime.UtcNow; database.Activities.Add(activity); database.SaveChanges(); From 4e63ba70e7210732f59538cb0cff1417781910d5 Mon Sep 17 00:00:00 2001 From: Slendy Date: Sat, 20 Jan 2024 15:47:39 -0600 Subject: [PATCH 25/40] Fix broken merge and recreate migrations --- ProjectLighthouse.Tests/Helpers/MockHelper.cs | 2 +- ...InitialActivity.cs => 20240120214525_InitialActivity.cs} | 4 +++- ...s => 20240120214606_AddPublishedAtToWebAnnouncements.cs} | 6 ++++-- .../Migrations/DatabaseContextModelSnapshot.cs | 3 +++ 4 files changed, 11 insertions(+), 4 deletions(-) rename ProjectLighthouse/Migrations/{20230725013522_InitialActivity.cs => 20240120214525_InitialActivity.cs} (98%) rename ProjectLighthouse/Migrations/{20230814031609_AddPublishedAtToAnnouncements.cs => 20240120214606_AddPublishedAtToWebAnnouncements.cs} (82%) diff --git a/ProjectLighthouse.Tests/Helpers/MockHelper.cs b/ProjectLighthouse.Tests/Helpers/MockHelper.cs index 64dc10c32..7982ac423 100644 --- a/ProjectLighthouse.Tests/Helpers/MockHelper.cs +++ b/ProjectLighthouse.Tests/Helpers/MockHelper.cs @@ -50,7 +50,7 @@ public static T2 CastTo(this IActionResult result) where T1 : ObjectResu return finalResult; } - private static async Task> GetInMemoryDbOptions() + public static async Task> GetInMemoryDbOptions() { DbConnection connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); diff --git a/ProjectLighthouse/Migrations/20230725013522_InitialActivity.cs b/ProjectLighthouse/Migrations/20240120214525_InitialActivity.cs similarity index 98% rename from ProjectLighthouse/Migrations/20230725013522_InitialActivity.cs rename to ProjectLighthouse/Migrations/20240120214525_InitialActivity.cs index da18f6cf6..9f4d01a47 100644 --- a/ProjectLighthouse/Migrations/20230725013522_InitialActivity.cs +++ b/ProjectLighthouse/Migrations/20240120214525_InitialActivity.cs @@ -9,9 +9,10 @@ namespace ProjectLighthouse.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20230725013522_InitialActivity")] + [Migration("20240120214525_InitialActivity")] public partial class InitialActivity : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( @@ -140,6 +141,7 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "UserId"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/ProjectLighthouse/Migrations/20230814031609_AddPublishedAtToAnnouncements.cs b/ProjectLighthouse/Migrations/20240120214606_AddPublishedAtToWebAnnouncements.cs similarity index 82% rename from ProjectLighthouse/Migrations/20230814031609_AddPublishedAtToAnnouncements.cs rename to ProjectLighthouse/Migrations/20240120214606_AddPublishedAtToWebAnnouncements.cs index 97840f9c5..cfd53fc96 100644 --- a/ProjectLighthouse/Migrations/20230814031609_AddPublishedAtToAnnouncements.cs +++ b/ProjectLighthouse/Migrations/20240120214606_AddPublishedAtToWebAnnouncements.cs @@ -8,9 +8,10 @@ namespace ProjectLighthouse.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20230814031609_AddPublishedAtToAnnouncements")] - public partial class AddPublishedAtToAnnouncements : Migration + [Migration("20240120214606_AddPublishedAtToWebAnnouncements")] + public partial class AddPublishedAtToWebAnnouncements : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn( @@ -21,6 +22,7 @@ protected override void Up(MigrationBuilder migrationBuilder) defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( diff --git a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs index af5224fce..5c33884b7 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -1107,6 +1107,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Content") .HasColumnType("longtext"); + b.Property("PublishedAt") + .HasColumnType("datetime(6)"); + b.Property("PublisherId") .HasColumnType("int"); From 0f02a93a8d0bb799e9be1d84f08845429b4e6836 Mon Sep 17 00:00:00 2001 From: Slendy Date: Sat, 20 Jan 2024 15:49:03 -0600 Subject: [PATCH 26/40] Start of activity grouping tests --- .../Unit/Activity/ActivityGroupingTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs new file mode 100644 index 000000000..617c0caa8 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity; + +public class ActivityGroupingTests +{ + [Fact] + public void ActivityGroupingTest() + { + List activities = new() + { + new ActivityDto + { + TargetPlaylistId = 1, + Activity = new ActivityEntity(), + }, + }; + List groups = activities.AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups(); + Assert.NotNull(groups); + Assert.Single(groups); + OuterActivityGroup groupEntry = groups.First(); + + Assert.Equal(ActivityGroupType.Playlist, groupEntry.Key.GroupType); + Assert.Equal(1, groupEntry.Key.TargetId); + } +} \ No newline at end of file From 0445a0b3a6af81f6ac45e671b027b2aac3872001 Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 11 Mar 2024 21:32:03 -0500 Subject: [PATCH 27/40] Update activity system to use new team pick time --- .../Unit/Activity/ActivityEventHandlerTests.cs | 2 +- .../Types/Activity/ActivityEntityEventHandler.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs index 6f08b8b4f..95d1ba05a 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -673,7 +673,7 @@ public async Task Slot_WithTeamPickChange_ShouldCreateLevelActivity() { SlotId = 1, CreatorId = 1, - TeamPick = true, + TeamPickTime = 1, }; eventHandler.OnEntityChanged(database, oldSlot, newSlot); diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 2342c566b..46f5ebb21 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -245,10 +245,13 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current { if (origEntity is not SlotEntity oldSlotEntity) break; - switch (oldSlotEntity.TeamPick) + bool oldIsTeamPick = oldSlotEntity.TeamPickTime != 0; + bool newIsTeamPick = slotEntity.TeamPickTime != 0; + + switch (oldIsTeamPick) { // When a level is team picked - case false when slotEntity.TeamPick: + case false when newIsTeamPick: activity = new LevelActivityEntity { Type = EventType.MMPickLevel, @@ -257,7 +260,7 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current }; break; // When a level has its team pick removed then remove the corresponding activity - case true when !slotEntity.TeamPick: + case true when !newIsTeamPick: database.Activities.OfType() .Where(a => a.Type == EventType.MMPickLevel) .Where(a => a.SlotId == slotEntity.SlotId) From c2011837d7f5798bd426490349ba77f3d6e26258 Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 25 Mar 2024 01:03:53 -0500 Subject: [PATCH 28/40] Remove giant ActivityDto ternary and add more documentation in some areas --- .config/dotnet-tools.json | 2 +- .../Controllers/ActivityController.cs | 13 +- .../Unit/Activity/ActivityGroupingTests.cs | 340 +++++++++++++++++- .../Database/ActivityInterceptor.cs | 2 +- ProjectLighthouse/Database/DatabaseContext.cs | 6 +- .../Extensions/ActivityQueryExtensions.cs | 112 +++--- ...y.cs => 20240325034658_InitialActivity.cs} | 10 +- .../DatabaseContextModelSnapshot.cs | 203 +++++++++-- .../Types/Activity/ActivityDto.cs | 6 +- .../Activity/ActivityEntityEventHandler.cs | 14 +- ProjectLighthouse/Types/Activity/EventType.cs | 16 +- .../Types/Entities/Activity/ActivityEntity.cs | 9 + .../Activity/CommentActivityEntity.cs | 25 +- .../Entities/Activity/LevelActivityEntity.cs | 7 +- .../Entities/Activity/NewsActivityEntity.cs | 11 +- .../Entities/Activity/PhotoActivityEntity.cs | 26 +- .../Activity/PlaylistActivityEntity.cs | 24 +- .../Entities/Activity/ReviewActivityEntity.cs | 12 +- .../Entities/Activity/ScoreActivityEntity.cs | 12 +- .../Entities/Activity/UserActivityEntity.cs | 7 +- .../Activity/Events/GameEvent.cs | 2 +- .../Serialization/Activity/GameStream.cs | 28 +- .../Serialization/Activity/GameStreamGroup.cs | 5 +- 23 files changed, 742 insertions(+), 150 deletions(-) rename ProjectLighthouse/Migrations/{20240120214525_InitialActivity.cs => 20240325034658_InitialActivity.cs} (97%) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 760f61729..9e01b2fa6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.13", + "version": "8.0.0", "commands": [ "dotnet-ef" ] diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index 792c67117..fb5112892 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -56,7 +56,7 @@ private async Task> GetFilters .ToListAsync(); List? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds; - friendIds ??= new List(); + friendIds ??= []; // This is how lbp3 does its filtering GameStreamFilter? filter = await this.DeserializeBody(); @@ -89,7 +89,7 @@ private async Task> GetFilters predicate = predicate.Or(dto => dto.TargetSlotCreatorId == token.UserId); } - List includedUserIds = new(); + List includedUserIds = []; if (!excludeFriends) { @@ -168,7 +168,7 @@ public Task GetMostRecentEventTime(IQueryable activity, D private static DateTime GetOldestTime (IReadOnlyCollection> groups, DateTime defaultTimestamp) => - groups.Any() + groups.Count != 0 ? groups.Min(g => g.MinBy(a => a.Activity.Timestamp)?.Activity.Timestamp ?? defaultTimestamp) : defaultTimestamp; @@ -247,7 +247,7 @@ bool excludeMyself { GameTokenEntity token = this.GetToken(); - if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.NotFound(); + if (token.GameVersion is GameVersion.LittleBigPlanet1 or GameVersion.LittleBigPlanetPSP) return this.NotFound(); IQueryable activityEvents = await this.GetFilters(this.database.Activities.ToActivityDto(true), token, @@ -305,10 +305,11 @@ public async Task LocalActivity(string? slotType, int slotId, str { GameTokenEntity token = this.GetToken(); - if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.NotFound(); + if (token.GameVersion is GameVersion.LittleBigPlanet1 or GameVersion.LittleBigPlanetPSP) return this.NotFound(); if ((SlotHelper.IsTypeInvalid(slotType) || slotId == 0) == (username == null)) return this.BadRequest(); + // User and Level activity will never contain news posts or MM pick events. IQueryable activityQuery = this.database.Activities.ToActivityDto() .Where(a => a.Activity.Type != EventType.NewsPost && a.Activity.Type != EventType.MMPickLevel); @@ -343,6 +344,8 @@ public async Task LocalActivity(string? slotType, int slotId, str List outerGroups = groups.ToOuterActivityGroups(); + PrintOuterGroups(outerGroups); + long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); await this.CacheEntities(outerGroups); diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs index 617c0caa8..8843091c9 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs @@ -1,31 +1,345 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.EntityFrameworkCore; using Xunit; namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity; +[Trait("Category", "Unit")] public class ActivityGroupingTests { [Fact] - public void ActivityGroupingTest() + public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_ObjectThenActor() { - List activities = new() - { - new ActivityDto + List activities = [ + new LevelActivityEntity + { + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, + }, + new LevelActivityEntity + { + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.ReviewLevel, + }, + new LevelActivityEntity + { + UserId = 2, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, + }, + new UserActivityEntity + { + TargetUserId = 2, + UserId = 1, + Type = EventType.HeartUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity { - TargetPlaylistId = 1, - Activity = new ActivityEntity(), + TargetUserId = 2, + UserId = 1, + Type = EventType.CommentOnUser, + Timestamp = DateTime.Now, }, - }; - List groups = activities.AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups(); + new UserActivityEntity + { + TargetUserId = 1, + UserId = 2, + Type = EventType.HeartUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 1, + UserId = 2, + Type = EventType.CommentOnUser, + Timestamp = DateTime.Now, + }, + ]; + + //TODO: fix test + List groups = activities.ToActivityDto().AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups(); Assert.NotNull(groups); Assert.Single(groups); - OuterActivityGroup groupEntry = groups.First(); - - Assert.Equal(ActivityGroupType.Playlist, groupEntry.Key.GroupType); - Assert.Equal(1, groupEntry.Key.TargetId); + OuterActivityGroup outerGroup = groups.First(); + + Assert.Equal(ActivityGroupType.Level, outerGroup.Key.GroupType); + Assert.Equal(1, outerGroup.Key.TargetSlotId); + + IGrouping? firstGroup = outerGroup.Groups.First(); + IGrouping? secondGroup = outerGroup.Groups.Last(); + + Assert.NotNull(secondGroup); + Assert.Equal(ActivityGroupType.User, secondGroup.Key.Type); + Assert.Equal(1, secondGroup.Key.TargetId); // user group should have the user id + Assert.Equal(1, secondGroup.ToList()[0].TargetSlotId); // events in user group should have t + Assert.Equal(1, secondGroup.ToList()[1].TargetSlotId); + + Assert.NotNull(firstGroup); + Assert.Equal(ActivityGroupType.User, firstGroup.Key.Type); + Assert.Equal(2, firstGroup.Key.TargetId); + Assert.Equal(1, firstGroup.ToList()[0].TargetSlotId); + } + + [Fact] + public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_ActorThenObject() + { + List activities = [ + new LevelActivityEntity + { + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, + }, + new LevelActivityEntity + { + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.ReviewLevel, + }, + new LevelActivityEntity + { + UserId = 2, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, + }, + new UserActivityEntity + { + TargetUserId = 2, + UserId = 1, + Type = EventType.HeartUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 2, + UserId = 1, + Type = EventType.CommentOnUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 1, + UserId = 2, + Type = EventType.HeartUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 1, + UserId = 2, + Type = EventType.CommentOnUser, + Timestamp = DateTime.Now, + }, + ]; + List groups = activities.ToActivityDto() + .AsQueryable() + .ToActivityGroups(true) + .ToList() + .ToOuterActivityGroups(true); + //TODO: fix test + Assert.Multiple(() => + { + Assert.NotNull(groups); + Assert.Equal(2, groups.Count); + OuterActivityGroup firstUserGroup = groups.FirstOrDefault(g => g.Key.UserId == 1); + OuterActivityGroup secondUserGroup = groups.FirstOrDefault(g => g.Key.UserId == 2); + Assert.NotNull(firstUserGroup.Groups); + Assert.NotNull(secondUserGroup.Groups); + + Assert.Equal(ActivityGroupType.User, firstUserGroup.Key.GroupType); + Assert.Equal(ActivityGroupType.User, secondUserGroup.Key.GroupType); + + Assert.Single(firstUserGroup.Groups); + Assert.Single(secondUserGroup.Groups); + + Assert.Equal(2, firstUserGroup.Groups.ToList()[0].Count()); + Assert.Single(secondUserGroup.Groups.ToList()[0]); + + // Assert.Equal(ActivityGroupType.Level, outerGroup.Key.GroupType); + // Assert.Equal(1, outerGroup.Key.TargetSlotId); + // + // IGrouping? firstGroup = outerGroup.Groups.First(); + // IGrouping? secondGroup = outerGroup.Groups.Last(); + // + // Assert.NotNull(secondGroup); + // Assert.Equal(ActivityGroupType.User, secondGroup.Key.Type); + // Assert.Equal(1, secondGroup.Key.TargetId); // user group should have the user id + // Assert.Equal(1, secondGroup.ToList()[0].TargetSlotId); // events in user group should have t + // Assert.Equal(1, secondGroup.ToList()[1].TargetSlotId); + // + // Assert.NotNull(firstGroup); + // Assert.Equal(ActivityGroupType.User, firstGroup.Key.Type); + // Assert.Equal(2, firstGroup.Key.TargetId); + // Assert.Equal(1, firstGroup.ToList()[0].TargetSlotId); + }); + } + + [Fact] + public async Task ToActivityDtoTest() + { + DatabaseContext db = await MockHelper.GetTestDatabase(); + db.Slots.Add(new SlotEntity + { + SlotId = 1, + CreatorId = 1, + GameVersion = GameVersion.LittleBigPlanet2, + }); + db.Slots.Add(new SlotEntity + { + SlotId = 2, + CreatorId = 1, + GameVersion = GameVersion.LittleBigPlanet2, + TeamPickTime = 1, + }); + db.Reviews.Add(new ReviewEntity + { + Timestamp = DateTime.Now.ToUnixTimeMilliseconds(), + SlotId = 1, + ReviewerId = 1, + ReviewId = 1, + }); + db.Comments.Add(new CommentEntity + { + TargetSlotId = 1, + PosterUserId = 1, + Message = "comment on level test", + CommentId = 1, + }); + db.Comments.Add(new CommentEntity + { + TargetUserId = 1, + PosterUserId = 1, + Message = "comment on user test", + CommentId = 2, + }); + db.WebsiteAnnouncements.Add(new WebsiteAnnouncementEntity + { + PublisherId = 1, + AnnouncementId = 1, + }); + db.Playlists.Add(new PlaylistEntity + { + PlaylistId = 1, + CreatorId = 1, + }); + db.Activities.Add(new LevelActivityEntity + { + Timestamp = DateTime.Now, + SlotId = 1, + Type = EventType.PlayLevel, + UserId = 1, + }); + db.Activities.Add(new ReviewActivityEntity + { + Timestamp = DateTime.Now, + SlotId = 1, + Type = EventType.ReviewLevel, + ReviewId = 1, + UserId = 1, + }); + db.Activities.Add(new UserCommentActivityEntity + { + Timestamp = DateTime.Now, + Type = EventType.CommentOnUser, + UserId = 1, + TargetUserId = 1, + CommentId = 2, + }); + db.Activities.Add(new LevelCommentActivityEntity + { + Timestamp = DateTime.Now, + Type = EventType.CommentOnLevel, + UserId = 1, + SlotId = 1, + CommentId = 1, + }); + db.Activities.Add(new NewsActivityEntity + { + Type = EventType.NewsPost, + NewsId = 1, + UserId = 1, + }); + db.Activities.Add(new PlaylistActivityEntity + { + Type = EventType.CreatePlaylist, + PlaylistId = 1, + UserId = 1, + }); + db.Activities.Add(new LevelActivityEntity + { + Type = EventType.MMPickLevel, + SlotId = 2, + UserId = 1, + }); + await db.SaveChangesAsync(); + + var sql = db.Activities.ToActivityDto().ToQueryString(); + + List resultDto = await db.Activities.ToActivityDto(includeSlotCreator: true, includeTeamPick: true).ToListAsync(); + + Assert.Equal(2, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetTeamPickId); + Assert.Equal(2, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetSlotId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetSlotCreatorId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CreatePlaylist)?.TargetPlaylistId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.NewsPost)?.TargetNewsId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnUser)?.TargetUserId); + + Assert.Null(resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetTeamPickId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetSlotId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetSlotCreatorId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.ReviewLevel)?.TargetSlotId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.ReviewLevel)?.TargetSlotCreatorId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.PlayLevel)?.TargetSlotId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.PlayLevel)?.TargetSlotCreatorId); } } \ No newline at end of file diff --git a/ProjectLighthouse/Database/ActivityInterceptor.cs b/ProjectLighthouse/Database/ActivityInterceptor.cs index ca5d66f53..fb364d958 100644 --- a/ProjectLighthouse/Database/ActivityInterceptor.cs +++ b/ProjectLighthouse/Database/ActivityInterceptor.cs @@ -93,7 +93,7 @@ private void ParseInsertedEntities(DbContextEventData eventData) { if (eventData.Context is not DatabaseContext context) return; - HashSet entities = new(); + HashSet entities = []; List entries = context.ChangeTracker.Entries().ToList(); diff --git a/ProjectLighthouse/Database/DatabaseContext.cs b/ProjectLighthouse/Database/DatabaseContext.cs index 6fc43ccfa..6c981b258 100644 --- a/ProjectLighthouse/Database/DatabaseContext.cs +++ b/ProjectLighthouse/Database/DatabaseContext.cs @@ -109,13 +109,15 @@ public static Action ConfigureBuilder() protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().UseTphMappingStrategy(); - modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); - modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); modelBuilder.Entity().UseTphMappingStrategy(); base.OnModelCreating(modelBuilder); diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index 67394c8c8..0697ec5ef 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -2,7 +2,6 @@ using System.Linq; using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Activity; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; namespace LBPUnion.ProjectLighthouse.Extensions; @@ -10,7 +9,7 @@ public static class ActivityQueryExtensions { public static List GetIds(this IReadOnlyCollection groups, ActivityGroupType type) { - List ids = new(); + List ids = []; // Add outer group ids ids.AddRange(groups.Where(g => g.Key.GroupType == type) .Where(g => g.Key.TargetId != 0) @@ -29,6 +28,12 @@ public static List GetIds(this IReadOnlyCollection grou return ids.Distinct().ToList(); } + /// + /// Turns a list of into a group based on its timestamp + /// + /// An to group + /// Whether or not the groups should be created based on the initiator of the event or the target of the event + /// The transformed query containing groups of public static IQueryable> ToActivityGroups (this IQueryable activityQuery, bool groupByActor = false) => groupByActor @@ -59,74 +64,65 @@ public static List ToOuterActivityGroups Groups = g.OrderByDescending(a => a.Activity.Timestamp) .GroupBy(gr => new InnerActivityGroup { - Type = groupByActor ? gr.GroupType : gr.GroupType != ActivityGroupType.News ? ActivityGroupType.User : ActivityGroupType.News, + Type = groupByActor + ? gr.GroupType + : gr.GroupType != ActivityGroupType.News + ? ActivityGroupType.User + : ActivityGroupType.News, UserId = gr.Activity.UserId, - TargetId = groupByActor ? gr.TargetId : gr.GroupType != ActivityGroupType.News ? gr.Activity.UserId : gr.TargetNewsId ?? 0, + TargetId = groupByActor + ? gr.TargetId + : gr.GroupType != ActivityGroupType.News + ? gr.Activity.UserId + : gr.TargetNewsId ?? 0, }) .ToList(), }) .ToList(); - // WARNING - To the next person who tries to improve this code: As of writing this, it's not possible - // to build a pattern matching switch statement with expression trees. so the only other option - // is to basically rewrite this nested ternary mess with expression trees which isn't much better - // The resulting SQL generated by EntityFramework uses a CASE statement which is probably fine - // TOTAL HOURS WASTED: 3 + /// + /// Converts an <> into an <> for grouping. + /// + /// The activity query to be converted. + /// Whether or not the field should be included. + /// Whether or not the field should be included. + /// The converted <> public static IQueryable ToActivityDto (this IQueryable activityQuery, bool includeSlotCreator = false, bool includeTeamPick = false) { return activityQuery.Select(a => new ActivityDto { Activity = a, - TargetSlotId = a is LevelActivityEntity - ? ((LevelActivityEntity)a).SlotId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 - ? ((PhotoActivityEntity)a).Photo.SlotId - : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level - ? ((CommentActivityEntity)a).Comment.TargetSlotId - : a is ScoreActivityEntity - ? ((ScoreActivityEntity)a).Score.SlotId - : a is ReviewActivityEntity - ? ((ReviewActivityEntity)a).Review.SlotId - : 0, - TargetSlotGameVersion = a is LevelActivityEntity - ? ((LevelActivityEntity)a).Slot.GameVersion - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 - ? ((PhotoActivityEntity)a).Photo.Slot.GameVersion - : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level - ? ((CommentActivityEntity)a).Comment.TargetSlot.GameVersion - : a is ScoreActivityEntity - ? ((ScoreActivityEntity)a).Score.Slot.GameVersion - : a is ReviewActivityEntity - ? ((ReviewActivityEntity)a).Review.Slot.GameVersion - : 0, - TargetSlotCreatorId = includeSlotCreator - ? a is LevelActivityEntity - ? ((LevelActivityEntity)a).Slot.CreatorId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0 - ? ((PhotoActivityEntity)a).Photo.Slot!.CreatorId - : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level - ? ((CommentActivityEntity)a).Comment.TargetSlot.CreatorId - : a is ScoreActivityEntity - ? ((ScoreActivityEntity)a).Score.Slot.CreatorId - : a is ReviewActivityEntity - ? ((ReviewActivityEntity)a).Review.Slot!.CreatorId - : 0 - : 0, + TargetSlotId = (a as LevelActivityEntity).SlotId, + TargetSlotGameVersion = (a as LevelActivityEntity).Slot.GameVersion, + TargetSlotCreatorId = includeSlotCreator ? (a as LevelActivityEntity).Slot.CreatorId : null, + TargetUserId = (a as UserActivityEntity).TargetUserId, + TargetNewsId = (a as NewsActivityEntity).NewsId, + TargetPlaylistId = (a as PlaylistActivityEntity).PlaylistId, + TargetTeamPickId = + includeTeamPick && a.Type == EventType.MMPickLevel ? (a as LevelActivityEntity).SlotId : null, }); + } - TargetUserId = a is UserActivityEntity - ? ((UserActivityEntity)a).TargetUserId - : a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile - ? ((CommentActivityEntity)a).Comment.TargetUserId - : a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId == 0 - ? ((PhotoActivityEntity)a).Photo.CreatorId - : 0, - TargetPlaylistId = a is PlaylistActivityEntity || a is PlaylistWithSlotActivityEntity - ? ((PlaylistActivityEntity)a).PlaylistId - : 0, - TargetNewsId = a is NewsActivityEntity ? ((NewsActivityEntity)a).NewsId : 0, - TargetTeamPickId = includeTeamPick - ? a.Type == EventType.MMPickLevel && a is LevelActivityEntity ? ((LevelActivityEntity)a).SlotId : 0 - : 0, }); + /// + /// Converts an IEnumerable<> into an IEnumerable<> for grouping. + /// + /// The activity query to be converted. + /// Whether or not the field should be included. + /// Whether or not the field should be included. + /// The converted IEnumerable<> + public static IEnumerable ToActivityDto + (this IEnumerable activityEnumerable, bool includeSlotCreator = false, bool includeTeamPick = false) + { + return activityEnumerable.Select(a => new ActivityDto + { + Activity = a, + TargetSlotId = (a as LevelActivityEntity)?.SlotId, + TargetSlotGameVersion = (a as LevelActivityEntity)?.Slot.GameVersion, + TargetSlotCreatorId = includeSlotCreator ? (a as LevelActivityEntity)?.Slot.CreatorId : null, + TargetUserId = (a as UserActivityEntity)?.TargetUserId, + TargetNewsId = (a as NewsActivityEntity)?.NewsId, + TargetPlaylistId = (a as PlaylistActivityEntity)?.PlaylistId, + TargetTeamPickId = + includeTeamPick && a.Type == EventType.MMPickLevel ? (a as LevelActivityEntity)?.SlotId : null, }); } } \ No newline at end of file diff --git a/ProjectLighthouse/Migrations/20240120214525_InitialActivity.cs b/ProjectLighthouse/Migrations/20240325034658_InitialActivity.cs similarity index 97% rename from ProjectLighthouse/Migrations/20240120214525_InitialActivity.cs rename to ProjectLighthouse/Migrations/20240325034658_InitialActivity.cs index 9f4d01a47..62ea0bd45 100644 --- a/ProjectLighthouse/Migrations/20240120214525_InitialActivity.cs +++ b/ProjectLighthouse/Migrations/20240325034658_InitialActivity.cs @@ -6,10 +6,10 @@ #nullable disable -namespace ProjectLighthouse.Migrations +namespace LBPUnion.ProjectLighthouse.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20240120214525_InitialActivity")] + [Migration("20240325034658_InitialActivity")] public partial class InitialActivity : Migration { /// @@ -24,12 +24,12 @@ protected override void Up(MigrationBuilder migrationBuilder) Timestamp = table.Column(type: "datetime(6)", nullable: false), UserId = table.Column(type: "int", nullable: false), Type = table.Column(type: "int", nullable: false), - Discriminator = table.Column(type: "longtext", nullable: false) + Discriminator = table.Column(type: "varchar(34)", maxLength: 34, nullable: false) .Annotation("MySql:CharSet", "utf8mb4"), - CommentId = table.Column(type: "int", nullable: true), SlotId = table.Column(type: "int", nullable: true), - NewsId = table.Column(type: "int", nullable: true), + CommentId = table.Column(type: "int", nullable: true), PhotoId = table.Column(type: "int", nullable: true), + NewsId = table.Column(type: "int", nullable: true), PlaylistId = table.Column(type: "int", nullable: true), ReviewId = table.Column(type: "int", nullable: true), ScoreId = table.Column(type: "int", nullable: true), diff --git a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs index 5c33884b7..7f069036c 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -27,7 +27,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Discriminator") .IsRequired() - .HasColumnType("longtext"); + .HasMaxLength(34) + .HasColumnType("varchar(34)"); b.Property("Timestamp") .HasColumnType("datetime(6)"); @@ -1123,54 +1124,70 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("WebsiteAnnouncements"); }); - modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.CommentActivityEntity", b => + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelActivityEntity", b => { b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); - b.Property("CommentId") - .HasColumnType("int"); + b.Property("SlotId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("SlotId"); - b.HasIndex("CommentId"); + b.HasIndex("SlotId"); - b.HasDiscriminator().HasValue("CommentActivityEntity"); + b.HasDiscriminator().HasValue("LevelActivityEntity"); }); - modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelActivityEntity", b => + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelCommentActivityEntity", b => { b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + b.Property("CommentId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int"); + b.Property("SlotId") .ValueGeneratedOnUpdateSometimes() .HasColumnType("int") .HasColumnName("SlotId"); + b.HasIndex("CommentId"); + b.HasIndex("SlotId"); - b.HasDiscriminator().HasValue("LevelActivityEntity"); + b.HasDiscriminator().HasValue("LevelCommentActivityEntity"); }); - modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b => + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelPhotoActivity", b => { b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); - b.Property("NewsId") + b.Property("PhotoId") + .ValueGeneratedOnUpdateSometimes() .HasColumnType("int"); - b.HasIndex("NewsId"); + b.Property("SlotId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("SlotId"); - b.HasDiscriminator().HasValue("NewsActivityEntity"); + b.HasIndex("PhotoId"); + + b.HasIndex("SlotId"); + + b.HasDiscriminator().HasValue("LevelPhotoActivity"); }); - modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PhotoActivityEntity", b => + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b => { b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); - b.Property("PhotoId") + b.Property("NewsId") .HasColumnType("int"); - b.HasIndex("PhotoId"); + b.HasIndex("NewsId"); - b.HasDiscriminator().HasValue("PhotoActivityEntity"); + b.HasDiscriminator().HasValue("NewsActivityEntity"); }); modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PlaylistActivityEntity", b => @@ -1213,8 +1230,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ReviewId") .HasColumnType("int"); + b.Property("SlotId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("SlotId"); + b.HasIndex("ReviewId"); + b.HasIndex("SlotId"); + b.HasDiscriminator().HasValue("ReviewActivityEntity"); }); @@ -1225,8 +1249,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ScoreId") .HasColumnType("int"); + b.Property("SlotId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("SlotId"); + b.HasIndex("ScoreId"); + b.HasIndex("SlotId"); + b.HasDiscriminator().HasValue("ScoreActivityEntity"); }); @@ -1235,13 +1266,55 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); b.Property("TargetUserId") - .HasColumnType("int"); + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("TargetUserId"); b.HasIndex("TargetUserId"); b.HasDiscriminator().HasValue("UserActivityEntity"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserCommentActivityEntity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("CommentId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int"); + + b.Property("TargetUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("TargetUserId"); + + b.HasIndex("CommentId"); + + b.HasIndex("TargetUserId"); + + b.HasDiscriminator().HasValue("UserCommentActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserPhotoActivity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("PhotoId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int"); + + b.Property("TargetUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("TargetUserId"); + + b.HasIndex("PhotoId"); + + b.HasIndex("TargetUserId"); + + b.HasDiscriminator().HasValue("UserPhotoActivity"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity", b => { b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") @@ -1646,48 +1719,64 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Publisher"); }); - modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.CommentActivityEntity", b => + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelActivityEntity", b => { - b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", "Comment") + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") .WithMany() - .HasForeignKey("CommentId") + .HasForeignKey("SlotId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Comment"); + b.Navigation("Slot"); }); - modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelActivityEntity", b => + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelCommentActivityEntity", b => { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", "Comment") + .WithMany() + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") .WithMany() .HasForeignKey("SlotId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Comment"); + b.Navigation("Slot"); }); - modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b => + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelPhotoActivity", b => { - b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Website.WebsiteAnnouncementEntity", "News") + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo") .WithMany() - .HasForeignKey("NewsId") + .HasForeignKey("PhotoId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("News"); + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Photo"); + + b.Navigation("Slot"); }); - modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PhotoActivityEntity", b => + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b => { - b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo") + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Website.WebsiteAnnouncementEntity", "News") .WithMany() - .HasForeignKey("PhotoId") + .HasForeignKey("NewsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Photo"); + b.Navigation("News"); }); modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PlaylistActivityEntity", b => @@ -1720,7 +1809,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Review"); + + b.Navigation("Slot"); }); modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ScoreActivityEntity", b => @@ -1731,7 +1828,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Score"); + + b.Navigation("Slot"); }); modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserActivityEntity", b => @@ -1745,6 +1850,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("TargetUser"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserCommentActivityEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", "Comment") + .WithMany() + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "TargetUser") + .WithMany() + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserPhotoActivity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo") + .WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "TargetUser") + .WithMany() + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Photo"); + + b.Navigation("TargetUser"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", b => { b.Navigation("PhotoSubjects"); diff --git a/ProjectLighthouse/Types/Activity/ActivityDto.cs b/ProjectLighthouse/Types/Activity/ActivityDto.cs index d813f76d9..877593697 100644 --- a/ProjectLighthouse/Types/Activity/ActivityDto.cs +++ b/ProjectLighthouse/Types/Activity/ActivityDto.cs @@ -25,11 +25,11 @@ public class ActivityDto }; public ActivityGroupType GroupType => - this.TargetSlotId != 0 + this.TargetSlotId != null ? ActivityGroupType.Level - : this.TargetUserId != 0 + : this.TargetUserId != null ? ActivityGroupType.User - : this.TargetPlaylistId != 0 + : this.TargetPlaylistId != null ? ActivityGroupType.Playlist : ActivityGroupType.News; } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 46f5ebb21..a2b520e65 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -41,40 +41,44 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl { CommentType.Level => comment.TargetSlot?.Type switch { - SlotType.User => new CommentActivityEntity + SlotType.User => new LevelCommentActivityEntity { Type = EventType.CommentOnLevel, CommentId = comment.CommentId, UserId = comment.PosterUserId, + SlotId = comment.TargetSlotId ?? throw new NullReferenceException("SlotId in Level comment is null, this shouldn't happen."), }, _ => null, }, - CommentType.Profile => new CommentActivityEntity + CommentType.Profile => new UserCommentActivityEntity() { Type = EventType.CommentOnUser, CommentId = comment.CommentId, UserId = comment.PosterUserId, + TargetUserId = comment.TargetUserId ?? throw new NullReferenceException("TargetUserId in User comment is null, this shouldn't happen."), }, _ => null, }, PhotoEntity photo => photo.SlotId switch { // Photos without levels - null => new PhotoActivityEntity + null => new UserPhotoActivity { Type = EventType.UploadPhoto, PhotoId = photo.PhotoId, UserId = photo.CreatorId, + TargetUserId = photo.CreatorId, }, _ => photo.Slot?.Type switch { SlotType.Developer => null, // Non-story levels (moon, pod, etc) - _ => new PhotoActivityEntity + _ => new LevelPhotoActivity { Type = EventType.UploadPhoto, PhotoId = photo.PhotoId, UserId = photo.CreatorId, + SlotId = photo.SlotId ?? throw new NullReferenceException("SlotId in Photo is null"), }, }, }, @@ -86,6 +90,7 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl Type = EventType.Score, ScoreId = score.ScoreId, UserId = score.UserId, + SlotId = score.SlotId, }, _ => null, }, @@ -122,6 +127,7 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl Type = EventType.ReviewLevel, ReviewId = review.ReviewId, UserId = review.ReviewerId, + SlotId = review.SlotId, }, RatedLevelEntity ratedLevel => new LevelActivityEntity { diff --git a/ProjectLighthouse/Types/Activity/EventType.cs b/ProjectLighthouse/Types/Activity/EventType.cs index f70100cdb..c497b0472 100644 --- a/ProjectLighthouse/Types/Activity/EventType.cs +++ b/ProjectLighthouse/Types/Activity/EventType.cs @@ -3,7 +3,12 @@ namespace LBPUnion.ProjectLighthouse.Types.Activity; /// -/// UnheartLevel, UnheartUser, DeleteLevelComment, and UnpublishLevel don't actually do anything +/// An enum of all possible event types that LBP recognizes in Recent Activity +/// +/// +/// , , , are ignored by the game +/// +/// /// public enum EventType { @@ -61,12 +66,21 @@ public enum EventType [XmlEnum("comment_on_user")] CommentOnUser = 17, + /// + /// This event is only used in LBP3 + /// > [XmlEnum("create_playlist")] CreatePlaylist = 18, + /// + /// This event is only used in LBP3 + /// > [XmlEnum("heart_playlist")] HeartPlaylist = 19, + /// + /// This event is only used in LBP3 + /// > [XmlEnum("add_level_to_playlist")] AddLevelToPlaylist = 20, } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs index 7a1c5bf19..39732e041 100644 --- a/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs @@ -11,12 +11,21 @@ public class ActivityEntity [Key] public int ActivityId { get; set; } + /// + /// The time that this event took place. + /// public DateTime Timestamp { get; set; } + /// + /// The of the that triggered this event. + /// public int UserId { get; set; } [ForeignKey(nameof(UserId))] public UserEntity User { get; set; } + /// + /// The type of this event. + /// public EventType Type { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs index 0c20175e7..61a80e883 100644 --- a/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs @@ -1,15 +1,38 @@ using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: CommentOnUser, CommentOnLevel, DeleteLevelComment +/// Supported event types: , , and . /// public class CommentActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// public int CommentId { get; set; } [ForeignKey(nameof(CommentId))] public CommentEntity Comment { get; set; } +} + +public class LevelCommentActivityEntity : CommentActivityEntity +{ + [Column("SlotId")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } +} + +public class UserCommentActivityEntity : CommentActivityEntity +{ + [Column("TargetUserId")] + public int TargetUserId { get; set; } + + [ForeignKey(nameof(TargetUserId))] + public UserEntity TargetUser { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs index 699a93f5b..27d7ca9af 100644 --- a/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs @@ -1,13 +1,18 @@ using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Level; namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: play_level, heart_level, publish_level, unheart_level, dpad_rate_level, rate_level, tag_level, mm_pick_level +/// Supported event types: , , , +/// , and . /// public class LevelActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// [Column("SlotId")] public int SlotId { get; set; } diff --git a/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs index 32a768ca7..59daccaa6 100644 --- a/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs @@ -1,13 +1,22 @@ using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Website; namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: NewsPost +/// Supported event types: . +/// +/// +/// This event type can only be grouped with other . +/// +/// /// public class NewsActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// public int NewsId { get; set; } [ForeignKey(nameof(NewsId))] diff --git a/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs index 6db40d59e..bf1d90839 100644 --- a/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs @@ -1,16 +1,38 @@ using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: UploadPhoto +/// Supported event types: . /// public class PhotoActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// public int PhotoId { get; set; } [ForeignKey(nameof(PhotoId))] public PhotoEntity Photo { get; set; } - +} + +public class LevelPhotoActivity : PhotoActivityEntity +{ + [Column("SlotId")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } +} + +public class UserPhotoActivity : PhotoActivityEntity +{ + [Column("TargetUserId")] + public int TargetUserId { get; set; } + + [ForeignKey(nameof(TargetUserId))] + public UserEntity TargetUser { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs index 4d5354594..9d4ef06b3 100644 --- a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs @@ -1,13 +1,17 @@ using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Level; namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: CreatePlaylist, HeartPlaylist +/// Supported event types: and . /// public class PlaylistActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// [Column("PlaylistId")] public int PlaylistId { get; set; } @@ -16,15 +20,19 @@ public class PlaylistActivityEntity : ActivityEntity } /// -/// Supported event types: AddLevelToPlaylist +/// Supported event types: . +/// /// /// The relationship between and -/// is slightly hacky but it allows conditional reuse of columns from other ActivityEntity's -/// -/// +/// is slightly hacky but it allows us to reuse columns that would normally only be user with other types. +/// +/// /// public class PlaylistWithSlotActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// [Column("PlaylistId")] public int PlaylistId { get; set; } @@ -33,7 +41,11 @@ public class PlaylistWithSlotActivityEntity : ActivityEntity /// /// This reuses the SlotId column of but has no ForeignKey definition so that it can be null - /// It effectively serves as extra storage for PlaylistActivityEntity to use for the AddLevelToPlaylistEvent + /// + /// + /// It effectively serves as extra storage for PlaylistActivityEntity to use for the AddLevelToPlaylistEvent + /// + /// /// [Column("SlotId")] public int SlotId { get; set; } diff --git a/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs index 9a722601f..e824541c2 100644 --- a/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs @@ -1,15 +1,25 @@ using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Level; namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: DpadRateLevel, ReviewLevel, RateLevel, TagLevel +/// Supported event types: , , , and . /// public class ReviewActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// public int ReviewId { get; set; } [ForeignKey(nameof(ReviewId))] public ReviewEntity Review { get; set; } + + [Column("SlotId")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs index 17c500ac0..4ecc8384d 100644 --- a/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs @@ -1,15 +1,25 @@ using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Level; namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: Score +/// Supported event types: . /// public class ScoreActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// public int ScoreId { get; set; } [ForeignKey(nameof(ScoreId))] public ScoreEntity Score { get; set; } + + [Column("SlotId")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs index 89d27e8b2..f1756f5b5 100644 --- a/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs @@ -1,13 +1,18 @@ using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; /// -/// Supported event types: HeartUser, UnheartUser +/// Supported event types: and . /// public class UserActivityEntity : ActivityEntity { + /// + /// The of the that this event refers to. + /// + [Column("TargetUserId")] public int TargetUserId { get; set; } [ForeignKey(nameof(TargetUserId))] diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs index 649907d5c..21d283147 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -58,7 +58,7 @@ protected async Task PrepareSerialization(DatabaseContext database) public static IEnumerable CreateFromActivities(IEnumerable activities) { - List events = new(); + List events = []; List> typeGroups = activities.GroupBy(g => g.Activity.Type).ToList(); foreach (IGrouping typeGroup in typeGroups) { diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs index 3f41b0250..f2d2d7a28 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -26,21 +26,34 @@ namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; [XmlRoot("stream")] public class GameStream : ILbpSerializable, INeedsPreparationForSerialization { + /// + /// A list of that should be included in the root + /// of the stream object. These will be loaded into + /// [XmlIgnore] public List SlotIds { get; set; } + /// + /// A list of that should be included in the root + /// of the stream object. These will be loaded into + /// [XmlIgnore] public List UserIds { get; set; } + /// + /// A list of that should be included in the root + /// of the stream object. These will be loaded into + /// [XmlIgnore] public List PlaylistIds { get; set; } + /// + /// A list of that should be included in the root + /// of the stream object. These will be loaded into + /// [XmlIgnore] public List NewsIds { get; set; } - [XmlIgnore] - private int TargetUserId { get; set; } - [XmlIgnore] private GameVersion TargetGame { get; set; } @@ -77,7 +90,7 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization public async Task PrepareSerialization(DatabaseContext database) { - this.Slots = await LoadEntities(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId), s => s.Type == SlotType.User); + this.Slots = await LoadEntities(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, 0), s => s.Type == SlotType.User); this.Users = await LoadEntities(this.UserIds, user => GameUser.CreateFromEntity(user, this.TargetGame)); this.Playlists = await LoadEntities(this.PlaylistIds, GamePlaylist.CreateFromEntity); this.News = await LoadEntities(this.NewsIds, a => GameNewsObject.CreateFromEntity(a, this.TargetGame)); @@ -86,16 +99,16 @@ public async Task PrepareSerialization(DatabaseContext database) async Task> LoadEntities(List ids, Func transformation, Func predicate = null) where TFrom : class { - List results = new(); + List results = []; if (ids.Count <= 0) return null; foreach (int id in ids) { TFrom entity = await database.Set().FindAsync(id); - if (predicate != null && !predicate(entity)) continue; - if (entity == null) continue; + if (predicate != null && !predicate(entity)) continue; + results.Add(transformation(entity)); } @@ -108,7 +121,6 @@ public static GameStream CreateFromGroups { GameStream gameStream = new() { - TargetUserId = token.UserId, TargetGame = token.GameVersion, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs index 4afc678b2..e4fea532a 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs @@ -37,8 +37,6 @@ public class GameStreamGroup : ILbpSerializable [XmlArray("events")] [XmlArrayItem("event")] [DefaultValue(null)] - // ReSharper disable once MemberCanBePrivate.Global - // (the serializer can't see this if it's private) public List Events { get; set; } public static GameStreamGroup CreateFromGroup(OuterActivityGroup group) @@ -56,8 +54,7 @@ public static GameStreamGroup CreateFromGroup(OuterActivityGroup group) g.Key.TargetId, streamGroup => { - streamGroup.Timestamp = - g.MaxBy(a => a.Activity.Timestamp).Activity.Timestamp.ToUnixTimeMilliseconds(); + streamGroup.Timestamp = g.Max(a => a.Activity.Timestamp).ToUnixTimeMilliseconds(); streamGroup.Events = GameEvent.CreateFromActivities(g).ToList(); })) .ToList()); From 180cac5aa9dd8de8b36b79223f9c66d6aa131e36 Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 13 May 2024 18:46:49 -0500 Subject: [PATCH 29/40] Remove debug prints and prevent activities from being registered in read only mode --- .../Controllers/ActivityController.cs | 7 ----- .../Database/ActivityInterceptor.cs | 21 ++++++++++---- .../Activity/ActivityEntityEventHandler.cs | 29 +++---------------- 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index fb5112892..69f388272 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -30,9 +30,6 @@ public ActivityController(DatabaseContext database) this.database = database; } - /// - /// This method is only used for LBP2 so we exclude playlists - /// private async Task> GetFilters ( IQueryable dtoQuery, @@ -123,8 +120,6 @@ private async Task> GetFilters dto.Activity.Type != EventType.AddLevelToPlaylist); } - Logger.Debug(predicate.ToString(), LogArea.Activity); - dtoQuery = dtoQuery.Where(predicate); return dtoQuery; @@ -344,8 +339,6 @@ public async Task LocalActivity(string? slotType, int slotId, str List outerGroups = groups.ToOuterActivityGroups(); - PrintOuterGroups(outerGroups); - long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); await this.CacheEntities(outerGroups); diff --git a/ProjectLighthouse/Database/ActivityInterceptor.cs b/ProjectLighthouse/Database/ActivityInterceptor.cs index fb364d958..7ded9cd52 100644 --- a/ProjectLighthouse/Database/ActivityInterceptor.cs +++ b/ProjectLighthouse/Database/ActivityInterceptor.cs @@ -22,13 +22,20 @@ private class CustomTrackedEntity public required object OldEntity { get; init; } } - private readonly ConcurrentDictionary<(Type Type, int HashCode), CustomTrackedEntity> unsavedEntities; + private struct TrackedEntityKey + { + public Type Type { get; set; } + public int HashCode { get; set; } + public Guid ContextId { get; set; } + } + + private readonly ConcurrentDictionary unsavedEntities; private readonly IEntityEventHandler eventHandler; public ActivityInterceptor(IEntityEventHandler eventHandler) { this.eventHandler = eventHandler; - this.unsavedEntities = new ConcurrentDictionary<(Type Type, int HashCode), CustomTrackedEntity>(); + this.unsavedEntities = new ConcurrentDictionary(); } #region Hooking stuff @@ -78,8 +85,12 @@ private void SaveNewEntities(DbContextEventData eventData) if (entry.Metadata.Name.Contains("Token")) continue; if (entry.State is not (EntityState.Added or EntityState.Deleted or EntityState.Modified)) continue; - - this.unsavedEntities.TryAdd((entry.Entity.GetType(), entry.Entity.GetHashCode()), + this.unsavedEntities.TryAdd(new TrackedEntityKey + { + ContextId = context.ContextId.InstanceId, + Type = entry.Entity.GetType(), + HashCode = entry.Entity.GetHashCode(), + }, new CustomTrackedEntity { State = entry.State, @@ -97,7 +108,7 @@ private void ParseInsertedEntities(DbContextEventData eventData) List entries = context.ChangeTracker.Entries().ToList(); - foreach (KeyValuePair<(Type Type, int HashCode), CustomTrackedEntity> kvp in this.unsavedEntities) + foreach (KeyValuePair kvp in this.unsavedEntities) { EntityEntry entry = entries.FirstOrDefault(e => e.Metadata.ClrType == kvp.Key.Type && e.Entity.GetHashCode() == kvp.Key.HashCode); diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index a2b520e65..91e3e6055 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Logging; @@ -13,10 +14,6 @@ using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; using Microsoft.EntityFrameworkCore; -#if DEBUG -using System.ComponentModel.DataAnnotations.Schema; -using System.Reflection; -#endif namespace LBPUnion.ProjectLighthouse.Types.Activity; @@ -24,7 +21,6 @@ public class ActivityEntityEventHandler : IEntityEventHandler { public void OnEntityInserted(DatabaseContext database, T entity) where T : class { - Logger.Debug($@"OnEntityInserted: {entity.GetType().Name}", LogArea.Activity); ActivityEntity? activity = entity switch { SlotEntity slot => slot.Type switch @@ -50,7 +46,7 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, _ => null, }, - CommentType.Profile => new UserCommentActivityEntity() + CommentType.Profile => new UserCommentActivityEntity { Type = EventType.CommentOnUser, CommentId = comment.CommentId, @@ -199,6 +195,8 @@ private static void InsertActivity(DatabaseContext database, ActivityEntity? act { if (activity == null) return; + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return; + Logger.Debug("Inserting activity: " + activity.GetType().Name, LogArea.Activity); RemoveDuplicateEvents(database, activity); @@ -210,24 +208,6 @@ private static void InsertActivity(DatabaseContext database, ActivityEntity? act public void OnEntityChanged(DatabaseContext database, T origEntity, T currentEntity) where T : class { - #if DEBUG - foreach (PropertyInfo propInfo in currentEntity.GetType().GetProperties()) - { - if (!propInfo.CanRead || !propInfo.CanWrite) continue; - - if (propInfo.CustomAttributes.Any(c => c.AttributeType == typeof(NotMappedAttribute))) continue; - - object? origVal = propInfo.GetValue(origEntity); - object? newVal = propInfo.GetValue(currentEntity); - if ((origVal == null && newVal == null) || (origVal != null && newVal != null && origVal.Equals(newVal))) continue; - - Logger.Debug($@"Value for {propInfo.Name} changed", LogArea.Activity); - Logger.Debug($@"Orig val: {origVal?.ToString() ?? "null"}", LogArea.Activity); - Logger.Debug($@"New val: {newVal?.ToString() ?? "null"}", LogArea.Activity); - } - Logger.Debug($@"OnEntityChanged: {currentEntity.GetType().Name}", LogArea.Activity); - #endif - ActivityEntity? activity = null; switch (currentEntity) { @@ -354,7 +334,6 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current public void OnEntityDeleted(DatabaseContext database, T entity) where T : class { - Logger.Debug($@"OnEntityDeleted: {entity.GetType().Name}", LogArea.Activity); ActivityEntity? activity = entity switch { HeartedLevelEntity heartedLevel => heartedLevel.Slot.Type switch From b41b01a6eb0a322cce9540a39b3ed0c1a7c4d630 Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 13 May 2024 19:07:28 -0500 Subject: [PATCH 30/40] Fix test compilation and level activity flattening --- .../Controllers/ActivityController.cs | 17 ++++++++++++++--- .../Unit/Controllers/ReviewControllerTests.cs | 2 +- .../Types/Serialization/Activity/GameStream.cs | 5 +++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index 69f388272..7fc867e29 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -283,11 +283,13 @@ private static void PrintOuterGroups(List outerGroups) foreach (IGrouping item in itemGroup) { Logger.Debug( - @$" Inner group key: TargetId={item.Key.TargetId}, UserId={item.Key.UserId}, Type={item.Key.Type}", LogArea.Activity); + @$" Inner group key: TargetId={item.Key.TargetId}, UserId={item.Key.UserId}, Type={item.Key.Type}", + LogArea.Activity); foreach (ActivityDto activity in item) { Logger.Debug( - @$" Activity: {activity.GroupType}, Timestamp: {activity.Activity.Timestamp}, UserId: {activity.Activity.UserId}, EventType: {activity.Activity.Type}, TargetId: {activity.TargetId}", LogArea.Activity); + @$" Activity: {activity.GroupType}, Timestamp: {activity.Activity.Timestamp}, UserId: {activity.Activity.UserId}, EventType: {activity.Activity.Type}, TargetId: {activity.TargetId}", + LogArea.Activity); } } } @@ -308,6 +310,14 @@ public async Task LocalActivity(string? slotType, int slotId, str IQueryable activityQuery = this.database.Activities.ToActivityDto() .Where(a => a.Activity.Type != EventType.NewsPost && a.Activity.Type != EventType.MMPickLevel); + if (token.GameVersion != GameVersion.LittleBigPlanet3) + { + activityQuery = activityQuery.Where(a => + a.Activity.Type != EventType.CreatePlaylist && + a.Activity.Type != EventType.HeartPlaylist && + a.Activity.Type != EventType.AddLevelToPlaylist); + } + bool isLevelActivity = username == null; // Slot activity @@ -346,6 +356,7 @@ public async Task LocalActivity(string? slotType, int slotId, str return this.Ok(GameStream.CreateFromGroups(token, outerGroups, times.Start.ToUnixTimeMilliseconds(), - oldestTimestamp, isLevelActivity)); + oldestTimestamp, + isLevelActivity)); } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs index 303b4f859..7e96e0165 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs index f2d2d7a28..f734b741b 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -134,9 +134,10 @@ public static GameStream CreateFromGroups gameStream.Groups = groups.Select(GameStreamGroup.CreateFromGroup).ToList(); // Workaround for level activity because it shouldn't contain nested activity groups - if (gameStream.Groups.Count == 1 && groups.First().Key.GroupType == ActivityGroupType.Level && removeNesting) + if (gameStream.Groups.Count >= 1 && groups.All(g => g.Key.GroupType == ActivityGroupType.Level) && removeNesting) { - gameStream.Groups = gameStream.Groups.First().Groups; + // Flatten all inner groups into a single list + gameStream.Groups = gameStream.Groups.Select(g => g.Groups).SelectMany(g => g).ToList(); } // Workaround to turn a single subgroup into the primary group for news and team picks From 221937361655b2cac31c2335575c73300d0cdb94 Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 13 May 2024 19:30:56 -0500 Subject: [PATCH 31/40] Fix unit tests --- .../Activity/ActivityEventHandlerTests.cs | 14 ++--- .../Unit/Activity/ActivityGroupingTests.cs | 61 ++++++------------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs index 95d1ba05a..ccf745a7e 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -76,7 +76,7 @@ public async Task LevelComment_Insert_ShouldCreateCommentActivity() eventHandler.OnEntityInserted(database, comment); - Assert.NotNull(database.Activities.OfType() + Assert.NotNull(database.Activities.ToList().OfType() .FirstOrDefault(a => a.Type == EventType.CommentOnLevel && a.CommentId == 1)); } @@ -105,7 +105,7 @@ public async Task ProfileComment_Insert_ShouldCreateCommentActivity() eventHandler.OnEntityInserted(database, comment); - Assert.NotNull(database.Activities.OfType() + Assert.NotNull(database.Activities.ToList().OfType() .FirstOrDefault(a => a.Type == EventType.CommentOnUser && a.CommentId == 1)); } @@ -132,7 +132,7 @@ public async Task Photo_Insert_ShouldCreatePhotoActivity() eventHandler.OnEntityInserted(database, photo); - Assert.NotNull(database.Activities.OfType() + Assert.NotNull(database.Activities.ToList().OfType() .FirstOrDefault(a => a.Type == EventType.UploadPhoto && a.PhotoId == 1)); } @@ -678,7 +678,7 @@ public async Task Slot_WithTeamPickChange_ShouldCreateLevelActivity() eventHandler.OnEntityChanged(database, oldSlot, newSlot); - Assert.NotNull(database.Activities.OfType() + Assert.NotNull(database.Activities.ToList().OfType() .FirstOrDefault(a => a.Type == EventType.MMPickLevel && a.SlotId == 1)); } @@ -713,7 +713,7 @@ public async Task Slot_WithRepublish_ShouldCreateLevelActivity() eventHandler.OnEntityChanged(database, oldSlot, newSlot); - Assert.NotNull(database.Activities.OfType() + Assert.NotNull(database.Activities.ToList().OfType() .FirstOrDefault(a => a.Type == EventType.PublishLevel && a.SlotId == 1)); } @@ -750,8 +750,8 @@ public async Task Comment_WithDeletion_ShouldCreateCommentActivity() eventHandler.OnEntityChanged(database, oldComment, newComment); - Assert.NotNull(database.Activities.OfType() - .FirstOrDefault(a => a.Type == EventType.DeleteLevelComment && a.CommentId == 1)); + Assert.NotNull(database.Activities.ToList() + .FirstOrDefault(a => a.Type == EventType.DeleteLevelComment && ((CommentActivityEntity)a).CommentId == 1)); } [Fact] diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs index 8843091c9..c328c9476 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs @@ -86,34 +86,27 @@ public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_Objec }, ]; - //TODO: fix test List groups = activities.ToActivityDto().AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups(); Assert.NotNull(groups); - Assert.Single(groups); - OuterActivityGroup outerGroup = groups.First(); + Assert.Equal(3, groups.Count); + + Assert.Equal(ActivityGroupType.User, groups.ElementAt(0).Key.GroupType); + Assert.Equal(ActivityGroupType.User, groups.ElementAt(1).Key.GroupType); + Assert.Equal(ActivityGroupType.Level, groups.ElementAt(2).Key.GroupType); - Assert.Equal(ActivityGroupType.Level, outerGroup.Key.GroupType); - Assert.Equal(1, outerGroup.Key.TargetSlotId); + Assert.Equal(1, groups.ElementAt(0).Key.TargetUserId); + Assert.Equal(2, groups.ElementAt(1).Key.TargetUserId); + Assert.Equal(1, groups.ElementAt(2).Key.TargetSlotId); - IGrouping? firstGroup = outerGroup.Groups.First(); - IGrouping? secondGroup = outerGroup.Groups.Last(); - - Assert.NotNull(secondGroup); - Assert.Equal(ActivityGroupType.User, secondGroup.Key.Type); - Assert.Equal(1, secondGroup.Key.TargetId); // user group should have the user id - Assert.Equal(1, secondGroup.ToList()[0].TargetSlotId); // events in user group should have t - Assert.Equal(1, secondGroup.ToList()[1].TargetSlotId); - - Assert.NotNull(firstGroup); - Assert.Equal(ActivityGroupType.User, firstGroup.Key.Type); - Assert.Equal(2, firstGroup.Key.TargetId); - Assert.Equal(1, firstGroup.ToList()[0].TargetSlotId); + Assert.Single(groups.ElementAt(0).Groups); + Assert.Single(groups.ElementAt(1).Groups); + Assert.Equal(2, groups.ElementAt(2).Groups.Count); } [Fact] public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_ActorThenObject() { - List activities = [ + List activities = [ new LevelActivityEntity { UserId = 1, @@ -181,11 +174,13 @@ public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_Actor .ToActivityGroups(true) .ToList() .ToOuterActivityGroups(true); - //TODO: fix test + Assert.Multiple(() => { Assert.NotNull(groups); Assert.Equal(2, groups.Count); + Assert.Equal(1, groups.Count(g => g.Key.UserId == 1)); + Assert.Equal(1, groups.Count(g => g.Key.UserId == 2)); OuterActivityGroup firstUserGroup = groups.FirstOrDefault(g => g.Key.UserId == 1); OuterActivityGroup secondUserGroup = groups.FirstOrDefault(g => g.Key.UserId == 2); Assert.NotNull(firstUserGroup.Groups); @@ -194,28 +189,8 @@ public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_Actor Assert.Equal(ActivityGroupType.User, firstUserGroup.Key.GroupType); Assert.Equal(ActivityGroupType.User, secondUserGroup.Key.GroupType); - Assert.Single(firstUserGroup.Groups); - Assert.Single(secondUserGroup.Groups); - - Assert.Equal(2, firstUserGroup.Groups.ToList()[0].Count()); - Assert.Single(secondUserGroup.Groups.ToList()[0]); - - // Assert.Equal(ActivityGroupType.Level, outerGroup.Key.GroupType); - // Assert.Equal(1, outerGroup.Key.TargetSlotId); - // - // IGrouping? firstGroup = outerGroup.Groups.First(); - // IGrouping? secondGroup = outerGroup.Groups.Last(); - // - // Assert.NotNull(secondGroup); - // Assert.Equal(ActivityGroupType.User, secondGroup.Key.Type); - // Assert.Equal(1, secondGroup.Key.TargetId); // user group should have the user id - // Assert.Equal(1, secondGroup.ToList()[0].TargetSlotId); // events in user group should have t - // Assert.Equal(1, secondGroup.ToList()[1].TargetSlotId); - // - // Assert.NotNull(firstGroup); - // Assert.Equal(ActivityGroupType.User, firstGroup.Key.Type); - // Assert.Equal(2, firstGroup.Key.TargetId); - // Assert.Equal(1, firstGroup.ToList()[0].TargetSlotId); + Assert.True(firstUserGroup.Groups.All(g => g.Key.UserId == 1)); + Assert.True(secondUserGroup.Groups.All(g => g.Key.UserId == 2)); }); } @@ -318,8 +293,6 @@ public async Task ToActivityDtoTest() }); await db.SaveChangesAsync(); - var sql = db.Activities.ToActivityDto().ToQueryString(); - List resultDto = await db.Activities.ToActivityDto(includeSlotCreator: true, includeTeamPick: true).ToListAsync(); Assert.Equal(2, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetTeamPickId); From 1820425038717a5216fb5fad5a1dd0e5583bda64 Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 13 May 2024 21:21:38 -0500 Subject: [PATCH 32/40] Fix LBP3 playlist recent activity --- .../Controllers/ActivityController.cs | 9 ++--- .../Controllers/Slots/ScoreController.cs | 4 ++- .../Extensions/ActivityQueryExtensions.cs | 12 +++---- .../Types/Activity/ActivityDto.cs | 14 ++++---- .../Activity/ActivityEntityEventHandler.cs | 35 +++++++++++++------ .../Types/Activity/ActivityGroup.cs | 22 ++++++------ .../Activity/Events/GameEvent.cs | 8 ++--- 7 files changed, 59 insertions(+), 45 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index 7fc867e29..f84e25aea 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -306,6 +306,9 @@ public async Task LocalActivity(string? slotType, int slotId, str if ((SlotHelper.IsTypeInvalid(slotType) || slotId == 0) == (username == null)) return this.BadRequest(); + bool isLevelActivity = username == null; + bool groupByActor = !isLevelActivity && token.GameVersion == GameVersion.LittleBigPlanet3; + // User and Level activity will never contain news posts or MM pick events. IQueryable activityQuery = this.database.Activities.ToActivityDto() .Where(a => a.Activity.Type != EventType.NewsPost && a.Activity.Type != EventType.MMPickLevel); @@ -318,8 +321,6 @@ public async Task LocalActivity(string? slotType, int slotId, str a.Activity.Type != EventType.AddLevelToPlaylist); } - bool isLevelActivity = username == null; - // Slot activity if (isLevelActivity) { @@ -345,9 +346,9 @@ public async Task LocalActivity(string? slotType, int slotId, str activityQuery = activityQuery.Where(dto => dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End); - List> groups = await activityQuery.ToActivityGroups().ToListAsync(); + List> groups = await activityQuery.ToActivityGroups(groupByActor).ToListAsync(); - List outerGroups = groups.ToOuterActivityGroups(); + List outerGroups = groups.ToOuterActivityGroups(groupByActor); long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index 841cef389..56c5bc849 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -131,7 +131,9 @@ public async Task SubmitScore(string slotType, int id, int childI await this.database.SaveChangesAsync(); - ScoreEntity? existingScore = await this.database.Scores.Where(s => s.SlotId == slot.SlotId) + ScoreEntity? existingScore = await this.database.Scores + .Include(s => s.Slot) + .Where(s => s.SlotId == slot.SlotId) .Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId) .Where(s => s.UserId == token.UserId) .Where(s => s.Type == score.Type) diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index 0697ec5ef..cbe70db8e 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -66,15 +66,15 @@ public static List ToOuterActivityGroups { Type = groupByActor ? gr.GroupType - : gr.GroupType != ActivityGroupType.News - ? ActivityGroupType.User - : ActivityGroupType.News, + : gr.GroupType == ActivityGroupType.News + ? ActivityGroupType.News + : ActivityGroupType.User, UserId = gr.Activity.UserId, TargetId = groupByActor ? gr.TargetId - : gr.GroupType != ActivityGroupType.News - ? gr.Activity.UserId - : gr.TargetNewsId ?? 0, + : gr.GroupType == ActivityGroupType.News + ? gr.TargetNewsId ?? 0 + : gr.Activity.UserId, }) .ToList(), }) diff --git a/ProjectLighthouse/Types/Activity/ActivityDto.cs b/ProjectLighthouse/Types/Activity/ActivityDto.cs index 877593697..6e696d8f5 100644 --- a/ProjectLighthouse/Types/Activity/ActivityDto.cs +++ b/ProjectLighthouse/Types/Activity/ActivityDto.cs @@ -25,11 +25,11 @@ public class ActivityDto }; public ActivityGroupType GroupType => - this.TargetSlotId != null - ? ActivityGroupType.Level - : this.TargetUserId != null - ? ActivityGroupType.User - : this.TargetPlaylistId != null - ? ActivityGroupType.Playlist - : ActivityGroupType.News; + this.TargetPlaylistId != null + ? ActivityGroupType.Playlist + : this.TargetNewsId != null + ? ActivityGroupType.News + : this.TargetSlotId != null + ? ActivityGroupType.Level + : ActivityGroupType.User; } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 91e3e6055..29810f8c9 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -57,25 +57,17 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, PhotoEntity photo => photo.SlotId switch { - // Photos without levels - null => new UserPhotoActivity - { - Type = EventType.UploadPhoto, - PhotoId = photo.PhotoId, - UserId = photo.CreatorId, - TargetUserId = photo.CreatorId, - }, _ => photo.Slot?.Type switch { - SlotType.Developer => null, - // Non-story levels (moon, pod, etc) - _ => new LevelPhotoActivity + SlotType.User => new LevelPhotoActivity { Type = EventType.UploadPhoto, PhotoId = photo.PhotoId, UserId = photo.CreatorId, SlotId = photo.SlotId ?? throw new NullReferenceException("SlotId in Photo is null"), }, + // All other photos (story, moon, pod, etc.) + _ => null, }, }, ScoreEntity score => score.Slot.Type switch @@ -227,6 +219,27 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current int Plays(VisitedLevelEntity entity) => entity.PlaysLBP1 + entity.PlaysLBP2 + entity.PlaysLBP3; } + case ScoreEntity score: + { + if (origEntity is not ScoreEntity oldScore) break; + + // don't track versus levels + if (oldScore.Type == 7) break; + + if (score.Slot.Type != SlotType.User) break; + + if (oldScore.Points > score.Points) break; + + activity = new ScoreActivityEntity + { + Type = EventType.Score, + ScoreId = score.ScoreId, + SlotId = score.SlotId, + UserId = score.UserId, + }; + + break; + } case SlotEntity slotEntity: { if (origEntity is not SlotEntity oldSlotEntity) break; diff --git a/ProjectLighthouse/Types/Activity/ActivityGroup.cs b/ProjectLighthouse/Types/Activity/ActivityGroup.cs index 51f981b0b..8cd189ae7 100644 --- a/ProjectLighthouse/Types/Activity/ActivityGroup.cs +++ b/ProjectLighthouse/Types/Activity/ActivityGroup.cs @@ -19,7 +19,7 @@ public struct ActivityGroup this.GroupType switch { ActivityGroupType.User => this.TargetUserId ?? this.UserId, - ActivityGroupType.Level => this.TargetSlotId?? 0, + ActivityGroupType.Level => this.TargetSlotId ?? 0, ActivityGroupType.TeamPick => this.TargetTeamPickSlotId ?? 0, ActivityGroupType.Playlist => this.TargetPlaylistId ?? 0, ActivityGroupType.News => this.TargetNewsId ?? 0, @@ -27,17 +27,15 @@ public struct ActivityGroup }; public ActivityGroupType GroupType => - (this.TargetSlotId ?? 0) != 0 - ? ActivityGroupType.Level - : (this.TargetUserId ?? 0) != 0 - ? ActivityGroupType.User - : (this.TargetPlaylistId ?? 0) != 0 - ? ActivityGroupType.Playlist - : (this.TargetNewsId ?? 0) != 0 - ? ActivityGroupType.News - : (this.TargetTeamPickSlotId ?? 0) != 0 - ? ActivityGroupType.TeamPick - : ActivityGroupType.User; + (this.TargetPlaylistId ?? 0) != 0 + ? ActivityGroupType.User + : (this.TargetNewsId ?? 0) != 0 + ? ActivityGroupType.News + : (this.TargetTeamPickSlotId ?? 0) != 0 + ? ActivityGroupType.TeamPick + : (this.TargetSlotId ?? 0) != 0 + ? ActivityGroupType.Level + : ActivityGroupType.User; public override string ToString() => $@"{this.GroupType} Group: Timestamp: {this.Timestamp}, UserId: {this.UserId}, TargetId: {this.TargetId}"; diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs index 21d283147..f9f48a0f8 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -187,7 +187,7 @@ private static GameEvent CreateFromActivity(ActivityDto activity) PhotoId = ((PhotoActivityEntity)activity.Activity).PhotoId, Slot = new ReviewSlot { - SlotId = targetId, + SlotId = activity.TargetSlotId ?? -1, }, }, EventType.MMPickLevel => new GameTeamPickLevelEvent @@ -211,15 +211,15 @@ private static GameEvent CreateFromActivity(ActivityDto activity) }, EventType.CreatePlaylist => new GameCreatePlaylistEvent { - TargetPlaylistId = targetId, + TargetPlaylistId = activity.TargetPlaylistId ?? -1, }, EventType.HeartPlaylist => new GameHeartPlaylistEvent { - TargetPlaylistId = targetId, + TargetPlaylistId = activity.TargetPlaylistId ?? -1, }, EventType.AddLevelToPlaylist => new GameAddLevelToPlaylistEvent { - TargetPlaylistId = targetId, + TargetPlaylistId = activity.TargetPlaylistId ?? -1, Slot = new ReviewSlot { SlotId = ((PlaylistWithSlotActivityEntity)activity.Activity).SlotId, From cd0c85308a6ce64b620a50270210bbd9341211e4 Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 13 May 2024 21:53:52 -0500 Subject: [PATCH 33/40] Manually fetch slot types instead of relying on foreign key being loaded --- .../Controllers/Slots/ScoreController.cs | 1 - .../Activity/ActivityEntityEventHandler.cs | 29 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index 56c5bc849..beca96d37 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -132,7 +132,6 @@ public async Task SubmitScore(string slotType, int id, int childI await this.database.SaveChangesAsync(); ScoreEntity? existingScore = await this.database.Scores - .Include(s => s.Slot) .Where(s => s.SlotId == slot.SlotId) .Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId) .Where(s => s.UserId == token.UserId) diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index 29810f8c9..f706c6e02 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -35,7 +35,9 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, CommentEntity comment => comment.Type switch { - CommentType.Level => comment.TargetSlot?.Type switch + CommentType.Level => database.Slots.Where(s => s.SlotId == comment.TargetSlotId) + .Select(s => s.Type) + .FirstOrDefault() switch { SlotType.User => new LevelCommentActivityEntity { @@ -55,10 +57,10 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, _ => null, }, - PhotoEntity photo => photo.SlotId switch + PhotoEntity photo => database.Slots.Where(s => s.SlotId == photo.SlotId) + .Select(s => s.Type) + .FirstOrDefault() switch { - _ => photo.Slot?.Type switch - { SlotType.User => new LevelPhotoActivity { Type = EventType.UploadPhoto, @@ -68,9 +70,10 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, // All other photos (story, moon, pod, etc.) _ => null, - }, }, - ScoreEntity score => score.Slot.Type switch + ScoreEntity score => database.Slots.Where(s => s.SlotId == score.SlotId) + .Select(s => s.Type) + .FirstOrDefault() switch { // Don't add story scores or versus scores SlotType.User when score.Type != 7 => new ScoreActivityEntity @@ -82,7 +85,9 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl }, _ => null, }, - HeartedLevelEntity heartedLevel => heartedLevel.Slot.Type switch + HeartedLevelEntity heartedLevel => database.Slots.Where(s => s.SlotId == heartedLevel.SlotId) + .Select(s => s.Type) + .FirstOrDefault() switch { SlotType.User => new LevelActivityEntity { @@ -226,7 +231,11 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current // don't track versus levels if (oldScore.Type == 7) break; - if (score.Slot.Type != SlotType.User) break; + SlotType slotType = database.Slots.Where(s => s.SlotId == score.SlotId) + .Select(s => s.Type) + .FirstOrDefault(); + + if (slotType != SlotType.User) break; if (oldScore.Points > score.Points) break; @@ -349,7 +358,9 @@ public void OnEntityDeleted(DatabaseContext database, T entity) where T : cla { ActivityEntity? activity = entity switch { - HeartedLevelEntity heartedLevel => heartedLevel.Slot.Type switch + HeartedLevelEntity heartedLevel => database.Slots.Where(s => s.SlotId == heartedLevel.SlotId) + .Select(s => s.Type) + .FirstOrDefault() switch { SlotType.User => new LevelActivityEntity { From 402fd4b493c31b313140489d157237d9ddcba51c Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 13 May 2024 21:57:24 -0500 Subject: [PATCH 34/40] Fix photo activity handler test --- .../Unit/Activity/ActivityEventHandlerTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs index ccf745a7e..9e82509e7 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -120,12 +120,21 @@ public async Task Photo_Insert_ShouldCreatePhotoActivity() Username = "test", UserId = 1, }, + }, + new List + { + new() + { + SlotId = 1, + CreatorId = 1, + }, }); PhotoEntity photo = new() { PhotoId = 1, CreatorId = 1, + SlotId = 1, }; database.Photos.Add(photo); await database.SaveChangesAsync(); From 1e276923d66024a442428c4e643285b3e2a8cb4a Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 13 May 2024 22:30:04 -0500 Subject: [PATCH 35/40] Recreate recent activity migrations --- ...214606_AddPublishedAtToWebAnnouncements.cs | 33 - .../20240325034658_InitialActivity.cs | 151 ---- .../20240514032512_AddRecentActivity.cs | 692 ++++++++++++++++++ .../DatabaseContextModelSnapshot.cs | 70 +- .../Activity/ActivityEntityEventHandler.cs | 2 + .../Entities/Activity/ScoreActivityEntity.cs | 2 + .../Activity/Events/GameEvent.cs | 1 + .../Activity/Events/GameScoreEvent.cs | 2 - 8 files changed, 766 insertions(+), 187 deletions(-) delete mode 100644 ProjectLighthouse/Migrations/20240120214606_AddPublishedAtToWebAnnouncements.cs delete mode 100644 ProjectLighthouse/Migrations/20240325034658_InitialActivity.cs create mode 100644 ProjectLighthouse/Migrations/20240514032512_AddRecentActivity.cs diff --git a/ProjectLighthouse/Migrations/20240120214606_AddPublishedAtToWebAnnouncements.cs b/ProjectLighthouse/Migrations/20240120214606_AddPublishedAtToWebAnnouncements.cs deleted file mode 100644 index cfd53fc96..000000000 --- a/ProjectLighthouse/Migrations/20240120214606_AddPublishedAtToWebAnnouncements.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using LBPUnion.ProjectLighthouse.Database; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ProjectLighthouse.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20240120214606_AddPublishedAtToWebAnnouncements")] - public partial class AddPublishedAtToWebAnnouncements : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "PublishedAt", - table: "WebsiteAnnouncements", - type: "datetime(6)", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "PublishedAt", - table: "WebsiteAnnouncements"); - } - } -} diff --git a/ProjectLighthouse/Migrations/20240325034658_InitialActivity.cs b/ProjectLighthouse/Migrations/20240325034658_InitialActivity.cs deleted file mode 100644 index 62ea0bd45..000000000 --- a/ProjectLighthouse/Migrations/20240325034658_InitialActivity.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using LBPUnion.ProjectLighthouse.Database; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LBPUnion.ProjectLighthouse.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20240325034658_InitialActivity")] - public partial class InitialActivity : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Activities", - columns: table => new - { - ActivityId = table.Column(type: "int", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - Timestamp = table.Column(type: "datetime(6)", nullable: false), - UserId = table.Column(type: "int", nullable: false), - Type = table.Column(type: "int", nullable: false), - Discriminator = table.Column(type: "varchar(34)", maxLength: 34, nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - SlotId = table.Column(type: "int", nullable: true), - CommentId = table.Column(type: "int", nullable: true), - PhotoId = table.Column(type: "int", nullable: true), - NewsId = table.Column(type: "int", nullable: true), - PlaylistId = table.Column(type: "int", nullable: true), - ReviewId = table.Column(type: "int", nullable: true), - ScoreId = table.Column(type: "int", nullable: true), - TargetUserId = table.Column(type: "int", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Activities", x => x.ActivityId); - table.ForeignKey( - name: "FK_Activities_Comments_CommentId", - column: x => x.CommentId, - principalTable: "Comments", - principalColumn: "CommentId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Activities_Photos_PhotoId", - column: x => x.PhotoId, - principalTable: "Photos", - principalColumn: "PhotoId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Activities_Playlists_PlaylistId", - column: x => x.PlaylistId, - principalTable: "Playlists", - principalColumn: "PlaylistId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Activities_Reviews_ReviewId", - column: x => x.ReviewId, - principalTable: "Reviews", - principalColumn: "ReviewId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Activities_Scores_ScoreId", - column: x => x.ScoreId, - principalTable: "Scores", - principalColumn: "ScoreId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Activities_Slots_SlotId", - column: x => x.SlotId, - principalTable: "Slots", - principalColumn: "SlotId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Activities_Users_TargetUserId", - column: x => x.TargetUserId, - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Activities_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Activities_WebsiteAnnouncements_NewsId", - column: x => x.NewsId, - principalTable: "WebsiteAnnouncements", - principalColumn: "AnnouncementId", - onDelete: ReferentialAction.Cascade); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_CommentId", - table: "Activities", - column: "CommentId"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_NewsId", - table: "Activities", - column: "NewsId"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_PhotoId", - table: "Activities", - column: "PhotoId"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_PlaylistId", - table: "Activities", - column: "PlaylistId"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_ReviewId", - table: "Activities", - column: "ReviewId"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_ScoreId", - table: "Activities", - column: "ScoreId"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_SlotId", - table: "Activities", - column: "SlotId"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_TargetUserId", - table: "Activities", - column: "TargetUserId"); - - migrationBuilder.CreateIndex( - name: "IX_Activities_UserId", - table: "Activities", - column: "UserId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Activities"); - } - } -} diff --git a/ProjectLighthouse/Migrations/20240514032512_AddRecentActivity.cs b/ProjectLighthouse/Migrations/20240514032512_AddRecentActivity.cs new file mode 100644 index 000000000..93c31f119 --- /dev/null +++ b/ProjectLighthouse/Migrations/20240514032512_AddRecentActivity.cs @@ -0,0 +1,692 @@ +using System; +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LBPUnion.ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240514032512_AddRecentActivity")] + public partial class AddRecentActivity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TokenId", + table: "WebTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "AnnouncementId", + table: "WebsiteAnnouncements", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "VisitedLevelId", + table: "VisitedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "Users", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "SlotId", + table: "Slots", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ScoreId", + table: "Scores", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ReviewId", + table: "Reviews", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ReportId", + table: "Reports", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "RegistrationTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatedReviewId", + table: "RatedReviews", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatedLevelId", + table: "RatedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatingId", + table: "RatedComments", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "QueuedLevelId", + table: "QueuedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PlaylistId", + table: "Playlists", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PlatformLinkAttemptId", + table: "PlatformLinkAttempts", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PhotoSubjectId", + table: "PhotoSubjects", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PhotoId", + table: "Photos", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "PasswordResetTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Notifications", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedProfileId", + table: "HeartedProfiles", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedPlaylistId", + table: "HeartedPlaylists", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedLevelId", + table: "HeartedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "GameTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "EmailVerificationTokenId", + table: "EmailVerificationTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "EmailSetTokenId", + table: "EmailSetTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "CustomCategories", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CommentId", + table: "Comments", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CaseId", + table: "Cases", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "BlockedProfileId", + table: "BlockedProfiles", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "APIKeys", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.CreateTable( + name: "Activities", + columns: table => new + { + ActivityId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Timestamp = table.Column(type: "datetime(6)", nullable: false), + UserId = table.Column(type: "int", nullable: false), + Type = table.Column(type: "int", nullable: false), + Discriminator = table.Column(type: "varchar(34)", maxLength: 34, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + SlotId = table.Column(type: "int", nullable: true), + CommentId = table.Column(type: "int", nullable: true), + PhotoId = table.Column(type: "int", nullable: true), + NewsId = table.Column(type: "int", nullable: true), + PlaylistId = table.Column(type: "int", nullable: true), + ReviewId = table.Column(type: "int", nullable: true), + ScoreId = table.Column(type: "int", nullable: true), + Points = table.Column(type: "int", nullable: true), + TargetUserId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Activities", x => x.ActivityId); + table.ForeignKey( + name: "FK_Activities_Comments_CommentId", + column: x => x.CommentId, + principalTable: "Comments", + principalColumn: "CommentId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Photos_PhotoId", + column: x => x.PhotoId, + principalTable: "Photos", + principalColumn: "PhotoId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Playlists_PlaylistId", + column: x => x.PlaylistId, + principalTable: "Playlists", + principalColumn: "PlaylistId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Reviews_ReviewId", + column: x => x.ReviewId, + principalTable: "Reviews", + principalColumn: "ReviewId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Scores_ScoreId", + column: x => x.ScoreId, + principalTable: "Scores", + principalColumn: "ScoreId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Slots_SlotId", + column: x => x.SlotId, + principalTable: "Slots", + principalColumn: "SlotId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Users_TargetUserId", + column: x => x.TargetUserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_WebsiteAnnouncements_NewsId", + column: x => x.NewsId, + principalTable: "WebsiteAnnouncements", + principalColumn: "AnnouncementId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_CommentId", + table: "Activities", + column: "CommentId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_NewsId", + table: "Activities", + column: "NewsId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_PhotoId", + table: "Activities", + column: "PhotoId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_PlaylistId", + table: "Activities", + column: "PlaylistId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_ReviewId", + table: "Activities", + column: "ReviewId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_ScoreId", + table: "Activities", + column: "ScoreId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_SlotId", + table: "Activities", + column: "SlotId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_TargetUserId", + table: "Activities", + column: "TargetUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_UserId", + table: "Activities", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Activities"); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "WebTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "AnnouncementId", + table: "WebsiteAnnouncements", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "VisitedLevelId", + table: "VisitedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "Users", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "SlotId", + table: "Slots", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ScoreId", + table: "Scores", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ReviewId", + table: "Reviews", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ReportId", + table: "Reports", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "RegistrationTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatedReviewId", + table: "RatedReviews", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatedLevelId", + table: "RatedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatingId", + table: "RatedComments", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "QueuedLevelId", + table: "QueuedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PlaylistId", + table: "Playlists", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PlatformLinkAttemptId", + table: "PlatformLinkAttempts", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PhotoSubjectId", + table: "PhotoSubjects", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PhotoId", + table: "Photos", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "PasswordResetTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Notifications", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedProfileId", + table: "HeartedProfiles", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedPlaylistId", + table: "HeartedPlaylists", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedLevelId", + table: "HeartedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "GameTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "EmailVerificationTokenId", + table: "EmailVerificationTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "EmailSetTokenId", + table: "EmailSetTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "CustomCategories", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CommentId", + table: "Comments", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CaseId", + table: "Cases", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "BlockedProfileId", + table: "BlockedProfiles", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "APIKeys", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs index 7f069036c..8c6bd550e 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -3,6 +3,7 @@ using LBPUnion.ProjectLighthouse.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable @@ -16,15 +17,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("ProductVersion", "8.0.3") .HasAnnotation("Relational:MaxIdentifierLength", 64); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity", b => { b.Property("ActivityId") .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ActivityId")); + b.Property("Discriminator") .IsRequired() .HasMaxLength(34) @@ -56,6 +61,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedLevelId")); + b.Property("SlotId") .HasColumnType("int"); @@ -77,6 +84,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedPlaylistId")); + b.Property("PlaylistId") .HasColumnType("int"); @@ -98,6 +107,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedProfileId")); + b.Property("HeartedUserId") .HasColumnType("int"); @@ -119,6 +130,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("QueuedLevelId")); + b.Property("SlotId") .HasColumnType("int"); @@ -140,6 +153,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + b.Property("CommentId") .HasColumnType("int"); @@ -164,6 +179,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatedLevelId")); + b.Property("Rating") .HasColumnType("int"); @@ -194,6 +211,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatedReviewId")); + b.Property("ReviewId") .HasColumnType("int"); @@ -218,6 +237,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("VisitedLevelId")); + b.Property("PlaysLBP1") .HasColumnType("int"); @@ -248,6 +269,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CategoryId")); + b.Property("Description") .HasColumnType("longtext"); @@ -274,6 +297,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PlaylistId")); + b.Property("CreatorId") .HasColumnType("int"); @@ -302,6 +327,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReviewId")); + b.Property("Deleted") .HasColumnType("tinyint(1)"); @@ -349,6 +376,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ScoreId")); + b.Property("ChildSlotId") .HasColumnType("int"); @@ -382,6 +411,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SlotId")); + b.Property("AuthorLabels") .IsRequired() .HasColumnType("longtext"); @@ -535,6 +566,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReportId")); + b.Property("Bounds") .HasColumnType("longtext"); @@ -581,6 +614,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CaseId")); + b.Property("AffectedId") .HasColumnType("int"); @@ -635,6 +670,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("IsDismissed") .HasColumnType("tinyint(1)"); @@ -660,6 +697,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BlockedProfileId")); + b.Property("BlockedUserId") .HasColumnType("int"); @@ -681,6 +720,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CommentId")); + b.Property("Deleted") .HasColumnType("tinyint(1)"); @@ -750,6 +791,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PhotoId")); + b.Property("CreatorId") .HasColumnType("int"); @@ -790,6 +833,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PhotoSubjectId")); + b.Property("Bounds") .HasColumnType("longtext"); @@ -814,6 +859,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PlatformLinkAttemptId")); + b.Property("IPAddress") .HasColumnType("longtext"); @@ -842,6 +889,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserId")); + b.Property("AdminGrantedSlots") .HasColumnType("int"); @@ -947,6 +996,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Created") .HasColumnType("datetime(6)"); @@ -967,6 +1018,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmailSetTokenId")); + b.Property("EmailToken") .HasColumnType("longtext"); @@ -989,6 +1042,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmailVerificationTokenId")); + b.Property("EmailToken") .HasColumnType("longtext"); @@ -1011,6 +1066,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + b.Property("ExpiresAt") .HasColumnType("datetime(6)"); @@ -1042,6 +1099,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + b.Property("Created") .HasColumnType("datetime(6)"); @@ -1062,6 +1121,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + b.Property("Created") .HasColumnType("datetime(6)"); @@ -1082,6 +1143,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + b.Property("ExpiresAt") .HasColumnType("datetime(6)"); @@ -1105,6 +1168,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + b.Property("Content") .HasColumnType("longtext"); @@ -1246,6 +1311,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + b.Property("Points") + .HasColumnType("int"); + b.Property("ScoreId") .HasColumnType("int"); diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs index f706c6e02..ef8e6fadb 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -82,6 +82,7 @@ public void OnEntityInserted(DatabaseContext database, T entity) where T : cl ScoreId = score.ScoreId, UserId = score.UserId, SlotId = score.SlotId, + Points = score.Points, }, _ => null, }, @@ -245,6 +246,7 @@ public void OnEntityChanged(DatabaseContext database, T origEntity, T current ScoreId = score.ScoreId, SlotId = score.SlotId, UserId = score.UserId, + Points = score.Points, }; break; diff --git a/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs index 4ecc8384d..6d6db8245 100644 --- a/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs @@ -22,4 +22,6 @@ public class ScoreActivityEntity : ActivityEntity [ForeignKey(nameof(SlotId))] public SlotEntity Slot { get; set; } + + public int Points { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs index f9f48a0f8..0b945c514 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -150,6 +150,7 @@ private static GameEvent CreateFromActivity(ActivityDto activity) EventType.Score => new GameScoreEvent { ScoreId = ((ScoreActivityEntity)activity.Activity).ScoreId, + Score = ((ScoreActivityEntity)activity.Activity).Points, Slot = new ReviewSlot { SlotId = targetId, diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs index 5f26cc90d..382080abe 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs @@ -32,8 +32,6 @@ public class GameScoreEvent : GameEvent SlotEntity slot = await database.Slots.FindAsync(score.SlotId); if (slot == null) return; - this.Score = score.Points; - //TODO is this correct? this.UserCount = score.Type; this.Slot = ReviewSlot.CreateFromEntity(slot); From 94fe2d4d99cff7d0a8b0bae4d2afc531e30fbe0b Mon Sep 17 00:00:00 2001 From: Slendy Date: Mon, 13 May 2024 22:34:26 -0500 Subject: [PATCH 36/40] Add missing announcement time migration --- ...4032620_AddPublishedAtToWebAnnouncement.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs diff --git a/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs b/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs new file mode 100644 index 000000000..66d5d8e77 --- /dev/null +++ b/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs @@ -0,0 +1,33 @@ +using System; +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LBPUnion.ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240514032620_AddPublishedAtToWebAnnouncement")] + public partial class AddPublishedAtToWebAnnouncement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PublishedAt", + table: "WebsiteAnnouncements", + type: "datetime(6)", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PublishedAt", + table: "WebsiteAnnouncements"); + } + } +} From 05145ea7cb8fefe8d64161f2696c128c2e522342 Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 14 May 2024 16:00:42 -0500 Subject: [PATCH 37/40] Set published at for announcements and set default value --- .../Pages/NotificationsPage.cshtml | 4 +++- .../Pages/NotificationsPage.cshtml.cs | 1 + .../20240514032620_AddPublishedAtToWebAnnouncement.cs | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml index a6eaa43dd..7bede06c2 100644 --- a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml @@ -2,13 +2,14 @@ @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.Types.Entities.Notifications @using LBPUnion.ProjectLighthouse.Types.Entities.Website -@using LBPUnion.ProjectLighthouse.Types.Notifications @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.NotificationsPage @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ Layout = "Layouts/BaseLayout"; Model.Title = Model.Translate(GeneralStrings.Notifications); + string timeZone = Model.GetTimeZone(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); } @if (Model.User != null && Model.User.IsAdmin) @@ -51,6 +52,7 @@ @announcement.Publisher.Username + at @TimeZoneInfo.ConvertTime(announcement.PublishedAt, TimeZoneInfo.Utc, timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt") } diff --git a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs index 8595ee858..0849304b0 100644 --- a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs @@ -54,6 +54,7 @@ public async Task OnPost([FromForm] string title, [FromForm] stri Title = title.Trim(), Content = content.Trim(), PublisherId = user.UserId, + PublishedAt = DateTime.UtcNow, }; this.Database.WebsiteAnnouncements.Add(announcement); diff --git a/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs b/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs index 66d5d8e77..88b54e9f3 100644 --- a/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs +++ b/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs @@ -19,7 +19,7 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "WebsiteAnnouncements", type: "datetime(6)", nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + defaultValue: DateTime.UtcNow); } /// From e95f636f6838738ad99a986c69156ce0138d02b0 Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 14 May 2024 16:24:48 -0500 Subject: [PATCH 38/40] Show announcement time on website landing page --- ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml index 1414c642e..77e32db66 100644 --- a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml @@ -14,6 +14,7 @@ bool isMobile = Request.IsMobile(); string language = Model.GetLanguage(); string timeZone = Model.GetTimeZone(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); }

@Model.Translate(LandingPageStrings.Welcome, ServerConfiguration.Instance.Customization.ServerName) @@ -82,6 +83,7 @@ @Model.LatestAnnouncement.Publisher.Username + at @TimeZoneInfo.ConvertTime(Model.LatestAnnouncement.PublishedAt, TimeZoneInfo.Utc, timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt") } From 02fbd731e60b0e230e51eb1a071a56980becd1c9 Mon Sep 17 00:00:00 2001 From: Slendy Date: Sat, 2 Nov 2024 13:19:14 -0500 Subject: [PATCH 39/40] Refactor method parameters into options class --- .../Controllers/ActivityController.cs | 59 +++++++++++-------- .../Controllers/Slots/SlotsController.cs | 1 + .../Extensions/ActivityQueryExtensions.cs | 10 ++-- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs index f84e25aea..ff904cff9 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -30,16 +30,21 @@ public ActivityController(DatabaseContext database) this.database = database; } + private class ActivityFilterOptions + { + public bool ExcludeNews { get; init; } + public bool ExcludeMyLevels { get; init; } + public bool ExcludeFriends { get; init; } + public bool ExcludeFavouriteUsers { get; init; } + public bool ExcludeMyself { get; init; } + public bool ExcludeMyPlaylists { get; init; } = true; + } + private async Task> GetFilters ( IQueryable dtoQuery, GameTokenEntity token, - bool excludeNews, - bool excludeMyLevels, - bool excludeFriends, - bool excludeFavouriteUsers, - bool excludeMyself, - bool excludeMyPlaylists = true + ActivityFilterOptions options ) { dtoQuery = token.GameVersion == GameVersion.LittleBigPlanetVita @@ -75,37 +80,37 @@ private async Task> GetFilters } } - Expression> newsPredicate = !excludeNews + Expression> newsPredicate = !options.ExcludeNews ? new IncludeNewsFilter().GetPredicate() : new ExcludeNewsFilter().GetPredicate(); predicate = predicate.Or(newsPredicate); - if (!excludeMyLevels) + if (!options.ExcludeMyLevels) { predicate = predicate.Or(dto => dto.TargetSlotCreatorId == token.UserId); } List includedUserIds = []; - if (!excludeFriends) + if (!options.ExcludeFriends) { includedUserIds.AddRange(friendIds); } - if (!excludeFavouriteUsers) + if (!options.ExcludeFavouriteUsers) { includedUserIds.AddRange(favouriteUsers); } - if (!excludeMyself) + if (!options.ExcludeMyself) { includedUserIds.Add(token.UserId); } predicate = predicate.Or(dto => includedUserIds.Contains(dto.Activity.UserId)); - if (!excludeMyPlaylists && !excludeMyself && token.GameVersion == GameVersion.LittleBigPlanet3) + if (!options.ExcludeMyPlaylists && !options.ExcludeMyself && token.GameVersion == GameVersion.LittleBigPlanet3) { List creatorPlaylists = await this.database.Playlists.Where(p => p.CreatorId == token.UserId) .Select(p => p.PlaylistId) @@ -200,14 +205,15 @@ public async Task GlobalActivityLBP3 if (token.GameVersion != GameVersion.LittleBigPlanet3) return this.NotFound(); IQueryable activityEvents = await this.GetFilters( - this.database.Activities.ToActivityDto(true, true), - token, - excludeNews, - true, - true, - true, - excludeMyself, - excludeMyPlaylists); + this.database.Activities.ToActivityDto(true, true), token, new ActivityFilterOptions() + { + ExcludeNews = excludeNews, + ExcludeMyLevels = true, + ExcludeFriends = true, + ExcludeFavouriteUsers = true, + ExcludeMyself = excludeMyself, + ExcludeMyPlaylists = excludeMyPlaylists, + }); (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityEvents, timestamp, null); @@ -246,11 +252,14 @@ bool excludeMyself IQueryable activityEvents = await this.GetFilters(this.database.Activities.ToActivityDto(true), token, - excludeNews, - excludeMyLevels, - excludeFriends, - excludeFavouriteUsers, - excludeMyself); + new ActivityFilterOptions + { + ExcludeNews = excludeNews, + ExcludeMyLevels = excludeMyLevels, + ExcludeFriends = excludeFriends, + ExcludeFavouriteUsers = excludeFavouriteUsers, + ExcludeMyself = excludeMyself, + }); (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityEvents, timestamp, endTimestamp); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs index a9580e387..198d3fe74 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -3,6 +3,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata; diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index cbe70db8e..135ffd4bd 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -32,7 +32,7 @@ public static List GetIds(this IReadOnlyCollection grou /// Turns a list of into a group based on its timestamp /// /// An to group - /// Whether or not the groups should be created based on the initiator of the event or the target of the event + /// Whether the groups should be created based on the initiator of the event or the target of the event /// The transformed query containing groups of public static IQueryable> ToActivityGroups (this IQueryable activityQuery, bool groupByActor = false) => @@ -84,8 +84,8 @@ public static List ToOuterActivityGroups /// Converts an <> into an <> for grouping. /// /// The activity query to be converted. - /// Whether or not the field should be included. - /// Whether or not the field should be included. + /// Whether the field should be included. + /// Whether the field should be included. /// The converted <> public static IQueryable ToActivityDto (this IQueryable activityQuery, bool includeSlotCreator = false, bool includeTeamPick = false) @@ -107,8 +107,8 @@ public static IQueryable ToActivityDto /// Converts an IEnumerable<> into an IEnumerable<> for grouping. /// /// The activity query to be converted. - /// Whether or not the field should be included. - /// Whether or not the field should be included. + /// Whether the field should be included. + /// Whether the field should be included. /// The converted IEnumerable<> public static IEnumerable ToActivityDto (this IEnumerable activityEnumerable, bool includeSlotCreator = false, bool includeTeamPick = false) From f2cfa6b093a2837db3b4fbeffb934ec3bd86fb00 Mon Sep 17 00:00:00 2001 From: Slendy Date: Tue, 5 Nov 2024 02:09:49 -0600 Subject: [PATCH 40/40] Add activity debug page and fix some bugs --- .../Pages/Debug/ActivityTestPage.cshtml | 85 +++++++++++++++++++ .../Pages/Debug/ActivityTestPage.cshtml.cs | 34 ++++++++ .../Extensions/ActivityQueryExtensions.cs | 13 +-- .../Types/Activity/ActivityDto.cs | 8 +- .../Types/Activity/ActivityGroup.cs | 18 ++-- 5 files changed, 139 insertions(+), 19 deletions(-) create mode 100644 ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml create mode 100644 ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs diff --git a/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml new file mode 100644 index 000000000..40aad32eb --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml @@ -0,0 +1,85 @@ +@page "/debug/activity" +@using System.Globalization +@using LBPUnion.ProjectLighthouse.Types.Activity +@using LBPUnion.ProjectLighthouse.Types.Entities.Activity +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug.ActivityTestPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.Title = "Debug - Activity Test"; +} + + +
Group By Activity
+
+ +
Group By Actor
+
+
+ +@foreach (OuterActivityGroup activity in Model.ActivityGroups) +{ +

@activity.Key.GroupType, Timestamp: @activity.Key.Timestamp.ToString(CultureInfo.InvariantCulture)

+
+ + @if (activity.Key.UserId != -1) + { +

UserId: @activity.Key.UserId

+ } + @if ((activity.Key.TargetNewsId ?? -1) != -1) + { +

TargetNewsId?: @activity.Key.TargetNewsId (targetId=@activity.Key.TargetId)

+ } + @if ((activity.Key.TargetPlaylistId ?? -1) != -1) + { +

TargetPlaylistId?: @activity.Key.TargetPlaylistId (targetId=@activity.Key.TargetId)

+ } + @if ((activity.Key.TargetSlotId ?? -1) != -1) + { +

TargetSlotId?: @activity.Key.TargetSlotId (targetId=@activity.Key.TargetId)

+ } + @if ((activity.Key.TargetTeamPickSlotId ?? -1) != -1) + { +

TargetTeamPickSlot?: @activity.Key.TargetTeamPickSlotId (targetId=@activity.Key.TargetId)

+ } + @if ((activity.Key.TargetUserId ?? -1) != -1) + { +

TargetUserId?: @activity.Key.TargetUserId (targetId=@activity.Key.TargetId)

+ } +
+ + @foreach (IGrouping? eventGroup in activity.Groups) + { +
+
Nested Group Type: @eventGroup.Key.Type
+ + @foreach (ActivityDto gameEvent in eventGroup.ToList()) + { +
+ @gameEvent.Activity.Type, Event Id: @gameEvent.Activity.ActivityId +
+
+

Event Group Type: @gameEvent.GroupType

+

Event Target ID: @gameEvent.TargetId

+ @if (gameEvent.Activity is LevelActivityEntity level) + { +

SlotId: @level.SlotId

+

SlotVersion: @gameEvent.TargetSlotGameVersion

+ } + @if (gameEvent.Activity is ScoreActivityEntity score) + { +

ScoreId: @score.ScoreId

+

SlotId: @score.SlotId

+

SlotVersion: @gameEvent.TargetSlotGameVersion

+ } +
+ } +
+ } +
+
+
+

Total events: @activity.Groups.Sum(g => g.ToList().Count)

+
+
+} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs new file mode 100644 index 000000000..dd24cef77 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs @@ -0,0 +1,34 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; +using LBPUnion.ProjectLighthouse.Types.Activity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug; + +public class ActivityTestPage : BaseLayout +{ + public ActivityTestPage(DatabaseContext database) : base(database) + { } + + public List ActivityGroups = []; + + public bool GroupByActor { get; set; } + + public async Task OnGet(bool groupByActor = false) + { + Console.WriteLine(groupByActor); + List? events = (await this.Database.Activities.ToActivityDto(true).ToActivityGroups(groupByActor).ToListAsync()) + .ToOuterActivityGroups(groupByActor); + + if (events == null) return this.Page(); + + this.GroupByActor = groupByActor; + + this.ActivityGroups = events; + return this.Page(); + } + + +} \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs index 135ffd4bd..9627a712b 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -41,16 +41,17 @@ public static IQueryable> ToActivityGroups { Timestamp = dto.Activity.Timestamp.Date, UserId = dto.Activity.UserId, - TargetNewsId = dto.TargetNewsId ?? 0, - TargetTeamPickSlotId = dto.TargetTeamPickId ?? 0, + TargetNewsId = dto.TargetNewsId ?? -1, + TargetTeamPickSlotId = dto.TargetTeamPickId ?? -1, }) : activityQuery.GroupBy(dto => new ActivityGroup { Timestamp = dto.Activity.Timestamp.Date, - TargetUserId = dto.TargetUserId ?? 0, - TargetSlotId = dto.TargetSlotId ?? 0, - TargetPlaylistId = dto.TargetPlaylistId ?? 0, - TargetNewsId = dto.TargetNewsId ?? 0, + UserId = -1, + TargetUserId = dto.TargetUserId ?? -1, + TargetSlotId = dto.TargetSlotId ?? -1, + TargetPlaylistId = dto.TargetPlaylistId ?? -1, + TargetNewsId = dto.TargetNewsId ?? -1, }); public static List ToOuterActivityGroups diff --git a/ProjectLighthouse/Types/Activity/ActivityDto.cs b/ProjectLighthouse/Types/Activity/ActivityDto.cs index 6e696d8f5..4382eede4 100644 --- a/ProjectLighthouse/Types/Activity/ActivityDto.cs +++ b/ProjectLighthouse/Types/Activity/ActivityDto.cs @@ -17,10 +17,10 @@ public class ActivityDto public int TargetId => this.GroupType switch { - ActivityGroupType.User => this.TargetUserId ?? 0, - ActivityGroupType.Level => this.TargetSlotId ?? 0, - ActivityGroupType.Playlist => this.TargetPlaylistId ?? 0, - ActivityGroupType.News => this.TargetNewsId ?? 0, + ActivityGroupType.User => this.TargetUserId ?? -1, + ActivityGroupType.Level => this.TargetSlotId ?? -1, + ActivityGroupType.Playlist => this.TargetPlaylistId ?? -1, + ActivityGroupType.News => this.TargetNewsId ?? -1, _ => this.Activity.UserId, }; diff --git a/ProjectLighthouse/Types/Activity/ActivityGroup.cs b/ProjectLighthouse/Types/Activity/ActivityGroup.cs index 8cd189ae7..ea16fab56 100644 --- a/ProjectLighthouse/Types/Activity/ActivityGroup.cs +++ b/ProjectLighthouse/Types/Activity/ActivityGroup.cs @@ -19,21 +19,21 @@ public struct ActivityGroup this.GroupType switch { ActivityGroupType.User => this.TargetUserId ?? this.UserId, - ActivityGroupType.Level => this.TargetSlotId ?? 0, - ActivityGroupType.TeamPick => this.TargetTeamPickSlotId ?? 0, - ActivityGroupType.Playlist => this.TargetPlaylistId ?? 0, - ActivityGroupType.News => this.TargetNewsId ?? 0, + ActivityGroupType.Level => this.TargetSlotId ?? -1, + ActivityGroupType.TeamPick => this.TargetTeamPickSlotId ?? -1, + ActivityGroupType.Playlist => this.TargetPlaylistId ?? -1, + ActivityGroupType.News => this.TargetNewsId ?? -1, _ => this.UserId, }; public ActivityGroupType GroupType => - (this.TargetPlaylistId ?? 0) != 0 - ? ActivityGroupType.User - : (this.TargetNewsId ?? 0) != 0 + (this.TargetPlaylistId ?? -1) != -1 + ? ActivityGroupType.Playlist + : (this.TargetNewsId ?? -1) != -1 ? ActivityGroupType.News - : (this.TargetTeamPickSlotId ?? 0) != 0 + : (this.TargetTeamPickSlotId ?? -1) != -1 ? ActivityGroupType.TeamPick - : (this.TargetSlotId ?? 0) != 0 + : (this.TargetSlotId ?? -1) != -1 ? ActivityGroupType.Level : ActivityGroupType.User;