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 0a2e09e1..699fc2f7 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 @@ public partial class DatabaseContext : DbContext 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 00000000..7e0c74d7 --- /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 00000000..cdb81944 --- /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 00000000..4aa02e0d --- /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 00000000..e0dac177 --- /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 00000000..647db324 --- /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 00000000..6db40d59 --- /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 00000000..502baf4b --- /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 00000000..17c500ac --- /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 00000000..74856367 --- /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 00000000..f78fda68 --- /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 76f2b715..ae170499 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; 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 class CommentController : ControllerBase 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 83e198d8..a0381f46 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Users; 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 e7962643..07a80775 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.Entities.Token; 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 60ac1c58..b9a36852 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.Levels; 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 67fff180..3db2016b 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.Profile; 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 4763708a..46e8b90e 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs @@ -4,7 +4,8 @@ using LBPUnion.ProjectLighthouse.Database; 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 e09a06e6..9a013a9e 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.Profile; 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 aa4e1c5b..c32df338 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.Interaction; 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 79ae3163..841cef38 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.Level; 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 a4f129b5..064af5ac 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs @@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; 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 13f9afa8..d0930e0a 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.Entities.Token; 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 3eb828e4..d45c1ff0 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Extensions; 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 fb5b6128..8caa0e14 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs @@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; 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 bef6943e..ee09930b 100644 --- a/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs +++ b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs @@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; 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 00ed79ce..a7212306 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.Level; 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 39d6986d..5ed58a06 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.Level; 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 d9826a92..bd188d7a 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.Profile; 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 782b3991..cb235655 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 70b98e64..66fea3d9 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 61034265..18c3d060 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 eacce233..e448f2db 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Tests.Helpers; 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 7408fcf5..66e5f6d6 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.Profile; 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 5335b2cf..42049a31 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Database; 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 e7228b2c..8c76f0b0 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Database; 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 a97b80af..ec9c80ca 100644 --- a/ProjectLighthouse.Tests/Unit/LocationTests.cs +++ b/ProjectLighthouse.Tests/Unit/LocationTests.cs @@ -3,7 +3,7 @@ using System.Xml.Serialization; 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 de591d94..ecf4eb9a 100644 --- a/ProjectLighthouse.Tests/Unit/PaginationTests.cs +++ b/ProjectLighthouse.Tests/Unit/PaginationTests.cs @@ -3,7 +3,7 @@ using System.Linq; 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 00000000..ca5d66f5 --- /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 699fc2f7..46394529 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 @@ public partial class DatabaseContext : DbContext 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 partial class DatabaseContext : DbContext 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 00000000..6cb03759 --- /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 00000000..60caca08 --- /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 00000000..61ae381c --- /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 7e0c74d7..e4ec2e9a 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 00000000..982857ce --- /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 cdb81944..7a1c5bf1 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 00000000..0c20175e --- /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 4aa02e0d..00000000 --- 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 502baf4b..fecf6b80 100644 --- a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs @@ -3,6 +3,9 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; 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 00000000..5c295033 --- /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 74856367..89d27e8b 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 59091953..c6f18240 100644 --- a/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs +++ b/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs @@ -3,7 +3,7 @@ using System; 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 c18a4a00..1b3c08de 100644 --- a/ProjectLighthouse/Types/Levels/Category.cs +++ b/ProjectLighthouse/Types/Levels/Category.cs @@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Database; 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 00000000..93d38257 --- /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 00000000..db9d6dcf --- /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 00000000..251d3f92 --- /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 00000000..527c9c7b --- /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 00000000..af00048c --- /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 00000000..74b35b7f --- /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 00000000..e089b987 --- /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 00000000..bed5c1ac --- /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 00000000..9c145fb5 --- /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 00000000..95dd7929 --- /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 00000000..ef783f54 --- /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 00000000..bc7cf53b --- /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 35d9023e..b9b11d0c 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 07919f5e..16597d13 100644 --- a/ProjectLighthouse/Types/Serialization/GameComment.cs +++ b/ProjectLighthouse/Types/Serialization/Comment/GameComment.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Database; 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 67bb2dfb..3c0cdd9e 100644 --- a/ProjectLighthouse/Types/Serialization/GamePhoto.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/GamePhoto.cs @@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; 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 94cbd50c..914b8227 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 a8d64028..d4f5fbfd 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 c4699720..82dea982 100644 --- a/ProjectLighthouse/Types/Serialization/PhotoSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/PhotoSlot.cs @@ -3,7 +3,7 @@ using System.ComponentModel; 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 7de0599a..cfd577b3 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 5ced447c..90d89329 100644 --- a/ProjectLighthouse/Types/Serialization/GamePlaylist.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs @@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Database; 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 ba85d8c8..f0f1f0f7 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 87a6f6cb..9d0e7f1c 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 81a46003..a5b309ab 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 7dba385e..1da3b77b 100644 --- a/ProjectLighthouse/Types/Serialization/GameReview.cs +++ b/ProjectLighthouse/Types/Serialization/Review/GameReview.cs @@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; 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 a15915dd..e13e99bb 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 00000000..11715e17 --- /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 f4148adf..00000000 --- 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 1195ce96..9c10ae73 100644 --- a/ProjectLighthouse/Types/Serialization/GameScore.cs +++ b/ProjectLighthouse/Types/Serialization/Score/GameScore.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Database; 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 831e5efe..3d00f141 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 f9c45763..2ee27c0b 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 904fb290..faa24508 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 e097de97..2f0a2069 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 3de55ee4..d7a67c6c 100644 --- a/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GameDeveloperSlot.cs @@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; 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 978818be..11890cce 100644 --- a/ProjectLighthouse/Types/Serialization/GameUserSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GameUserSlot.cs @@ -13,10 +13,12 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; 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 b00e8575..036c02c5 100644 --- a/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GenericSlotResponse.cs @@ -3,7 +3,7 @@ using System.ComponentModel; 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 0457d282..9dbcd2e5 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 49d3ce60..9778a941 100644 --- a/ProjectLighthouse/Types/Serialization/SlotBase.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/SlotBase.cs @@ -3,9 +3,10 @@ using System.Xml.Serialization; 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 abstract class SlotBase : ILbpSerializable 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 7f9954db..585f4a94 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 bf12e68c..ed8abca8 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 cb365ed1..054e3b6d 100644 --- a/ProjectLighthouse/Types/Serialization/GameUser.cs +++ b/ProjectLighthouse/Types/Serialization/User/GameUser.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Misc; 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 ef97d42c..43084165 100644 --- a/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/GenericUserResponse.cs @@ -3,7 +3,7 @@ using System.ComponentModel; 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 c289bdc0..be99ba4c 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 c7952936..8138f0ae 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 e5a15630..32b8731d 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 struct UserListResponse : ILbpSerializable, IHasCustomRoot } [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 a070862c..ff85fc0a 100644 --- a/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs +++ b/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs @@ -28,11 +28,7 @@ public class ApiStartup } ); - 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 0d372e8f..e35d95e6 100644 --- a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs +++ b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs @@ -54,11 +54,7 @@ public class GameServerStartup } ); - 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 a67e667f..e83c60b9 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs @@ -64,7 +64,7 @@ public class CompleteEmailVerificationPage : BaseLayout 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 8c883462..b3c6f8f4 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 0af0f304..c2ac9291 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs @@ -100,7 +100,7 @@ public class LoginForm : BaseLayout 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 bb89c6bf..61460102 100644 --- a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs +++ b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs @@ -13,7 +13,6 @@ using LBPUnion.ProjectLighthouse.Services; 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 class WebsiteStartup 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 00000000..6d84cbf6 --- /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 6cb03759..3fdb6768 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 6bf1754d..3adb5e4d 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 be99ba4c..64f06c29 100644 --- a/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs @@ -19,7 +19,6 @@ public struct MinimalUserListResponse : ILbpSerializable 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 | 433 ++++++----- .../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 | 151 +++- .../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 | 216 ++++-- .../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, 2725 insertions(+), 443 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 dcd9a5c2..736e4766 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 f78fda68..107c3ebb 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 class ActivityController : ControllerBase 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); - - 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)); + List? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds; + friendIds ??= new List(); - predicate = excludeMyself - ? predicate.Or(a => a.Activity.UserId != token.UserId) - : predicate.Or(a => a.Activity.UserId == token.UserId); + // 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, + }; + } + } - query = dtoQuery.Where(predicate).Select(dto => dto.Activity); + Expression> newsPredicate = !excludeNews + ? new IncludeNewsFilter().GetPredicate() + : new ExcludeNewsFilter().GetPredicate(); - return query.OrderByDescending(a => a.Timestamp); + predicate = predicate.Or(newsPredicate); + + if (!excludeMyLevels) + { + predicate = predicate.Or(dto => dto.TargetSlotCreatorId == token.UserId); + } + + List includedUserIds = new(); + + if (!excludeFriends) + { + includedUserIds.AddRange(friendIds); + } + + if (!excludeFavouriteUsers) + { + includedUserIds.AddRange(favouriteUsers); + } + + if (!excludeMyself) + { + includedUserIds.Add(token.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); + } + + Console.WriteLine(predicate); + + dtoQuery = dtoQuery.Where(predicate); + + 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 @@ public class ActivityController : ControllerBase { GameTokenEntity token = this.GetToken(); - if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest(); + if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.NotFound(); - 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, + 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); - activityEvents = activityEvents.Where(a => a.Timestamp < start && a.Timestamp > end); + List> groups = await activityEvents + .Where(dto => dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End) + .ToActivityGroups() + .ToListAsync(); - Console.WriteLine($@"start: {start}, end: {end}"); + List outerGroups = groups.ToOuterActivityGroups(); - List> groups = await GroupActivities(activityEvents).ToListAsync(); + long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); - foreach (IGrouping group in groups) + await this.CacheEntities(outerGroups); + + GameStream? gameStream = GameStream.CreateFromGroups(token, + outerGroups, + times.Start.ToUnixTimeMilliseconds(), + oldestTimestamp); + + return this.Ok(gameStream); + } + + #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 (token.GameVersion == GameVersion.LittleBigPlanet1) return this.NotFound(); - if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis; + if ((SlotHelper.IsTypeInvalid(slotType) || slotId == 0) == (username == null)) return this.BadRequest(); - long endTimestamp = timestamp - 864_000; + IQueryable activityQuery = this.database.Activities.ToActivityDto() + .Where(a => a.Activity.Type != EventType.NewsPost && a.Activity.Type != EventType.MMPickLevel); - if (slotType is not ("developer" or "user")) return this.BadRequest(); + bool isLevelActivity = username == null; - if (slotType == "developer") - slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); + // Slot activity + if (isLevelActivity) + { + if (slotType == "developer") + slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); - IQueryable slotActivity = this.database.Activities.Select(ActivityToDto()) - .Where(a => a.TargetSlotId == slotId); + if (!await this.database.Slots.AnyAsync(s => s.SlotId == slotId)) return this.NotFound(); - DateTime start = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime; - DateTime end = DateTimeOffset.FromUnixTimeMilliseconds(endTimestamp).DateTime; + 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); + } - slotActivity = slotActivity.Where(a => a.Activity.Timestamp < start && a.Activity.Timestamp > end); + (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityQuery, timestamp, null); - List> groups = await GroupActivities(slotActivity).ToListAsync(); + activityQuery = activityQuery.Where(dto => + dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End); - DateTime oldestTime = groups.Max(g => g.Max(a => a.Timestamp)); - long oldestTimestamp = new DateTimeOffset(oldestTime).ToUnixTimeMilliseconds(); + List> groups = await activityQuery.ToActivityGroups().ToListAsync(); - return this.Ok(await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp)); - } + List outerGroups = groups.ToOuterActivityGroups(); - [HttpGet("user2/{userId:int}/")] - public async Task UserActivity(int userId, long timestamp) - { - GameTokenEntity token = this.GetToken(); + long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); - if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest(); + await this.CacheEntities(outerGroups); - 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)); + 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 00000000..9fb7df7c --- /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 c32df338..8e077246 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -141,6 +141,22 @@ public class ReviewController : ControllerBase 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 064af5ac..367335f9 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 d0930e0a..ff35092b 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 d45c1ff0..f6417ab3 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization; 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 7c654a6f..5f8d6a20 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 b483f546..fb6003bf 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 606557a7..ee8396d0 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 00000000..169ccd42 --- /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 00000000..1aa1d38d --- /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 4a00ab68..00d98693 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 a3eae2e9..0be1bdbb 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 46394529..6fc43ccf 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 partial class DatabaseContext : DbContext 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 00000000..e2b1363d --- /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 db779f21..cb955398 100644 --- a/ProjectLighthouse/Extensions/ControllerExtensions.cs +++ b/ProjectLighthouse/Extensions/ControllerExtensions.cs @@ -44,6 +44,7 @@ public static partial class ControllerExtensions 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 00000000..0ff56fbe --- /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 00000000..80d98113 --- /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 00000000..36d545c8 --- /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 00000000..ddd489a5 --- /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 00000000..c158a24d --- /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 00000000..5ab3ccac --- /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 00000000..c35c00ee --- /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 c7756361..3dfdebbc 100644 --- a/ProjectLighthouse/Filter/Filters/AdventureFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/AdventureFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 e203cf67..8d967447 100644 --- a/ProjectLighthouse/Filter/Filters/AuthorLabelFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/AuthorLabelFilter.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Extensions; 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 class AuthorLabelFilter : ISlotFilter { 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 a3283b54..22e26a20 100644 --- a/ProjectLighthouse/Filter/Filters/CreatorFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/CreatorFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 37a8bd81..395a6efd 100644 --- a/ProjectLighthouse/Filter/Filters/CrossControlFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/CrossControlFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 6d6510d6..c518d401 100644 --- a/ProjectLighthouse/Filter/Filters/ExcludeAdventureFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ExcludeAdventureFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 069eb345..15cd1d9e 100644 --- a/ProjectLighthouse/Filter/Filters/ExcludeCrossControlFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ExcludeCrossControlFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 5d813008..3a275885 100644 --- a/ProjectLighthouse/Filter/Filters/ExcludeLBP1OnlyFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ExcludeLBP1OnlyFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; 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 2b03fab7..33e15708 100644 --- a/ProjectLighthouse/Filter/Filters/ExcludeMovePackFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ExcludeMovePackFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 07e4d865..818585a0 100644 --- a/ProjectLighthouse/Filter/Filters/FirstUploadedFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/FirstUploadedFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Extensions; 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 b3444739..425e72ae 100644 --- a/ProjectLighthouse/Filter/Filters/GameVersionFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/GameVersionFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; 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 813b2e75..6e64b263 100644 --- a/ProjectLighthouse/Filter/Filters/GameVersionListFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/GameVersionListFilter.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; 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 class GameVersionListFilter : ISlotFilter 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 eab9820b..38999a1d 100644 --- a/ProjectLighthouse/Filter/Filters/HiddenSlotFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/HiddenSlotFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 5c29834a..c7170ce8 100644 --- a/ProjectLighthouse/Filter/Filters/MovePackFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/MovePackFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 71a64733..fd68f4a2 100644 --- a/ProjectLighthouse/Filter/Filters/PlayerCountFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/PlayerCountFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Extensions; 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 3460fb57..427e2a14 100644 --- a/ProjectLighthouse/Filter/Filters/ResultTypeFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/ResultTypeFilter.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; 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 17412712..0d900549 100644 --- a/ProjectLighthouse/Filter/Filters/SlotIdFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/SlotIdFilter.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Extensions; 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 class SlotIdFilter : ISlotFilter 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 96ea578d..0b1c1a79 100644 --- a/ProjectLighthouse/Filter/Filters/SlotTypeFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/SlotTypeFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; 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 2116ae66..b7365c0d 100644 --- a/ProjectLighthouse/Filter/Filters/SubLevelFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/SubLevelFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 eb77c5ea..ad9088c8 100644 --- a/ProjectLighthouse/Filter/Filters/TeamPickFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/TeamPickFilter.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; 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 7566ca44..00778517 100644 --- a/ProjectLighthouse/Filter/Filters/TextFilter.cs +++ b/ProjectLighthouse/Filter/Filters/Slot/TextFilter.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Extensions; 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 00000000..da18f6cf --- /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 34e58686..af5224fc 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -19,6 +19,36 @@ namespace ProjectLighthouse.Migrations .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 @@ namespace ProjectLighthouse.Migrations 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 @@ namespace ProjectLighthouse.Migrations 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 00000000..b6f12d44 --- /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 60caca08..76d84e99 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -4,12 +4,13 @@ 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.Entities.Website; using LBPUnion.ProjectLighthouse.Types.Levels; +using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Types.Activity; @@ -44,7 +45,7 @@ public class ActivityEntityEventHandler : IEntityEventHandler 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 class ActivityEntityEventHandler : IEntityEventHandler 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 @@ public class ActivityEntityEventHandler : IEntityEventHandler 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 class ActivityEntityEventHandler : IEntityEventHandler 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 class ActivityEntityEventHandler : IEntityEventHandler { 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, - }; - } - else if (oldSlotEntity.SlotId == slotEntity.SlotId && slotEntity.Type == SlotType.User) - { - activity = new LevelActivityEntity - { - Type = EventType.PublishLevel, - SlotId = slotEntity.SlotId, - UserId = slotEntity.CreatorId, - }; - } + 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; + } + } + 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 + { + 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.AddLevelToPlaylist, + PlaylistId = playlist.PlaylistId, + SlotId = slotId, + UserId = playlist.CreatorId, + }; + InsertActivity(database, entity); + } break; } } @@ -149,17 +249,6 @@ public class ActivityEntityEventHandler : IEntityEventHandler 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 61ae381c..51f981b0 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 e4ec2e9a..f70100cd 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 e0dac177..699a93f5 100644 --- a/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs @@ -4,14 +4,13 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; 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 647db324..32a768ca 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 fecf6b80..4d535459 100644 --- a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs @@ -4,12 +4,37 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; 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 5c295033..9a722601 100644 --- a/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs +++ b/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs @@ -3,12 +3,13 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; 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 00000000..659e2af1 --- /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 00000000..38419457 --- /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 00000000..94744c24 --- /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 00000000..d61de1c0 --- /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 db9d6dcf..c9b8079f 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.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, + 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.Activity).CommentId, + }, + EventType.CommentOnUser => new GameUserCommentEvent + { + CommentId = ((CommentActivityEntity)activity.Activity).CommentId, + }, + EventType.HeartUser or EventType.UnheartUser => new GameHeartUserEvent + { + TargetUserId = targetId, + }, + EventType.ReviewLevel => new GameReviewEvent + { + 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 = ((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 251d3f92..81583459 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 00000000..818f46cc --- /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 527c9c7b..fa839555 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 74b35b7f..4d802f2a 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 00000000..c3fd2d56 --- /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 e089b987..af8c5b82 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 00000000..9f502dce --- /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 00000000..f5030206 --- /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 00000000..337cd934 --- /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 95dd7929..df8fdfeb 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)); } + + return results; } - 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)); - } - } + 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 00000000..d235b718 --- /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 ef783f54..4afc678b 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; 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, + }, + ActivityGroupType.Playlist => new GamePlaylistStreamGroup + { + PlaylistId = targetId, + }, + ActivityGroupType.News => new GameNewsStreamGroup + { + NewsId = 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(), - }, - }; - + 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 00000000..51bfa9cd --- /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 00000000..ac4956e8 --- /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 00000000..90d15bac --- /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 90d89329..80a9d6a1 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 class GamePlaylist : ILbpSerializable, INeedsPreparationForSerialization 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 00000000..c59dfccd --- /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 054e3b6d..ec4fdc42 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 class GameUser : ILbpSerializable, INeedsPreparationForSerialization 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 @@ public class GameUser : ILbpSerializable, INeedsPreparationForSerialization 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 107c3ebb..03252a69 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -295,7 +295,7 @@ public class ActivityController : ControllerBase [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 class ActivityController : ControllerBase 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 df8fdfeb..c803d7d0 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -99,7 +99,7 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization } public static GameStream CreateFromGroups - (GameTokenEntity token, List groups, long startTimestamp, long endTimestamp) + (GameTokenEntity token, List groups, long startTimestamp, long endTimestamp, bool dontNest = false) { GameStream gameStream = new() { @@ -117,7 +117,7 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization 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 c803d7d0..158f166e 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -99,7 +99,7 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization } public static GameStream CreateFromGroups - (GameTokenEntity token, List 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 class GameStream : ILbpSerializable, INeedsPreparationForSerialization 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 07b6859e..d66bda25 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 90d15bac..cc4b4659 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 class GameNewsObject : ILbpSerializable { 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 00000000..97840f9c --- /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 e2b1363d..132ad0dc 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -82,7 +82,7 @@ public static class ActivityQueryExtensions : 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 class ActivityQueryExtensions 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 class ActivityQueryExtensions : 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 76d84e99..aa3bb5ab 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 class ActivityEntityEventHandler : IEntityEventHandler { 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 93d38257..1b297785 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 169ccd42..ba86e0e3 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -60,7 +60,7 @@ public class ActivityEventHandlerTests { CommentId = 1, PosterUserId = 1, - TargetId = 1, + TargetSlotId = 1, Type = CommentType.Level, }; database.Comments.Add(comment); @@ -89,7 +89,7 @@ public class ActivityEventHandlerTests { CommentId = 1, PosterUserId = 1, - TargetId = 1, + TargetUserId = 1, Type = CommentType.Profile, }; database.Comments.Add(comment); @@ -152,7 +152,7 @@ public class ActivityEventHandlerTests { 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 aa3bb5ab..dd8447f1 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -9,6 +9,10 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; 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 ba86e0e3..36f60b22 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -56,6 +56,12 @@ public class ActivityEventHandlerTests }, }); + 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 36f60b22..ccfcc134 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -59,6 +59,7 @@ public class ActivityEventHandlerTests 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 03252a69..f023f443 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -102,7 +102,7 @@ public class ActivityController : ControllerBase 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 1aa1d38d..b37ed595 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 dd8447f1..58b766fb 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -140,8 +140,6 @@ public class ActivityEntityEventHandler : IEntityEventHandler { 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 class ActivityEntityEventHandler : IEntityEventHandler 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 bed5c1ac..a6fd19d6 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 f023f443..a87594a9 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -43,6 +43,10 @@ public class ActivityController : ControllerBase 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 132ad0dc..4ae5cc22 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -71,6 +71,7 @@ public static class ActivityQueryExtensions // 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 class ActivityQueryExtensions 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 class ActivityQueryExtensions ? ((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 class ActivityQueryExtensions ? ((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 b6f12d44..d813f76d 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 58b766fb..741f8149 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -16,7 +16,6 @@ using System.Reflection; 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 class ActivityEntityEventHandler : IEntityEventHandler 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 class ActivityEntityEventHandler : IEntityEventHandler 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 class ActivityEntityEventHandler : IEntityEventHandler { 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 class ActivityEntityEventHandler : IEntityEventHandler }; InsertActivity(database, entity); } + break; } } @@ -250,11 +278,15 @@ public class ActivityEntityEventHandler : IEntityEventHandler 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 a6fd19d6..5f26cc90 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 a87594a9..792c6711 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -3,10 +3,12 @@ using LBPUnion.ProjectLighthouse.Database; 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 @@ public class ActivityController : ControllerBase dto.Activity.Type != EventType.AddLevelToPlaylist); } - Console.WriteLine(predicate); + Logger.Debug(predicate.ToString(), LogArea.Activity); dtoQuery = dtoQuery.Where(predicate); @@ -281,16 +283,16 @@ public class ActivityController : ControllerBase { 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 741f8149..33390375 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 @@ public class ActivityEntityEventHandler : IEntityEventHandler { 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 class ActivityEntityEventHandler : IEntityEventHandler 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 class ActivityEntityEventHandler : IEntityEventHandler 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 class ActivityEntityEventHandler : IEntityEventHandler 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 10146e22..9a8cc178 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 c9b8079f..d69d626f 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -6,9 +6,11 @@ using System.Threading.Tasks; 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 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization { 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 ccfcc134..6990c910 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -196,6 +196,7 @@ public class ActivityEventHandlerTests 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 33390375..f338f9f4 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -35,25 +35,44 @@ public class ActivityEntityEventHandler : IEntityEventHandler }, _ => 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 4ae5cc22..c92cc60d 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -88,7 +88,7 @@ public static class ActivityQueryExtensions ? ((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 class ActivityQueryExtensions ? ((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 class ActivityQueryExtensions ? ((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 class ActivityQueryExtensions ? ((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 c92cc60d..92fbee97 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -61,7 +61,7 @@ public static class ActivityQueryExtensions { 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 158f166e..3081f210 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 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization 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 GameStream CreateFromGroups diff --git a/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs b/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs index cc4b4659..ec1eeee0 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 f338f9f4..29d7f9a8 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -57,6 +57,7 @@ public class ActivityEntityEventHandler : IEntityEventHandler }, PhotoEntity photo => photo.SlotId switch { + // Photos without levels null => new PhotoActivityEntity { Type = EventType.UploadPhoto, @@ -65,13 +66,14 @@ public class ActivityEntityEventHandler : IEntityEventHandler }, _ => 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 fa839555..c2da06ed 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs @@ -6,6 +6,7 @@ using System.Xml.Serialization; 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 d69d626f..649907d5 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -184,6 +184,7 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization }, 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 c2da06ed..62b9a948 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 3081f210..3f41b025 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -11,6 +11,7 @@ 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.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 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization foreach (int id in ids) { TFrom entity = await database.Set().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 6990c910..6f08b8b4 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 class ActivityEventHandlerTests .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 class ActivityEventHandlerTests .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 class ActivityEventHandlerTests .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 class ActivityEventHandlerTests .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 class ActivityEventHandlerTests 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 92fbee97..67394c8c 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -118,7 +118,7 @@ public static class ActivityQueryExtensions ? ((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 29d7f9a8..2342c566 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 class ActivityEntityEventHandler : IEntityEventHandler 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 64dc10c3..7982ac42 100644 --- a/ProjectLighthouse.Tests/Helpers/MockHelper.cs +++ b/ProjectLighthouse.Tests/Helpers/MockHelper.cs @@ -50,7 +50,7 @@ public static class MockHelper 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 da18f6cf..9f4d01a4 100644 --- a/ProjectLighthouse/Migrations/20230725013522_InitialActivity.cs +++ b/ProjectLighthouse/Migrations/20240120214525_InitialActivity.cs @@ -9,9 +9,10 @@ using Microsoft.EntityFrameworkCore.Migrations; 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 @@ namespace ProjectLighthouse.Migrations 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 97840f9c..cfd53fc9 100644 --- a/ProjectLighthouse/Migrations/20230814031609_AddPublishedAtToAnnouncements.cs +++ b/ProjectLighthouse/Migrations/20240120214606_AddPublishedAtToWebAnnouncements.cs @@ -8,9 +8,10 @@ using Microsoft.EntityFrameworkCore.Migrations; 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 @@ namespace ProjectLighthouse.Migrations 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 af5224fc..5c33884b 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -1107,6 +1107,9 @@ namespace ProjectLighthouse.Migrations 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 00000000..617c0caa --- /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 6f08b8b4..95d1ba05 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -673,7 +673,7 @@ public class ActivityEventHandlerTests { 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 2342c566..46f5ebb2 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -245,10 +245,13 @@ public class ActivityEntityEventHandler : IEntityEventHandler { 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 class ActivityEntityEventHandler : IEntityEventHandler }; 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 | 239 +++++++++--- .../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, 760 insertions(+), 168 deletions(-) rename ProjectLighthouse/Migrations/{20240120214525_InitialActivity.cs => 20240325034658_InitialActivity.cs} (97%) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 760f6172..9e01b2fa 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 792c6711..fb511289 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -56,7 +56,7 @@ public class ActivityController : ControllerBase .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 @@ public class ActivityController : ControllerBase predicate = predicate.Or(dto => dto.TargetSlotCreatorId == token.UserId); } - List includedUserIds = new(); + List includedUserIds = []; if (!excludeFriends) { @@ -168,7 +168,7 @@ public class ActivityController : ControllerBase 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 @@ public class ActivityController : ControllerBase { 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 class ActivityController : ControllerBase { 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 class ActivityController : ControllerBase 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 617c0caa..8843091c 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 { - TargetPlaylistId = 1, - Activity = new ActivityEntity(), + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, }, - }; - List groups = activities.AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups(); + 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, + }, + ]; + + //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 ca5d66f5..fb364d95 100644 --- a/ProjectLighthouse/Database/ActivityInterceptor.cs +++ b/ProjectLighthouse/Database/ActivityInterceptor.cs @@ -93,7 +93,7 @@ public class ActivityInterceptor : SaveChangesInterceptor { 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 6fc43ccf..6c981b25 100644 --- a/ProjectLighthouse/Database/DatabaseContext.cs +++ b/ProjectLighthouse/Database/DatabaseContext.cs @@ -109,13 +109,15 @@ public partial class DatabaseContext : DbContext 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 67394c8c..0697ec5e 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 class ActivityQueryExtensions 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 class ActivityQueryExtensions 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 9f4d01a4..62ea0bd4 100644 --- a/ProjectLighthouse/Migrations/20240120214525_InitialActivity.cs +++ b/ProjectLighthouse/Migrations/20240325034658_InitialActivity.cs @@ -6,10 +6,10 @@ using Microsoft.EntityFrameworkCore.Migrations; #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 @@ namespace ProjectLighthouse.Migrations 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 5c33884b..7f069036 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -27,7 +27,8 @@ namespace ProjectLighthouse.Migrations b.Property("Discriminator") .IsRequired() - .HasColumnType("longtext"); + .HasMaxLength(34) + .HasColumnType("varchar(34)"); b.Property("Timestamp") .HasColumnType("datetime(6)"); @@ -1123,18 +1124,6 @@ namespace ProjectLighthouse.Migrations 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"); @@ -1149,6 +1138,46 @@ namespace ProjectLighthouse.Migrations b.HasDiscriminator().HasValue("LevelActivityEntity"); }); + 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("LevelCommentActivityEntity"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelPhotoActivity", b => + { + b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); + + b.Property("PhotoId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int"); + + b.Property("SlotId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("int") + .HasColumnName("SlotId"); + + b.HasIndex("PhotoId"); + + b.HasIndex("SlotId"); + + b.HasDiscriminator().HasValue("LevelPhotoActivity"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b => { b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity"); @@ -1161,18 +1190,6 @@ namespace ProjectLighthouse.Migrations 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"); @@ -1213,8 +1230,15 @@ namespace ProjectLighthouse.Migrations 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 @@ namespace ProjectLighthouse.Migrations 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 @@ namespace ProjectLighthouse.Migrations 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,17 +1719,6 @@ namespace ProjectLighthouse.Migrations 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") @@ -1668,6 +1730,44 @@ namespace ProjectLighthouse.Migrations b.Navigation("Slot"); }); + 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.LevelPhotoActivity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo") + .WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.NewsActivityEntity", b => { b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Website.WebsiteAnnouncementEntity", "News") @@ -1679,17 +1779,6 @@ namespace ProjectLighthouse.Migrations 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") @@ -1720,7 +1809,15 @@ namespace ProjectLighthouse.Migrations .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 @@ namespace ProjectLighthouse.Migrations .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 @@ namespace ProjectLighthouse.Migrations 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 d813f76d..87759369 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 46f5ebb2..a2b520e6 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -41,40 +41,44 @@ public class ActivityEntityEventHandler : IEntityEventHandler { 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 class ActivityEntityEventHandler : IEntityEventHandler Type = EventType.Score, ScoreId = score.ScoreId, UserId = score.UserId, + SlotId = score.SlotId, }, _ => null, }, @@ -122,6 +127,7 @@ public class ActivityEntityEventHandler : IEntityEventHandler 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 f70100cd..c497b047 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 7a1c5bf1..39732e04 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 0c20175e..61a80e88 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 699a93f5..27d7ca9a 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 32a768ca..59daccaa 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 6db40d59..bf1d9083 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 4d535459..9d4ef06b 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 9a722601..e824541c 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 17c500ac..4ecc8384 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 89d27e8b..f1756f5b 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 649907d5..21d28314 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -58,7 +58,7 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization 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 3f41b025..f2d2d7a2 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 class GameStream : ILbpSerializable, INeedsPreparationForSerialization 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 class GameStream : ILbpSerializable, INeedsPreparationForSerialization { 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 4afc678b..e4fea532 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 class GameStreamGroup : ILbpSerializable 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 fb511289..69f38827 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -30,9 +30,6 @@ public class ActivityController : ControllerBase this.database = database; } - /// - /// This method is only used for LBP2 so we exclude playlists - /// private async Task> GetFilters ( IQueryable dtoQuery, @@ -123,8 +120,6 @@ public class ActivityController : ControllerBase dto.Activity.Type != EventType.AddLevelToPlaylist); } - Logger.Debug(predicate.ToString(), LogArea.Activity); - dtoQuery = dtoQuery.Where(predicate); return dtoQuery; @@ -344,8 +339,6 @@ public class ActivityController : ControllerBase 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 fb364d95..7ded9cd5 100644 --- a/ProjectLighthouse/Database/ActivityInterceptor.cs +++ b/ProjectLighthouse/Database/ActivityInterceptor.cs @@ -22,13 +22,20 @@ public class ActivityInterceptor : SaveChangesInterceptor 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 @@ public class ActivityInterceptor : SaveChangesInterceptor 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 @@ public class ActivityInterceptor : SaveChangesInterceptor 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 a2b520e6..91e3e605 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.Entities.Website; 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 class ActivityEntityEventHandler : IEntityEventHandler }, _ => null, }, - CommentType.Profile => new UserCommentActivityEntity() + CommentType.Profile => new UserCommentActivityEntity { Type = EventType.CommentOnUser, CommentId = comment.CommentId, @@ -199,6 +195,8 @@ public class ActivityEntityEventHandler : IEntityEventHandler { 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 @@ public class ActivityEntityEventHandler : IEntityEventHandler 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 class ActivityEntityEventHandler : IEntityEventHandler 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 69f38827..7fc867e2 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -283,11 +283,13 @@ public class ActivityController : ControllerBase 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 class ActivityController : ControllerBase 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 class ActivityController : ControllerBase 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 303b4f85..7e96e016 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; 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 f2d2d7a2..f734b741 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -134,9 +134,10 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization 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 95d1ba05..ccf745a7 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -76,7 +76,7 @@ public class ActivityEventHandlerTests 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 class ActivityEventHandlerTests 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 class ActivityEventHandlerTests 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 class ActivityEventHandlerTests 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 class ActivityEventHandlerTests 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 class ActivityEventHandlerTests 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 8843091c..c328c947 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs @@ -86,34 +86,27 @@ public class ActivityGroupingTests }, ]; - //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 class ActivityGroupingTests .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 class ActivityGroupingTests 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 class ActivityGroupingTests }); 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 7fc867e2..f84e25ae 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -306,6 +306,9 @@ public class ActivityController : ControllerBase 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 class ActivityController : ControllerBase a.Activity.Type != EventType.AddLevelToPlaylist); } - bool isLevelActivity = username == null; - // Slot activity if (isLevelActivity) { @@ -345,9 +346,9 @@ public class ActivityController : ControllerBase 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 841cef38..56c5bc84 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -131,7 +131,9 @@ public class ScoreController : ControllerBase 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 0697ec5e..cbe70db8 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -66,15 +66,15 @@ public static class ActivityQueryExtensions { 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 87759369..6e696d8f 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 91e3e605..29810f8c 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -57,25 +57,17 @@ public class ActivityEntityEventHandler : IEntityEventHandler }, 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 class ActivityEntityEventHandler : IEntityEventHandler 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 51f981b0..8cd189ae 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 21d28314..f9f48a0f 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -187,7 +187,7 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization PhotoId = ((PhotoActivityEntity)activity.Activity).PhotoId, Slot = new ReviewSlot { - SlotId = targetId, + SlotId = activity.TargetSlotId ?? -1, }, }, EventType.MMPickLevel => new GameTeamPickLevelEvent @@ -211,15 +211,15 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization }, 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 56c5bc84..beca96d3 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -132,7 +132,6 @@ public class ScoreController : ControllerBase 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 29810f8c..f706c6e0 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -35,7 +35,9 @@ public class ActivityEntityEventHandler : IEntityEventHandler }, 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 class ActivityEntityEventHandler : IEntityEventHandler }, _ => 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 class ActivityEntityEventHandler : IEntityEventHandler }, // 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 class ActivityEntityEventHandler : IEntityEventHandler }, _ => 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 class ActivityEntityEventHandler : IEntityEventHandler // 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 class ActivityEntityEventHandler : IEntityEventHandler { 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 ccf745a7..9e82509e 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -120,12 +120,21 @@ public class ActivityEventHandlerTests 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 cfd53fc9..00000000 --- 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 62ea0bd4..00000000 --- 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 00000000..93c31f11 --- /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 7f069036..8c6bd550 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -3,6 +3,7 @@ using System; 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 @@ namespace ProjectLighthouse.Migrations { #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 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedLevelId")); + b.Property("SlotId") .HasColumnType("int"); @@ -77,6 +84,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedPlaylistId")); + b.Property("PlaylistId") .HasColumnType("int"); @@ -98,6 +107,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedProfileId")); + b.Property("HeartedUserId") .HasColumnType("int"); @@ -119,6 +130,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("QueuedLevelId")); + b.Property("SlotId") .HasColumnType("int"); @@ -140,6 +153,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + b.Property("CommentId") .HasColumnType("int"); @@ -164,6 +179,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatedLevelId")); + b.Property("Rating") .HasColumnType("int"); @@ -194,6 +211,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatedReviewId")); + b.Property("ReviewId") .HasColumnType("int"); @@ -218,6 +237,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("VisitedLevelId")); + b.Property("PlaysLBP1") .HasColumnType("int"); @@ -248,6 +269,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CategoryId")); + b.Property("Description") .HasColumnType("longtext"); @@ -274,6 +297,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PlaylistId")); + b.Property("CreatorId") .HasColumnType("int"); @@ -302,6 +327,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReviewId")); + b.Property("Deleted") .HasColumnType("tinyint(1)"); @@ -349,6 +376,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ScoreId")); + b.Property("ChildSlotId") .HasColumnType("int"); @@ -382,6 +411,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SlotId")); + b.Property("AuthorLabels") .IsRequired() .HasColumnType("longtext"); @@ -535,6 +566,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReportId")); + b.Property("Bounds") .HasColumnType("longtext"); @@ -581,6 +614,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CaseId")); + b.Property("AffectedId") .HasColumnType("int"); @@ -635,6 +670,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("IsDismissed") .HasColumnType("tinyint(1)"); @@ -660,6 +697,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BlockedProfileId")); + b.Property("BlockedUserId") .HasColumnType("int"); @@ -681,6 +720,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CommentId")); + b.Property("Deleted") .HasColumnType("tinyint(1)"); @@ -750,6 +791,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PhotoId")); + b.Property("CreatorId") .HasColumnType("int"); @@ -790,6 +833,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PhotoSubjectId")); + b.Property("Bounds") .HasColumnType("longtext"); @@ -814,6 +859,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PlatformLinkAttemptId")); + b.Property("IPAddress") .HasColumnType("longtext"); @@ -842,6 +889,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserId")); + b.Property("AdminGrantedSlots") .HasColumnType("int"); @@ -947,6 +996,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Created") .HasColumnType("datetime(6)"); @@ -967,6 +1018,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmailSetTokenId")); + b.Property("EmailToken") .HasColumnType("longtext"); @@ -989,6 +1042,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmailVerificationTokenId")); + b.Property("EmailToken") .HasColumnType("longtext"); @@ -1011,6 +1066,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + b.Property("ExpiresAt") .HasColumnType("datetime(6)"); @@ -1042,6 +1099,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + b.Property("Created") .HasColumnType("datetime(6)"); @@ -1062,6 +1121,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + b.Property("Created") .HasColumnType("datetime(6)"); @@ -1082,6 +1143,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + b.Property("ExpiresAt") .HasColumnType("datetime(6)"); @@ -1105,6 +1168,8 @@ namespace ProjectLighthouse.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + b.Property("Content") .HasColumnType("longtext"); @@ -1246,6 +1311,9 @@ namespace ProjectLighthouse.Migrations { 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 f706c6e0..ef8e6fad 100644 --- a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -82,6 +82,7 @@ public class ActivityEntityEventHandler : IEntityEventHandler ScoreId = score.ScoreId, UserId = score.UserId, SlotId = score.SlotId, + Points = score.Points, }, _ => null, }, @@ -245,6 +246,7 @@ public class ActivityEntityEventHandler : IEntityEventHandler 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 4ecc8384..6d6db824 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 f9f48a0f..0b945c51 100644 --- a/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -150,6 +150,7 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization 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 5f26cc90..382080ab 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 00000000..66d5d8e7 --- /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 a6eaa43d..7bede06c 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 8595ee85..0849304b 100644 --- a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs @@ -54,6 +54,7 @@ public class NotificationsPage : BaseLayout 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 66d5d8e7..88b54e9f 100644 --- a/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs +++ b/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs @@ -19,7 +19,7 @@ namespace LBPUnion.ProjectLighthouse.Migrations 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 1414c642..77e32db6 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 f84e25ae..ff904cff 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -30,16 +30,21 @@ public class ActivityController : ControllerBase 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 @@ public class ActivityController : ControllerBase } } - 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 class ActivityController : ControllerBase 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 @@ public class ActivityController : ControllerBase 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 a9580e38..198d3fe7 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; 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 cbe70db8..135ffd4b 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -32,7 +32,7 @@ public static class ActivityQueryExtensions /// 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 class ActivityQueryExtensions /// 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 class ActivityQueryExtensions /// 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 00000000..40aad32e --- /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 00000000..dd24cef7 --- /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 135ffd4b..9627a712 100644 --- a/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -41,16 +41,17 @@ public static class ActivityQueryExtensions { 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 6e696d8f..4382eede 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 8cd189ae..ea16fab5 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;