diff --git a/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs b/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs index 76310aec..a906793b 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.API/Startup/ApiStartup.cs b/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs index a070862c..ff85fc0a 100644 --- a/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs +++ b/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs @@ -28,11 +28,7 @@ public class ApiStartup } ); - services.AddDbContext(builder => - { - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); - }); + services.AddDbContext(DatabaseContext.ConfigureBuilder()); services.AddSwaggerGen ( diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs new file mode 100644 index 00000000..ff904cff --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -0,0 +1,372 @@ +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.Logging; +using LBPUnion.ProjectLighthouse.StorableLists.Stores; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Logging; +using LBPUnion.ProjectLighthouse.Types.Serialization.Activity; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; + +[ApiController] +[Authorize] +[Route("LITTLEBIGPLANETPS3_XML/stream")] +[Produces("text/xml")] +public class ActivityController : ControllerBase +{ + private readonly DatabaseContext database; + + public ActivityController(DatabaseContext database) + { + this.database = database; + } + + private class ActivityFilterOptions + { + public bool ExcludeNews { get; init; } + public bool ExcludeMyLevels { get; init; } + public bool ExcludeFriends { get; init; } + public bool ExcludeFavouriteUsers { get; init; } + public bool ExcludeMyself { get; init; } + public bool ExcludeMyPlaylists { get; init; } = true; + } + + private async Task> GetFilters + ( + IQueryable dtoQuery, + GameTokenEntity token, + ActivityFilterOptions options + ) + { + dtoQuery = token.GameVersion == GameVersion.LittleBigPlanetVita + ? dtoQuery.Where(dto => dto.TargetSlotGameVersion == null || dto.TargetSlotGameVersion == token.GameVersion) + : dtoQuery.Where(dto => dto.TargetSlotGameVersion == null || dto.TargetSlotGameVersion <= token.GameVersion); + + Expression> predicate = PredicateExtensions.False(); + + List favouriteUsers = await this.database.HeartedProfiles.Where(hp => hp.UserId == token.UserId) + .Select(hp => hp.HeartedUserId) + .ToListAsync(); + + List? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds; + friendIds ??= []; + + // 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, + }; + } + } + + Expression> newsPredicate = !options.ExcludeNews + ? new IncludeNewsFilter().GetPredicate() + : new ExcludeNewsFilter().GetPredicate(); + + predicate = predicate.Or(newsPredicate); + + if (!options.ExcludeMyLevels) + { + predicate = predicate.Or(dto => dto.TargetSlotCreatorId == token.UserId); + } + + List includedUserIds = []; + + if (!options.ExcludeFriends) + { + includedUserIds.AddRange(friendIds); + } + + if (!options.ExcludeFavouriteUsers) + { + includedUserIds.AddRange(favouriteUsers); + } + + if (!options.ExcludeMyself) + { + includedUserIds.Add(token.UserId); + } + + predicate = predicate.Or(dto => includedUserIds.Contains(dto.Activity.UserId)); + + if (!options.ExcludeMyPlaylists && !options.ExcludeMyself && token.GameVersion == GameVersion.LittleBigPlanet3) + { + List creatorPlaylists = await this.database.Playlists.Where(p => p.CreatorId == token.UserId) + .Select(p => p.PlaylistId) + .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); + } + + dtoQuery = dtoQuery.Where(predicate); + + return dtoQuery; + } + + public Task GetMostRecentEventTime(IQueryable activity, DateTime upperBound) + { + 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.Count != 0 + ? 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, new ActivityFilterOptions() + { + ExcludeNews = excludeNews, + ExcludeMyLevels = true, + ExcludeFriends = true, + ExcludeFavouriteUsers = true, + ExcludeMyself = excludeMyself, + ExcludeMyPlaylists = excludeMyPlaylists, + }); + + (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityEvents, timestamp, null); + + // 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, + bool excludeFavouriteUsers, + bool excludeMyself + ) + { + GameTokenEntity token = this.GetToken(); + + if (token.GameVersion is GameVersion.LittleBigPlanet1 or GameVersion.LittleBigPlanetPSP) return this.NotFound(); + + IQueryable activityEvents = await this.GetFilters(this.database.Activities.ToActivityDto(true), + token, + new ActivityFilterOptions + { + ExcludeNews = excludeNews, + ExcludeMyLevels = excludeMyLevels, + ExcludeFriends = excludeFriends, + ExcludeFavouriteUsers = excludeFavouriteUsers, + ExcludeMyself = excludeMyself, + }); + + (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityEvents, timestamp, endTimestamp); + + List> groups = await activityEvents + .Where(dto => dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End) + .ToActivityGroups() + .ToListAsync(); + + List outerGroups = groups.ToOuterActivityGroups(); + + long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); + + 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) + { + Logger.Debug(@$"Outer group key: {outer.Key}", LogArea.Activity); + List> itemGroup = outer.Groups; + foreach (IGrouping item in itemGroup) + { + Logger.Debug( + @$" Inner group key: TargetId={item.Key.TargetId}, UserId={item.Key.UserId}, Type={item.Key.Type}", + LogArea.Activity); + foreach (ActivityDto activity in item) + { + Logger.Debug( + @$" Activity: {activity.GroupType}, Timestamp: {activity.Activity.Timestamp}, UserId: {activity.Activity.UserId}, EventType: {activity.Activity.Type}, TargetId: {activity.TargetId}", + LogArea.Activity); + } + } + } + } + #endif + + [HttpGet("slot/{slotType}/{slotId:int}")] + [HttpGet("user2/{username}")] + public async Task LocalActivity(string? slotType, int slotId, string? username, long? timestamp) + { + GameTokenEntity token = this.GetToken(); + + if (token.GameVersion is GameVersion.LittleBigPlanet1 or GameVersion.LittleBigPlanetPSP) return this.NotFound(); + + if ((SlotHelper.IsTypeInvalid(slotType) || slotId == 0) == (username == null)) return this.BadRequest(); + + bool isLevelActivity = username == null; + bool groupByActor = !isLevelActivity && token.GameVersion == GameVersion.LittleBigPlanet3; + + // User and Level activity will never contain news posts or MM pick events. + IQueryable activityQuery = this.database.Activities.ToActivityDto() + .Where(a => a.Activity.Type != EventType.NewsPost && a.Activity.Type != EventType.MMPickLevel); + + if (token.GameVersion != GameVersion.LittleBigPlanet3) + { + activityQuery = activityQuery.Where(a => + a.Activity.Type != EventType.CreatePlaylist && + a.Activity.Type != EventType.HeartPlaylist && + a.Activity.Type != EventType.AddLevelToPlaylist); + } + + // Slot activity + if (isLevelActivity) + { + if (slotType == "developer") + slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); + + if (!await this.database.Slots.AnyAsync(s => s.SlotId == slotId)) return this.NotFound(); + + 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); + } + + (DateTime Start, DateTime End) times = await this.GetTimeBounds(activityQuery, timestamp, null); + + activityQuery = activityQuery.Where(dto => + dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End); + + List> groups = await activityQuery.ToActivityGroups(groupByActor).ToListAsync(); + + List outerGroups = groups.ToOuterActivityGroups(groupByActor); + + long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds(); + + await this.CacheEntities(outerGroups); + + return this.Ok(GameStream.CreateFromGroups(token, + outerGroups, + times.Start.ToUnixTimeMilliseconds(), + oldestTimestamp, + isLevelActivity)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs index a906b170..4bc205ce 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Comment; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -44,6 +44,20 @@ public class CommentController : ControllerBase return this.Ok(); } + [HttpGet("userComment/{username}")] + [HttpGet("comment/{slotType}/{slotId:int}")] + public async Task GetSingleComment(string? username, string? slotType, int? slotId, int commentId) + { + GameTokenEntity token = this.GetToken(); + + if (username == null == (SlotHelper.IsTypeInvalid(slotType) || slotId == null)) return this.BadRequest(); + + CommentEntity? comment = await this.database.Comments.FindAsync(commentId); + if (comment == null) return this.NotFound(); + + return this.Ok(GameComment.CreateFromEntity(comment, token.UserId)); + } + [HttpGet("comments/{slotType}/{slotId:int}")] [HttpGet("userComments/{username}")] public async Task GetComments(string? username, string? slotType, int slotId) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs index 83e198d8..a0381f46 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Users; using LBPUnion.ProjectLighthouse.StorableLists.Stores; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/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/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index 4c73d0a0..0d8c64ca 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -12,7 +12,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Photo; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs index 60ac1c58..b9a36852 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs @@ -13,6 +13,9 @@ using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Misc; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs index 67fff180..3db2016b 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs @@ -9,7 +9,9 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs index 4763708a..46e8b90e 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs @@ -4,7 +4,8 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs index 7bedc01d..06500725 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Resources; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index 0dd54c07..c76e8d71 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -145,6 +145,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/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index 79ae3163..beca96d3 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Score; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -131,7 +131,8 @@ public class ScoreController : ControllerBase await this.database.SaveChangesAsync(); - ScoreEntity? existingScore = await this.database.Scores.Where(s => s.SlotId == slot.SlotId) + ScoreEntity? existingScore = await this.database.Scores + .Where(s => s.SlotId == slot.SlotId) .Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId) .Where(s => s.UserId == token.UserId) .Where(s => s.Type == score.Type) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs index a4f129b5..367335f9 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs @@ -2,13 +2,13 @@ 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; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs index f71267ba..198d3fe7 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -4,6 +4,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata; using LBPUnion.ProjectLighthouse.Helpers; @@ -13,7 +14,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs index 3f70b3d0..78e70e61 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs @@ -4,9 +4,9 @@ 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; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs index 7019cdd7..583d623f 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Extensions/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/Extensions/DatabaseContextExtensions.cs b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs index bef6943e..ee09930b 100644 --- a/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs +++ b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs @@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; diff --git a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs index 0d372e8f..e35d95e6 100644 --- a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs +++ b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs @@ -54,11 +54,7 @@ public class GameServerStartup } ); - services.AddDbContext(builder => - { - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); - }); + services.AddDbContext(DatabaseContext.ConfigureBuilder()); IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled ? new MailQueueService(new SmtpMailSender()) diff --git a/ProjectLighthouse.Servers.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/PlaylistCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs index 00ed79ce..a7212306 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs @@ -5,6 +5,8 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs index 39d6986d..5ed58a06 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs @@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/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.Servers.GameServer/Types/Categories/UserCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs index d9826a92..bd188d7a 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs @@ -5,6 +5,8 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs b/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs index ec088e13..3ba7ab19 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs @@ -1,7 +1,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml new file mode 100644 index 00000000..40aad32e --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml @@ -0,0 +1,85 @@ +@page "/debug/activity" +@using System.Globalization +@using LBPUnion.ProjectLighthouse.Types.Activity +@using LBPUnion.ProjectLighthouse.Types.Entities.Activity +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug.ActivityTestPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.Title = "Debug - Activity Test"; +} + + +
Group By Activity
+
+ +
Group By Actor
+
+
+ +@foreach (OuterActivityGroup activity in Model.ActivityGroups) +{ +

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

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

UserId: @activity.Key.UserId

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

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

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

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

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

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

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

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

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

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

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

Event Group Type: @gameEvent.GroupType

+

Event Target ID: @gameEvent.TargetId

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

SlotId: @level.SlotId

+

SlotVersion: @gameEvent.TargetSlotGameVersion

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

ScoreId: @score.ScoreId

+

SlotId: @score.SlotId

+

SlotVersion: @gameEvent.TargetSlotGameVersion

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

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

+
+
+} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs new file mode 100644 index 00000000..dd24cef7 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs @@ -0,0 +1,34 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; +using LBPUnion.ProjectLighthouse.Types.Activity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug; + +public class ActivityTestPage : BaseLayout +{ + public ActivityTestPage(DatabaseContext database) : base(database) + { } + + public List ActivityGroups = []; + + public bool GroupByActor { get; set; } + + public async Task OnGet(bool groupByActor = false) + { + Console.WriteLine(groupByActor); + List? events = (await this.Database.Activities.ToActivityDto(true).ToActivityGroups(groupByActor).ToListAsync()) + .ToOuterActivityGroups(groupByActor); + + if (events == null) return this.Page(); + + this.GroupByActor = groupByActor; + + this.ActivityGroups = events; + return this.Page(); + } + + +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs index a67e667f..e83c60b9 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs @@ -64,7 +64,7 @@ public class CompleteEmailVerificationPage : BaseLayout webToken.UserToken, new CookieOptions { - Expires = DateTimeOffset.Now.AddDays(7), + Expires = DateTimeOffset.UtcNow.AddDays(7), }); return this.Redirect("/passwordReset"); } diff --git a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml index 1414c642..77e32db6 100644 --- a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml @@ -14,6 +14,7 @@ bool isMobile = Request.IsMobile(); string language = Model.GetLanguage(); string timeZone = Model.GetTimeZone(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); }

@Model.Translate(LandingPageStrings.Welcome, ServerConfiguration.Instance.Customization.ServerName) @@ -82,6 +83,7 @@ @Model.LatestAnnouncement.Publisher.Username + at @TimeZoneInfo.ConvertTime(Model.LatestAnnouncement.PublishedAt, TimeZoneInfo.Utc, timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt") } diff --git a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml index 8c883462..b3c6f8f4 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml @@ -38,7 +38,7 @@

- Instance logo diff --git a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs index 0af0f304..c2ac9291 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml.cs @@ -100,7 +100,7 @@ public class LoginForm : BaseLayout webToken.UserToken, new CookieOptions { - Expires = DateTimeOffset.Now.AddDays(7), + Expires = DateTimeOffset.UtcNow.AddDays(7), } ); diff --git a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml index a6eaa43d..7bede06c 100644 --- a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml @@ -2,13 +2,14 @@ @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.Types.Entities.Notifications @using LBPUnion.ProjectLighthouse.Types.Entities.Website -@using LBPUnion.ProjectLighthouse.Types.Notifications @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.NotificationsPage @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ Layout = "Layouts/BaseLayout"; Model.Title = Model.Translate(GeneralStrings.Notifications); + string timeZone = Model.GetTimeZone(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); } @if (Model.User != null && Model.User.IsAdmin) @@ -51,6 +52,7 @@ @announcement.Publisher.Username + at @TimeZoneInfo.ConvertTime(announcement.PublishedAt, TimeZoneInfo.Utc, timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt")

} diff --git a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs index 8595ee85..0849304b 100644 --- a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs @@ -54,6 +54,7 @@ public class NotificationsPage : BaseLayout Title = title.Trim(), Content = content.Trim(), PublisherId = user.UserId, + PublishedAt = DateTime.UtcNow, }; this.Database.WebsiteAnnouncements.Add(announcement); diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml index 70b98e64..66fea3d9 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml @@ -3,7 +3,7 @@ @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @using LBPUnion.ProjectLighthouse.Types.Entities.Profile @using LBPUnion.ProjectLighthouse.Types.Levels -@using LBPUnion.ProjectLighthouse.Types.Serialization +@using LBPUnion.ProjectLighthouse.Types.Serialization.Photo @model LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity @{ diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml index f8c16a5f..09b2b31e 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml @@ -3,7 +3,8 @@ @using LBPUnion.ProjectLighthouse.Files @using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Types.Entities.Level -@using LBPUnion.ProjectLighthouse.Types.Serialization +@using LBPUnion.ProjectLighthouse.Types.Serialization.Review + @{ bool isMobile = (bool?)ViewData["IsMobile"] ?? false; bool canDelete = (bool?)ViewData["CanDelete"] ?? false; diff --git a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs index bb89c6bf..61460102 100644 --- a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs +++ b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs @@ -13,7 +13,6 @@ using LBPUnion.ProjectLighthouse.Services; using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Localization; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; #if !DEBUG @@ -39,11 +38,7 @@ public class WebsiteStartup services.AddControllers(); services.AddRazorPages().WithRazorPagesAtContentRoot(); - services.AddDbContext(builder => - { - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); - }); + services.AddDbContext(DatabaseContext.ConfigureBuilder()); IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled ? new MailQueueService(new SmtpMailSender()) diff --git a/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs index eacce233..e448f2db 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Tests.Integration; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Xunit; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs new file mode 100644 index 00000000..9e82509e --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityEventHandlerTests.cs @@ -0,0 +1,969 @@ +using System; +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, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + CommentEntity comment = new() + { + CommentId = 1, + PosterUserId = 1, + TargetSlotId = 1, + Type = CommentType.Level, + }; + database.Comments.Add(comment); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, comment); + + Assert.NotNull(database.Activities.ToList().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, + TargetUserId = 1, + Type = CommentType.Profile, + }; + database.Comments.Add(comment); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, comment); + + Assert.NotNull(database.Activities.ToList().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, + }, + }, + new List + { + new() + { + SlotId = 1, + CreatorId = 1, + }, + }); + + PhotoEntity photo = new() + { + PhotoId = 1, + CreatorId = 1, + SlotId = 1, + }; + database.Photos.Add(photo); + await database.SaveChangesAsync(); + + eventHandler.OnEntityInserted(database, photo); + + Assert.NotNull(database.Activities.ToList().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, + UserId = 1, + }; + 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, + Slot = slot, + }; + + eventHandler.OnEntityInserted(database, heartedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1)); + } + + [Fact] + public async Task HeartedLevel_InsertDuplicate_ShouldRemoveOldActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + LevelActivityEntity levelActivity = new() + { + UserId = 1, + SlotId = 1, + Type = EventType.HeartLevel, + Timestamp = DateTime.MinValue, + }; + + database.Activities.Add(levelActivity); + + await database.SaveChangesAsync(); + + HeartedLevelEntity heartedLevel = new() + { + HeartedLevelId = 1, + UserId = 1, + SlotId = 1, + Slot = slot, + }; + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1 && a.Timestamp == DateTime.MinValue)); + + eventHandler.OnEntityInserted(database, heartedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1 && a.Timestamp != DateTime.MinValue)); + } + + [Fact] + public async Task HeartedProfile_Insert_ShouldCreateUserActivity() + { + 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 HeartedProfile_InsertDuplicate_ShouldRemoveOldActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + UserActivityEntity userActivity = new() + { + UserId = 1, + TargetUserId = 1, + Type = EventType.HeartUser, + Timestamp = DateTime.MinValue, + }; + + database.Activities.Add(userActivity); + await database.SaveChangesAsync(); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1 && a.Timestamp == DateTime.MinValue)); + + HeartedProfileEntity heartedProfile = new() + { + HeartedProfileId = 1, + UserId = 1, + HeartedUserId = 1, + }; + + eventHandler.OnEntityInserted(database, heartedProfile); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1 && a.Timestamp != DateTime.MinValue)); + } + + [Fact] + public async Task HeartedPlaylist_Insert_ShouldCreatePlaylistActivity() + { + 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 HeartedPlaylist_InsertDuplicate_ShouldCreatePlaylistActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + PlaylistEntity playlist = new() + { + PlaylistId = 1, + CreatorId = 1, + }; + database.Playlists.Add(playlist); + + PlaylistActivityEntity playlistActivity = new() + { + UserId = 1, + PlaylistId = 1, + Type = EventType.HeartPlaylist, + Timestamp = DateTime.MinValue, + }; + database.Activities.Add(playlistActivity); + + await database.SaveChangesAsync(); + + HeartedPlaylistEntity heartedPlaylist = new() + { + HeartedPlaylistId = 1, + UserId = 1, + PlaylistId = 1, + }; + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => + a.Type == EventType.HeartPlaylist && a.PlaylistId == 1 && a.Timestamp == DateTime.MinValue)); + + eventHandler.OnEntityInserted(database, heartedPlaylist); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.HeartPlaylist && a.PlaylistId == 1 && a.Timestamp != DateTime.MinValue)); + } + + [Fact] + public async Task VisitedLevel_Insert_ShouldCreateLevelActivity() + { + 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, + TeamPickTime = 1, + }; + + eventHandler.OnEntityChanged(database, oldSlot, newSlot); + + Assert.NotNull(database.Activities.ToList().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.ToList().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.ToList() + .FirstOrDefault(a => a.Type == EventType.DeleteLevelComment && ((CommentActivityEntity)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 HeartedLevel_DeleteDuplicate_ShouldRemoveOldActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + SlotEntity slot = new() + { + SlotId = 1, + CreatorId = 1, + }; + database.Slots.Add(slot); + + LevelActivityEntity levelActivity = new() + { + UserId = 1, + SlotId = 1, + Type = EventType.UnheartLevel, + Timestamp = DateTime.MinValue, + }; + + database.Activities.Add(levelActivity); + + HeartedLevelEntity heartedLevel = new() + { + HeartedLevelId = 1, + UserId = 1, + SlotId = 1, + }; + + database.HeartedLevels.Add(heartedLevel); + await database.SaveChangesAsync(); + + eventHandler.OnEntityDeleted(database, heartedLevel); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UnheartLevel && a.SlotId == 1 && a.Timestamp != DateTime.MinValue)); + } + + [Fact] + public async Task HeartedProfile_Delete_ShouldCreateLevelActivity() + { + 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)); + } + + [Fact] + public async Task HeartedProfile_DeleteDuplicate_ShouldCreateLevelActivity() + { + ActivityEntityEventHandler eventHandler = new(); + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + new() + { + Username = "test", + UserId = 1, + }, + }); + + UserActivityEntity userActivity = new() + { + UserId = 1, + TargetUserId = 1, + Type = EventType.UnheartUser, + Timestamp = DateTime.MinValue, + }; + database.Activities.Add(userActivity); + + HeartedProfileEntity heartedProfile = new() + { + HeartedProfileId = 1, + UserId = 1, + HeartedUserId = 1, + }; + + database.HeartedProfiles.Add(heartedProfile); + await database.SaveChangesAsync(); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1 && a.Timestamp == DateTime.MinValue)); + + eventHandler.OnEntityDeleted(database, heartedProfile); + + Assert.NotNull(database.Activities.OfType() + .FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1 && a.Timestamp != DateTime.MinValue)); + } + #endregion +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs new file mode 100644 index 00000000..c328c947 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityGroupingTests.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity; + +[Trait("Category", "Unit")] +public class ActivityGroupingTests +{ + [Fact] + public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_ObjectThenActor() + { + List activities = [ + new LevelActivityEntity + { + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, + }, + new LevelActivityEntity + { + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.ReviewLevel, + }, + new LevelActivityEntity + { + UserId = 2, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, + }, + new UserActivityEntity + { + TargetUserId = 2, + UserId = 1, + Type = EventType.HeartUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 2, + UserId = 1, + Type = EventType.CommentOnUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 1, + UserId = 2, + Type = EventType.HeartUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 1, + UserId = 2, + Type = EventType.CommentOnUser, + Timestamp = DateTime.Now, + }, + ]; + + List groups = activities.ToActivityDto().AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups(); + Assert.NotNull(groups); + Assert.Equal(3, groups.Count); + + Assert.Equal(ActivityGroupType.User, groups.ElementAt(0).Key.GroupType); + Assert.Equal(ActivityGroupType.User, groups.ElementAt(1).Key.GroupType); + Assert.Equal(ActivityGroupType.Level, groups.ElementAt(2).Key.GroupType); + + Assert.Equal(1, groups.ElementAt(0).Key.TargetUserId); + Assert.Equal(2, groups.ElementAt(1).Key.TargetUserId); + Assert.Equal(1, groups.ElementAt(2).Key.TargetSlotId); + + Assert.Single(groups.ElementAt(0).Groups); + Assert.Single(groups.ElementAt(1).Groups); + Assert.Equal(2, groups.ElementAt(2).Groups.Count); + } + + [Fact] + public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_ActorThenObject() + { + List activities = [ + new LevelActivityEntity + { + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, + }, + new LevelActivityEntity + { + UserId = 1, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.ReviewLevel, + }, + new LevelActivityEntity + { + UserId = 2, + SlotId = 1, + Slot = new SlotEntity + { + GameVersion = GameVersion.LittleBigPlanet2, + }, + Timestamp = DateTime.Now, + Type = EventType.PlayLevel, + }, + new UserActivityEntity + { + TargetUserId = 2, + UserId = 1, + Type = EventType.HeartUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 2, + UserId = 1, + Type = EventType.CommentOnUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 1, + UserId = 2, + Type = EventType.HeartUser, + Timestamp = DateTime.Now, + }, + new UserActivityEntity + { + TargetUserId = 1, + UserId = 2, + Type = EventType.CommentOnUser, + Timestamp = DateTime.Now, + }, + ]; + List groups = activities.ToActivityDto() + .AsQueryable() + .ToActivityGroups(true) + .ToList() + .ToOuterActivityGroups(true); + + Assert.Multiple(() => + { + Assert.NotNull(groups); + Assert.Equal(2, groups.Count); + Assert.Equal(1, groups.Count(g => g.Key.UserId == 1)); + Assert.Equal(1, groups.Count(g => g.Key.UserId == 2)); + OuterActivityGroup firstUserGroup = groups.FirstOrDefault(g => g.Key.UserId == 1); + OuterActivityGroup secondUserGroup = groups.FirstOrDefault(g => g.Key.UserId == 2); + Assert.NotNull(firstUserGroup.Groups); + Assert.NotNull(secondUserGroup.Groups); + + Assert.Equal(ActivityGroupType.User, firstUserGroup.Key.GroupType); + Assert.Equal(ActivityGroupType.User, secondUserGroup.Key.GroupType); + + Assert.True(firstUserGroup.Groups.All(g => g.Key.UserId == 1)); + Assert.True(secondUserGroup.Groups.All(g => g.Key.UserId == 2)); + }); + } + + [Fact] + public async Task ToActivityDtoTest() + { + DatabaseContext db = await MockHelper.GetTestDatabase(); + db.Slots.Add(new SlotEntity + { + SlotId = 1, + CreatorId = 1, + GameVersion = GameVersion.LittleBigPlanet2, + }); + db.Slots.Add(new SlotEntity + { + SlotId = 2, + CreatorId = 1, + GameVersion = GameVersion.LittleBigPlanet2, + TeamPickTime = 1, + }); + db.Reviews.Add(new ReviewEntity + { + Timestamp = DateTime.Now.ToUnixTimeMilliseconds(), + SlotId = 1, + ReviewerId = 1, + ReviewId = 1, + }); + db.Comments.Add(new CommentEntity + { + TargetSlotId = 1, + PosterUserId = 1, + Message = "comment on level test", + CommentId = 1, + }); + db.Comments.Add(new CommentEntity + { + TargetUserId = 1, + PosterUserId = 1, + Message = "comment on user test", + CommentId = 2, + }); + db.WebsiteAnnouncements.Add(new WebsiteAnnouncementEntity + { + PublisherId = 1, + AnnouncementId = 1, + }); + db.Playlists.Add(new PlaylistEntity + { + PlaylistId = 1, + CreatorId = 1, + }); + db.Activities.Add(new LevelActivityEntity + { + Timestamp = DateTime.Now, + SlotId = 1, + Type = EventType.PlayLevel, + UserId = 1, + }); + db.Activities.Add(new ReviewActivityEntity + { + Timestamp = DateTime.Now, + SlotId = 1, + Type = EventType.ReviewLevel, + ReviewId = 1, + UserId = 1, + }); + db.Activities.Add(new UserCommentActivityEntity + { + Timestamp = DateTime.Now, + Type = EventType.CommentOnUser, + UserId = 1, + TargetUserId = 1, + CommentId = 2, + }); + db.Activities.Add(new LevelCommentActivityEntity + { + Timestamp = DateTime.Now, + Type = EventType.CommentOnLevel, + UserId = 1, + SlotId = 1, + CommentId = 1, + }); + db.Activities.Add(new NewsActivityEntity + { + Type = EventType.NewsPost, + NewsId = 1, + UserId = 1, + }); + db.Activities.Add(new PlaylistActivityEntity + { + Type = EventType.CreatePlaylist, + PlaylistId = 1, + UserId = 1, + }); + db.Activities.Add(new LevelActivityEntity + { + Type = EventType.MMPickLevel, + SlotId = 2, + UserId = 1, + }); + await db.SaveChangesAsync(); + + List resultDto = await db.Activities.ToActivityDto(includeSlotCreator: true, includeTeamPick: true).ToListAsync(); + + Assert.Equal(2, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetTeamPickId); + Assert.Equal(2, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetSlotId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetSlotCreatorId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CreatePlaylist)?.TargetPlaylistId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.NewsPost)?.TargetNewsId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnUser)?.TargetUserId); + + Assert.Null(resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetTeamPickId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetSlotId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetSlotCreatorId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.ReviewLevel)?.TargetSlotId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.ReviewLevel)?.TargetSlotCreatorId); + + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.PlayLevel)?.TargetSlotId); + Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.PlayLevel)?.TargetSlotCreatorId); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityInterceptorTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityInterceptorTests.cs new file mode 100644 index 00000000..6d84cbf6 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Activity/ActivityInterceptorTests.cs @@ -0,0 +1,83 @@ +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using Microsoft.EntityFrameworkCore; +using Moq; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity; + +[Trait("Category", "Unit")] +public class ActivityInterceptorTests +{ + private static async Task GetTestDatabase(IMock eventHandlerMock) + { + DbContextOptionsBuilder optionsBuilder = await MockHelper.GetInMemoryDbOptions(); + + optionsBuilder.AddInterceptors(new ActivityInterceptor(eventHandlerMock.Object)); + DatabaseContext database = new(optionsBuilder.Options); + await database.Database.EnsureCreatedAsync(); + return database; + } + + [Fact] + public async Task SaveChangesWithNewEntity_ShouldCallEntityInserted() + { + Mock eventHandlerMock = new(); + DatabaseContext database = await GetTestDatabase(eventHandlerMock); + + database.Users.Add(new UserEntity + { + UserId = 1, + Username = "test", + }); + await database.SaveChangesAsync(); + + eventHandlerMock.Verify(x => x.OnEntityInserted(It.IsAny(), It.Is(user => user is UserEntity)), Times.Once); + } + + [Fact] + public async Task SaveChangesWithModifiedEntity_ShouldCallEntityChanged() + { + Mock eventHandlerMock = new(); + DatabaseContext database = await GetTestDatabase(eventHandlerMock); + + UserEntity user = new() + { + Username = "test", + }; + + database.Users.Add(user); + await database.SaveChangesAsync(); + + user.Username = "test2"; + await database.SaveChangesAsync(); + + eventHandlerMock.Verify(x => x.OnEntityChanged(It.IsAny(), + It.Is(u => u is UserEntity && ((UserEntity)u).Username == "test"), + It.Is(u => u is UserEntity && ((UserEntity)u).Username == "test2")), + Times.Once); + } + + [Fact] + public async Task SaveChangesWithModifiedEntity_ShouldCallEntityDeleted() + { + Mock eventHandlerMock = new(); + DatabaseContext database = await GetTestDatabase(eventHandlerMock); + + UserEntity user = new() + { + Username = "test", + }; + + database.Users.Add(user); + await database.SaveChangesAsync(); + + database.Users.Remove(user); + await database.SaveChangesAsync(); + + eventHandlerMock.Verify(x => x.OnEntityDeleted(It.IsAny(), It.Is(u => u is UserEntity)), Times.Once); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs new file mode 100644 index 00000000..b37ed595 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ActivityControllerTests.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Serialization.Activity; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] +public class ActivityControllerTests +{ + private static void SetupToken(ControllerBase controller, GameVersion version) + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = version; + controller.SetupTestController(token); + } + + [Fact] + public async Task LBP2GlobalActivity_ShouldReturnNothing_WhenEmpty() + { + DatabaseContext database = await MockHelper.GetTestDatabase(); + ActivityController activityController = new(database); + SetupToken(activityController, GameVersion.LittleBigPlanet2); + + long timestamp = TimeHelper.TimestampMillis; + + IActionResult response = await activityController.GlobalActivity(timestamp, 0, false, false, false, false, false); + GameStream stream = response.CastTo(); + Assert.Null(stream.Groups); + Assert.Equal(timestamp, stream.StartTimestamp); + } + //TODO write activity controller tests +} \ No newline at end of file diff --git a/ProjectLighthouse.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.GameApiTests/Unit/Controllers/ReviewControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs index d6eb94f8..36d44a11 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs index 7408fcf5..66e5f6d6 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs @@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs index 5335b2cf..42049a31 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs index 3347db1e..e16b2fad 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/ProjectLighthouse.Tests/Helpers/MockHelper.cs b/ProjectLighthouse.Tests/Helpers/MockHelper.cs index 64dc10c3..7982ac42 100644 --- a/ProjectLighthouse.Tests/Helpers/MockHelper.cs +++ b/ProjectLighthouse.Tests/Helpers/MockHelper.cs @@ -50,7 +50,7 @@ public static class MockHelper return finalResult; } - private static async Task> GetInMemoryDbOptions() + public static async Task> GetInMemoryDbOptions() { DbConnection connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); diff --git a/ProjectLighthouse.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.Tests/Unit/LocationTests.cs b/ProjectLighthouse.Tests/Unit/LocationTests.cs index a97b80af..ec9c80ca 100644 --- a/ProjectLighthouse.Tests/Unit/LocationTests.cs +++ b/ProjectLighthouse.Tests/Unit/LocationTests.cs @@ -3,7 +3,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Xunit; namespace LBPUnion.ProjectLighthouse.Tests.Unit; diff --git a/ProjectLighthouse.Tests/Unit/PaginationTests.cs b/ProjectLighthouse.Tests/Unit/PaginationTests.cs index de591d94..ecf4eb9a 100644 --- a/ProjectLighthouse.Tests/Unit/PaginationTests.cs +++ b/ProjectLighthouse.Tests/Unit/PaginationTests.cs @@ -3,7 +3,7 @@ using System.Linq; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Filter; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Xunit; diff --git a/ProjectLighthouse/Database/ActivityInterceptor.cs b/ProjectLighthouse/Database/ActivityInterceptor.cs new file mode 100644 index 00000000..7ded9cd5 --- /dev/null +++ b/ProjectLighthouse/Database/ActivityInterceptor.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace LBPUnion.ProjectLighthouse.Database; + +public class ActivityInterceptor : SaveChangesInterceptor +{ + private class CustomTrackedEntity + { + public required EntityState State { get; init; } + public required object Entity { get; init; } + public required object OldEntity { get; init; } + } + + private struct TrackedEntityKey + { + public Type Type { get; set; } + public int HashCode { get; set; } + public Guid ContextId { get; set; } + } + + private readonly ConcurrentDictionary unsavedEntities; + private readonly IEntityEventHandler eventHandler; + + public ActivityInterceptor(IEntityEventHandler eventHandler) + { + this.eventHandler = eventHandler; + this.unsavedEntities = new ConcurrentDictionary(); + } + + #region Hooking stuff + + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + this.SaveNewEntities(eventData); + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync + (DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = new()) + { + this.SaveNewEntities(eventData); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) + { + this.ParseInsertedEntities(eventData); + return base.SavedChanges(eventData, result); + } + + public override ValueTask SavedChangesAsync + (SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = new()) + { + this.ParseInsertedEntities(eventData); + return base.SavedChangesAsync(eventData, result, cancellationToken); + } + + #endregion + + private void SaveNewEntities(DbContextEventData eventData) + { + if (eventData.Context == null) return; + + DbContext context = eventData.Context; + + this.unsavedEntities.Clear(); + + foreach (EntityEntry entry in context.ChangeTracker.Entries()) + { + // Ignore activities + if (entry.Metadata.BaseType?.ClrType == typeof(ActivityEntity) || entry.Metadata.ClrType == typeof(LastContactEntity)) continue; + + // Ignore tokens + if (entry.Metadata.Name.Contains("Token")) continue; + + if (entry.State is not (EntityState.Added or EntityState.Deleted or EntityState.Modified)) continue; + this.unsavedEntities.TryAdd(new TrackedEntityKey + { + ContextId = context.ContextId.InstanceId, + Type = entry.Entity.GetType(), + HashCode = entry.Entity.GetHashCode(), + }, + new CustomTrackedEntity + { + State = entry.State, + Entity = entry.Entity, + OldEntity = entry.OriginalValues.ToObject(), + }); + } + } + + private void ParseInsertedEntities(DbContextEventData eventData) + { + if (eventData.Context is not DatabaseContext context) return; + + HashSet entities = []; + + List entries = context.ChangeTracker.Entries().ToList(); + + foreach (KeyValuePair kvp in this.unsavedEntities) + { + EntityEntry entry = entries.FirstOrDefault(e => + e.Metadata.ClrType == kvp.Key.Type && e.Entity.GetHashCode() == kvp.Key.HashCode); + switch (kvp.Value.State) + { + case EntityState.Added: + case EntityState.Modified: + if (entry != null) entities.Add(kvp.Value); + break; + case EntityState.Deleted: + if (entry == null) entities.Add(kvp.Value); + break; + case EntityState.Detached: + case EntityState.Unchanged: + default: + break; + } + } + + foreach (CustomTrackedEntity entity in entities) + { + switch (entity.State) + { + case EntityState.Added: + this.eventHandler.OnEntityInserted(context, entity.Entity); + break; + case EntityState.Deleted: + this.eventHandler.OnEntityDeleted(context, entity.Entity); + break; + case EntityState.Modified: + this.eventHandler.OnEntityChanged(context, entity.OldEntity, entity.Entity); + break; + case EntityState.Detached: + case EntityState.Unchanged: + default: + continue; + } + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Database/DatabaseContext.cs b/ProjectLighthouse/Database/DatabaseContext.cs index 0a2e09e1..6c981b25 100644 --- a/ProjectLighthouse/Database/DatabaseContext.cs +++ b/ProjectLighthouse/Database/DatabaseContext.cs @@ -1,4 +1,7 @@ +using System; using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Maintenance; @@ -26,6 +29,10 @@ public partial class DatabaseContext : DbContext public DbSet WebTokens { get; set; } #endregion + #region Activity + public DbSet Activities { get; set; } + #endregion + #region Users public DbSet Comments { get; set; } public DbSet LastContacts { get; set; } @@ -84,8 +91,36 @@ 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) + { + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + modelBuilder.Entity().UseTphMappingStrategy(); + base.OnModelCreating(modelBuilder); + } + #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..9627a712 --- /dev/null +++ b/ProjectLighthouse/Extensions/ActivityQueryExtensions.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Linq; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +namespace LBPUnion.ProjectLighthouse.Extensions; + +public static class ActivityQueryExtensions +{ + public static List GetIds(this IReadOnlyCollection groups, ActivityGroupType type) + { + List ids = []; + // 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(); + } + + /// + /// Turns a list of into a group based on its timestamp + /// + /// An to group + /// Whether the groups should be created based on the initiator of the event or the target of the event + /// The transformed query containing groups of + public static IQueryable> ToActivityGroups + (this IQueryable activityQuery, bool groupByActor = false) => + groupByActor + ? activityQuery.GroupBy(dto => new ActivityGroup + { + Timestamp = dto.Activity.Timestamp.Date, + UserId = dto.Activity.UserId, + TargetNewsId = dto.TargetNewsId ?? -1, + TargetTeamPickSlotId = dto.TargetTeamPickId ?? -1, + }) + : activityQuery.GroupBy(dto => new ActivityGroup + { + Timestamp = dto.Activity.Timestamp.Date, + UserId = -1, + TargetUserId = dto.TargetUserId ?? -1, + TargetSlotId = dto.TargetSlotId ?? -1, + TargetPlaylistId = dto.TargetPlaylistId ?? -1, + TargetNewsId = dto.TargetNewsId ?? -1, + }); + + 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.News + : ActivityGroupType.User, + UserId = gr.Activity.UserId, + TargetId = groupByActor + ? gr.TargetId + : gr.GroupType == ActivityGroupType.News + ? gr.TargetNewsId ?? 0 + : gr.Activity.UserId, + }) + .ToList(), + }) + .ToList(); + + /// + /// Converts an <> into an <> for grouping. + /// + /// The activity query to be converted. + /// Whether the field should be included. + /// Whether the field should be included. + /// The converted <> + public static IQueryable ToActivityDto + (this IQueryable activityQuery, bool includeSlotCreator = false, bool includeTeamPick = false) + { + return activityQuery.Select(a => new ActivityDto + { + Activity = a, + TargetSlotId = (a as LevelActivityEntity).SlotId, + TargetSlotGameVersion = (a as LevelActivityEntity).Slot.GameVersion, + TargetSlotCreatorId = includeSlotCreator ? (a as LevelActivityEntity).Slot.CreatorId : null, + TargetUserId = (a as UserActivityEntity).TargetUserId, + TargetNewsId = (a as NewsActivityEntity).NewsId, + TargetPlaylistId = (a as PlaylistActivityEntity).PlaylistId, + TargetTeamPickId = + includeTeamPick && a.Type == EventType.MMPickLevel ? (a as LevelActivityEntity).SlotId : null, }); + } + + /// + /// Converts an IEnumerable<> into an IEnumerable<> for grouping. + /// + /// The activity query to be converted. + /// Whether the field should be included. + /// Whether the field should be included. + /// The converted IEnumerable<> + public static IEnumerable ToActivityDto + (this IEnumerable activityEnumerable, bool includeSlotCreator = false, bool includeTeamPick = false) + { + return activityEnumerable.Select(a => new ActivityDto + { + Activity = a, + TargetSlotId = (a as LevelActivityEntity)?.SlotId, + TargetSlotGameVersion = (a as LevelActivityEntity)?.Slot.GameVersion, + TargetSlotCreatorId = includeSlotCreator ? (a as LevelActivityEntity)?.Slot.CreatorId : null, + TargetUserId = (a as UserActivityEntity)?.TargetUserId, + TargetNewsId = (a as NewsActivityEntity)?.NewsId, + TargetPlaylistId = (a as PlaylistActivityEntity)?.PlaylistId, + TargetTeamPickId = + includeTeamPick && a.Type == EventType.MMPickLevel ? (a as LevelActivityEntity)?.SlotId : null, }); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/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/Extensions/DateTimeExtensions.cs b/ProjectLighthouse/Extensions/DateTimeExtensions.cs new file mode 100644 index 00000000..3fdb6768 --- /dev/null +++ b/ProjectLighthouse/Extensions/DateTimeExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace LBPUnion.ProjectLighthouse.Extensions; + +public static class DateTimeExtensions +{ + public static long ToUnixTimeMilliseconds(this DateTime dateTime) => + ((DateTimeOffset)DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)).ToUnixTimeMilliseconds(); + + public static DateTime FromUnixTimeMilliseconds(long timestamp) => + DateTimeOffset.FromUnixTimeMilliseconds(timestamp).UtcDateTime; +} \ No newline at end of file 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/20240514032512_AddRecentActivity.cs b/ProjectLighthouse/Migrations/20240514032512_AddRecentActivity.cs new file mode 100644 index 00000000..93c31f11 --- /dev/null +++ b/ProjectLighthouse/Migrations/20240514032512_AddRecentActivity.cs @@ -0,0 +1,692 @@ +using System; +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LBPUnion.ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240514032512_AddRecentActivity")] + public partial class AddRecentActivity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TokenId", + table: "WebTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "AnnouncementId", + table: "WebsiteAnnouncements", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "VisitedLevelId", + table: "VisitedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "Users", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "SlotId", + table: "Slots", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ScoreId", + table: "Scores", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ReviewId", + table: "Reviews", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ReportId", + table: "Reports", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "RegistrationTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatedReviewId", + table: "RatedReviews", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatedLevelId", + table: "RatedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatingId", + table: "RatedComments", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "QueuedLevelId", + table: "QueuedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PlaylistId", + table: "Playlists", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PlatformLinkAttemptId", + table: "PlatformLinkAttempts", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PhotoSubjectId", + table: "PhotoSubjects", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PhotoId", + table: "Photos", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "PasswordResetTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Notifications", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedProfileId", + table: "HeartedProfiles", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedPlaylistId", + table: "HeartedPlaylists", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedLevelId", + table: "HeartedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "GameTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "EmailVerificationTokenId", + table: "EmailVerificationTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "EmailSetTokenId", + table: "EmailSetTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "CustomCategories", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CommentId", + table: "Comments", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CaseId", + table: "Cases", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "BlockedProfileId", + table: "BlockedProfiles", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "APIKeys", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.CreateTable( + name: "Activities", + columns: table => new + { + ActivityId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Timestamp = table.Column(type: "datetime(6)", nullable: false), + UserId = table.Column(type: "int", nullable: false), + Type = table.Column(type: "int", nullable: false), + Discriminator = table.Column(type: "varchar(34)", maxLength: 34, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + SlotId = table.Column(type: "int", nullable: true), + CommentId = table.Column(type: "int", nullable: true), + PhotoId = table.Column(type: "int", nullable: true), + NewsId = table.Column(type: "int", nullable: true), + PlaylistId = table.Column(type: "int", nullable: true), + ReviewId = table.Column(type: "int", nullable: true), + ScoreId = table.Column(type: "int", nullable: true), + Points = table.Column(type: "int", nullable: true), + TargetUserId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Activities", x => x.ActivityId); + table.ForeignKey( + name: "FK_Activities_Comments_CommentId", + column: x => x.CommentId, + principalTable: "Comments", + principalColumn: "CommentId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Photos_PhotoId", + column: x => x.PhotoId, + principalTable: "Photos", + principalColumn: "PhotoId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Playlists_PlaylistId", + column: x => x.PlaylistId, + principalTable: "Playlists", + principalColumn: "PlaylistId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Reviews_ReviewId", + column: x => x.ReviewId, + principalTable: "Reviews", + principalColumn: "ReviewId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Scores_ScoreId", + column: x => x.ScoreId, + principalTable: "Scores", + principalColumn: "ScoreId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Slots_SlotId", + column: x => x.SlotId, + principalTable: "Slots", + principalColumn: "SlotId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Users_TargetUserId", + column: x => x.TargetUserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Activities_WebsiteAnnouncements_NewsId", + column: x => x.NewsId, + principalTable: "WebsiteAnnouncements", + principalColumn: "AnnouncementId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_CommentId", + table: "Activities", + column: "CommentId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_NewsId", + table: "Activities", + column: "NewsId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_PhotoId", + table: "Activities", + column: "PhotoId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_PlaylistId", + table: "Activities", + column: "PlaylistId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_ReviewId", + table: "Activities", + column: "ReviewId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_ScoreId", + table: "Activities", + column: "ScoreId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_SlotId", + table: "Activities", + column: "SlotId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_TargetUserId", + table: "Activities", + column: "TargetUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_UserId", + table: "Activities", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Activities"); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "WebTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "AnnouncementId", + table: "WebsiteAnnouncements", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "VisitedLevelId", + table: "VisitedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "Users", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "SlotId", + table: "Slots", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ScoreId", + table: "Scores", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ReviewId", + table: "Reviews", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "ReportId", + table: "Reports", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "RegistrationTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatedReviewId", + table: "RatedReviews", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatedLevelId", + table: "RatedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "RatingId", + table: "RatedComments", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "QueuedLevelId", + table: "QueuedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PlaylistId", + table: "Playlists", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PlatformLinkAttemptId", + table: "PlatformLinkAttempts", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PhotoSubjectId", + table: "PhotoSubjects", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "PhotoId", + table: "Photos", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "PasswordResetTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Notifications", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedProfileId", + table: "HeartedProfiles", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedPlaylistId", + table: "HeartedPlaylists", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "HeartedLevelId", + table: "HeartedLevels", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "TokenId", + table: "GameTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "EmailVerificationTokenId", + table: "EmailVerificationTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "EmailSetTokenId", + table: "EmailSetTokens", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "CustomCategories", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CommentId", + table: "Comments", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "CaseId", + table: "Cases", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "BlockedProfileId", + table: "BlockedProfiles", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "APIKeys", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + } + } +} diff --git a/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs b/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs new file mode 100644 index 00000000..88b54e9f --- /dev/null +++ b/ProjectLighthouse/Migrations/20240514032620_AddPublishedAtToWebAnnouncement.cs @@ -0,0 +1,33 @@ +using System; +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LBPUnion.ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240514032620_AddPublishedAtToWebAnnouncement")] + public partial class AddPublishedAtToWebAnnouncement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PublishedAt", + table: "WebsiteAnnouncements", + type: "datetime(6)", + nullable: false, + defaultValue: DateTime.UtcNow); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PublishedAt", + table: "WebsiteAnnouncements"); + } + } +} diff --git a/ProjectLighthouse/StorableLists/Stores/RoomStore.cs b/ProjectLighthouse/StorableLists/Stores/RoomStore.cs index 6bf1754d..3adb5e4d 100644 --- a/ProjectLighthouse/StorableLists/Stores/RoomStore.cs +++ b/ProjectLighthouse/StorableLists/Stores/RoomStore.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms; namespace LBPUnion.ProjectLighthouse.StorableLists.Stores; @@ -10,7 +11,7 @@ public static class RoomStore public static StorableList GetRooms() { - if (RedisDatabase.Initialized) + if (!ServerStatics.IsUnitTesting && RedisDatabase.Initialized) { return new RedisStorableList(RedisDatabase.GetRooms()); } diff --git a/ProjectLighthouse/Types/Activity/ActivityDto.cs b/ProjectLighthouse/Types/Activity/ActivityDto.cs new file mode 100644 index 00000000..4382eede --- /dev/null +++ b/ProjectLighthouse/Types/Activity/ActivityDto.cs @@ -0,0 +1,35 @@ +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Users; + +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 GameVersion? TargetSlotGameVersion { 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 ?? -1, + ActivityGroupType.Level => this.TargetSlotId ?? -1, + ActivityGroupType.Playlist => this.TargetPlaylistId ?? -1, + ActivityGroupType.News => this.TargetNewsId ?? -1, + _ => this.Activity.UserId, + }; + + public ActivityGroupType GroupType => + this.TargetPlaylistId != null + ? ActivityGroupType.Playlist + : this.TargetNewsId != null + ? ActivityGroupType.News + : this.TargetSlotId != null + ? ActivityGroupType.Level + : ActivityGroupType.User; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs new file mode 100644 index 00000000..ef8e6fad --- /dev/null +++ b/ProjectLighthouse/Types/Activity/ActivityEntityEventHandler.cs @@ -0,0 +1,385 @@ +#nullable enable +using System; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Logging; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +public class ActivityEntityEventHandler : IEntityEventHandler +{ + public void OnEntityInserted(DatabaseContext database, T entity) where T : class + { + ActivityEntity? activity = entity switch + { + SlotEntity slot => slot.Type switch + { + SlotType.User => new LevelActivityEntity + { + Type = EventType.PublishLevel, + SlotId = slot.SlotId, + UserId = slot.CreatorId, + }, + _ => null, + }, + CommentEntity comment => comment.Type switch + { + CommentType.Level => database.Slots.Where(s => s.SlotId == comment.TargetSlotId) + .Select(s => s.Type) + .FirstOrDefault() switch + { + SlotType.User => new LevelCommentActivityEntity + { + Type = EventType.CommentOnLevel, + CommentId = comment.CommentId, + UserId = comment.PosterUserId, + SlotId = comment.TargetSlotId ?? throw new NullReferenceException("SlotId in Level comment is null, this shouldn't happen."), + }, + _ => null, + }, + CommentType.Profile => new UserCommentActivityEntity + { + Type = EventType.CommentOnUser, + CommentId = comment.CommentId, + UserId = comment.PosterUserId, + TargetUserId = comment.TargetUserId ?? throw new NullReferenceException("TargetUserId in User comment is null, this shouldn't happen."), + }, + _ => null, + }, + PhotoEntity photo => database.Slots.Where(s => s.SlotId == photo.SlotId) + .Select(s => s.Type) + .FirstOrDefault() switch + { + SlotType.User => new LevelPhotoActivity + { + Type = EventType.UploadPhoto, + PhotoId = photo.PhotoId, + UserId = photo.CreatorId, + SlotId = photo.SlotId ?? throw new NullReferenceException("SlotId in Photo is null"), + }, + // All other photos (story, moon, pod, etc.) + _ => null, + }, + ScoreEntity score => database.Slots.Where(s => s.SlotId == score.SlotId) + .Select(s => s.Type) + .FirstOrDefault() switch + { + // Don't add story scores or versus scores + SlotType.User when score.Type != 7 => new ScoreActivityEntity + { + Type = EventType.Score, + ScoreId = score.ScoreId, + UserId = score.UserId, + SlotId = score.SlotId, + Points = score.Points, + }, + _ => null, + }, + HeartedLevelEntity heartedLevel => database.Slots.Where(s => s.SlotId == heartedLevel.SlotId) + .Select(s => s.Type) + .FirstOrDefault() switch + { + SlotType.User => new LevelActivityEntity + { + Type = EventType.HeartLevel, + SlotId = heartedLevel.SlotId, + UserId = heartedLevel.UserId, + }, + _ => null, + }, + HeartedProfileEntity heartedProfile => new UserActivityEntity + { + Type = EventType.HeartUser, + 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, + SlotId = review.SlotId, + }, + 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); + } + + private static void RemoveDuplicateEvents(DatabaseContext database, ActivityEntity activity) + { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (activity.Type) + { + case EventType.HeartLevel: + case EventType.UnheartLevel: + { + if (activity is not LevelActivityEntity levelActivity) break; + + DeleteActivity(a => a.TargetSlotId == levelActivity.SlotId); + break; + } + case EventType.HeartUser: + case EventType.UnheartUser: + { + if (activity is not UserActivityEntity userActivity) break; + + DeleteActivity(a => a.TargetUserId == userActivity.TargetUserId); + break; + } + case EventType.HeartPlaylist: + { + if (activity is not PlaylistActivityEntity playlistActivity) break; + + DeleteActivity(a => a.TargetPlaylistId == playlistActivity.PlaylistId); + break; + } + } + + return; + + void DeleteActivity(Expression> predicate) + { + database.Activities.ToActivityDto() + .Where(a => a.Activity.UserId == activity.UserId) + .Where(a => a.Activity.Type == activity.Type) + .Where(predicate) + .Select(a => a.Activity) + .ExecuteDelete(); + } + } + + private static void InsertActivity(DatabaseContext database, ActivityEntity? activity) + { + if (activity == null) return; + + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return; + + Logger.Debug("Inserting activity: " + activity.GetType().Name, LogArea.Activity); + + RemoveDuplicateEvents(database, activity); + + activity.Timestamp = DateTime.UtcNow; + database.Activities.Add(activity); + database.SaveChanges(); + } + + public void OnEntityChanged(DatabaseContext database, T origEntity, T currentEntity) where T : class + { + ActivityEntity? activity = null; + switch (currentEntity) + { + case VisitedLevelEntity visitedLevel: + { + if (origEntity is not VisitedLevelEntity oldVisitedLevel) break; + + if (Plays(oldVisitedLevel) >= Plays(visitedLevel)) break; + + activity = new LevelActivityEntity + { + Type = EventType.PlayLevel, + SlotId = visitedLevel.SlotId, + UserId = visitedLevel.UserId, + }; + break; + + int Plays(VisitedLevelEntity entity) => entity.PlaysLBP1 + entity.PlaysLBP2 + entity.PlaysLBP3; + } + case ScoreEntity score: + { + if (origEntity is not ScoreEntity oldScore) break; + + // don't track versus levels + if (oldScore.Type == 7) break; + + SlotType slotType = database.Slots.Where(s => s.SlotId == score.SlotId) + .Select(s => s.Type) + .FirstOrDefault(); + + if (slotType != SlotType.User) break; + + if (oldScore.Points > score.Points) break; + + activity = new ScoreActivityEntity + { + Type = EventType.Score, + ScoreId = score.ScoreId, + SlotId = score.SlotId, + UserId = score.UserId, + Points = score.Points, + }; + + break; + } + case SlotEntity slotEntity: + { + if (origEntity is not SlotEntity oldSlotEntity) break; + + bool oldIsTeamPick = oldSlotEntity.TeamPickTime != 0; + bool newIsTeamPick = slotEntity.TeamPickTime != 0; + + switch (oldIsTeamPick) + { + // When a level is team picked + case false when newIsTeamPick: + 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 !newIsTeamPick: + database.Activities.OfType() + .Where(a => a.Type == EventType.MMPickLevel) + .Where(a => a.SlotId == slotEntity.SlotId) + .ExecuteDelete(); + break; + default: + { + 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 (comment.TargetSlotId != null) + { + SlotType slotType = database.Slots.Where(s => s.SlotId == comment.TargetSlotId) + .Select(s => s.Type) + .FirstOrDefault(); + if (slotType != SlotType.User) break; + } + + if (oldComment.Deleted || !comment.Deleted) break; + + if (comment.Type != CommentType.Level) break; + + 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; + Logger.Debug($@"Old playlist slots: {string.Join(",", oldSlots)}", LogArea.Activity); + Logger.Debug($@"New playlist slots: {string.Join(",", newSlots)}", LogArea.Activity); + + int[] addedSlots = newSlots.Except(oldSlots).ToArray(); + + Logger.Debug($@"Added playlist slots: {string.Join(",", addedSlots)}", LogArea.Activity); + + // 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; + } + } + + InsertActivity(database, activity); + } + + public void OnEntityDeleted(DatabaseContext database, T entity) where T : class + { + ActivityEntity? activity = entity switch + { + HeartedLevelEntity heartedLevel => database.Slots.Where(s => s.SlotId == heartedLevel.SlotId) + .Select(s => s.Type) + .FirstOrDefault() switch + { + SlotType.User => new LevelActivityEntity + { + Type = EventType.UnheartLevel, + SlotId = heartedLevel.SlotId, + UserId = heartedLevel.UserId, + }, + _ => null, + }, + HeartedProfileEntity heartedProfile => new UserActivityEntity + { + Type = EventType.UnheartUser, + TargetUserId = heartedProfile.HeartedUserId, + UserId = heartedProfile.UserId, + }, + _ => null, + }; + InsertActivity(database, activity); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/ActivityGroup.cs b/ProjectLighthouse/Types/Activity/ActivityGroup.cs new file mode 100644 index 00000000..ea16fab5 --- /dev/null +++ b/ProjectLighthouse/Types/Activity/ActivityGroup.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +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 ?? this.UserId, + ActivityGroupType.Level => this.TargetSlotId ?? -1, + ActivityGroupType.TeamPick => this.TargetTeamPickSlotId ?? -1, + ActivityGroupType.Playlist => this.TargetPlaylistId ?? -1, + ActivityGroupType.News => this.TargetNewsId ?? -1, + _ => this.UserId, + }; + + public ActivityGroupType GroupType => + (this.TargetPlaylistId ?? -1) != -1 + ? ActivityGroupType.Playlist + : (this.TargetNewsId ?? -1) != -1 + ? ActivityGroupType.News + : (this.TargetTeamPickSlotId ?? -1) != -1 + ? ActivityGroupType.TeamPick + : (this.TargetSlotId ?? -1) != -1 + ? ActivityGroupType.Level + : 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 +{ + [XmlEnum("user")] + User, + + [XmlEnum("slot")] + Level, + + [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 new file mode 100644 index 00000000..c497b047 --- /dev/null +++ b/ProjectLighthouse/Types/Activity/EventType.cs @@ -0,0 +1,86 @@ +using System.Xml.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +/// +/// An enum of all possible event types that LBP recognizes in Recent Activity +/// +/// +/// , , , are ignored by the game +/// +/// +/// +public enum EventType +{ + [XmlEnum("heart_level")] + HeartLevel = 0, + + [XmlEnum("unheart_level")] + UnheartLevel = 1, + + [XmlEnum("heart_user")] + HeartUser = 2, + + [XmlEnum("unheart_user")] + UnheartUser = 3, + + [XmlEnum("play_level")] + PlayLevel = 4, + + [XmlEnum("rate_level")] + RateLevel = 5, + + [XmlEnum("tag_level")] + TagLevel = 6, + + [XmlEnum("comment_on_level")] + CommentOnLevel = 7, + + [XmlEnum("delete_level_comment")] + DeleteLevelComment = 8, + + [XmlEnum("upload_photo")] + UploadPhoto = 9, + + [XmlEnum("publish_level")] + PublishLevel = 10, + + [XmlEnum("unpublish_level")] + UnpublishLevel = 11, + + [XmlEnum("score")] + Score = 12, + + [XmlEnum("news_post")] + NewsPost = 13, + + [XmlEnum("mm_pick_level")] + MMPickLevel = 14, + + [XmlEnum("dpad_rate_level")] + DpadRateLevel = 15, + + [XmlEnum("review_level")] + ReviewLevel = 16, + + [XmlEnum("comment_on_user")] + CommentOnUser = 17, + + /// + /// This event is only used in LBP3 + /// > + [XmlEnum("create_playlist")] + CreatePlaylist = 18, + + /// + /// This event is only used in LBP3 + /// > + [XmlEnum("heart_playlist")] + HeartPlaylist = 19, + + /// + /// This event is only used in LBP3 + /// > + [XmlEnum("add_level_to_playlist")] + AddLevelToPlaylist = 20, +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Activity/IEntityEventHandler.cs b/ProjectLighthouse/Types/Activity/IEntityEventHandler.cs new file mode 100644 index 00000000..982857ce --- /dev/null +++ b/ProjectLighthouse/Types/Activity/IEntityEventHandler.cs @@ -0,0 +1,10 @@ +using LBPUnion.ProjectLighthouse.Database; + +namespace LBPUnion.ProjectLighthouse.Types.Activity; + +public interface IEntityEventHandler +{ + public void OnEntityInserted(DatabaseContext database, T entity) where T : class; + public void OnEntityChanged(DatabaseContext database, T origEntity, T currentEntity) where T : class; + public void OnEntityDeleted(DatabaseContext database, T entity) where T : class; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs new file mode 100644 index 00000000..39732e04 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/ActivityEntity.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +public class ActivityEntity +{ + [Key] + public int ActivityId { get; set; } + + /// + /// The time that this event took place. + /// + public DateTime Timestamp { get; set; } + + /// + /// The of the that triggered this event. + /// + public int UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public UserEntity User { get; set; } + + /// + /// The type of this event. + /// + public EventType Type { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs new file mode 100644 index 00000000..61a80e88 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/CommentActivityEntity.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: , , and . +/// +public class CommentActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + public int CommentId { get; set; } + + [ForeignKey(nameof(CommentId))] + public CommentEntity Comment { get; set; } +} + +public class LevelCommentActivityEntity : CommentActivityEntity +{ + [Column("SlotId")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } +} + +public class UserCommentActivityEntity : CommentActivityEntity +{ + [Column("TargetUserId")] + public int TargetUserId { get; set; } + + [ForeignKey(nameof(TargetUserId))] + public UserEntity TargetUser { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs new file mode 100644 index 00000000..27d7ca9a --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/LevelActivityEntity.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: , , , +/// , and . +/// +public class LevelActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + [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 new file mode 100644 index 00000000..59daccaa --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/NewsActivityEntity.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: . +/// +/// +/// This event type can only be grouped with other . +/// +/// +/// +public class NewsActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + public int NewsId { get; set; } + + [ForeignKey(nameof(NewsId))] + public WebsiteAnnouncementEntity News { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs new file mode 100644 index 00000000..bf1d9083 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/PhotoActivityEntity.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: . +/// +public class PhotoActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + public int PhotoId { get; set; } + + [ForeignKey(nameof(PhotoId))] + public PhotoEntity Photo { get; set; } +} + +public class LevelPhotoActivity : PhotoActivityEntity +{ + [Column("SlotId")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } +} + +public class UserPhotoActivity : PhotoActivityEntity +{ + [Column("TargetUserId")] + public int TargetUserId { get; set; } + + [ForeignKey(nameof(TargetUserId))] + public UserEntity TargetUser { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs new file mode 100644 index 00000000..9d4ef06b --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/PlaylistActivityEntity.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: and . +/// +public class PlaylistActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + [Column("PlaylistId")] + public int PlaylistId { get; set; } + + [ForeignKey(nameof(PlaylistId))] + public PlaylistEntity Playlist { get; set; } +} + +/// +/// Supported event types: . +/// +/// +/// The relationship between and +/// is slightly hacky but it allows us to reuse columns that would normally only be user with other types. +/// +/// +/// +public class PlaylistWithSlotActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + [Column("PlaylistId")] + public int PlaylistId { get; set; } + + [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 new file mode 100644 index 00000000..e824541c --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/ReviewActivityEntity.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: , , , and . +/// +public class ReviewActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + public int ReviewId { get; set; } + + [ForeignKey(nameof(ReviewId))] + public ReviewEntity Review { get; set; } + + [Column("SlotId")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs new file mode 100644 index 00000000..6d6db824 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/ScoreActivityEntity.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: . +/// +public class ScoreActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + public int ScoreId { get; set; } + + [ForeignKey(nameof(ScoreId))] + public ScoreEntity Score { get; set; } + + [Column("SlotId")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public SlotEntity Slot { get; set; } + + public int Points { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs b/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs new file mode 100644 index 00000000..f1756f5b --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Activity/UserActivityEntity.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations.Schema; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity; + +/// +/// Supported event types: and . +/// +public class UserActivityEntity : ActivityEntity +{ + /// + /// The of the that this event refers to. + /// + [Column("TargetUserId")] + public int TargetUserId { get; set; } + + [ForeignKey(nameof(TargetUserId))] + public UserEntity TargetUser { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs b/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs index 59091953..c6f18240 100644 --- a/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs +++ b/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs @@ -3,7 +3,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; namespace LBPUnion.ProjectLighthouse.Types.Entities.Level; diff --git a/ProjectLighthouse/Types/Entities/Website/WebsiteAnnouncementEntity.cs b/ProjectLighthouse/Types/Entities/Website/WebsiteAnnouncementEntity.cs index 07b6859e..d66bda25 100644 --- a/ProjectLighthouse/Types/Entities/Website/WebsiteAnnouncementEntity.cs +++ b/ProjectLighthouse/Types/Entities/Website/WebsiteAnnouncementEntity.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; @@ -13,6 +14,8 @@ public class WebsiteAnnouncementEntity public string Content { get; set; } + public DateTime PublishedAt { get; set; } + #nullable enable public int? PublisherId { get; set; } diff --git a/ProjectLighthouse/Types/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/Levels/Category.cs b/ProjectLighthouse/Types/Levels/Category.cs index c18a4a00..1b3c08de 100644 --- a/ProjectLighthouse/Types/Levels/Category.cs +++ b/ProjectLighthouse/Types/Levels/Category.cs @@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; namespace LBPUnion.ProjectLighthouse.Types.Levels; diff --git a/ProjectLighthouse/Types/Logging/LogArea.cs b/ProjectLighthouse/Types/Logging/LogArea.cs index 10146e22..9a8cc178 100644 --- a/ProjectLighthouse/Types/Logging/LogArea.cs +++ b/ProjectLighthouse/Types/Logging/LogArea.cs @@ -28,4 +28,5 @@ public enum LogArea Email, Serialization, Synchronization, + Activity, } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/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/GameCommentEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs new file mode 100644 index 00000000..1b297785 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameCommentEvent.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +[XmlInclude(typeof(GameUserCommentEvent))] +[XmlInclude(typeof(GameSlotCommentEvent))] +public class GameCommentEvent : GameEvent +{ + [XmlElement("comment_id")] + public int CommentId { get; set; } +} + +public class GameUserCommentEvent : GameCommentEvent +{ + [XmlElement("object_user")] + public string TargetUsername { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + CommentEntity comment = await database.Comments.FindAsync(this.CommentId); + if (comment == null) return; + + UserEntity user = await database.Users.FindAsync(comment.TargetUserId); + if (user == null) return; + + this.TargetUsername = user.Username; + } +} + +public class GameSlotCommentEvent : GameCommentEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot TargetSlot { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + CommentEntity comment = await database.Comments.FindAsync(this.CommentId); + if (comment == null) return; + + SlotEntity slot = await database.Slots.FindAsync(comment.TargetSlotId); + + if (slot == null) return; + + this.TargetSlot = 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 new file mode 100644 index 00000000..0b945c51 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameEvent.cs @@ -0,0 +1,236 @@ +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.Logging; +using LBPUnion.ProjectLighthouse.Types.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Activity; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Logging; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +[XmlInclude(typeof(GameCommentEvent))] +[XmlInclude(typeof(GamePhotoUploadEvent))] +[XmlInclude(typeof(GamePlayLevelEvent))] +[XmlInclude(typeof(GameReviewEvent))] +[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] + protected int UserId { get; set; } + + [XmlAttribute("type")] + public EventType Type { get; set; } + + [XmlElement("timestamp")] + public long Timestamp { get; set; } + + [XmlElement("actor")] + [DefaultValue(null)] + public string Username { get; set; } + + protected async Task PrepareSerialization(DatabaseContext database) + { + #if DEBUG + Logger.Debug($@"EVENT SERIALIZATION!! userId: {this.UserId} - hashCode: {this.GetHashCode()}", LogArea.Activity); + #endif + UserEntity user = await database.Users.FindAsync(this.UserId); + if (user == null) return; + this.Username = user.Username; + } + + public static IEnumerable CreateFromActivities(IEnumerable activities) + { + List events = []; + List> typeGroups = activities.GroupBy(g => g.Activity.Type).ToList(); + foreach (IGrouping typeGroup in typeGroups) + { + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + // Events with Count need special treatment + switch (typeGroup.Key) + { + case EventType.PlayLevel: + { + if (typeGroup.First().Activity is not LevelActivityEntity levelActivity) break; + + events.Add(new GamePlayLevelEvent + { + 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; + } + } + + return events.AsEnumerable(); + } + + private static bool IsValidActivity(ActivityEntity activity) + { + 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)) + { + Logger.Error(@"Invalid Activity: " + activity.Activity.ActivityId, LogArea.Activity); + return null; + } + + int targetId = activity.TargetId; + + GameEvent gameEvent = activity.Activity.Type switch + { + EventType.PlayLevel => new GamePlayLevelEvent + { + Slot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.HeartLevel or EventType.UnheartLevel => new GameHeartLevelEvent + { + TargetSlot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.DpadRateLevel => new GameDpadRateLevelEvent + { + Slot = new ReviewSlot + { + SlotId = targetId, + }, + }, + EventType.Score => new GameScoreEvent + { + ScoreId = ((ScoreActivityEntity)activity.Activity).ScoreId, + Score = ((ScoreActivityEntity)activity.Activity).Points, + 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 + { + PhotoId = ((PhotoActivityEntity)activity.Activity).PhotoId, + Slot = new ReviewSlot + { + SlotId = activity.TargetSlotId ?? -1, + }, + }, + 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 = activity.TargetPlaylistId ?? -1, + }, + EventType.HeartPlaylist => new GameHeartPlaylistEvent + { + TargetPlaylistId = activity.TargetPlaylistId ?? -1, + }, + EventType.AddLevelToPlaylist => new GameAddLevelToPlaylistEvent + { + TargetPlaylistId = activity.TargetPlaylistId ?? -1, + Slot = new ReviewSlot + { + SlotId = ((PlaylistWithSlotActivityEntity)activity.Activity).SlotId, + }, + }, + _ => new GameEvent(), + }; + 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 new file mode 100644 index 00000000..81583459 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameHeartEvent.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameHeartUserEvent : GameEvent +{ + [XmlIgnore] + public int TargetUserId { get; set; } + + [XmlElement("object_user")] + public string TargetUsername { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + UserEntity targetUser = await database.Users.FindAsync(this.TargetUserId); + if (targetUser == null) return; + + this.TargetUsername = targetUser.Username; + } +} + +public class GameHeartLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot TargetSlot { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.TargetSlot.SlotId); + if (slot == null) return; + + this.TargetSlot = ReviewSlot.CreateFromEntity(slot); + } +} + +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 new file mode 100644 index 00000000..62b9a948 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePhotoUploadEvent.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GamePhotoUploadEvent : GameEvent +{ + [XmlElement("photo_id")] + public int PhotoId { get; set; } + + [XmlElement("object_slot_id")] + [DefaultValue(null)] + public ReviewSlot Slot { get; set; } + + [XmlElement("user_in_photo")] + public List PhotoParticipants { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + PhotoEntity photo = await database.Photos.Where(p => p.PhotoId == this.PhotoId) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User) + .FirstOrDefaultAsync(); + if (photo == null) return; + + this.PhotoParticipants = photo.PhotoSubjects.Select(ps => ps.User.Username).ToList(); + + if (photo.SlotId == null) return; + + SlotEntity slot = await database.Slots.FindAsync(photo.SlotId); + if (slot == null) return; + + if (slot.Type == SlotType.User) this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePlayLevelEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePlayLevelEvent.cs new file mode 100644 index 00000000..af00048c --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePlayLevelEvent.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GamePlayLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("count")] + public int Count { get; set; } = 1; + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs new file mode 100644 index 00000000..4d802f2a --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GamePublishLevelEvent.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GamePublishLevelEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("republish")] + public int IsRepublish { get; set; } + + [XmlElement("count")] + public int Count { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + // TODO does this work? + 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 new file mode 100644 index 00000000..af8c5b82 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameReviewEvent.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameReviewEvent : GameEvent +{ + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("review_id")] + public int ReviewId { get; set; } + + [XmlElement("review_modified")] + [DefaultValue(0)] + public long ReviewTimestamp { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + 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; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs new file mode 100644 index 00000000..382080ab --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/Events/GameScoreEvent.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events; + +public class GameScoreEvent : GameEvent +{ + [XmlIgnore] + public int ScoreId { get; set; } + + [XmlElement("object_slot_id")] + public ReviewSlot Slot { get; set; } + + [XmlElement("score")] + public int Score { get; set; } + + [DefaultValue(0)] + [XmlElement("user_count")] + public int UserCount { get; set; } + + public new async Task PrepareSerialization(DatabaseContext database) + { + await base.PrepareSerialization(database); + + ScoreEntity score = await database.Scores.FindAsync(this.ScoreId); + if (score == null) return; + + SlotEntity slot = await database.Slots.FindAsync(score.SlotId); + if (slot == null) return; + + this.UserCount = score.Type; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file 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/GameSlotStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GameSlotStreamGroup.cs new file mode 100644 index 00000000..9c145fb5 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameSlotStreamGroup.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +public class GameSlotStreamGroup : GameStreamGroup, INeedsPreparationForSerialization +{ + [XmlElement("slot_id")] + public ReviewSlot Slot { get; set; } + + public async Task PrepareSerialization(DatabaseContext database) + { + SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId); + if (slot == null) return; + + this.Slot = ReviewSlot.CreateFromEntity(slot); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs new file mode 100644 index 00000000..f734b741 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStream.cs @@ -0,0 +1,155 @@ +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.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization.News; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; +using LBPUnion.ProjectLighthouse.Types.Users; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +/// +/// The global stream object, contains all +/// +[XmlRoot("stream")] +public class GameStream : ILbpSerializable, INeedsPreparationForSerialization +{ + /// + /// A list of that should be included in the root + /// of the stream object. These will be loaded into + /// + [XmlIgnore] + public List SlotIds { get; set; } + + /// + /// A list of that should be included in the root + /// of the stream object. These will be loaded into + /// + [XmlIgnore] + public List UserIds { get; set; } + + /// + /// A list of that should be included in the root + /// of the stream object. These will be loaded into + /// + [XmlIgnore] + public List PlaylistIds { get; set; } + + /// + /// A list of that should be included in the root + /// of the stream object. These will be loaded into + /// + [XmlIgnore] + public List NewsIds { get; set; } + + [XmlIgnore] + private GameVersion TargetGame { get; set; } + + [XmlElement("start_timestamp")] + public long StartTimestamp { get; set; } + + [XmlElement("end_timestamp")] + public long EndTimestamp { get; set; } + + [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")] + [DefaultValue(null)] + public List News { get; set; } + + public async Task PrepareSerialization(DatabaseContext database) + { + this.Slots = await LoadEntities(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, 0), s => s.Type == SlotType.User); + this.Users = await LoadEntities(this.UserIds, user => GameUser.CreateFromEntity(user, this.TargetGame)); + this.Playlists = await LoadEntities(this.PlaylistIds, GamePlaylist.CreateFromEntity); + this.News = await LoadEntities(this.NewsIds, a => GameNewsObject.CreateFromEntity(a, this.TargetGame)); + return; + + async Task> LoadEntities(List ids, Func transformation, Func predicate = null) + where TFrom : class + { + List results = []; + if (ids.Count <= 0) return null; + foreach (int id in ids) + { + TFrom entity = await database.Set().FindAsync(id); + + if (entity == null) continue; + + if (predicate != null && !predicate(entity)) continue; + + results.Add(transformation(entity)); + } + + return results; + } + } + + public static GameStream CreateFromGroups + (GameTokenEntity token, List groups, long startTimestamp, long endTimestamp, bool removeNesting = false) + { + GameStream gameStream = new() + { + TargetGame = token.GameVersion, + StartTimestamp = startTimestamp, + EndTimestamp = endTimestamp, + SlotIds = groups.GetIds(ActivityGroupType.Level), + UserIds = groups.GetIds(ActivityGroupType.User), + PlaylistIds = groups.GetIds(ActivityGroupType.Playlist), + NewsIds = groups.GetIds(ActivityGroupType.News), + }; + 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.All(g => g.Key.GroupType == ActivityGroupType.Level) && removeNesting) + { + // Flatten all inner groups into a single list + gameStream.Groups = gameStream.Groups.Select(g => g.Groups).SelectMany(g => g).ToList(); + } + + // Workaround to turn a single subgroup into the primary group for news and team picks + 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; + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..e4fea532 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameStreamGroup.cs @@ -0,0 +1,95 @@ +using System; +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.Serialization.Activity.Events; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +/// +/// Top level groups generally contain all events for a given level or user +/// +/// The sub-groups are always and contain all activities from a single user +/// for the top level group entity +/// +/// +[XmlInclude(typeof(GameUserStreamGroup))] +[XmlInclude(typeof(GameSlotStreamGroup))] +[XmlInclude(typeof(GamePlaylistStreamGroup))] +[XmlInclude(typeof(GameNewsStreamGroup))] +public class GameStreamGroup : ILbpSerializable +{ + [XmlAttribute("type")] + public ActivityGroupType Type { get; set; } + + [XmlElement("timestamp")] + public long Timestamp { get; set; } + + [XmlArray("subgroups")] + [XmlArrayItem("group")] + [DefaultValue(null)] + public List Groups { get; set; } + + [XmlArray("events")] + [XmlArrayItem("event")] + [DefaultValue(null)] + public List Events { get; set; } + + public static GameStreamGroup 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.Max(a => a.Activity.Timestamp).ToUnixTimeMilliseconds(); + streamGroup.Events = GameEvent.CreateFromActivities(g).ToList(); + })) + .ToList()); + + return gameGroup; + } + + private static GameStreamGroup CreateGroup + (ActivityGroupType type, int targetId, Action groupAction) + { + GameStreamGroup gameGroup = type switch + { + ActivityGroupType.Level or ActivityGroupType.TeamPick => new GameSlotStreamGroup + { + Slot = new ReviewSlot + { + SlotId = targetId, + }, + }, + ActivityGroupType.User => new GameUserStreamGroup + { + UserId = targetId, + }, + ActivityGroupType.Playlist => new GamePlaylistStreamGroup + { + PlaylistId = targetId, + }, + ActivityGroupType.News => new GameNewsStreamGroup + { + NewsId = targetId, + }, + _ => new GameStreamGroup(), + }; + gameGroup.Type = type; + groupAction(gameGroup); + return gameGroup; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/Activity/GameUserStreamGroup.cs b/ProjectLighthouse/Types/Serialization/Activity/GameUserStreamGroup.cs new file mode 100644 index 00000000..bc7cf53b --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Activity/GameUserStreamGroup.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity; + +public class GameUserStreamGroup : GameStreamGroup, INeedsPreparationForSerialization +{ + [XmlIgnore] + public int UserId { get; set; } + + [XmlElement("user_id")] + public string Username { get; set; } + + public async Task PrepareSerialization(DatabaseContext database) + { + UserEntity user = await database.Users.FindAsync(this.UserId); + if (user == null) return; + + this.Username = user.Username; + } + + public static GameUserStreamGroup Create(int userId) => + new() + { + UserId = userId, + }; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/CommentListResponse.cs b/ProjectLighthouse/Types/Serialization/Comment/CommentListResponse.cs similarity index 83% rename from ProjectLighthouse/Types/Serialization/CommentListResponse.cs rename to ProjectLighthouse/Types/Serialization/Comment/CommentListResponse.cs index 35d9023e..b9b11d0c 100644 --- a/ProjectLighthouse/Types/Serialization/CommentListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Comment/CommentListResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Comment; [XmlRoot("comments")] public struct CommentListResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameComment.cs b/ProjectLighthouse/Types/Serialization/Comment/GameComment.cs similarity index 97% rename from ProjectLighthouse/Types/Serialization/GameComment.cs rename to ProjectLighthouse/Types/Serialization/Comment/GameComment.cs index 07919f5e..16597d13 100644 --- a/ProjectLighthouse/Types/Serialization/GameComment.cs +++ b/ProjectLighthouse/Types/Serialization/Comment/GameComment.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Comment; [XmlRoot("comment")] [XmlType("comment")] diff --git a/ProjectLighthouse/Types/Serialization/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..ec1eeee0 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/News/GameNewsObject.cs @@ -0,0 +1,57 @@ +using System.ComponentModel; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Users; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.News; + +/// +/// 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, GameVersion gameVersion) => + new() + { + Id = entity.AnnouncementId, + Title = entity.Title, + Summary = gameVersion == GameVersion.LittleBigPlanet2 ? entity.Content : "", + Text = entity.Content, + Category = "no_category", + Timestamp = entity.PublishedAt.ToUnixTimeMilliseconds(), + }; +} + +[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/GamePhoto.cs b/ProjectLighthouse/Types/Serialization/Photo/GamePhoto.cs similarity index 98% rename from ProjectLighthouse/Types/Serialization/GamePhoto.cs rename to ProjectLighthouse/Types/Serialization/Photo/GamePhoto.cs index 67bb2dfb..3c0cdd9e 100644 --- a/ProjectLighthouse/Types/Serialization/GamePhoto.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/GamePhoto.cs @@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Levels; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo; [XmlRoot("photo")] [XmlType("photo")] diff --git a/ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs b/ProjectLighthouse/Types/Serialization/Photo/GamePhotoSubject.cs similarity index 91% rename from ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs rename to ProjectLighthouse/Types/Serialization/Photo/GamePhotoSubject.cs index 94cbd50c..914b8227 100644 --- a/ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/GamePhotoSubject.cs @@ -1,7 +1,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo; [XmlType("subject")] [XmlRoot("subject")] diff --git a/ProjectLighthouse/Types/Serialization/PhotoListResponse.cs b/ProjectLighthouse/Types/Serialization/Photo/PhotoListResponse.cs similarity index 83% rename from ProjectLighthouse/Types/Serialization/PhotoListResponse.cs rename to ProjectLighthouse/Types/Serialization/Photo/PhotoListResponse.cs index a8d64028..d4f5fbfd 100644 --- a/ProjectLighthouse/Types/Serialization/PhotoListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/PhotoListResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo; [XmlRoot("photos")] public struct PhotoListResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/PhotoSlot.cs b/ProjectLighthouse/Types/Serialization/Photo/PhotoSlot.cs similarity index 88% rename from ProjectLighthouse/Types/Serialization/PhotoSlot.cs rename to ProjectLighthouse/Types/Serialization/Photo/PhotoSlot.cs index c4699720..82dea982 100644 --- a/ProjectLighthouse/Types/Serialization/PhotoSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Photo/PhotoSlot.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Levels; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo; [XmlRoot("slot")] public class PhotoSlot : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/Author.cs b/ProjectLighthouse/Types/Serialization/Playlist/Author.cs similarity index 80% rename from ProjectLighthouse/Types/Serialization/Author.cs rename to ProjectLighthouse/Types/Serialization/Playlist/Author.cs index 7de0599a..cfd577b3 100644 --- a/ProjectLighthouse/Types/Serialization/Author.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/Author.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; [XmlRoot("author")] public struct Author : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GamePlaylist.cs b/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GamePlaylist.cs rename to ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs index 5ced447c..80a9d6a1 100644 --- a/ProjectLighthouse/Types/Serialization/GamePlaylist.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/GamePlaylist.cs @@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; [XmlRoot("playlist")] public class GamePlaylist : ILbpSerializable, INeedsPreparationForSerialization @@ -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/GenericPlaylistResponse.cs b/ProjectLighthouse/Types/Serialization/Playlist/GenericPlaylistResponse.cs similarity index 93% rename from ProjectLighthouse/Types/Serialization/GenericPlaylistResponse.cs rename to ProjectLighthouse/Types/Serialization/Playlist/GenericPlaylistResponse.cs index ba85d8c8..f0f1f0f7 100644 --- a/ProjectLighthouse/Types/Serialization/GenericPlaylistResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/GenericPlaylistResponse.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; public struct GenericPlaylistResponse : ILbpSerializable, IHasCustomRoot where T : ILbpSerializable { diff --git a/ProjectLighthouse/Types/Serialization/IconList.cs b/ProjectLighthouse/Types/Serialization/Playlist/IconList.cs similarity index 82% rename from ProjectLighthouse/Types/Serialization/IconList.cs rename to ProjectLighthouse/Types/Serialization/Playlist/IconList.cs index 87a6f6cb..9d0e7f1c 100644 --- a/ProjectLighthouse/Types/Serialization/IconList.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/IconList.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; public struct IconList : ILbpSerializable { diff --git a/ProjectLighthouse/Types/Serialization/PlaylistResponse.cs b/ProjectLighthouse/Types/Serialization/Playlist/PlaylistResponse.cs similarity index 85% rename from ProjectLighthouse/Types/Serialization/PlaylistResponse.cs rename to ProjectLighthouse/Types/Serialization/Playlist/PlaylistResponse.cs index 81a46003..a5b309ab 100644 --- a/ProjectLighthouse/Types/Serialization/PlaylistResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Playlist/PlaylistResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; [XmlRoot("playlists")] public struct PlaylistResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameReview.cs b/ProjectLighthouse/Types/Serialization/Review/GameReview.cs similarity index 98% rename from ProjectLighthouse/Types/Serialization/GameReview.cs rename to ProjectLighthouse/Types/Serialization/Review/GameReview.cs index 7dba385e..1da3b77b 100644 --- a/ProjectLighthouse/Types/Serialization/GameReview.cs +++ b/ProjectLighthouse/Types/Serialization/Review/GameReview.cs @@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review; [XmlRoot("deleted_by")] public enum DeletedBy diff --git a/ProjectLighthouse/Types/Serialization/ReviewResponse.cs b/ProjectLighthouse/Types/Serialization/Review/ReviewResponse.cs similarity index 90% rename from ProjectLighthouse/Types/Serialization/ReviewResponse.cs rename to ProjectLighthouse/Types/Serialization/Review/ReviewResponse.cs index a15915dd..e13e99bb 100644 --- a/ProjectLighthouse/Types/Serialization/ReviewResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Review/ReviewResponse.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review; [XmlRoot("reviews")] public struct ReviewResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/Review/ReviewSlot.cs b/ProjectLighthouse/Types/Serialization/Review/ReviewSlot.cs new file mode 100644 index 00000000..11715e17 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/Review/ReviewSlot.cs @@ -0,0 +1,22 @@ +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Levels; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review; + +[XmlRoot("slot")] +public class ReviewSlot : ILbpSerializable +{ + [XmlAttribute("type")] + public SlotType SlotType { get; set; } + + [XmlText] + public int SlotId { get; set; } + + public static ReviewSlot CreateFromEntity(SlotEntity slot) => + new() + { + SlotType = slot.Type, + SlotId = slot.Type == SlotType.User ? slot.SlotId : slot.InternalSlotId, + }; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/ReviewSlot.cs b/ProjectLighthouse/Types/Serialization/ReviewSlot.cs deleted file mode 100644 index f4148adf..00000000 --- a/ProjectLighthouse/Types/Serialization/ReviewSlot.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Xml.Serialization; -using LBPUnion.ProjectLighthouse.Types.Levels; - -namespace LBPUnion.ProjectLighthouse.Types.Serialization; - -[XmlRoot("slot")] -public class ReviewSlot : ILbpSerializable -{ - [XmlAttribute("type")] - public SlotType SlotType { get; set; } - - [XmlText] - public int SlotId { get; set; } -} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/GameScore.cs b/ProjectLighthouse/Types/Serialization/Score/GameScore.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GameScore.cs rename to ProjectLighthouse/Types/Serialization/Score/GameScore.cs index 1195ce96..9c10ae73 100644 --- a/ProjectLighthouse/Types/Serialization/GameScore.cs +++ b/ProjectLighthouse/Types/Serialization/Score/GameScore.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score; [XmlRoot("playRecord")] [XmlType("playRecord")] diff --git a/ProjectLighthouse/Types/Serialization/MultiScoreboardResponse.cs b/ProjectLighthouse/Types/Serialization/Score/MultiScoreboardResponse.cs similarity index 93% rename from ProjectLighthouse/Types/Serialization/MultiScoreboardResponse.cs rename to ProjectLighthouse/Types/Serialization/Score/MultiScoreboardResponse.cs index 831e5efe..3d00f141 100644 --- a/ProjectLighthouse/Types/Serialization/MultiScoreboardResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Score/MultiScoreboardResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score; [XmlRoot("scoreboards")] public class MultiScoreboardResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/ScoreboardResponse.cs b/ProjectLighthouse/Types/Serialization/Score/ScoreboardResponse.cs similarity index 94% rename from ProjectLighthouse/Types/Serialization/ScoreboardResponse.cs rename to ProjectLighthouse/Types/Serialization/Score/ScoreboardResponse.cs index f9c45763..2ee27c0b 100644 --- a/ProjectLighthouse/Types/Serialization/ScoreboardResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Score/ScoreboardResponse.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score; public struct ScoreboardResponse: ILbpSerializable, IHasCustomRoot { diff --git a/ProjectLighthouse/Types/Serialization/CategoryListResponse.cs b/ProjectLighthouse/Types/Serialization/Slot/CategoryListResponse.cs similarity index 93% rename from ProjectLighthouse/Types/Serialization/CategoryListResponse.cs rename to ProjectLighthouse/Types/Serialization/Slot/CategoryListResponse.cs index 904fb290..faa24508 100644 --- a/ProjectLighthouse/Types/Serialization/CategoryListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/CategoryListResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("categories")] public class CategoryListResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameCategory.cs b/ProjectLighthouse/Types/Serialization/Slot/GameCategory.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GameCategory.cs rename to ProjectLighthouse/Types/Serialization/Slot/GameCategory.cs index e097de97..2f0a2069 100644 --- a/ProjectLighthouse/Types/Serialization/GameCategory.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GameCategory.cs @@ -2,7 +2,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Levels; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("category")] public class GameCategory : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs b/ProjectLighthouse/Types/Serialization/Slot/GameDeveloperSlot.cs similarity index 96% rename from ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs rename to ProjectLighthouse/Types/Serialization/Slot/GameDeveloperSlot.cs index 93f8436c..9b49f284 100644 --- a/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GameDeveloperSlot.cs @@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Levels; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("slot")] public class GameDeveloperSlot : SlotBase, INeedsPreparationForSerialization diff --git a/ProjectLighthouse/Types/Serialization/GameUserSlot.cs b/ProjectLighthouse/Types/Serialization/Slot/GameUserSlot.cs similarity index 98% rename from ProjectLighthouse/Types/Serialization/GameUserSlot.cs rename to ProjectLighthouse/Types/Serialization/Slot/GameUserSlot.cs index 978818be..11890cce 100644 --- a/ProjectLighthouse/Types/Serialization/GameUserSlot.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GameUserSlot.cs @@ -13,10 +13,12 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Misc; +using LBPUnion.ProjectLighthouse.Types.Serialization.Review; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("slot")] public class GameUserSlot : SlotBase, INeedsPreparationForSerialization diff --git a/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs b/ProjectLighthouse/Types/Serialization/Slot/GenericSlotResponse.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs rename to ProjectLighthouse/Types/Serialization/Slot/GenericSlotResponse.cs index b00e8575..036c02c5 100644 --- a/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/GenericSlotResponse.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; public struct GenericSlotResponse : ILbpSerializable, IHasCustomRoot { diff --git a/ProjectLighthouse/Types/Serialization/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/PlanetStatsResponse.cs b/ProjectLighthouse/Types/Serialization/Slot/PlanetStatsResponse.cs similarity index 88% rename from ProjectLighthouse/Types/Serialization/PlanetStatsResponse.cs rename to ProjectLighthouse/Types/Serialization/Slot/PlanetStatsResponse.cs index 0457d282..9dbcd2e5 100644 --- a/ProjectLighthouse/Types/Serialization/PlanetStatsResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/PlanetStatsResponse.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("planetStats")] public class PlanetStatsResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/SlotBase.cs b/ProjectLighthouse/Types/Serialization/Slot/SlotBase.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/SlotBase.cs rename to ProjectLighthouse/Types/Serialization/Slot/SlotBase.cs index 49d3ce60..9778a941 100644 --- a/ProjectLighthouse/Types/Serialization/SlotBase.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/SlotBase.cs @@ -3,9 +3,10 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlInclude(typeof(GameUserSlot))] [XmlInclude(typeof(GameDeveloperSlot))] @@ -49,7 +50,7 @@ public abstract class SlotBase : ILbpSerializable public static SlotBase CreateFromEntity(SlotEntity slot, GameTokenEntity token) => CreateFromEntity(slot, token.GameVersion, token.UserId); - private static SlotBase CreateFromEntity(SlotEntity slot, GameVersion targetGame, int targetUserId) + public static SlotBase CreateFromEntity(SlotEntity slot, GameVersion targetGame, int targetUserId) { if (slot == null) { diff --git a/ProjectLighthouse/Types/Serialization/SlotResourceResponse.cs b/ProjectLighthouse/Types/Serialization/Slot/SlotResourceResponse.cs similarity index 86% rename from ProjectLighthouse/Types/Serialization/SlotResourceResponse.cs rename to ProjectLighthouse/Types/Serialization/Slot/SlotResourceResponse.cs index 7f9954db..585f4a94 100644 --- a/ProjectLighthouse/Types/Serialization/SlotResourceResponse.cs +++ b/ProjectLighthouse/Types/Serialization/Slot/SlotResourceResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot; [XmlRoot("slot")] public struct SlotResourceResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/FriendResponse.cs b/ProjectLighthouse/Types/Serialization/User/FriendResponse.cs similarity index 85% rename from ProjectLighthouse/Types/Serialization/FriendResponse.cs rename to ProjectLighthouse/Types/Serialization/User/FriendResponse.cs index bf12e68c..ed8abca8 100644 --- a/ProjectLighthouse/Types/Serialization/FriendResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/FriendResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; [XmlRoot("npdata")] public struct FriendResponse : ILbpSerializable diff --git a/ProjectLighthouse/Types/Serialization/GameUser.cs b/ProjectLighthouse/Types/Serialization/User/GameUser.cs similarity index 89% rename from ProjectLighthouse/Types/Serialization/GameUser.cs rename to ProjectLighthouse/Types/Serialization/User/GameUser.cs index cb365ed1..ec4fdc42 100644 --- a/ProjectLighthouse/Types/Serialization/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; @@ -11,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Misc; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; [XmlRoot("user")] public class GameUser : ILbpSerializable, INeedsPreparationForSerialization @@ -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; diff --git a/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs b/ProjectLighthouse/Types/Serialization/User/GenericUserResponse.cs similarity index 95% rename from ProjectLighthouse/Types/Serialization/GenericUserResponse.cs rename to ProjectLighthouse/Types/Serialization/User/GenericUserResponse.cs index ef97d42c..43084165 100644 --- a/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/GenericUserResponse.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Filter; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; public struct GenericUserResponse : ILbpSerializable, IHasCustomRoot where T : ILbpSerializable { diff --git a/ProjectLighthouse/Types/Serialization/MinimalUserListResponse.cs b/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs similarity index 89% rename from ProjectLighthouse/Types/Serialization/MinimalUserListResponse.cs rename to ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs index c289bdc0..64f06c29 100644 --- a/ProjectLighthouse/Types/Serialization/MinimalUserListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/MinimalUserListResponse.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; [XmlRoot("users")] public struct MinimalUserListResponse : ILbpSerializable @@ -19,7 +19,6 @@ public struct MinimalUserListResponse : ILbpSerializable public class MinimalUserProfile : ILbpSerializable { - [XmlElement("npHandle")] public NpHandle UserHandle { get; set; } = new(); } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/NpHandle.cs b/ProjectLighthouse/Types/Serialization/User/NpHandle.cs similarity index 86% rename from ProjectLighthouse/Types/Serialization/NpHandle.cs rename to ProjectLighthouse/Types/Serialization/User/NpHandle.cs index c7952936..8138f0ae 100644 --- a/ProjectLighthouse/Types/Serialization/NpHandle.cs +++ b/ProjectLighthouse/Types/Serialization/User/NpHandle.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; public class NpHandle : ILbpSerializable { diff --git a/ProjectLighthouse/Types/Serialization/UserListResponse.cs b/ProjectLighthouse/Types/Serialization/User/UserListResponse.cs similarity index 89% rename from ProjectLighthouse/Types/Serialization/UserListResponse.cs rename to ProjectLighthouse/Types/Serialization/User/UserListResponse.cs index e5a15630..32b8731d 100644 --- a/ProjectLighthouse/Types/Serialization/UserListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/User/UserListResponse.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Xml.Serialization; -namespace LBPUnion.ProjectLighthouse.Types.Serialization; +namespace LBPUnion.ProjectLighthouse.Types.Serialization.User; public struct UserListResponse : ILbpSerializable, IHasCustomRoot { @@ -23,7 +23,7 @@ public struct UserListResponse : ILbpSerializable, IHasCustomRoot } [XmlIgnore] - public string RootTag { get; set; } + private string RootTag { get; set; } [XmlElement("user")] public List Users { get; set; }