Skip to content

Commit 7f27da4

Browse files
authored
Moderation Actions (#981)
This is another attempt at implementing a moderation log, using an independent `ModerationAction` entity. Also adds DB methods for creating and retrieving them, aswell as tests for these methods.
2 parents d11bc7b + b63da3c commit 7f27da4

File tree

8 files changed

+441
-1
lines changed

8 files changed

+441
-1
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using Refresh.Database.Models.Users;
2+
using Refresh.Database.Models.Moderation;
3+
using Refresh.Database.Models.Levels;
4+
using Refresh.Database.Models.Levels.Scores;
5+
using Refresh.Database.Models.Photos;
6+
using Refresh.Database.Models.Comments;
7+
using Refresh.Database.Models.Playlists;
8+
using Refresh.Database.Models.Assets;
9+
using Refresh.Database.Models.Levels.Challenges;
10+
11+
namespace Refresh.Database;
12+
13+
public partial class GameDatabaseContext // Moderation
14+
{
15+
private IQueryable<ModerationAction> ModerationActionsIncluded => this.ModerationActions
16+
.Include(s => s.Actor)
17+
.Include(s => s.InvolvedUser);
18+
19+
#region Retrieval
20+
21+
public DatabaseList<ModerationAction> GetModerationActions(int skip, int count)
22+
{
23+
return new(this.ModerationActionsIncluded.OrderByDescending(a => a.Timestamp), skip, count);
24+
}
25+
26+
public DatabaseList<ModerationAction> GetModerationActionsByActor(GameUser actor, int skip, int count)
27+
{
28+
return new(this.ModerationActionsIncluded
29+
.Where(a => a.ActorId == actor.UserId)
30+
.OrderByDescending(a => a.Timestamp), skip, count);
31+
}
32+
33+
public DatabaseList<ModerationAction> GetModerationActionsForInvolvedUser(GameUser involvedUser, int skip, int count)
34+
{
35+
return new(this.ModerationActionsIncluded
36+
.Where(a => a.InvolvedUserId == involvedUser.UserId)
37+
.OrderByDescending(a => a.Timestamp), skip, count);
38+
}
39+
40+
public DatabaseList<ModerationAction> GetModerationActionsForObject(string id, ModerationObjectType objectType, int skip, int count)
41+
{
42+
return new(this.ModerationActionsIncluded
43+
.Where(a => a.ModeratedObjectType == objectType && a.ModeratedObjectId == id)
44+
.OrderByDescending(a => a.Timestamp), skip, count);
45+
}
46+
47+
#endregion
48+
49+
#region Creation
50+
51+
public ModerationAction CreateModerationAction(GameUser user, ModerationActionType actionType, GameUser actor, string description)
52+
=> this.CreateModerationActionInternal(user.UserId.ToString(), ModerationObjectType.User, actionType, actor, user, description);
53+
54+
public ModerationAction CreateModerationAction(GameLevel level, ModerationActionType actionType, GameUser actor, string description)
55+
=> this.CreateModerationActionInternal(level.LevelId.ToString(), ModerationObjectType.Level, actionType, actor, level.Publisher, description);
56+
57+
public ModerationAction CreateModerationAction(GameScore score, ModerationActionType actionType, GameUser actor, string description)
58+
=> this.CreateModerationActionInternal(score.ScoreId.ToString(), ModerationObjectType.Score, actionType, actor, score.Publisher, description);
59+
60+
public ModerationAction CreateModerationAction(GamePhoto photo, ModerationActionType actionType, GameUser actor, string description)
61+
=> this.CreateModerationActionInternal(photo.PhotoId.ToString(), ModerationObjectType.Photo, actionType, actor, photo.Publisher, description);
62+
63+
public ModerationAction CreateModerationAction(GameReview review, ModerationActionType actionType, GameUser actor, string description)
64+
=> this.CreateModerationActionInternal(review.ReviewId.ToString(), ModerationObjectType.Review, actionType, actor, review.Publisher, description);
65+
66+
public ModerationAction CreateModerationAction(GameLevelComment comment, ModerationActionType actionType, GameUser actor, string description)
67+
=> this.CreateModerationActionInternal(comment.SequentialId.ToString(), ModerationObjectType.LevelComment, actionType, actor, comment.Author, description);
68+
69+
public ModerationAction CreateModerationAction(GameProfileComment comment, ModerationActionType actionType, GameUser actor, string description)
70+
=> this.CreateModerationActionInternal(comment.SequentialId.ToString(), ModerationObjectType.UserComment, actionType, actor, comment.Author, description);
71+
72+
public ModerationAction CreateModerationAction(GamePlaylist playlist, ModerationActionType actionType, GameUser actor, string description)
73+
=> this.CreateModerationActionInternal(playlist.PlaylistId.ToString(), ModerationObjectType.Playlist, actionType, actor, playlist.Publisher, description);
74+
75+
public ModerationAction CreateModerationAction(GameAsset asset, ModerationActionType actionType, GameUser actor, string description)
76+
=> this.CreateModerationActionInternal(asset.AssetHash, ModerationObjectType.Asset, actionType, actor, asset.OriginalUploader, description);
77+
78+
public ModerationAction CreateModerationAction(GameChallenge challenge, ModerationActionType actionType, GameUser actor, string description)
79+
=> this.CreateModerationActionInternal(challenge.ChallengeId.ToString(), ModerationObjectType.Challenge, actionType, actor, challenge.Publisher, description);
80+
81+
public ModerationAction CreateModerationAction(GameChallengeScore score, ModerationActionType actionType, GameUser actor, string description)
82+
=> this.CreateModerationActionInternal(score.ScoreId.ToString(), ModerationObjectType.Score, actionType, actor, score.Publisher, description);
83+
84+
private ModerationAction CreateModerationActionInternal(string id, ModerationObjectType objectType, ModerationActionType actionType, GameUser actor, GameUser? involvedUser, string description)
85+
{
86+
ModerationAction moderationAction = new()
87+
{
88+
ModeratedObjectId = id,
89+
ModeratedObjectType = objectType,
90+
ActionType = actionType,
91+
Actor = actor,
92+
InvolvedUser = involvedUser,
93+
Description = description,
94+
Timestamp = this._time.Now,
95+
};
96+
97+
this.ModerationActions.Add(moderationAction);
98+
this.SaveChanges();
99+
100+
return moderationAction;
101+
}
102+
103+
#endregion
104+
}

Refresh.Database/GameDatabaseContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Refresh.Database.Models.Statistics;
2323
using Refresh.Database.Models.Workers;
2424
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
25+
using Refresh.Database.Models.Moderation;
2526

2627
namespace Refresh.Database;
2728

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

8385
#pragma warning disable CS8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable.
8486
internal GameDatabaseContext(Logger logger, IDateTimeProvider time, IDatabaseConfig dbConfig)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
4+
#nullable disable
5+
6+
namespace Refresh.Database.Migrations
7+
{
8+
/// <inheritdoc />
9+
[DbContext(typeof(GameDatabaseContext))]
10+
[Migration("20251028183803_AddModerationAction")]
11+
public partial class AddModerationAction : Migration
12+
{
13+
/// <inheritdoc />
14+
protected override void Up(MigrationBuilder migrationBuilder)
15+
{
16+
migrationBuilder.CreateTable(
17+
name: "ModerationActions",
18+
columns: table => new
19+
{
20+
ActionId = table.Column<string>(type: "text", nullable: false),
21+
ActionType = table.Column<byte>(type: "smallint", nullable: false),
22+
ModeratedObjectType = table.Column<byte>(type: "smallint", nullable: false),
23+
ModeratedObjectId = table.Column<string>(type: "text", nullable: false),
24+
ActorId = table.Column<string>(type: "text", nullable: false),
25+
InvolvedUserId = table.Column<string>(type: "text", nullable: true),
26+
Description = table.Column<string>(type: "text", nullable: false),
27+
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
28+
},
29+
constraints: table =>
30+
{
31+
table.PrimaryKey("PK_ModerationActions", x => x.ActionId);
32+
table.ForeignKey(
33+
name: "FK_ModerationActions_GameUsers_ActorId",
34+
column: x => x.ActorId,
35+
principalTable: "GameUsers",
36+
principalColumn: "UserId",
37+
onDelete: ReferentialAction.Cascade);
38+
table.ForeignKey(
39+
name: "FK_ModerationActions_GameUsers_InvolvedUserId",
40+
column: x => x.InvolvedUserId,
41+
principalTable: "GameUsers",
42+
principalColumn: "UserId");
43+
});
44+
45+
migrationBuilder.CreateIndex(
46+
name: "IX_ModerationActions_ActorId",
47+
table: "ModerationActions",
48+
column: "ActorId");
49+
50+
migrationBuilder.CreateIndex(
51+
name: "IX_ModerationActions_InvolvedUserId",
52+
table: "ModerationActions",
53+
column: "InvolvedUserId");
54+
}
55+
56+
/// <inheritdoc />
57+
protected override void Down(MigrationBuilder migrationBuilder)
58+
{
59+
migrationBuilder.DropTable(
60+
name: "ModerationActions");
61+
}
62+
}
63+
}

Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
1818
{
1919
#pragma warning disable 612, 618
2020
modelBuilder
21-
.HasAnnotation("ProductVersion", "9.0.9")
21+
.HasAnnotation("ProductVersion", "9.0.10")
2222
.HasAnnotation("Relational:MaxIdentifierLength", 63);
2323

2424
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -598,6 +598,43 @@ protected override void BuildModel(ModelBuilder modelBuilder)
598598
b.ToTable("GameScores");
599599
});
600600

