Implement online story features and photos taken in levels (#389)

* Initial commit to support developer slots

* Remove hearting story levels, prevent race condition in adding dev slots, and remove LastContactHelper local db object.

* Fix photos taken in pod showing wrong level.

* Add support for pod and create mode photos

* Add time display to photos and added photo display to level page

* Add pagination to in game photos

* Update in pod description

* Fix migration

* Adjust wording of photos taken on local slots

* Set slot default type to User

Fixes old slots being set to developer slots

* Apply suggestions

* Add player count to developer slots

Co-authored-by: Jayden <jvyden@jvyden.xyz>
This commit is contained in:
Josh 2022-08-01 14:46:29 -05:00 committed by GitHub
parent 395412eecb
commit fdf1988a34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 483 additions and 47 deletions

View file

@ -23,32 +23,40 @@ public class CommentController : ControllerBase
}
[HttpPost("rateUserComment/{username}")]
[HttpPost("rateComment/user/{slotId:int}")]
public async Task<IActionResult> RateComment([FromQuery] int commentId, [FromQuery] int rating, string? username, int? slotId)
[HttpPost("rateComment/{slotType}/{slotId:int}")]
public async Task<IActionResult> RateComment([FromQuery] int commentId, [FromQuery] int rating, string? username, string? slotType, int slotId)
{
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();
bool success = await this.database.RateComment(user, commentId, rating);
if (!success) return this.BadRequest();
return this.Ok();
}
[HttpGet("comments/user/{slotId:int}")]
[HttpGet("comments/{slotType}/{slotId:int}")]
[HttpGet("userComments/{username}")]
public async Task<IActionResult> GetComments([FromQuery] int pageStart, [FromQuery] int pageSize, string? username, int? slotId)
public async Task<IActionResult> GetComments([FromQuery] int pageStart, [FromQuery] int pageSize, string? username, string? slotType, int slotId)
{
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
int targetId = slotId.GetValueOrDefault();
int targetId = slotId;
CommentType type = CommentType.Level;
if (!string.IsNullOrWhiteSpace(username))
{
targetId = this.database.Users.First(u => u.Username.Equals(username)).UserId;
type = CommentType.Profile;
}
else
{
if (SlotHelper.IsTypeInvalid(slotType) || slotId == 0) return this.BadRequest();
}
if (type == CommentType.Level && slotType == "developer") targetId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
List<Comment> comments = await this.database.Comments.Include
(c => c.Poster)
@ -72,8 +80,8 @@ public class CommentController : ControllerBase
}
[HttpPost("postUserComment/{username}")]
[HttpPost("postComment/user/{slotId:int}")]
public async Task<IActionResult> PostComment(string? username, int? slotId)
[HttpPost("postComment/{slotType}/{slotId:int}")]
public async Task<IActionResult> PostComment(string? username, string? slotType, int slotId)
{
User? poster = await this.database.UserFromGameRequest(this.Request);
if (poster == null) return this.StatusCode(403, "");
@ -86,14 +94,18 @@ public class CommentController : ControllerBase
SanitizationHelper.SanitizeStringsInClass(comment);
CommentType type = (slotId.GetValueOrDefault() == 0 ? CommentType.Profile : CommentType.Level);
CommentType type = (slotId == 0 ? CommentType.Profile : CommentType.Level);
if (type == CommentType.Level && (SlotHelper.IsTypeInvalid(slotType) || slotId == 0)) return this.BadRequest();
if (comment == null) return this.BadRequest();
int targetId = slotId.GetValueOrDefault();
int targetId = slotId;
if (type == CommentType.Profile) targetId = this.database.Users.First(u => u.Username == username).UserId;
if (slotType == "developer") targetId = await SlotHelper.GetPlaceholderSlotId(this.database, targetId, SlotType.Developer);
bool success = await this.database.PostComment(poster, targetId, type, comment.Message);
if (success) return this.Ok();
@ -101,8 +113,8 @@ public class CommentController : ControllerBase
}
[HttpPost("deleteUserComment/{username}")]
[HttpPost("deleteComment/user/{slotId:int}")]
public async Task<IActionResult> DeleteComment([FromQuery] int commentId, string? username, int? slotId)
[HttpPost("deleteComment/{slotType}/{slotId:int}")]
public async Task<IActionResult> DeleteComment([FromQuery] int commentId, string? username, string? slotType, int slotId)
{
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
@ -110,6 +122,10 @@ public class CommentController : ControllerBase
Comment? comment = await this.database.Comments.FirstOrDefaultAsync(c => c.CommentId == commentId);
if (comment == null) return this.NotFound();
if (comment.Type == CommentType.Level && (SlotHelper.IsTypeInvalid(slotType) || slotId == 0)) return this.BadRequest();
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
// if you are not the poster
if (comment.PosterUserId != user.UserId)
{
@ -125,7 +141,7 @@ public class CommentController : ControllerBase
{
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == comment.TargetId);
// if you aren't the creator of the level
if (slot == null || slot.CreatorId != user.UserId || slotId.GetValueOrDefault() != slot.SlotId)
if (slot == null || slot.CreatorId != user.UserId || slotId != slot.SlotId)
{
return this.StatusCode(403, "");
}

View file

@ -8,7 +8,6 @@ using LBPUnion.ProjectLighthouse.Match.MatchCommands;
using LBPUnion.ProjectLighthouse.Match.Rooms;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -71,7 +70,7 @@ public class MatchController : ControllerBase
#endregion
await LastContactHelper.SetLastContact(user, gameToken.GameVersion, gameToken.Platform);
await LastContactHelper.SetLastContact(this.database, user, gameToken.GameVersion, gameToken.Platform);
#region Process match data

View file

@ -3,11 +3,11 @@ using System.Xml.Serialization;
using Discord;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -53,6 +53,38 @@ public class PhotosController : ControllerBase
photo.CreatorId = user.UserId;
photo.Creator = user;
if (photo.XmlLevelInfo != null)
{
bool validLevel = false;
PhotoSlot photoSlot = photo.XmlLevelInfo;
if (photoSlot.SlotType is SlotType.Pod or SlotType.Local) photoSlot.SlotId = 0;
switch (photoSlot.SlotType)
{
case SlotType.User:
{
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.SlotId == photoSlot.SlotId);
if (slot != null) validLevel = slot.RootLevel == photoSlot.RootLevel;
break;
}
case SlotType.Pod:
case SlotType.Local:
case SlotType.Developer:
{
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == photoSlot.SlotType && s.InternalSlotId == photoSlot.SlotId);
if (slot != null)
photoSlot.SlotId = slot.SlotId;
else
photoSlot.SlotId = await SlotHelper.GetPlaceholderSlotId(this.database, photoSlot.SlotId, photoSlot.SlotType);
validLevel = true;
break;
}
default: Logger.Warn($"Invalid photo level type: {photoSlot.SlotType}", LogArea.Photos);
break;
}
if (validLevel) photo.SlotId = photo.XmlLevelInfo.SlotId;
}
if (photo.Subjects.Count > 4) return this.BadRequest();
if (photo.Timestamp > TimeHelper.Timestamp) photo.Timestamp = TimeHelper.Timestamp;
@ -104,11 +136,23 @@ public class PhotosController : ControllerBase
return this.Ok();
}
[HttpGet("photos/user/{id:int}")]
public async Task<IActionResult> SlotPhotos(int id)
[HttpGet("photos/{slotType}/{id:int}")]
public async Task<IActionResult> SlotPhotos([FromQuery] int pageStart, [FromQuery] int pageSize, string slotType, int id)
{
List<Photo> photos = await this.database.Photos.Include(p => p.Creator).Take(10).ToListAsync();
string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize(id));
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();
if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
List<Photo> photos = await this.database.Photos.Include(p => p.Creator)
.Where(p => p.SlotId == id)
.OrderByDescending(s => s.Timestamp)
.Skip(pageStart - 1)
.Take(Math.Min(pageSize, 30))
.ToListAsync();
string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize(id, SlotHelper.ParseType(slotType)));
return this.Ok(LbpSerializer.StringElement("photos", response));
}
@ -126,7 +170,7 @@ public class PhotosController : ControllerBase
.Skip(pageStart - 1)
.Take(Math.Min(pageSize, 30))
.ToListAsync();
string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize(0));
string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize());
return this.Ok(LbpSerializer.StringElement("photos", response));
}
@ -145,7 +189,7 @@ public class PhotosController : ControllerBase
(s => s.Timestamp)
.Skip(pageStart - 1)
.Take(Math.Min(pageSize, 30))
.Aggregate(string.Empty, (s, photo) => s + photo.Serialize(0));
.Aggregate(string.Empty, (s, photo) => s + photo.Serialize());
return this.Ok(LbpSerializer.StringElement("photos", response));
}

