Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rate-limit submissions #1736

Merged
merged 6 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
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 = 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>
}
}
}
32 changes: 31 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 (!SubmissionAllowed(User.GetUserId()))
{
return RedirectToPage("/Submissions/Submit");
}

await ValidateModel();

if (!ModelState.IsValid)
Expand Down Expand Up @@ -183,4 +193,24 @@ 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 bool SubmissionAllowed(int userId)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be async

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