601+
modelBuilder.Entity("Refresh.Database.Models.Moderation.ModerationAction", b =>
602+
{
603+
b.Property<string>("ActionId")
604+
.HasColumnType("text");
605+
606+
b.Property<byte>("ActionType")
607+
.HasColumnType("smallint");
608+
609+
b.Property<string>("ActorId")
610+
.IsRequired()
611+
.HasColumnType("text");
612+
613+
b.Property<string>("Description")
614+
.HasColumnType("text");
615+
616+
b.Property<string>("InvolvedUserId")
617+
.HasColumnType("text");
618+
619+
b.Property<string>("ModeratedObjectId")
620+
.IsRequired()
621+
.HasColumnType("text");
622+
623+
b.Property<byte>("ModeratedObjectType")
624+
.HasColumnType("smallint");
625+
626+
b.Property<DateTimeOffset>("Timestamp")
627+
.HasColumnType("timestamp with time zone");
628+
629+
b.HasKey("ActionId");
630+
631+
b.HasIndex("ActorId");
632+
633+
b.HasIndex("InvolvedUserId");
634+
635+
b.ToTable("ModerationActions");
636+
});
637+
601638
modelBuilder.Entity("Refresh.Database.Models.Notifications.GameAnnouncement", b =>
602639
{
603640
b.Property<string>("AnnouncementId")
@@ -1912,6 +1949,23 @@ protected override void BuildModel(ModelBuilder modelBuilder)
19121949
b.Navigation("Publisher");
19131950
});
19141951

