Skip to content
Merged
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
19 changes: 19 additions & 0 deletions Refresh.Core/Configuration/EntityUploadRateLimitProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Refresh.Core.Configuration;

public class EntityUploadRateLimitProperties
{
public EntityUploadRateLimitProperties() {}

/// <summary>
/// Whether to rate-limit uploads of a certain entity (level/photo/playlist) using the database
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// The duration of this rate-limit
/// </summary>
public int TimeSpanHours { get; set; }
/// <summary>
/// The amount of entities the user is allowed to upload during the specified time span
/// </summary>
public int UploadQuota { get; set; }
}
28 changes: 21 additions & 7 deletions Refresh.Core/Configuration/GameServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Refresh.Core.Configuration;
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class GameServerConfig : Config
{
public override int CurrentConfigVersion => 27;
public override int CurrentConfigVersion => 28;
public override int Version { get; set; } = 0;

protected override void Migrate(int oldVer, dynamic oldConfig)
Expand Down Expand Up @@ -82,13 +82,13 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
// Timed level upload limits were added in version 19.
if (oldVer >= 19)
{
this.NormalUserPermissions.TimedLevelUploadLimits.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled;
this.NormalUserPermissions.TimedLevelUploadLimits.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours;
this.NormalUserPermissions.TimedLevelUploadLimits.LevelQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota;
this.NormalUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled;
this.NormalUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours;
this.NormalUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota;

this.TrustedUserPermissions.TimedLevelUploadLimits.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled;
this.TrustedUserPermissions.TimedLevelUploadLimits.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours;
this.TrustedUserPermissions.TimedLevelUploadLimits.LevelQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota;
this.TrustedUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled;
this.TrustedUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours;
this.TrustedUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota;
}

// Read-only mode was added for both normal and trusted users in version 20.
Expand All @@ -98,6 +98,20 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
this.TrustedUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadonlyModeForTrustedUsers;
}
}

// In version 28, PhotoUploadRateLimit and PlaylistUploadRateLimit were added to RolePermissions, and various renamings related to level upload rate-limiting
// were done to prepare for this: the class TimedLevelUploadLimitProperties was renamed to EntityUploadRateLimitProperties, its attribute LevelQuota was renamed to UploadQuota,
// and RolePermissions' attribute TimedLevelUploadLimits was renamed to LevelUploadRateLimit
else if (oldVer == 27)
{
this.NormalUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.Enabled;
this.NormalUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.TimeSpanHours;
this.NormalUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.LevelQuota;

this.TrustedUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.Enabled;
this.TrustedUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.TimeSpanHours;
this.TrustedUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.LevelQuota;
}
}

public string LicenseText { get; set; } = "Welcome to Refresh!";
Expand Down
18 changes: 16 additions & 2 deletions Refresh.Core/Configuration/RolePermissions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@ public RolePermissions() {}

public bool ReadOnlyMode { get; set; } = false;
public ConfigAssetFlags BlockedAssetFlags { get; set; } = new(AssetFlags.Dangerous | AssetFlags.Modded);
public TimedLevelUploadLimitProperties TimedLevelUploadLimits = new()
public EntityUploadRateLimitProperties LevelUploadRateLimit = new()
{
Enabled = false,
TimeSpanHours = 24,
LevelQuota = 10,
UploadQuota = 10,
};

public EntityUploadRateLimitProperties PhotoUploadRateLimit = new()
{
Enabled = false,
TimeSpanHours = 24,
UploadQuota = 10,
};

public EntityUploadRateLimitProperties PlaylistUploadRateLimit = new()
{
Enabled = false,
TimeSpanHours = 24,
UploadQuota = 8,
};

/// <summary>
Expand Down
19 changes: 0 additions & 19 deletions Refresh.Core/Configuration/TimedLevelUploadLimitProperties.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ namespace Refresh.Core.RateLimits.Playlists;
/// </summary>
public static class PlaylistCreationEndpointLimits
{
public const int UploadTimeoutDuration = 450;
public const int MaxCreateAmount = 8; // should be enough
public const int MaxUpdateAmount = 12;
public const int UploadBlockDuration = 300;
public const int UploadTimeoutDuration = 300;
public const int MaxCreateAmount = 30;
public const int MaxUpdateAmount = 50;
public const int UploadBlockDuration = 240;
public const string CreateBucket = "playlist-create";
public const string UpdateBucket = "playlist-update";
}
68 changes: 55 additions & 13 deletions Refresh.Database/GameDatabaseContext.Users.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Refresh.Database.Models.Photos;
using Refresh.Database.Models.Assets;
using System.Diagnostics;
using Refresh.Database.Models;

namespace Refresh.Database;

Expand Down Expand Up @@ -620,31 +621,72 @@ public void MarkAllReuploads(GameUser user)
});
}


public void SetUserPresenceAuthToken(GameUser user, string? token)
{
this.Write(() =>
{
user.PresenceServerAuthToken = token;
});
}
public void IncrementTimedLevelLimit(GameUser user, int hours)

