diff --git a/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs b/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs index 5439907f0..32bbebe21 100644 --- a/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs +++ b/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs @@ -1,7 +1,7 @@ using LBPUnion.ProjectLighthouse.Configuration; 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 a070862c2..ff85fc0a5 100644 --- a/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs +++ b/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs @@ -28,11 +28,7 @@ public void ConfigureServices(IServiceCollection services) } ); - services.AddDbContext(builder => - { - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); - }); + services.AddDbContext(DatabaseContext.ConfigureBuilder()); services.AddSwaggerGen ( diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs new file mode 100644 index 000000000..c9d592fd3 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ActivityController.cs @@ -0,0 +1,377 @@ +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 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 + (List> 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); + + // Handle version filtering (don't show unplayable/incompatible levels) + activityQuery = token.GameVersion == GameVersion.LittleBigPlanetVita + ? activityQuery.Where(dto => dto.TargetSlotGameVersion == null || dto.TargetSlotGameVersion == token.GameVersion) + : activityQuery.Where(dto => dto.TargetSlotGameVersion == null || dto.TargetSlotGameVersion <= token.GameVersion); + + 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 7e3baeb9e..7708dc762 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -7,7 +7,7 @@ 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.Comment; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -42,6 +42,20 @@ public async Task RateComment([FromQuery] int commentId, [FromQue return this.Ok(); } + [HttpGet("userComment/{username}")] + [HttpGet("comment/{slotType}/{slotId:int}")] + public async Task GetSingleComment(string? username, string? slotType, int? slotId, int commentId) + { + GameTokenEntity token = this.GetToken(); + + if (username == null == (SlotHelper.IsTypeInvalid(slotType) || slotId == null)) return this.BadRequest(); + + CommentEntity? comment = await this.database.Comments.FindAsync(commentId); + if (comment == null) return this.NotFound(); + + return this.Ok(GameComment.CreateFromEntity(comment, token.UserId)); + } + [HttpGet("comments/{slotType}/{slotId:int}")] [HttpGet("userComments/{username}")] public async Task GetComments(string? username, string? slotType, int slotId) diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs index 83e198d8e..a0381f465 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/FriendsController.cs @@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.StorableLists.Stores; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/NewsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/NewsController.cs new file mode 100644 index 000000000..9fb7df7c9 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Controllers/NewsController.cs @@ -0,0 +1,31 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Website; +using LBPUnion.ProjectLighthouse.Types.Serialization.News; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; + +[ApiController] +[Authorize] +[Route("LITTLEBIGPLANETPS3_XML/")] +[Produces("text/xml")] +public class NewsController : ControllerBase +{ + private readonly DatabaseContext database; + + public NewsController(DatabaseContext database) + { + this.database = database; + } + + [HttpGet("news")] + public async Task GetNews() + { + List websiteAnnouncements = + await this.database.WebsiteAnnouncements.OrderByDescending(a => a.AnnouncementId).ToListAsync(); + + return this.Ok(GameNews.CreateFromEntity(websiteAnnouncements)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index 4c73d0a04..4a405e1e9 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -12,7 +12,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Photo; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -171,6 +171,15 @@ await WebhookHelper.SendWebhook return this.Ok(); } + [HttpGet("photo/{photoId:int}")] + public async Task GetPhoto(int photoId) + { + PhotoEntity? photo = await database.Photos.FirstOrDefaultAsync(p => p.PhotoId == photoId); + if (photo == null) return this.NotFound(); + + return this.Ok(GamePhoto.CreateFromEntity(photo)); + } + [HttpGet("photos/{slotType}/{id:int}")] public async Task SlotPhotos(string slotType, int id, [FromQuery] string? by) { diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs index 60ac1c580..b9a36852d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs @@ -13,6 +13,9 @@ using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Misc; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs index 67fff1804..3db2016b1 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs @@ -9,7 +9,9 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs index 4763708a5..46e8b90e7 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PlaylistController.cs @@ -4,7 +4,8 @@ using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs index 642594891..95641339d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs @@ -12,7 +12,7 @@ using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Filter; 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 3b2a7fbbf..cee49c230 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.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; @@ -147,6 +147,22 @@ public async Task PostReview(int slotId) return this.Ok(); } + [HttpGet("review/user/{slotId:int}/{reviewerName}")] + public async Task GetReview(int slotId, string reviewerName) + { + GameTokenEntity token = this.GetToken(); + + int reviewerId = await this.database.Users.Where(u => u.Username == reviewerName) + .Select(s => s.UserId) + .FirstOrDefaultAsync(); + if (reviewerId == 0) return this.NotFound(); + + ReviewEntity? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.ReviewerId == reviewerId && r.SlotId == slotId); + if (review == null) return this.NotFound(); + + return this.Ok(GameReview.CreateFromEntity(review, token)); + } + [HttpGet("reviewsFor/user/{slotId:int}")] public async Task ReviewsFor(int slotId) { diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index a7c41dd6e..5c74274a8 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Score; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -132,7 +132,8 @@ public async Task SubmitScore(string slotType, int id, int childI await this.database.SaveChangesAsync(); - ScoreEntity? existingScore = await this.database.Scores.Where(s => s.SlotId == slot.SlotId) + ScoreEntity? existingScore = await this.database.Scores + .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 a4f129b5f..367335f97 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 f71267bab..198d3fe74 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -4,6 +4,7 @@ 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.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 3f70b3d00..78e70e612 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs @@ -4,9 +4,9 @@ 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 7301bc52c..0cc7f6c92 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs @@ -13,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Filter; -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 7c654a6f2..5f8d6a20c 100644 --- a/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs +++ b/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs @@ -1,5 +1,5 @@ using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Users; diff --git a/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs index bef6943ee..ee09930ba 100644 --- a/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs +++ b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs @@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; diff --git a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs index 0d372e8f1..4d0cfc940 100644 --- a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs +++ b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; @@ -54,11 +53,7 @@ public void ConfigureServices(IServiceCollection services) } ); - services.AddDbContext(builder => - { - builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, - MySqlServerVersion.LatestSupportedServerVersion); - }); + services.AddDbContext(DatabaseContext.ConfigureBuilder()); IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled ? new MailQueueService(new SmtpMailSender()) diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs index b483f546f..fb6003bfd 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs @@ -1,7 +1,7 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs index 00ed79ce2..a72123068 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs @@ -5,6 +5,8 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs index 39d6986dd..5ed58a06b 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs @@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs index 606557a77..ee8396d01 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs @@ -2,7 +2,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Filter; -using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Filters.Slot; using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs index d9826a923..bd188d7aa 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs @@ -5,6 +5,8 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Serialization.Slot; +using LBPUnion.ProjectLighthouse.Types.Serialization.User; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; diff --git a/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs b/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationRemovalController.cs index ec088e13b..3ba7ab193 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/Extensions/PartialExtensions.cs b/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs index 6248411ee..64ff3aec0 100644 --- a/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs +++ b/ProjectLighthouse.Servers.Website/Extensions/PartialExtensions.cs @@ -35,6 +35,17 @@ private static ViewDataDictionary WithKeyValue(this ViewDataDictionary public static Task ToLink(this UserEntity user, IHtmlHelper helper, ViewDataDictionary viewData, string language, string timeZone = "", bool includeStatus = false) => helper.PartialAsync("Partials/Links/UserLinkPartial", user, viewData.WithLang(language).WithTime(timeZone).WithKeyValue("IncludeStatus", includeStatus)); + public static Task ToLink + ( + this SlotEntity slot, + IHtmlHelper helper, + ViewDataDictionary viewData, + string language + ) => + helper.PartialAsync("Partials/Links/SlotLinkPartial", + slot, + viewData.WithLang(language)); + public static Task ToHtml ( this SlotEntity slot, diff --git a/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml new file mode 100644 index 000000000..25838a6e4 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml @@ -0,0 +1,85 @@ +@page "/debug/activity" +@using System.Globalization +@using LBPUnion.ProjectLighthouse.Types.Activity +@using LBPUnion.ProjectLighthouse.Types.Entities.Activity +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug.ActivityTestPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.Title = "Debug - Activity Test"; +} + + +
Group By Activity
+
+ +
Group By Actor
+
+
+ +@foreach (OuterActivityGroup activity in Model.ActivityGroups) +{ +

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

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

UserId: @activity.Key.UserId

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

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

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

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

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

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

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

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

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

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

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

Event Group Type: @gameEvent.GroupType

+

Event Target ID: @gameEvent.TargetId

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

SlotId: @level.SlotId

+

SlotVersion: @gameEvent.TargetSlotGameVersion

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

ScoreId: @score.ScoreId

+

SlotId: @score.SlotId

+

SlotVersion: @gameEvent.TargetSlotGameVersion

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

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

+
+
+} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs new file mode 100644 index 000000000..fe86ee837 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Debug/ActivityTestPage.cshtml.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +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); + + Stopwatch timer = new(); + timer.Start(); + + List> eventList = + (await this.Database.Activities.ToActivityDto(true).ToActivityGroups(groupByActor).ToListAsync()); + + Console.WriteLine($@"Fetching from database took {timer.ElapsedMilliseconds}ms"); + + timer.Restart(); + + List? events = eventList + .ToOuterActivityGroups(groupByActor); + + Console.WriteLine($@"Local grouping took {timer.ElapsedMilliseconds}ms"); + + 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 a67e667f6..e83c60b96 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Email/CompleteEmailVerificationPage.cshtml.cs @@ -64,7 +64,7 @@ public async Task OnGet(string token) webToken.UserToken, new CookieOptions { - Expires = DateTimeOffset.Now.AddDays(7), + Expires = DateTimeOffset.UtcNow.AddDays(7), }); return this.Redirect("/passwordReset"); } diff --git a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml index 49f969d42..f3ac96e21 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) @@ -85,6 +86,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 8c8834623..b3c6f8f4e 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Login/LoginForm.cshtml @@ -38,7 +38,7 @@

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

} diff --git a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs index 8595ee858..0849304b0 100644 --- a/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs @@ -54,6 +54,7 @@ public async Task OnPost([FromForm] string title, [FromForm] stri Title = title.Trim(), Content = content.Trim(), PublisherId = user.UserId, + PublishedAt = DateTime.UtcNow, }; this.Database.WebsiteAnnouncements.Add(announcement); diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ActivityPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ActivityPartial.cshtml new file mode 100644 index 000000000..35a936d64 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/ActivityPartial.cshtml @@ -0,0 +1,122 @@ +@using LBPUnion.ProjectLighthouse.Localization +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions +@using LBPUnion.ProjectLighthouse.Types.Activity +@using LBPUnion.ProjectLighthouse.Types.Entities.Activity +@model LBPUnion.ProjectLighthouse.Types.Activity.ActivityDto + +@{ + string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; +} + +@* ReSharper disable SwitchStatementHandlesSomeKnownEnumValuesWithDefault *@ +
+ + @switch (Model.Activity) + { + case LevelActivityEntity levelActivity: + switch (Model.Activity.Type) + { + case EventType.HeartLevel: + hearted @await levelActivity.Slot.ToLink(Html, ViewData, language) + break; + case EventType.PlayLevel: + played @await levelActivity.Slot.ToLink(Html, ViewData, language) @(Model.Activity.Data > 1 ? Model.Activity.Data + " times" : "") + break; + case EventType.PublishLevel: + @(Model.Activity.Data == 2 ? "republished" : "published") @await levelActivity.Slot.ToLink(Html, ViewData, language) + break; + case EventType.UnheartLevel: + unhearted @await levelActivity.Slot.ToLink(Html, ViewData, language) + break; + case EventType.DpadRateLevel: + case EventType.RateLevel: + left a rating on @await levelActivity.Slot.ToLink(Html, ViewData, language) + break; + case EventType.TagLevel: + added a tag on @await levelActivity.Slot.ToLink(Html, ViewData, language) + break; + case EventType.MMPickLevel: + team picked @await levelActivity.Slot.ToLink(Html, ViewData, language) + break; + default: throw new ArgumentOutOfRangeException(nameof(Model.Activity.Type), Model.Activity.Type, @"Invalid level activity type"); + } + + break; + case LevelPhotoActivity levelPhoto: + uploaded a photo in @await levelPhoto.Slot.ToLink(Html, ViewData, language) + break; + case LevelCommentActivityEntity levelComment: + switch (Model.Activity.Type) + { + case EventType.CommentOnLevel: + left a comment on @await levelComment.Slot.ToLink(Html, ViewData, language) + break; + case EventType.DeleteLevelComment: + switch (levelComment.Comment.DeletedType) + { + case "user": + deleted their comment on @await levelComment.Slot.ToLink(Html, ViewData, language) + break; + case "moderator": + had their comment deleted on @await levelComment.Slot.ToLink(Html, ViewData, language) + break; + } + + break; + default: throw new ArgumentOutOfRangeException(nameof(Model.Activity.Type), Model.Activity.Type, @"Invalid level comment activity type"); + } + + break; + case UserCommentActivityEntity userComment: + left a comment on @await userComment.TargetUser.ToLink(Html, ViewData, language) + break; + case ReviewActivityEntity reviewActivity: + left a review on @await reviewActivity.Slot.ToLink(Html, ViewData, language) + break; + case UserActivityEntity userActivity: + switch (Model.Activity.Type) + { + case EventType.CommentOnUser: + left a comment on @await userActivity.User.ToLink(Html, ViewData, language) + break; + + case EventType.HeartUser: + hearted @await userActivity.User.ToLink(Html, ViewData, language) + break; + case EventType.UnheartUser: + unhearted @await userActivity.User.ToLink(Html, ViewData, language) + break; + default: throw new ArgumentOutOfRangeException(nameof(Model.Activity.Type), Model.Activity.Type, @"Invalid user activity type"); + } + + break; + case UserPhotoActivity userPhoto: + uploaded a photo on @await userPhoto.TargetUser.ToLink(Html, ViewData, language) + break; + case PlaylistActivityEntity playlistActivity: + switch (Model.Activity.Type) + { + case EventType.CreatePlaylist: + created playlist @playlistActivity.Playlist.Name + break; + case EventType.HeartPlaylist: + hearted playlist @playlistActivity.Playlist.Name + break; + default: throw new ArgumentOutOfRangeException(nameof(Model.Activity.Type), Model.Activity.Type, @"Invalid playlist activity type"); + } + + break; + case PlaylistWithSlotActivityEntity playlistWithLevel: + added @await playlistWithLevel.Slot.ToLink(Html, ViewData, language) to playlist @playlistWithLevel.Playlist.Name + break; + case ScoreActivityEntity score: + completed @await score.Slot.ToLink(Html, ViewData, language) with @score.Data points in @(score.Score.Type == 7 ? "versus" : score.Score.Type + "-player") mode + break; + case NewsActivityEntity: + created a news post + break; + default: + Unhandled activity type: @Model.Activity.Type + break; + } +
\ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/SlotLinkPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/SlotLinkPartial.cshtml new file mode 100644 index 000000000..64700b83d --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/SlotLinkPartial.cshtml @@ -0,0 +1,46 @@ +@using LBPUnion.ProjectLighthouse.Configuration +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions +@using LBPUnion.ProjectLighthouse.Types.Levels +@model LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity + +@switch (Model.Type) +{ + case SlotType.User: + + @{ + int size = 20; + bool isAdventure = Model.IsAdventurePlanet; + string adventureStyle = isAdventure ? "-webkit-mask-image: url(/assets/advSlotCardMask.png); -webkit-mask-size: contain; border-radius: 0%;" : ""; + string sizing = $"min-width: {size}px; width: {size}px; height: {size}px;"; + } +
+ + + +
+ + @Model.Name + +
+ break; + case SlotType.Developer: + a story mode level + break; + case SlotType.Local: + a level on the moon + break; + case SlotType.Pod: + the pod + break; + case SlotType.Moon: + case SlotType.Unknown: + case SlotType.Unknown2: + case SlotType.DLC: + Somewhere in the imagisphere + break; + default: throw new ArgumentOutOfRangeException(nameof(Model.Type)); +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml index 70b98e64a..66fea3d9e 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml @@ -3,7 +3,7 @@ @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @using LBPUnion.ProjectLighthouse.Types.Entities.Profile @using LBPUnion.ProjectLighthouse.Types.Levels -@using LBPUnion.ProjectLighthouse.Types.Serialization +@using LBPUnion.ProjectLighthouse.Types.Serialization.Photo @model LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity @{ diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/RecentActivityPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/RecentActivityPartial.cshtml new file mode 100644 index 000000000..ebcf6e85d --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/RecentActivityPartial.cshtml @@ -0,0 +1,78 @@ +@using LBPUnion.ProjectLighthouse.Database +@using LBPUnion.ProjectLighthouse.Localization +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions +@using LBPUnion.ProjectLighthouse.Types.Activity +@using LBPUnion.ProjectLighthouse.Types.Entities.Profile +@inject DatabaseContext Database + +@{ + string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; + string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); + + void RenderRelativeTime(DateTime time) + { + TimeSpan timeSpan = DateTime.UtcNow - time; +

+ @if (timeSpan.TotalSeconds < 5) + { + just now. + } + else if (timeSpan.TotalSeconds < 60) + { + @(Pluralize(Math.Round(timeSpan.TotalSeconds), "second")) ago. + } + else if (timeSpan.TotalMinutes < 60) + { + @(Pluralize(Math.Round(timeSpan.TotalMinutes), "minute")) ago. + } + else if (timeSpan.TotalHours < 24) + { + @(Pluralize(Math.Round(timeSpan.TotalHours), "hour")) ago. + } + else if (timeSpan.TotalDays < 30) + { + @(Pluralize(Math.Round(timeSpan.TotalDays), "day")) ago. + } + else if (timeSpan.TotalDays < 365) + { + @(Pluralize(Math.Round(timeSpan.TotalDays / 30.4375), "month")) ago. + } + else + { + @(Pluralize(Math.Round(timeSpan.TotalDays / 365), "year")) ago. + } +

+ return; + string Pluralize(double amount, string unit) => $"{amount} {unit}{((int)amount == 1 ? "" : "s")}"; + } +} + +
+ @if (Model.Activity.Count == 0) + { +

There is no recent activity.

+ } + else + { + @foreach (IGrouping group in Model.Activity) + { +
+ @{ + UserEntity user = await Database.Users.FindAsync(group.First().Activity.UserId) ?? throw new InvalidOperationException(); + } + @await user.ToLink(Html, ViewData, language) + @{ + RenderRelativeTime(group.MaxBy(g => g.Activity.Timestamp)?.Activity.Timestamp ?? group.Key.Timestamp); + } +
+ @foreach (ActivityDto activity in group) + { + @await Html.PartialAsync("Partials/ActivityPartial", activity) + } +
+
+ } + } +
\ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml index f8c16a5ff..09b2b31e6 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/Pages/SlotPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml index 9ea6a0294..15f0fae27 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml @@ -134,7 +134,10 @@ else }
- + + @Model.Translate(GeneralStrings.RecentActivity) + + Comments @@ -153,15 +156,21 @@ else string divLength = isMobile ? "sixteen" : "thirteen"; }
-
- @await Html.PartialAsync("Partials/CommentsPartial", new ViewDataDictionary(ViewData.WithLang(language).WithTime(timeZone)) - { - { - "PageOwner", Model.Slot?.CreatorId - }, - }) +
+
+ @await Html.PartialAsync("Partials/RecentActivityPartial", ViewData.WithLang(language).WithTime(timeZone)) +
-
+ + -
- @await Html.PartialAsync("Partials/ReviewPartial", new ViewDataDictionary(ViewData) - { - { - "isMobile", isMobile - }, - { - "CanDelete", Model.User?.IsModerator ?? false - }, - }) + -
+