Skip to content

Commit

Permalink
rate-limit submissions (#1736)
Browse files Browse the repository at this point in the history
* fix #1709

* only pull subs from db that are newer than the cutoff
decide if allowed to submit by count of those

* bail if the user happened to have the form opened and still pressed Submit

probably needs to tell them it didn't go through?

* clearer message about the bail
stop hardcoding notice lines

* async?

* prod settings
  • Loading branch information
vadosnaprimer authored Jan 14, 2024
1 parent a0a3cc4 commit cf63d59
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 139 deletions.
9 changes: 9 additions & 0 deletions TASVideos.Core/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public class AppSettings

public Connections ConnectionStrings { get; set; } = new();

public SubmissionRateLimit SubmissionRate { get; set; } = new ();

public IrcConnection Irc { get; set; } = new();
public DiscordConnection Discord { get; set; } = new();

Expand All @@ -24,6 +26,13 @@ public class AppSettings
// Minimum number of hours before a judge can set a submission to accepted/rejected
public int MinimumHoursBeforeJudgment { get; set; }

// User is only allowed to submit X submissions in Y days
public class SubmissionRateLimit
{
public int Submissions { get; set; }
public int Days { get; set; }
}

public class IrcConnection : DistributorConnection
{
public string Server { get; set; } = "";
Expand Down
289 changes: 154 additions & 135 deletions TASVideos/Pages/Submissions/Submit.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -5,143 +5,162 @@
ViewData.SetTitle("Submit Movie");
var notSupportedError = ViewData.ModelState.Values.Any(v => v.Errors.Any(e => e.ErrorMessage.Contains("not currently supported"))); // TODO: a less brittle check
var parseErrors = !notSupportedError && ViewData.ModelState.Keys.Any(e => e == "Parser");
var submissionAllowed = await Model.SubmissionAllowed(User.GetUserId());
var notice = Model.Notice(User.GetUserId());
}
<info-alert dismissible="true">
@await Component.RenderWiki(SystemWiki.SubmitMovieHeader)
</info-alert>
<hr />
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<warning-alert dismissible="true" condition="notSupportedError">
@await Component.RenderWiki(SystemWiki.SupportedMovieTypes)
</warning-alert>
<danger-alert dismissible="true" condition="parseErrors">
@await Component.RenderWiki(SystemWiki.SubmissionZipFailure)
</danger-alert>
<span id="backup-submission-determinator" class="d-none">@Model.BackupSubmissionDeterminator</span>
<form method="post" enctype="multipart/form-data">
<row>
<column lg="6">
<fieldset>
<label asp-for="Create.MovieFile" class="form-control-label"></label>
<input asp-for="Create.MovieFile" class="form-control" />
<div>@Html.DescriptionFor(m => m.Create.MovieFile)</div>
<span asp-validation-for="Create.MovieFile" class="text-danger"></span>
</fieldset>
</column>
<column lg="6">
<fieldset>
<label asp-for="Create.Authors" class="form-control-label"></label>
<string-list asp-for="Create.Authors" />
<span asp-validation-for="Create.Authors" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.AdditionalAuthors" class="form-control-label"></label>
<input asp-for="Create.AdditionalAuthors" class="form-control" />
<div>@Html.DescriptionFor(m => m.Create.AdditionalAuthors)</div>
<span asp-validation-for="Create.AdditionalAuthors" class="text-danger"></span>
</fieldset>
</column>
</row>
<row>
<column lg="6">
<fieldset>
<label asp-for="Create.GameVersion" class="form-control-label"></label>
<input asp-for="Create.GameVersion" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.GameVersion)" />
<span asp-validation-for="Create.GameVersion" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.GameName" class="form-control-label"></label>
<input asp-for="Create.GameName" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.GameName)" />
<span asp-validation-for="Create.GameName" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.Emulator" class="form-control-label"></label>
<input asp-for="Create.Emulator" spellcheck="false" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.Emulator)" />
<span asp-validation-for="Create.Emulator" class="text-danger"></span>
</fieldset>
</column>
<column lg="6">
<fieldset>
<label asp-for="Create.Branch" class="form-control-label"></label>
<input asp-for="Create.Branch" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.Branch)" />
<span asp-validation-for="Create.Branch" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.RomName" class="form-control-label"></label>
<input asp-for="Create.RomName" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.RomName)" />
<span asp-validation-for="Create.RomName" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.EncodeEmbedLink" class="form-control-label"></label>
<input asp-for="Create.EncodeEmbedLink" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.EncodeEmbedLink)" />
<span asp-validation-for="Create.EncodeEmbedLink" class="text-danger"></span>
</fieldset>
</column>
</row>
<fullrow>
<fieldset>
<label asp-for="Create.Markup" class="form-control-label"></label>
<span>
@await Component.RenderWiki(SystemWiki.SubmissionImportant)
</span>
<partial name="_WikiEditHelper" model="@("Create_Markup")" />
<textarea asp-for="Create.Markup" rows="12" class="form-control wiki-edit backup-content" data-backup-key="backup-submission" placeholder="Enter your __wiki markup__ here..."></textarea>
<span asp-validation-for="Create.Markup" class="text-danger"></span>
<fullrow id="backup-restore" class="d-none mt-2">
<button id="backup-restore-button" type="button" class="btn btn-secondary">Restore Text</button>
<label class="text-body-tertiary">from <span id="backup-time"></span></label>

