Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions Refresh.Database/GameDatabaseContext.Moderation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Refresh.Database.Models.Users;
using Refresh.Database.Models.Moderation;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Levels.Scores;
using Refresh.Database.Models.Photos;
using Refresh.Database.Models.Comments;
using Refresh.Database.Models.Playlists;
using Refresh.Database.Models.Assets;
using Refresh.Database.Models.Levels.Challenges;

namespace Refresh.Database;

public partial class GameDatabaseContext // Moderation
{
private IQueryable<ModerationAction> ModerationActionsIncluded => this.ModerationActions
.Include(s => s.Actor)
.Include(s => s.InvolvedUser);

#region Retrieval

public DatabaseList<ModerationAction> GetModerationActions(int skip, int count)
{
return new(this.ModerationActionsIncluded.OrderByDescending(a => a.Timestamp), skip, count);
}

public DatabaseList<ModerationAction> GetModerationActionsByActor(GameUser actor, int skip, int count)
{
return new(this.ModerationActionsIncluded
.Where(a => a.ActorId == actor.UserId)
.OrderByDescending(a => a.Timestamp), skip, count);
}

public DatabaseList<ModerationAction> GetModerationActionsForInvolvedUser(GameUser involvedUser, int skip, int count)
{
return new(this.ModerationActionsIncluded
.Where(a => a.InvolvedUserId == involvedUser.UserId)
.OrderByDescending(a => a.Timestamp), skip, count);
}

public DatabaseList<ModerationAction> GetModerationActionsForObject(string id, ModerationObjectType objectType, int skip, int count)
{
return new(this.ModerationActionsIncluded
.Where(a => a.ModeratedObjectType == objectType && a.ModeratedObjectId == id)
.OrderByDescending(a => a.Timestamp), skip, count);
}

#endregion

#region Creation

public ModerationAction CreateModerationAction(GameUser user, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(user.UserId.ToString(), ModerationObjectType.User, actionType, actor, user, description);

public ModerationAction CreateModerationAction(GameLevel level, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(level.LevelId.ToString(), ModerationObjectType.Level, actionType, actor, level.Publisher, description);

public ModerationAction CreateModerationAction(GameScore score, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(score.ScoreId.ToString(), ModerationObjectType.Score, actionType, actor, score.Publisher, description);

public ModerationAction CreateModerationAction(GamePhoto photo, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(photo.PhotoId.ToString(), ModerationObjectType.Photo, actionType, actor, photo.Publisher, description);

public ModerationAction CreateModerationAction(GameReview review, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(review.ReviewId.ToString(), ModerationObjectType.Review, actionType, actor, review.Publisher, description);

public ModerationAction CreateModerationAction(GameLevelComment comment, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(comment.SequentialId.ToString(), ModerationObjectType.LevelComment, actionType, actor, comment.Author, description);

public ModerationAction CreateModerationAction(GameProfileComment comment, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(comment.SequentialId.ToString(), ModerationObjectType.UserComment, actionType, actor, comment.Author, description);

public ModerationAction CreateModerationAction(GamePlaylist playlist, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(playlist.PlaylistId.ToString(), ModerationObjectType.Playlist, actionType, actor, playlist.Publisher, description);

public ModerationAction CreateModerationAction(GameAsset asset, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(asset.AssetHash, ModerationObjectType.Asset, actionType, actor, asset.OriginalUploader, description);

public ModerationAction CreateModerationAction(GameChallenge challenge, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(challenge.ChallengeId.ToString(), ModerationObjectType.Challenge, actionType, actor, challenge.Publisher, description);

public ModerationAction CreateModerationAction(GameChallengeScore score, ModerationActionType actionType, GameUser actor, string description)
=> this.CreateModerationActionInternal(score.ScoreId.ToString(), ModerationObjectType.Score, actionType, actor, score.Publisher, description);

private ModerationAction CreateModerationActionInternal(string id, ModerationObjectType objectType, ModerationActionType actionType, GameUser actor, GameUser? involvedUser, string description)
{
ModerationAction moderationAction = new()
{
ModeratedObjectId = id,
ModeratedObjectType = objectType,
ActionType = actionType,
Actor = actor,
InvolvedUser = involvedUser,
Description = description,
Timestamp = this._time.Now,
};

this.ModerationActions.Add(moderationAction);
this.SaveChanges();

return moderationAction;
}

#endregion
}
2 changes: 2 additions & 0 deletions Refresh.Database/GameDatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using Refresh.Database.Models.Statistics;
using Refresh.Database.Models.Workers;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
using Refresh.Database.Models.Moderation;

namespace Refresh.Database;

Expand Down Expand Up @@ -79,6 +80,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext
internal DbSet<WorkerInfo> Workers { get; set; }
internal DbSet<PersistentJobState> JobStates { get; set; }
internal DbSet<GameLevelRevision> GameLevelRevisions { get; set; }
internal DbSet<ModerationAction> ModerationActions { get; set; }

#pragma warning disable CS8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable.
internal GameDatabaseContext(Logger logger, IDateTimeProvider time, IDatabaseConfig dbConfig)
Expand Down
63 changes: 63 additions & 0 deletions Refresh.Database/Migrations/20251028183803_AddModerationAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Refresh.Database.Migrations
{
/// <inheritdoc />
[DbContext(typeof(GameDatabaseContext))]
[Migration("20251028183803_AddModerationAction")]
public partial class AddModerationAction : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ModerationActions",
columns: table => new
{
ActionId = table.Column<string>(type: "text", nullable: false),
ActionType = table.Column<byte>(type: "smallint", nullable: false),
ModeratedObjectType = table.Column<byte>(type: "smallint", nullable: false),
ModeratedObjectId = table.Column<string>(type: "text", nullable: false),
ActorId = table.Column<string>(type: "text", nullable: false),
InvolvedUserId = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: false),
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ModerationActions", x => x.ActionId);
table.ForeignKey(
name: "FK_ModerationActions_GameUsers_ActorId",
column: x => x.ActorId,
principalTable: "GameUsers",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ModerationActions_GameUsers_InvolvedUserId",
column: x => x.InvolvedUserId,
principalTable: "GameUsers",
principalColumn: "UserId");
});

migrationBuilder.CreateIndex(
name: "IX_ModerationActions_ActorId",
table: "ModerationActions",
column: "ActorId");

migrationBuilder.CreateIndex(
name: "IX_ModerationActions_InvolvedUserId",
table: "ModerationActions",
column: "InvolvedUserId");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ModerationActions");
}
}
}
56 changes: 55 additions & 1 deletion Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("ProductVersion", "9.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);

NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
Expand Down Expand Up @@ -598,6 +598,43 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("GameScores");
});

modelBuilder.Entity("Refresh.Database.Models.Moderation.ModerationAction", b =>
{
b.Property<string>("ActionId")
.HasColumnType("text");

b.Property<byte>("ActionType")
.HasColumnType("smallint");

b.Property<string>("ActorId")
.IsRequired()
.HasColumnType("text");

b.Property<string>("Description")
.HasColumnType("text");

b.Property<string>("InvolvedUserId")
.HasColumnType("text");

b.Property<string>("ModeratedObjectId")
.IsRequired()
.HasColumnType("text");

b.Property<byte>("ModeratedObjectType")
.HasColumnType("smallint");

b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone");

b.HasKey("ActionId");

b.HasIndex("ActorId");

b.HasIndex("InvolvedUserId");

b.ToTable("ModerationActions");
});

modelBuilder.Entity("Refresh.Database.Models.Notifications.GameAnnouncement", b =>
{
b.Property<string>("AnnouncementId")
Expand Down Expand Up @@ -1912,6 +1949,23 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("Publisher");
});

modelBuilder.Entity("Refresh.Database.Models.Moderation.ModerationAction", b =>
{
b.HasOne("Refresh.Database.Models.Users.GameUser", "Actor")
.WithMany()
.HasForeignKey("ActorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("Refresh.Database.Models.Users.GameUser", "InvolvedUser")
.WithMany()
.HasForeignKey("InvolvedUserId");

b.Navigation("Actor");

b.Navigation("InvolvedUser");
});

modelBuilder.Entity("Refresh.Database.Models.Notifications.GameNotification", b =>
{
b.HasOne("Refresh.Database.Models.Users.GameUser", "User")
Expand Down
50 changes: 50 additions & 0 deletions Refresh.Database/Models/Moderation/ModerationAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using MongoDB.Bson;
using Refresh.Database.Models.Users;

namespace Refresh.Database.Models.Moderation;

#nullable disable

public partial class ModerationAction
{
[Key] public ObjectId ActionId { get; set; } = ObjectId.GenerateNewId();

/// <summary>
/// Describes what was done with the object.
/// </summary>
public ModerationActionType ActionType { get; set; }

/// <summary>
/// The type of data/object that was moderated.
/// </summary>
public ModerationObjectType ModeratedObjectType { get; set; }

/// <summary>
/// The ID of the object that was moderated. May be a UUID, sequential ID, or a GameAsset hash,
/// depending on ModeratedObjectType's value
/// </summary>
[Required] public string ModeratedObjectId { get; set; }

/// <summary>
/// The user in question who has moderated the object.
/// </summary>
[Required, ForeignKey(nameof(ActorId))] public GameUser Actor { get; set; }
[Required] public ObjectId ActorId { get; set; }

#nullable restore

/// <summary>
/// Usually the publisher/owner of the object that was moderated. May also see this moderation action.
/// </summary>
[ForeignKey(nameof(InvolvedUserId))] public GameUser? InvolvedUser { get; set; }
public ObjectId? InvolvedUserId { get; set; }

#nullable disable

/// <summary>
/// A description, stating the reason of this moderation action.
/// </summary>
public string Description { get; set; }

public DateTimeOffset Timestamp { get; set; }
}
51 changes: 51 additions & 0 deletions Refresh.Database/Models/Moderation/ModerationActionType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Newtonsoft.Json.Converters;

namespace Refresh.Database.Models.Moderation;

[JsonConverter(typeof(StringEnumConverter))]
public enum ModerationActionType : byte
{
// Users
UserModification,
UserDeletion,
UserPunishment,
UserPardon,
PinProgressDeletion,

// Levels
LevelModification,
LevelDeletion,

// Playlists
PlaylistModification,
PlaylistDeletion,

// Photos
PhotoDeletion,
PhotosByUserDeletion,

// Scores
ScoreDeletion,
ScoresByUserForLevelDeletion,
ScoresByUserDeletion,

// Reviews
ReviewDeletion,
ReviewsByUserDeletion,

// Comments
LevelCommentDeletion,
LevelCommentsByUserDeletion,
ProfileCommentDeletion,
ProfileCommentsByUserDeletion,

// Assets
BlockAsset,
UnblockAsset,

// Challenges
ChallengeDeletion,
ChallengesByUserDeletion,
ChallengeScoreDeletion,
ChallengeScoresByUserDeletion,
}
16 changes: 16 additions & 0 deletions Refresh.Database/Models/Moderation/ModerationObjectType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Refresh.Database.Models.Moderation;

public enum ModerationObjectType : byte
{
User,
Level,
Score,
Photo,
Review,
LevelComment,
UserComment,
Playlist,
Asset,
Challenge,
ChallengeScore,
}
Loading