diff --git a/Refresh.GameServer/Database/GameDatabaseContext.Comments.cs b/Refresh.GameServer/Database/GameDatabaseContext.Comments.cs index 1df42c9f..093129fc 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.Comments.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.Comments.cs @@ -7,67 +7,75 @@ namespace Refresh.GameServer.Database; public partial class GameDatabaseContext // Comments { - public GameComment? GetCommentById(int id) => - this.GameComments.FirstOrDefault(c => c.SequentialId == id); - public GameComment PostCommentToProfile(GameUser profile, GameUser author, string content) + public GameProfileComment? GetProfileCommentById(int id) => + this.GameProfileComments.FirstOrDefault(c => c.SequentialId == id); + + public GameProfileComment PostCommentToProfile(GameUser profile, GameUser author, string content) { - GameComment comment = new() + GameProfileComment comment = new() { Author = author, + Profile = profile, Content = content, Timestamp = this._time.TimestampMilliseconds, }; - this.AddSequentialObject(comment, profile.ProfileComments); + this.AddSequentialObject(comment); return comment; } - public IEnumerable GetProfileComments(GameUser profile, int count, int skip) => - profile.ProfileComments + public IEnumerable GetProfileComments(GameUser profile, int count, int skip) => + this.GameProfileComments + .Where(c => c.Profile == profile) .OrderByDescending(c => c.Timestamp) .AsEnumerable() .Skip(skip) .Take(count); [Pure] - public int GetTotalCommentsForProfile(GameUser profile) => profile.ProfileComments.Count; + public int GetTotalCommentsForProfile(GameUser profile) => this.GameProfileComments.Count(c => c.Profile == profile); - public void DeleteProfileComment(GameComment comment, GameUser profile) + public void DeleteProfileComment(GameProfileComment comment, GameUser profile) { this.Write(() => { - profile.ProfileComments.Remove(comment); + this.GameProfileComments.Remove(comment); }); } + + public GameLevelComment? GetLevelCommentById(int id) => + this.GameLevelComments.FirstOrDefault(c => c.SequentialId == id); - public GameComment PostCommentToLevel(GameLevel level, GameUser author, string content) + public GameLevelComment PostCommentToLevel(GameLevel level, GameUser author, string content) { - GameComment comment = new() + GameLevelComment comment = new() { Author = author, + Level = level, Content = content, Timestamp = this._time.TimestampMilliseconds, }; - this.AddSequentialObject(comment, level.LevelComments); + this.AddSequentialObject(comment); return comment; } - public IEnumerable GetLevelComments(GameLevel level, int count, int skip) => - level.LevelComments - .OrderByDescending(c => c.Timestamp) - .AsEnumerable() - .Skip(skip) - .Take(count); + public IEnumerable GetLevelComments(GameLevel level, int count, int skip) => + this.GameLevelComments + .Where(c => c.Level == level) + .OrderByDescending(c => c.Timestamp) + .AsEnumerable() + .Skip(skip) + .Take(count); [Pure] - public int GetTotalCommentsForLevel(GameLevel level) => level.LevelComments.Count; + public int GetTotalCommentsForLevel(GameLevel level) => this.GameLevelComments.Count(c => c.Level == level); - public void DeleteLevelComment(GameComment comment, GameLevel level) + public void DeleteLevelComment(GameLevelComment comment, GameLevel level) { this.Write(() => { - level.LevelComments.Remove(comment); + this.GameLevelComments.Remove(comment); }); } } \ No newline at end of file diff --git a/Refresh.GameServer/Database/GameDatabaseContext.Relations.cs b/Refresh.GameServer/Database/GameDatabaseContext.Relations.cs index 7813f87d..60df85b2 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.Relations.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.Relations.cs @@ -1,9 +1,11 @@ using System.Diagnostics.Contracts; +using Realms; using Refresh.GameServer.Authentication; using Refresh.GameServer.Endpoints.Game.Levels.FilterSettings; using Refresh.GameServer.Extensions; using Refresh.GameServer.Types; using Refresh.GameServer.Types.Comments; +using Refresh.GameServer.Types.Comments.Relations; using Refresh.GameServer.Types.Levels; using Refresh.GameServer.Types.Relations; using Refresh.GameServer.Types.Reviews; @@ -414,33 +416,52 @@ public int GetUniquePlaysForLevel(GameLevel level) => #region Comments - private CommentRelation? GetCommentRelationByUser(GameComment comment, GameUser user) => this.CommentRelations + private ProfileCommentRelation? GetProfileCommentRelationByUser(GameProfileComment comment, GameUser user) => this.ProfileCommentRelations + .FirstOrDefault(r => r.Comment == comment && r.User == user); + + private LevelCommentRelation? GetLevelCommentRelationByUser(GameLevelComment comment, GameUser user) => this.LevelCommentRelations .FirstOrDefault(r => r.Comment == comment && r.User == user); /// - /// Get a user's rating on a particular comment. + /// Get a user's rating on a particular profile comment. /// A null return value means a user has not set a rating. /// /// The comment to check /// The user to check /// The rating if found [Pure] - public RatingType? GetRatingByUser(GameComment comment, GameUser user) - => this.GetCommentRelationByUser(comment, user)?.RatingType; + public RatingType? GetProfileCommentRatingByUser(GameProfileComment comment, GameUser user) + => this.GetProfileCommentRelationByUser(comment, user)?.RatingType; - public int GetTotalRatingsForComment(GameComment comment, RatingType type) => - this.CommentRelations.Count(r => r.Comment == comment && r._RatingType == (int)type); + /// + /// Get a user's rating on a particular level comment. + /// A null return value means a user has not set a rating. + /// + /// The comment to check + /// The user to check + /// The rating if found + [Pure] + public RatingType? GetLevelCommentRatingByUser(GameLevelComment comment, GameUser user) + => this.GetLevelCommentRelationByUser(comment, user)?.RatingType; + + public int GetTotalRatingsForProfileComment(GameProfileComment comment, RatingType type) => + this.ProfileCommentRelations.Count(r => r.Comment == comment && r._RatingType == (int)type); - public bool RateComment(GameUser user, GameComment comment, RatingType ratingType) + public int GetTotalRatingsForLevelComment(GameLevelComment comment, RatingType type) => + this.LevelCommentRelations.Count(r => r.Comment == comment && r._RatingType == (int)type); + + private bool RateComment(GameUser user, TComment comment, RatingType ratingType, RealmDbSet list) + where TComment : class, IGameComment + where TCommentRelation : class, ICommentRelation, new() { if (ratingType == RatingType.Neutral) return false; - CommentRelation? relation = GetCommentRelationByUser(comment, user); - + TCommentRelation? relation = list.FirstOrDefault(r => r.Comment == comment && r.User == user); + if (relation == null) { - relation = new CommentRelation + relation = new TCommentRelation { User = user, Comment = comment, @@ -450,7 +471,7 @@ public bool RateComment(GameUser user, GameComment comment, RatingType ratingTyp this.Write(() => { - this.CommentRelations.Add(relation); + list.Add(relation); }); } else @@ -464,6 +485,12 @@ public bool RateComment(GameUser user, GameComment comment, RatingType ratingTyp return true; } + + public bool RateProfileComment(GameUser user, GameProfileComment comment, RatingType ratingType) + => this.RateComment(user, comment, ratingType, this.ProfileCommentRelations); + + public bool RateLevelComment(GameUser user, GameLevelComment comment, RatingType ratingType) + => this.RateComment(user, comment, ratingType, this.LevelCommentRelations); #endregion } \ No newline at end of file diff --git a/Refresh.GameServer/Database/GameDatabaseContext.cs b/Refresh.GameServer/Database/GameDatabaseContext.cs index a1b248fe..6df31a23 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.cs @@ -8,9 +8,9 @@ using Refresh.GameServer.Types.Activity; using Refresh.GameServer.Types.Assets; using Refresh.GameServer.Types.Comments; +using Refresh.GameServer.Types.Comments.Relations; using Refresh.GameServer.Types.Contests; using Refresh.GameServer.Types.Levels; -using Refresh.GameServer.Types.Levels.SkillRewards; using Refresh.GameServer.Types.Notifications; using Refresh.GameServer.Types.Photos; using Refresh.GameServer.Types.Relations; @@ -30,8 +30,10 @@ public partial class GameDatabaseContext : RealmDatabaseContext private RealmDbSet GameUsers => new(this._realm); private RealmDbSet Tokens => new(this._realm); private RealmDbSet GameLevels => new(this._realm); - private RealmDbSet GameComments => new(this._realm); - private RealmDbSet CommentRelations => new(this._realm); + private RealmDbSet GameProfileComments => new(this._realm); + private RealmDbSet GameLevelComments => new(this._realm); + private RealmDbSet ProfileCommentRelations => new(this._realm); + private RealmDbSet LevelCommentRelations => new(this._realm); private RealmDbSet FavouriteLevelRelations => new(this._realm); private RealmDbSet QueueLevelRelations => new(this._realm); private RealmDbSet FavouriteUserRelations => new(this._realm); diff --git a/Refresh.GameServer/Database/GameDatabaseProvider.cs b/Refresh.GameServer/Database/GameDatabaseProvider.cs index 6587f430..07673d57 100644 --- a/Refresh.GameServer/Database/GameDatabaseProvider.cs +++ b/Refresh.GameServer/Database/GameDatabaseProvider.cs @@ -8,6 +8,7 @@ using Refresh.GameServer.Time; using Refresh.GameServer.Types.Activity; using Refresh.GameServer.Types.Assets; +using Refresh.GameServer.Types.Comments.Relations; using Refresh.GameServer.Types.Contests; using Refresh.GameServer.Types.Levels.SkillRewards; using Refresh.GameServer.Types.Notifications; @@ -32,7 +33,7 @@ protected GameDatabaseProvider(IDateTimeProvider time) this._time = time; } - protected override ulong SchemaVersion => 134; + protected override ulong SchemaVersion => 135; protected override string Filename => "refreshGameServer.realm"; @@ -43,8 +44,10 @@ protected GameDatabaseProvider(IDateTimeProvider time) typeof(Token), typeof(GameLevel), typeof(GameSkillReward), - typeof(GameComment), - typeof(CommentRelation), + typeof(GameProfileComment), + typeof(GameLevelComment), + typeof(ProfileCommentRelation), + typeof(LevelCommentRelation), typeof(FavouriteLevelRelation), typeof(QueueLevelRelation), typeof(FavouriteUserRelation), @@ -181,6 +184,30 @@ protected override void Migrate(Migration migration, ulong oldVersion) newUser.LocationX = (int)oldUser.Location.X; newUser.LocationY = (int)oldUser.Location.Y; } + + // In version 134, we split GameComments into multiple tables. + // This migration creates GameProfileComments + if (oldVersion < 134) + { + foreach (dynamic comment in oldUser.ProfileComments) + { + GameUser? author = comment.Author != null ? migration.NewRealm.Find(comment.Author.UserId) : null; + if (author == null) + { + Console.WriteLine($"Skipping migration for profile comment id {comment.SequentialId} due to missing author"); + continue; + } + + migration.NewRealm.Add(new GameProfileComment + { + SequentialId = (int)comment.SequentialId, + Author = author, + Profile = newUser, + Content = comment.Content, + Timestamp = comment.Timestamp, + }); + } + } } IQueryable? oldLevels = migration.OldRealm.DynamicApi.All("GameLevel"); @@ -248,6 +275,30 @@ protected override void Migrate(Migration migration, ulong oldVersion) newLevel.LocationX = (int)oldLevel.Location.X; newLevel.LocationY = (int)oldLevel.Location.Y; } + + // In version 134, we split GameComments into multiple tables. + // This migration creates GameLevelComments + if (oldVersion < 134) + { + foreach (dynamic comment in oldLevel.LevelComments) + { + GameUser? author = comment.Author != null ? migration.NewRealm.Find(comment.Author.UserId) : null; + if (author == null) + { + Console.WriteLine($"Skipping migration for level comment id {comment.SequentialId} due to missing author"); + continue; + } + + migration.NewRealm.Add(new GameLevelComment + { + SequentialId = (int)comment.SequentialId, + Author = author, + Level = newLevel, + Content = comment.Content, + Timestamp = comment.Timestamp, + }); + } + } } // In version 22, tokens added expiry and types so just wipe them all @@ -309,21 +360,6 @@ protected override void Migrate(Migration migration, ulong oldVersion) if (oldVersion < 36) newToken.LoginDate = DateTimeOffset.FromUnixTimeMilliseconds(timestampMilliseconds); } - IQueryable? oldComments = migration.OldRealm.DynamicApi.All("GameComment"); - IQueryable? newComments = migration.NewRealm.All(); - - for (int i = 0; i < newComments.Count(); i++) - { - dynamic oldComment = oldComments.ElementAt(i); - GameComment newComment = newComments.ElementAt(i); - - // In version 40, we switched to Realm source generators which requires some values to be reset - if (oldVersion < 40) - { - newComment.Content = oldComment.Content; - } - } - IQueryable? oldPhotos = migration.OldRealm.DynamicApi.All("GamePhoto"); IQueryable? newPhotos = migration.NewRealm.All(); diff --git a/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs b/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs index 5f0b9923..518ed9f7 100644 --- a/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs @@ -19,7 +19,7 @@ namespace Refresh.GameServer.Endpoints.Game; public class CommentEndpoints : EndpointGroup { [GameEndpoint("postUserComment/{username}", ContentType.Xml, HttpMethods.Post)] - public Response PostProfileComment(RequestContext context, GameDatabaseContext database, string username, GameComment body, GameUser user, IDateTimeProvider timeProvider) + public Response PostProfileComment(RequestContext context, GameDatabaseContext database, string username, SerializedComment body, GameUser user, IDateTimeProvider timeProvider) { if (body.Content.Length > 4096) { @@ -43,10 +43,7 @@ public Response PostProfileComment(RequestContext context, GameDatabaseContext d (int skip, int count) = context.GetPageData(); - List comments = database.GetProfileComments(profile, count, skip).ToList(); - foreach (GameComment comment in comments) comment.PrepareForSerialization(user, dataContext); - - return new SerializedCommentList(comments); + return new SerializedCommentList(SerializedComment.FromOldList(database.GetProfileComments(profile, count, skip), dataContext)); } [GameEndpoint("deleteUserComment/{username}", HttpMethods.Post)] @@ -57,7 +54,7 @@ public Response DeleteProfileComment(RequestContext context, GameDatabaseContext GameUser? profile = database.GetUserByUsername(username); if (profile == null) return NotFound; - GameComment? comment = profile.ProfileComments.FirstOrDefault(comment => comment.SequentialId == commentId); + GameProfileComment? comment = database.GetProfileCommentById(commentId); if (comment == null) return BadRequest; //Validate someone doesnt try to delete someone elses comment @@ -73,7 +70,7 @@ public Response DeleteProfileComment(RequestContext context, GameDatabaseContext } [GameEndpoint("postComment/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] - public Response PostLevelComment(RequestContext context, GameDatabaseContext database, string slotType, int id, GameComment body, GameUser user) + public Response PostLevelComment(RequestContext context, GameDatabaseContext database, string slotType, int id, SerializedComment body, GameUser user) { if (body.Content.Length > 4096) { @@ -98,10 +95,7 @@ public Response PostLevelComment(RequestContext context, GameDatabaseContext dat (int skip, int count) = context.GetPageData(); - List comments = database.GetLevelComments(level, count, skip).ToList(); - foreach(GameComment comment in comments) comment.PrepareForSerialization(user, dataContext); - - return new SerializedCommentList(comments); + return new SerializedCommentList(SerializedComment.FromOldList(database.GetLevelComments(level, count, skip), dataContext)); } [GameEndpoint("deleteComment/{slotType}/{id}", HttpMethods.Post)] @@ -111,8 +105,8 @@ public Response DeleteLevelComment(RequestContext context, GameDatabaseContext d GameLevel? level = database.GetLevelByIdAndType(slotType, id); if (level == null) return NotFound; - - GameComment? comment = level.LevelComments.FirstOrDefault(comment => comment.SequentialId == commentId); + + GameLevelComment? comment = database.GetLevelCommentById(commentId); if (comment == null) return BadRequest; //Validate someone doesnt try to delete someone else's comment @@ -126,20 +120,35 @@ public Response DeleteLevelComment(RequestContext context, GameDatabaseContext d return OK; } + + [GameEndpoint("rateUserComment/{content}", HttpMethods.Post)] // profile comments + public Response RateProfileComment(RequestContext context, GameDatabaseContext database, GameUser user, string content) + { + if (!int.TryParse(context.QueryString["commentId"], out int commentId)) return BadRequest; + if (!Enum.TryParse(context.QueryString["rating"], out RatingType ratingType)) return BadRequest; + GameProfileComment? comment = database.GetProfileCommentById(commentId); + if (comment == null) + return NotFound; + + if (!database.RateProfileComment(user, comment, ratingType)) + return BadRequest; + + return OK; + } + [GameEndpoint("rateComment/user/{content}", HttpMethods.Post)] // `user` level comments [GameEndpoint("rateComment/developer/{content}", HttpMethods.Post)] // `developer` level comments - [GameEndpoint("rateUserComment/{content}", HttpMethods.Post)] // profile comments - public Response RateComment(RequestContext context, GameDatabaseContext database, GameUser user, string content) + public Response RateLevelComment(RequestContext context, GameDatabaseContext database, GameUser user, string content) { if (!int.TryParse(context.QueryString["commentId"], out int commentId)) return BadRequest; if (!Enum.TryParse(context.QueryString["rating"], out RatingType ratingType)) return BadRequest; - GameComment? comment = database.GetCommentById(commentId); + GameLevelComment? comment = database.GetLevelCommentById(commentId); if (comment == null) return NotFound; - if (!database.RateComment(user, comment, ratingType)) + if (!database.RateLevelComment(user, comment, ratingType)) return BadRequest; return OK; diff --git a/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameLevelResponse.cs b/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameLevelResponse.cs index 9c663e52..0e45b364 100644 --- a/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameLevelResponse.cs +++ b/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameLevelResponse.cs @@ -173,7 +173,7 @@ public static GameLevelResponse FromHash(string hash, DataContext dataContext) Links = "", AverageStarRating = old.CalculateAverageStarRating(dataContext.Database), ReviewCount = old.Reviews.Count, - CommentCount = old.LevelComments.Count, + CommentCount = dataContext.Database.GetTotalCommentsForLevel(old), }; response.Type = "user"; @@ -221,7 +221,7 @@ public static GameLevelResponse FromHash(string hash, DataContext dataContext) response.IconHash = dataContext.Database.GetAssetFromHash(old.IconHash)?.GetAsIcon(dataContext.Game, dataContext) ?? response.IconHash; - response.CommentCount = old.LevelComments.Count; + response.CommentCount = dataContext.Database.GetTotalCommentsForLevel(old); return response; } diff --git a/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameUserResponse.cs b/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameUserResponse.cs index b7526959..2f72d91e 100644 --- a/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameUserResponse.cs +++ b/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameUserResponse.cs @@ -63,7 +63,7 @@ public class GameUserResponse : IDataConvertableFrom PlanetsHash = "0", Handle = SerializedUserHandle.FromUser(old, dataContext), - CommentCount = old.ProfileComments.Count, + CommentCount = dataContext.Database.GetTotalCommentsForProfile(old), CommentsEnabled = true, FavouriteLevelCount = old.IsManaged ? dataContext.Database.GetTotalLevelsFavouritedByUser(old) : 0, FavouriteUserCount = old.IsManaged ? dataContext.Database.GetTotalUsersFavouritedByUser(old) : 0, diff --git a/Refresh.GameServer/Types/Comments/GameComment.cs b/Refresh.GameServer/Types/Comments/GameComment.cs deleted file mode 100644 index 0dd6ba68..00000000 --- a/Refresh.GameServer/Types/Comments/GameComment.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Xml.Serialization; -using Realms; -using Refresh.GameServer.Database; -using Refresh.GameServer.Types.Data; -using Refresh.GameServer.Types.Reviews; -using Refresh.GameServer.Types.UserData; -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - -namespace Refresh.GameServer.Types.Comments; - -[XmlRoot("comment")] -[XmlType("comment")] -public partial class GameComment : IRealmObject, ISequentialId -{ - [PrimaryKey] [XmlElement("id")] public int SequentialId { get; set; } - - [XmlIgnore] public GameUser Author { get; set; } = null!; - [XmlElement("message")] public string Content { get; set; } = string.Empty; - - /// - /// Timestamp in Unix milliseconds - /// - [XmlElement("timestamp")] public long Timestamp { get; set; } - - #region LBP Serialization Quirks - - // Comments are special; they do not include icons in the npHandle - [XmlElement("npHandle")] [Ignored] public string? Handle { get; set; } - [XmlElement("thumbsup")] [Ignored] public int? ThumbsUp { get; set; } - [XmlElement("thumbsdown")] [Ignored] public int? ThumbsDown { get; set; } - [XmlElement("yourthumb")] [Ignored] public int? YourThumb { get; set; } - - public void PrepareForSerialization(GameUser user, DataContext dataContext) - { - this.Handle = this.Author.Username; - - this.ThumbsUp = dataContext.Database.GetTotalRatingsForComment(this, RatingType.Yay); - this.ThumbsDown = dataContext.Database.GetTotalRatingsForComment(this, RatingType.Boo); - - this.YourThumb = (int?)dataContext.Database.GetRatingByUser(this, user) ?? 0; - } - - #endregion - -} diff --git a/Refresh.GameServer/Types/Comments/GameLevelComment.cs b/Refresh.GameServer/Types/Comments/GameLevelComment.cs new file mode 100644 index 00000000..4d68cf3b --- /dev/null +++ b/Refresh.GameServer/Types/Comments/GameLevelComment.cs @@ -0,0 +1,25 @@ +using Realms; +using Refresh.GameServer.Database; +using Refresh.GameServer.Types.Levels; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Comments; + +public partial class GameLevelComment : IRealmObject, IGameComment, ISequentialId +{ + [PrimaryKey] public int SequentialId { get; set; } + + /// + public GameUser Author { get; set; } = null!; + + /// + /// The destination level this comment was posted to. + /// + public GameLevel Level { get; set; } = null!; + + /// + public string Content { get; set; } = string.Empty; + + /// + public long Timestamp { get; set; } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Comments/GameProfileComment.cs b/Refresh.GameServer/Types/Comments/GameProfileComment.cs new file mode 100644 index 00000000..7a15f6cd --- /dev/null +++ b/Refresh.GameServer/Types/Comments/GameProfileComment.cs @@ -0,0 +1,24 @@ +using Realms; +using Refresh.GameServer.Database; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Comments; + +public partial class GameProfileComment : IRealmObject, IGameComment, ISequentialId +{ + [PrimaryKey] public int SequentialId { get; set; } + + /// + public GameUser Author { get; set; } = null!; + + /// + /// The destination profile this comment was posted to. + /// + public GameUser Profile { get; set; } = null!; + + /// + public string Content { get; set; } = string.Empty; + + /// + public long Timestamp { get; set; } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Comments/IGameComment.cs b/Refresh.GameServer/Types/Comments/IGameComment.cs new file mode 100644 index 00000000..04d0f063 --- /dev/null +++ b/Refresh.GameServer/Types/Comments/IGameComment.cs @@ -0,0 +1,23 @@ +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Comments; + +public interface IGameComment +{ + int SequentialId { get; set; } + + /// + /// The user who originally posted the comment. + /// + GameUser Author { get; set; } + + /// + /// The text content of the comment. + /// + string Content { get; set; } + + /// + /// Timestamp in Unix milliseconds + /// + long Timestamp { get; set; } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Comments/Relations/ICommentRelation.cs b/Refresh.GameServer/Types/Comments/Relations/ICommentRelation.cs new file mode 100644 index 00000000..e5c8f6c8 --- /dev/null +++ b/Refresh.GameServer/Types/Comments/Relations/ICommentRelation.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson; +using Realms; +using Refresh.GameServer.Types.Reviews; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Comments.Relations; + +public interface ICommentRelation : IRealmObject + where TComment : IGameComment +{ + ObjectId CommentRelationId { get; set; } + GameUser User { get; set; } + TComment Comment { get; set; } + RatingType RatingType { get; set; } + DateTimeOffset Timestamp { get; set; } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Relations/CommentRelation.cs b/Refresh.GameServer/Types/Comments/Relations/LevelCommentRelation.cs similarity index 69% rename from Refresh.GameServer/Types/Relations/CommentRelation.cs rename to Refresh.GameServer/Types/Comments/Relations/LevelCommentRelation.cs index ad01817d..86f04bad 100644 --- a/Refresh.GameServer/Types/Relations/CommentRelation.cs +++ b/Refresh.GameServer/Types/Comments/Relations/LevelCommentRelation.cs @@ -1,17 +1,15 @@ -using MongoDB.Bson; +using MongoDB.Bson; using Realms; -using Refresh.GameServer.Types.Comments; using Refresh.GameServer.Types.Reviews; using Refresh.GameServer.Types.UserData; -namespace Refresh.GameServer.Types.Relations; -#nullable disable +namespace Refresh.GameServer.Types.Comments.Relations; -public partial class CommentRelation : IRealmObject +public partial class LevelCommentRelation : IRealmObject, ICommentRelation { public ObjectId CommentRelationId { get; set; } = ObjectId.GenerateNewId(); public GameUser User { get; set; } - public GameComment Comment { get; set; } + public GameLevelComment Comment { get; set; } [Ignored] public RatingType RatingType { diff --git a/Refresh.GameServer/Types/Comments/Relations/ProfileCommentRelation.cs b/Refresh.GameServer/Types/Comments/Relations/ProfileCommentRelation.cs new file mode 100644 index 00000000..1f76ed10 --- /dev/null +++ b/Refresh.GameServer/Types/Comments/Relations/ProfileCommentRelation.cs @@ -0,0 +1,23 @@ +using MongoDB.Bson; +using Realms; +using Refresh.GameServer.Types.Reviews; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Comments.Relations; + +public partial class ProfileCommentRelation : IRealmObject, ICommentRelation +{ + public ObjectId CommentRelationId { get; set; } = ObjectId.GenerateNewId(); + public GameUser User { get; set; } + public GameProfileComment Comment { get; set; } + [Ignored] + public RatingType RatingType + { + get => (RatingType)this._RatingType; + set => this._RatingType = (int)value; + } + + // ReSharper disable once InconsistentNaming + internal int _RatingType { get; set; } + public DateTimeOffset Timestamp { get; set; } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Comments/SerializedComment.cs b/Refresh.GameServer/Types/Comments/SerializedComment.cs new file mode 100644 index 00000000..d7750fbd --- /dev/null +++ b/Refresh.GameServer/Types/Comments/SerializedComment.cs @@ -0,0 +1,61 @@ +using System.Xml.Serialization; +using Refresh.GameServer.Endpoints.ApiV3.DataTypes; +using Refresh.GameServer.Types.Data; +using Refresh.GameServer.Types.Reviews; + +namespace Refresh.GameServer.Types.Comments; + +[XmlRoot("comment")] +[XmlType("comment")] +public class SerializedComment : IDataConvertableFrom, IDataConvertableFrom +{ + [XmlElement("id")] public required int CommentId { get; set; } + + [XmlElement("message")] public required string Content { get; set; } = string.Empty; + + /// + /// Timestamp in Unix milliseconds + /// + [XmlElement("timestamp")] public required long Timestamp { get; set; } + + // Comments are special; they do not include icons in the npHandle + [XmlElement("npHandle")] public required string? Handle { get; set; } + + [XmlElement("thumbsup")] public int? ThumbsUp { get; set; } + [XmlElement("thumbsdown")] public int? ThumbsDown { get; set; } + [XmlElement("yourthumb")] public int? YourThumb { get; set; } + + private static SerializedComment FromBase(IGameComment comment) => + new() + { + CommentId = comment.SequentialId, + Content = comment.Content, + Timestamp = comment.Timestamp, + Handle = comment.Author.Username, + }; + + public static SerializedComment? FromOld(GameProfileComment? old, DataContext dataContext) + { + if (old == null) return null; + + SerializedComment comment = FromBase(old); + comment.ThumbsUp = dataContext.Database.GetTotalRatingsForProfileComment(old, RatingType.Yay); + comment.ThumbsDown = dataContext.Database.GetTotalRatingsForProfileComment(old, RatingType.Boo); + comment.YourThumb = (int?)dataContext.Database.GetProfileCommentRatingByUser(old, dataContext.User!) ?? 0; + return comment; + } + + public static SerializedComment? FromOld(GameLevelComment? old, DataContext dataContext) + { + if (old == null) return null; + + SerializedComment comment = FromBase(old); + comment.ThumbsUp = dataContext.Database.GetTotalRatingsForLevelComment(old, RatingType.Yay); + comment.ThumbsDown = dataContext.Database.GetTotalRatingsForLevelComment(old, RatingType.Boo); + comment.YourThumb = (int?)dataContext.Database.GetLevelCommentRatingByUser(old, dataContext.User!) ?? 0; + return comment; + } + + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) => oldList.Select(old => FromOld(old, dataContext)).ToList()!; + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) => oldList.Select(old => FromOld(old, dataContext)).ToList()!; +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Levels/GameLevel.cs b/Refresh.GameServer/Types/Levels/GameLevel.cs index e31099c0..fee85df6 100644 --- a/Refresh.GameServer/Types/Levels/GameLevel.cs +++ b/Refresh.GameServer/Types/Levels/GameLevel.cs @@ -92,8 +92,6 @@ [Ignored] public GameLevelSource Source public float Score { get; set; } #nullable disable - public IList LevelComments { get; } - // ILists can't be serialized to XML, and Lists/Arrays cannot be stored in realm, // hence _SkillRewards and SkillRewards both existing // ReSharper disable once InconsistentNaming diff --git a/Refresh.GameServer/Types/Lists/SerializedCommentList.cs b/Refresh.GameServer/Types/Lists/SerializedCommentList.cs index 0c11053c..12a5eace 100644 --- a/Refresh.GameServer/Types/Lists/SerializedCommentList.cs +++ b/Refresh.GameServer/Types/Lists/SerializedCommentList.cs @@ -9,11 +9,11 @@ public class SerializedCommentList { public SerializedCommentList() {} - public SerializedCommentList(IEnumerable comments) + public SerializedCommentList(IEnumerable comments) { this.Items = comments.ToList(); } [XmlElement("comment")] - public List Items { get; set; } = new(); + public List Items { get; set; } = []; } \ No newline at end of file diff --git a/Refresh.GameServer/Types/UserData/GameUser.cs b/Refresh.GameServer/Types/UserData/GameUser.cs index 539c9718..aefe81dc 100644 --- a/Refresh.GameServer/Types/UserData/GameUser.cs +++ b/Refresh.GameServer/Types/UserData/GameUser.cs @@ -61,10 +61,6 @@ public partial class GameUser : IRealmObject, IRateLimitUser public DateTimeOffset JoinDate { get; set; } public UserPins Pins { get; set; } = new(); - - #nullable disable - public IList ProfileComments { get; } - #nullable restore public string BetaPlanetsHash { get; set; } = "0"; public string Lbp2PlanetsHash { get; set; } = "0"; diff --git a/RefreshTests.GameServer/Tests/Comments/CommentTests.cs b/RefreshTests.GameServer/Tests/Comments/CommentTests.cs index 72c45d7b..deacd791 100644 --- a/RefreshTests.GameServer/Tests/Comments/CommentTests.cs +++ b/RefreshTests.GameServer/Tests/Comments/CommentTests.cs @@ -9,7 +9,7 @@ namespace RefreshTests.GameServer.Tests.Comments; public class CommentTests : GameServerTest { - public static void RateComment(TestContext context, GameUser user, GameComment comment, string rateCommentUrl, string getCommentsUrl) + public static void RateComment(TestContext context, GameUser user, IGameComment comment, string rateCommentUrl, string getCommentsUrl) { using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); @@ -24,7 +24,7 @@ public static void RateComment(TestContext context, GameUser user, GameComment c HttpResponseMessage response = client.GetAsync(getCommentsUrl).Result; SerializedCommentList userComments = response.Content.ReadAsXML(); - comment = userComments.Items.First(); + SerializedComment serializedComment = userComments.Items.First(); int expectedThumbsUp, expectedThumbsDown; @@ -48,9 +48,9 @@ public static void RateComment(TestContext context, GameUser user, GameComment c Assert.Multiple(() => { - Assert.That(comment.YourThumb, Is.EqualTo(ratingType.ToDPad())); - Assert.That(comment.ThumbsUp, Is.EqualTo(expectedThumbsUp)); - Assert.That(comment.ThumbsDown, Is.EqualTo(expectedThumbsDown)); + Assert.That(serializedComment.YourThumb, Is.EqualTo(ratingType.ToDPad())); + Assert.That(serializedComment.ThumbsUp, Is.EqualTo(expectedThumbsUp)); + Assert.That(serializedComment.ThumbsDown, Is.EqualTo(expectedThumbsDown)); }); } diff --git a/RefreshTests.GameServer/Tests/Comments/LevelCommentTests.cs b/RefreshTests.GameServer/Tests/Comments/LevelCommentTests.cs index 7c52b2c7..86639eaa 100644 --- a/RefreshTests.GameServer/Tests/Comments/LevelCommentTests.cs +++ b/RefreshTests.GameServer/Tests/Comments/LevelCommentTests.cs @@ -18,10 +18,12 @@ public void PostAndDeleteLevelComment() using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); - GameComment comment = new() + SerializedComment comment = new() { - Author = user, Content = "This is a test comment!", + CommentId = 0, + Timestamp = 0, + Handle = null, }; HttpResponseMessage response = client.PostAsync($"/lbp/postComment/user/{level.LevelId}", new StringContent(comment.AsXML())).Result; @@ -32,7 +34,7 @@ public void PostAndDeleteLevelComment() Assert.That(userComments.Items, Has.Count.EqualTo(1)); Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content)); - response = client.PostAsync($"/lbp/deleteComment/user/{level.LevelId}?commentId={userComments.Items[0].SequentialId}", new ByteArrayContent(Array.Empty())).Result; + response = client.PostAsync($"/lbp/deleteComment/user/{level.LevelId}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty())).Result; Assert.That(response.StatusCode, Is.EqualTo(OK)); response = client.GetAsync($"/lbp/comments/user/{level.LevelId}").Result; @@ -49,10 +51,12 @@ public void CantPostTooLongLevelComment() using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); - GameComment comment = new() + SerializedComment comment = new() { - Author = user, Content = new string('S', 5000), + CommentId = 0, + Timestamp = 0, + Handle = null, }; HttpResponseMessage response = client.PostAsync($"/lbp/postComment/user/{level.LevelId}", new StringContent(comment.AsXML())).Result; @@ -67,10 +71,12 @@ public void CantPostCommentToInvalidLevel() using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); - GameComment comment = new() + SerializedComment comment = new() { - Author = user, - Content = "This is a test comment", + Content = "This is a test comment!", + CommentId = 0, + Timestamp = 0, + Handle = null, }; HttpResponseMessage response = client.PostAsync($"/lbp/postComment/user/I_AM_NOT_REAL", new StringContent(comment.AsXML())).Result; @@ -138,10 +144,12 @@ public void CantDeleteAnotherUsersComment() using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, user1); using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, user2); - GameComment comment = new() + SerializedComment comment = new() { - Author = user1, Content = "This is a test comment!", + CommentId = 0, + Timestamp = 0, + Handle = null, }; HttpResponseMessage response = client1.PostAsync($"/lbp/postComment/user/{level.LevelId}", new StringContent(comment.AsXML())).Result; @@ -152,7 +160,7 @@ public void CantDeleteAnotherUsersComment() Assert.That(userComments.Items, Has.Count.EqualTo(1)); Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content)); - response = client2.PostAsync($"/lbp/deleteComment/user/{level.LevelId}?commentId={userComments.Items[0].SequentialId}", new ByteArrayContent(Array.Empty())).Result; + response = client2.PostAsync($"/lbp/deleteComment/user/{level.LevelId}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty())).Result; Assert.That(response.StatusCode, Is.EqualTo(Unauthorized)); } @@ -162,7 +170,7 @@ public void RateUserLevelComment() using TestContext context = this.GetServer(); GameUser user = context.CreateUser(); GameLevel level = context.CreateLevel(user); - GameComment comment = context.Database.PostCommentToLevel(level, user, "This is a test comment!"); + GameLevelComment comment = context.Database.PostCommentToLevel(level, user, "This is a test comment!"); CommentTests.RateComment(context, user, comment, $"/lbp/rateComment/user/{level.LevelId}", $"/lbp/comments/user/{level.LevelId}"); } @@ -175,7 +183,7 @@ public void RateDeveloperLevelComment() using TestContext context = this.GetServer(); GameUser user = context.CreateUser(); GameLevel level = context.Database.GetStoryLevelById(levelId); - GameComment comment = context.Database.PostCommentToLevel(level, user, "This is a test comment!"); + GameLevelComment comment = context.Database.PostCommentToLevel(level, user, "This is a test comment!"); CommentTests.RateComment(context, user, comment, $"/lbp/rateComment/developer/{level.LevelId}", $"/lbp/comments/developer/{level.LevelId}"); } diff --git a/RefreshTests.GameServer/Tests/Comments/UserCommentTests.cs b/RefreshTests.GameServer/Tests/Comments/UserCommentTests.cs index 6b5cebd6..f062d70e 100644 --- a/RefreshTests.GameServer/Tests/Comments/UserCommentTests.cs +++ b/RefreshTests.GameServer/Tests/Comments/UserCommentTests.cs @@ -17,10 +17,12 @@ public void PostAndDeleteUserComment() using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user1); - GameComment comment = new() + SerializedComment comment = new() { - Author = user1, Content = "This is a test comment!", + CommentId = 0, + Timestamp = 0, + Handle = null, }; HttpResponseMessage response = client.PostAsync($"/lbp/postUserComment/{user2.Username}", new StringContent(comment.AsXML())).Result; @@ -31,7 +33,7 @@ public void PostAndDeleteUserComment() Assert.That(userComments.Items, Has.Count.EqualTo(1)); Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content)); - response = client.PostAsync($"/lbp/deleteUserComment/{user2.Username}?commentId={userComments.Items[0].SequentialId}", new ByteArrayContent(Array.Empty())).Result; + response = client.PostAsync($"/lbp/deleteUserComment/{user2.Username}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty())).Result; Assert.That(response.StatusCode, Is.EqualTo(OK)); response = client.GetAsync($"/lbp/userComments/{user2.Username}").Result; @@ -48,10 +50,12 @@ public void CantPostTooLongUserComment() using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user1); - GameComment comment = new() + SerializedComment comment = new() { - Author = user1, Content = new string('S', 5000), + CommentId = 0, + Timestamp = 0, + Handle = null, }; HttpResponseMessage response = client.PostAsync($"/lbp/postUserComment/{user2.Username}", new StringContent(comment.AsXML())).Result; @@ -66,10 +70,12 @@ public void CantUserCommentToInvalidUser() using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); - GameComment comment = new() + SerializedComment comment = new() { - Author = user, - Content = "This is a test comment", + Content = "This is a test comment!", + CommentId = 0, + Timestamp = 0, + Handle = null, }; HttpResponseMessage response = client.PostAsync($"/lbp/postUserComment/I_AM_NOT_REAL", new StringContent(comment.AsXML())).Result; @@ -134,10 +140,12 @@ public void CantDeleteAnotherUsersComment() using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, user1); using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, user2); - GameComment comment = new() + SerializedComment comment = new() { - Author = user1, Content = "This is a test comment!", + CommentId = 0, + Timestamp = 0, + Handle = null, }; HttpResponseMessage response = client1.PostAsync($"/lbp/postUserComment/{user2.Username}", new StringContent(comment.AsXML())).Result; @@ -148,7 +156,7 @@ public void CantDeleteAnotherUsersComment() Assert.That(userComments.Items, Has.Count.EqualTo(1)); Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content)); - response = client2.PostAsync($"/lbp/deleteUserComment/{user2.Username}?commentId={userComments.Items[0].SequentialId}", new ByteArrayContent(Array.Empty())).Result; + response = client2.PostAsync($"/lbp/deleteUserComment/{user2.Username}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty())).Result; Assert.That(response.StatusCode, Is.EqualTo(Unauthorized)); } @@ -157,7 +165,7 @@ public void RateProfileComment() { using TestContext context = this.GetServer(); GameUser user = context.CreateUser(); - GameComment comment = context.Database.PostCommentToProfile(user, user, "This is a test comment!"); + GameProfileComment comment = context.Database.PostCommentToProfile(user, user, "This is a test comment!"); CommentTests.RateComment(context, user, comment, $"/lbp/rateUserComment/{user.Username}", $"/lbp/userComments/{user.Username}"); } diff --git a/RefreshTests.GameServer/Tests/Relations/CommentPublishTests.cs b/RefreshTests.GameServer/Tests/Relations/CommentPublishTests.cs index 707de8c2..6e60d5b4 100644 --- a/RefreshTests.GameServer/Tests/Relations/CommentPublishTests.cs +++ b/RefreshTests.GameServer/Tests/Relations/CommentPublishTests.cs @@ -12,7 +12,7 @@ public void CanCreateCommentOnProfile() GameUser profile = context.CreateUser(); GameUser commenter = context.CreateUser(); - GameComment comment = context.Database.PostCommentToProfile(profile, commenter, "Hi!"); + GameProfileComment comment = context.Database.PostCommentToProfile(profile, commenter, "Hi!"); Assert.That(context.Database.GetProfileComments(profile, 1, 0).First(), Is.EqualTo(comment)); }