@{
if (!submissionAllowed)
{
<info-alert>
<row>
@foreach (var n in notice)
{
<div class="col-12">@n</div>
}
</row>
</info-alert>
}
else
{
<info-alert dismissible="true">
@await Component.RenderWiki(SystemWiki.SubmitMovieHeader)
</info-alert>
<hr />
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<warning-alert dismissible="true" condition="notSupportedError">
@await Component.RenderWiki(SystemWiki.SupportedMovieTypes)
</warning-alert>
<danger-alert dismissible="true" condition="parseErrors">
@await Component.RenderWiki(SystemWiki.SubmissionZipFailure)
</danger-alert>
<span id="backup-submission-determinator" class="d-none">@Model.BackupSubmissionDeterminator</span>
<form method="post" enctype="multipart/form-data">
<row>
<column lg="6">
<fieldset>
<label asp-for="Create.MovieFile" class="form-control-label"></label>
<input asp-for="Create.MovieFile" class="form-control" />
<div>@Html.DescriptionFor(m => m.Create.MovieFile)</div>
<span asp-validation-for="Create.MovieFile" class="text-danger"></span>
</fieldset>
</column>
<column lg="6">
<fieldset>
<label asp-for="Create.Authors" class="form-control-label"></label>
<string-list asp-for="Create.Authors" />
<span asp-validation-for="Create.Authors" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.AdditionalAuthors" class="form-control-label"></label>
<input asp-for="Create.AdditionalAuthors" class="form-control" />
<div>@Html.DescriptionFor(m => m.Create.AdditionalAuthors)</div>
<span asp-validation-for="Create.AdditionalAuthors" class="text-danger"></span>
</fieldset>
</column>
</row>
<row>
<column lg="6">
<fieldset>
<label asp-for="Create.GameVersion" class="form-control-label"></label>
<input asp-for="Create.GameVersion" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.GameVersion)" />
<span asp-validation-for="Create.GameVersion" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.GameName" class="form-control-label"></label>
<input asp-for="Create.GameName" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.GameName)" />
<span asp-validation-for="Create.GameName" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.Emulator" class="form-control-label"></label>
<input asp-for="Create.Emulator" spellcheck="false" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.Emulator)" />
<span asp-validation-for="Create.Emulator" class="text-danger"></span>
</fieldset>
</column>
<column lg="6">
<fieldset>
<label asp-for="Create.Branch" class="form-control-label"></label>
<input asp-for="Create.Branch" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.Branch)" />
<span asp-validation-for="Create.Branch" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.RomName" class="form-control-label"></label>
<input asp-for="Create.RomName" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.RomName)" />
<span asp-validation-for="Create.RomName" class="text-danger"></span>
</fieldset>
<fieldset>
<label asp-for="Create.EncodeEmbedLink" class="form-control-label"></label>
<input asp-for="Create.EncodeEmbedLink" class="form-control" placeholder="@Html.DescriptionFor(m => m.Create.EncodeEmbedLink)" />
<span asp-validation-for="Create.EncodeEmbedLink" class="text-danger"></span>
</fieldset>
</column>
</row>
<fullrow>
<fieldset>
<label asp-for="Create.Markup" class="form-control-label"></label>
<span>
@await Component.RenderWiki(SystemWiki.SubmissionImportant)
</span>
<partial name="_WikiEditHelper" model="@("Create_Markup")" />
<textarea asp-for="Create.Markup" rows="12" class="form-control wiki-edit backup-content" data-backup-key="backup-submission" placeholder="Enter your __wiki markup__ here..."></textarea>
<span asp-validation-for="Create.Markup" class="text-danger"></span>
<fullrow id="backup-restore" class="d-none mt-2">
<button id="backup-restore-button" type="button" class="btn btn-secondary">Restore Text</button>
<label class="text-body-tertiary">from <span id="backup-time"></span></label>
</fullrow>
<div>
<button id="prefill-btn" type="button" class="btn btn-secondary mt-2">Prefill comments</button>
</div>
</fieldset>
</fullrow>
<div>
<button id="prefill-btn" type="button" class="btn btn-secondary mt-2">Prefill comments</button>
</div>
</fieldset>
</fullrow>
<row class="text-center justify-content-center mb-1 fs-6">
<div class="col-auto">
<div class="form-check">
<input asp-for="Create.AgreeToInstructions" class="form-check-input" />
<label asp-for="Create.AgreeToInstructions" class="form-check-label">I have read and followed the TASVideos <a href="/SubmissionInstructions">Submission Instructions</a>.</label>
<br />
<span asp-validation-for="Create.AgreeToInstructions" class="text-danger"></span>
</div>
</div>
</row>
<row class="text-center justify-content-center mb-3 fs-6">
<div class="col-auto">
<div class="form-check">
<input asp-for="Create.AgreeToLicense" class="form-check-input" />
<label asp-for="Create.AgreeToLicense" class="form-check-label">I agree to publishing this content under the <a href="https://creativecommons.org/licenses/by/2.0/">Creative Commons Attribution 2.0</a> license.</label>
<br />
<span asp-validation-for="Create.AgreeToLicense" class="text-danger"></span>
</div>
</div>
</row>
<fullrow class="text-center">
<button id="preview-button" type="button" class="btn btn-secondary"><i class="fa fa-eye"></i> Preview</button>
<submit-button id="submit-btn" class="btn btn-primary border border-warning @(Model.Create.Markup.Length > 0 ? "" : "d-none")">Submit</submit-button>
</fullrow>
</form>
<row class="text-center justify-content-center mb-1 fs-6">
<div class="col-auto">
<div class="form-check">
<input asp-for="Create.AgreeToInstructions" class="form-check-input" />
<label asp-for="Create.AgreeToInstructions" class="form-check-label">I have read and followed the TASVideos <a href="/SubmissionInstructions">Submission Instructions</a>.</label>
<br />
<span asp-validation-for="Create.AgreeToInstructions" class="text-danger"></span>
</div>
</div>
</row>
<row class="text-center justify-content-center mb-3 fs-6">
<div class="col-auto">
<div class="form-check">
<input asp-for="Create.AgreeToLicense" class="form-check-input" />
<label asp-for="Create.AgreeToLicense" class="form-check-label">I agree to publishing this content under the <a href="https://creativecommons.org/licenses/by/2.0/">Creative Commons Attribution 2.0</a> license.</label>
<br />
<span asp-validation-for="Create.AgreeToLicense" class="text-danger"></span>
</div>
</div>
</row>
<fullrow class="text-center">
<button id="preview-button" type="button" class="btn btn-secondary"><i class="fa fa-eye"></i> Preview</button>
<submit-button id="submit-btn" class="btn btn-primary border border-warning @(Model.Create.Markup.Length > 0 ? "" : "d-none")">Submit</submit-button>
</fullrow>
</form>

