From 60d851fb153d3f52ad7ed57714484831b93e432d Mon Sep 17 00:00:00 2001 From: Slendy Date: Fri, 28 Jul 2023 17:45:28 -0500 Subject: [PATCH] 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;