public EntityUploadRateLimit? GetUploadRateLimit(GameUser user, GameDatabaseEntity entity, bool save = true)
{
this.Write(() =>
EntityUploadRateLimit? limit = this.EntityUploadRateLimits.FirstOrDefault(r => r.UserId == user.UserId && r.Entity == entity);

// remove if expired
if (limit != null && limit.ExpiryDate <= this._time.Now)
{
// Set expiry date if the timed limits have been reset previously
user.TimedLevelUploadExpiryDate ??= this._time.Now + TimeSpan.FromHours(hours);
user.TimedLevelUploads++;
});
this.EntityUploadRateLimits.Remove(limit);
if (save) this.SaveChanges();
return null;
}

return limit;
}

public void ResetTimedLevelLimit(GameUser user)
/// <returns>
/// Time until expiry date if the corresponding upload rate-limit has been reached, otherwise null
/// </returns>
public TimeSpan? GetRemainingTimeIfUploadRateLimitReached(GameUser user, GameDatabaseEntity entity, int uploadQuota)
{
this.Write(() =>
EntityUploadRateLimit? limit = this.GetUploadRateLimit(user, entity); // will be null if expired already, see above
DateTimeOffset now = this._time.Now;

if (limit != null && limit.UploadCount >= uploadQuota)
{
user.TimedLevelUploadExpiryDate = null;
user.TimedLevelUploads = 0;
});
return limit.ExpiryDate - now;
}

return null;
}

public void IncrementUploadRateLimitForEntity(GameUser user, GameDatabaseEntity entity, int timeSpanHours)
{
EntityUploadRateLimit? existingLimit = this.GetUploadRateLimit(user, entity, false); // will be null if expired already, see above
DateTimeOffset now = this._time.Now;

if (existingLimit == null)
{
EntityUploadRateLimit newLimit = new()
{
Entity = entity,
User = user,
UploadCount = 1,
ExpiryDate = now + TimeSpan.FromHours(timeSpanHours),
};
this.EntityUploadRateLimits.Add(newLimit);
}
else
{
this.EntityUploadRateLimits.Update(existingLimit);
existingLimit.UploadCount++;
}

this.SaveChanges();
}

public void ResetUploadRateLimit(GameUser user, GameDatabaseEntity entity)
{
this.EntityUploadRateLimits.RemoveRange(r => r.UserId == user.UserId && r.Entity == entity);
}
}
1 change: 1 addition & 0 deletions Refresh.Database/GameDatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext
internal DbSet<PersistentJobState> JobStates { get; set; }
internal DbSet<GameLevelRevision> GameLevelRevisions { get; set; }
internal DbSet<ModerationAction> ModerationActions { get; set; }
internal DbSet<EntityUploadRateLimit> EntityUploadRateLimits { 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Refresh.Database.Migrations
{
/// <inheritdoc />
[DbContext(typeof(GameDatabaseContext))]
[Migration("20260421181503_SplitUploadRateLimitsToSeparateTable")]
public partial class SplitUploadRateLimitsToSeparateTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// probably no need to migrate these, they're temporary anyway
migrationBuilder.DropColumn(
name: "TimedLevelUploadExpiryDate",
table: "GameUsers");

migrationBuilder.DropColumn(
name: "TimedLevelUploads",
table: "GameUsers");

migrationBuilder.CreateTable(
name: "EntityUploadRateLimits",
columns: table => new
{
Entity = table.Column<byte>(type: "smallint", nullable: false),
UserId = table.Column<string>(type: "text", nullable: false),
UploadCount = table.Column<int>(type: "integer", nullable: false),
ExpiryDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EntityUploadRateLimits", x => new { x.UserId, x.Entity });
table.ForeignKey(
name: "FK_EntityUploadRateLimits_GameUsers_UserId",
column: x => x.UserId,
principalTable: "GameUsers",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
});
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EntityUploadRateLimits");

migrationBuilder.AddColumn<DateTimeOffset>(
name: "TimedLevelUploadExpiryDate",
table: "GameUsers",
type: "timestamp with time zone",
nullable: true);

migrationBuilder.AddColumn<int>(
name: "TimedLevelUploads",
table: "GameUsers",
type: "integer",
nullable: false,
defaultValue: 0);
}
}
}
36 changes: 30 additions & 6 deletions Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,25 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("EmailVerificationCodes");
});

modelBuilder.Entity("Refresh.Database.Models.Users.EntityUploadRateLimit", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");

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

b.Property<int>("UploadCount")
.HasColumnType("integer");

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

b.HasKey("UserId", "Entity");

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

modelBuilder.Entity("Refresh.Database.Models.Users.GameIpVerificationRequest", b =>
{
b.Property<string>("UserId")
Expand Down Expand Up @@ -1735,12 +1754,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<string>("StatisticsUserId")
.HasColumnType("text");

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

b.Property<int>("TimedLevelUploads")
.HasColumnType("integer");

b.Property<bool>("UnescapeXmlSequences")
.HasColumnType("boolean");

Expand Down Expand Up @@ -2521,6 +2534,17 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("User");
});

modelBuilder.Entity("Refresh.Database.Models.Users.EntityUploadRateLimit", b =>
{
b.HasOne("Refresh.Database.Models.Users.GameUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

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

modelBuilder.Entity("Refresh.Database.Models.Users.GameIpVerificationRequest", b =>
{
b.HasOne("Refresh.Database.Models.Users.GameUser", "User")
Expand Down
17 changes: 17 additions & 0 deletions Refresh.Database/Models/GameDatabaseEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Refresh.Database.Models;

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