<partial name="_PreviewWindow" model="@((Html.IdFor(m => m.Create.Markup), "/Wiki/Preview"))" />
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
document.getElementById('prefill-btn').onclick = function () {
const markup = document.getElementById('@Html.IdFor(m => m.Create.Markup)').value;
if (markup) {
return;
}
<partial name="_PreviewWindow" model="@((Html.IdFor(m => m.Create.Markup), "/Wiki/Preview"))" />
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
document.getElementById('prefill-btn').onclick = function () {
const markup = document.getElementById('@Html.IdFor(m => m.Create.Markup)').value;
if (markup) {
return;
}
fetch("/Submissions/Submit?handler=PrefillText")
.then(handleFetchErrors)
.then(r => r.json())
.then(data => {
document.getElementById('@Html.IdFor(m => m.Create.Markup)').value = data.text;
fetch("/Submissions/Submit?handler=PrefillText")
.then(handleFetchErrors)
.then(r => r.json())
.then(data => {
document.getElementById('@Html.IdFor(m => m.Create.Markup)').value = data.text;
});
};
document.getElementById('preview-button').addEventListener('click', function () {
document.getElementById('submit-btn').classList.remove('d-none');
});
};
document.getElementById('preview-button').addEventListener('click', function () {
document.getElementById('submit-btn').classList.remove('d-none');
});
</script>
<script src="~/js/backup-text.js"></script>
}
</script>
<script src="~/js/backup-text.js"></script>
}
}
}
35 changes: 34 additions & 1 deletion TASVideos/Pages/Submissions/Submit.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using TASVideos.Core.Services.ExternalMediaPublisher;
using TASVideos.Core.Services.Wiki;
using TASVideos.Core.Services.Youtube;
using TASVideos.Core.Settings;
using TASVideos.Data;
using TASVideos.Data.Entity;
using TASVideos.MovieParsers;
Expand All @@ -23,6 +24,8 @@ public class SubmitModel : BasePageModel
private readonly IYoutubeSync _youtubeSync;
private readonly IMovieFormatDeprecator _deprecator;
private readonly IQueueService _queueService;
private readonly AppSettings _settings;
private DateTime _earliestTimestamp;