View file

@ -1,10 +1,10 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

View file

@ -1,6 +1,7 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
@ -23,13 +24,15 @@ public class ScoreController : ControllerBase
this.database = database;
}
[HttpPost("scoreboard/user/{id:int}")]
public async Task<IActionResult> SubmitScore(int id, [FromQuery] bool lbp1 = false, [FromQuery] bool lbp2 = false, [FromQuery] bool lbp3 = false)
[HttpPost("scoreboard/{slotType}/{id:int}")]
public async Task<IActionResult> SubmitScore(string slotType, int id, [FromQuery] bool lbp1 = false, [FromQuery] bool lbp2 = false, [FromQuery] bool lbp3 = false)
{
(User, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request);
if (userAndToken == null) return this.StatusCode(403, "");
if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();
// ReSharper disable once PossibleInvalidOperationException
User user = userAndToken.Value.Item1;
GameToken gameToken = userAndToken.Value.Item2;
@ -43,6 +46,8 @@ public class ScoreController : ControllerBase
SanitizationHelper.SanitizeStringsInClass(score);
if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
score.SlotId = id;
Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId);
@ -92,15 +97,19 @@ public class ScoreController : ControllerBase
//=> await TopScores(slotId, type);
=> this.Ok(LbpSerializer.BlankElement("scores"));
[HttpGet("topscores/user/{slotId:int}/{type:int}")]
[HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")]
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public async Task<IActionResult> TopScores(int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5)
public async Task<IActionResult> TopScores(string slotType, int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5)
{
// Get username
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
return this.Ok(this.getScores(slotId, type, user, pageStart, pageSize));
}

View file

@ -8,7 +8,6 @@ using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.PlayerData.Reviews;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -65,6 +64,47 @@ public class SlotsController : ControllerBase
);
}
[HttpGet("slotList")]
public async Task<IActionResult> GetSlotListAlt([FromQuery] int[] s)
{
GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, "");
List<string?> serializedSlots = new();
foreach (int slotId in s)
{
Slot? slot = await this.database.Slots.Include(t => t.Creator).Include(t => t.Location).Where(t => t.SlotId == slotId && t.Type == SlotType.User).FirstOrDefaultAsync();
if (slot == null)
{
slot = await this.database.Slots.Where(t => t.InternalSlotId == slotId && t.Type == SlotType.Developer).FirstOrDefaultAsync();
if (slot == null)
{
serializedSlots.Add($"<slot type=\"developer\"><id>{slotId}</id></slot>");
continue;
}
}
serializedSlots.Add(slot.Serialize());
}
string serialized = serializedSlots.Aggregate(string.Empty, (current, slot) => slot == null ? current : current + slot);
return this.Ok(LbpSerializer.TaggedStringElement("slots", serialized, "total", serializedSlots.Count));
}
[HttpGet("s/developer/{id:int}")]
public async Task<IActionResult> SDev(int id)
{
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, "");
int slotId = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
Slot slot = await this.database.Slots.FirstAsync(s => s.SlotId == slotId);
return this.Ok(slot.SerializeDevSlot());
}
[HttpGet("s/user/{id:int}")]
public async Task<IActionResult> SUser(int id)
{
@ -458,10 +498,10 @@ public class SlotsController : ControllerBase
if (gameFilterType == "both")
// Get game versions less than the current version
// Needs support for LBP3 ("both" = LBP1+2)
whereSlots = this.database.Slots.Where(s => s.GameVersion <= gameVersion && s.FirstUploaded >= oldestTime);
whereSlots = this.database.Slots.Where(s => s.Type == SlotType.User && s.GameVersion <= gameVersion && s.FirstUploaded >= oldestTime);
else
// Get game versions exactly equal to gamefiltertype
whereSlots = this.database.Slots.Where(s => s.GameVersion == gameVersion && s.FirstUploaded >= oldestTime);
whereSlots = this.database.Slots.Where(s => s.Type == SlotType.User && s.GameVersion == gameVersion && s.FirstUploaded >= oldestTime);
return whereSlots.Include(s => s.Creator).Include(s => s.Location);
}