1952+
modelBuilder.Entity("Refresh.Database.Models.Moderation.ModerationAction", b =>
1953+
{
1954+
b.HasOne("Refresh.Database.Models.Users.GameUser", "Actor")
1955+
.WithMany()
1956+
.HasForeignKey("ActorId")
1957+
.OnDelete(DeleteBehavior.Cascade)
1958+
.IsRequired();
1959+
1960+
b.HasOne("Refresh.Database.Models.Users.GameUser", "InvolvedUser")
1961+
.WithMany()
1962+
.HasForeignKey("InvolvedUserId");
1963+
1964+
b.Navigation("Actor");
1965+
1966+
b.Navigation("InvolvedUser");
1967+
});
1968+
19151969
modelBuilder.Entity("Refresh.Database.Models.Notifications.GameNotification", b =>
19161970
{
19171971
b.HasOne("Refresh.Database.Models.Users.GameUser", "User")
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using MongoDB.Bson;
2+
using Refresh.Database.Models.Users;
3+
4+
namespace Refresh.Database.Models.Moderation;
5+
6+
#nullable disable
7+
8+
public partial class ModerationAction
9+
{
10+
[Key] public ObjectId ActionId { get; set; } = ObjectId.GenerateNewId();
11+
12+
/// <summary>
13+
/// Describes what was done with the object.
14+
/// </summary>
15+
public ModerationActionType ActionType { get; set; }
16+
17+
/// <summary>
18+
/// The type of data/object that was moderated.
19+
/// </summary>
20+
public ModerationObjectType ModeratedObjectType { get; set; }
21+
22+
/// <summary>
23+
/// The ID of the object that was moderated. May be a UUID, sequential ID, or a GameAsset hash,
24+
/// depending on ModeratedObjectType's value
25+
/// </summary>
26+
[Required] public string ModeratedObjectId { get; set; }
27+
28+
/// <summary>
29+
/// The user in question who has moderated the object.
30+
/// </summary>
31+
[Required, ForeignKey(nameof(ActorId))] public GameUser Actor { get; set; }
32+
[Required] public ObjectId ActorId { get; set; }
33+
34+
#nullable restore
35+
36+
/// <summary>
37+
/// Usually the publisher/owner of the object that was moderated. May also see this moderation action.
38+
/// </summary>
39+
[ForeignKey(nameof(InvolvedUserId))] public GameUser? InvolvedUser { get; set; }
40+
public ObjectId? InvolvedUserId { get; set; }
41+
42+
#nullable disable
43+
44+
/// <summary>
45+
/// A description, stating the reason of this moderation action.
46+
/// </summary>
47+
public string Description { get; set; }
48+
49+
public DateTimeOffset Timestamp { get; set; }
50+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using Newtonsoft.Json.Converters;
2+
3+
namespace Refresh.Database.Models.Moderation;
4+
5+
[JsonConverter(typeof(StringEnumConverter))]
6+
public enum ModerationActionType : byte
7+
{
8+
// Users
9+
UserModification,
10+
UserDeletion,
11+
UserPunishment,
12+
UserPardon,
13+
PinProgressDeletion,
14+
15+
// Levels
16+
LevelModification,
17+
LevelDeletion,
18+
19+
// Playlists
20+
PlaylistModification,
21+
PlaylistDeletion,
22+
23+
// Photos
24+
PhotoDeletion,
25+
PhotosByUserDeletion,
26+
27+
// Scores
28+
ScoreDeletion,
29+
ScoresByUserForLevelDeletion,
30+
ScoresByUserDeletion,
31+
32+
// Reviews
33+
ReviewDeletion,
34+
ReviewsByUserDeletion,
35+
36+
// Comments
37+
LevelCommentDeletion,
38+
LevelCommentsByUserDeletion,
39+
ProfileCommentDeletion,
40+
ProfileCommentsByUserDeletion,
41+
42+
// Assets
43+
BlockAsset,
44+
UnblockAsset,
45+
46+
// Challenges
47+
ChallengeDeletion,
48+
ChallengesByUserDeletion,
49+
ChallengeScoreDeletion,
50+
ChallengeScoresByUserDeletion,
51+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Refresh.Database.Models.Moderation;
2+
3+
public enum ModerationObjectType : byte
4+
{
5+
User,
6+
Level,
7+
Score,
8+
Photo,
9+
Review,
10+
LevelComment,
11+
UserComment,
12+
Playlist,
13+
Asset,
14+
Challenge,
15+
ChallengeScore,
16+
}

0 commit comments

Comments
 (0)