public SubmitModel(
ApplicationDbContext db,
Expand All @@ -33,7 +36,8 @@ public SubmitModel(
ITASVideoAgent tasVideoAgent,
IYoutubeSync youtubeSync,
IMovieFormatDeprecator deprecator,
IQueueService queueService)
IQueueService queueService,
AppSettings settings)
{
_db = db;
_publisher = publisher;
Expand All @@ -44,6 +48,7 @@ public SubmitModel(
_youtubeSync = youtubeSync;
_deprecator = deprecator;
_queueService = queueService;
_settings = settings;
}

[BindProperty]
Expand All @@ -66,6 +71,11 @@ public async Task OnGet()

public async Task<IActionResult> OnPost()
{
if (!await SubmissionAllowed(User.GetUserId()))
{
return RedirectToPage("/Submissions/Submit");
}

await ValidateModel();

if (!ModelState.IsValid)
Expand Down Expand Up @@ -183,4 +193,27 @@ private async Task ValidateModel()
}
}
}

public string[] Notice(int userId) => new string[]
{
"Sorry, you can not submit at this time.",
"We limit submissions to " +
_settings.SubmissionRate.Submissions +
" in " +
_settings.SubmissionRate.Days +
" days per user. ",
"You will be able to submit again on " +
_earliestTimestamp.AddDays(_settings.SubmissionRate.Days)
};

public async Task<bool> SubmissionAllowed(int userId)
{
var subs = await _db.Submissions
.Where(s => s.Submitter != null
&& s.SubmitterId == userId
&& s.CreateTimestamp > DateTime.UtcNow.AddDays(-_settings.SubmissionRate.Days))
.ToListAsync();
_earliestTimestamp = subs.Select(s => s.CreateTimestamp).Min();
return subs.Count() < _settings.SubmissionRate.Submissions;
}
}
Loading

0 comments on commit cf63d59

Please sign in to comment.