Files
vtuber-awards/Backend/Endpoints/PublicVoteEndpoints.cs
T

146 lines
5.6 KiB
C#

using Backend.Common;
using Backend.Contracts;
using Backend.Data;
using Backend.Domain;
using Backend.Services;
using Microsoft.EntityFrameworkCore;
namespace Backend.Endpoints;
public static partial class PublicEndpoints
{
private static async Task<IResult> CreateVote(
HttpContext context,
CreateVoteRequest request,
AwardsDbContext db,
IUserSessionService userSessionService,
IRiskFlagService riskFlagService,
IRiskRuleService riskRuleService)
{
if (request.Entries.Length == 0)
{
return Results.BadRequest(new { message = "At least one vote entry is required." });
}
var distinctCategoryCount = request.Entries
.Select(item => item.CategoryId)
.Distinct()
.Count();
if (distinctCategoryCount != request.Entries.Length)
{
return Results.BadRequest(new { message = "Only one vote entry per category is allowed." });
}
var season = await db.Seasons
.AsNoTracking()
.FirstOrDefaultAsync(item => item.Id == request.SeasonId);
var voteSeasonResolution = EnsurePublicWriteSeason(season, "voting");
if (voteSeasonResolution.Result is not null)
{
return voteSeasonResolution.Result;
}
var submitterIdResult = await ResolveSubmitterIdAsync(context, request.TwitchUserId, userSessionService);
if (submitterIdResult.Result is not null)
{
return submitterIdResult.Result;
}
var submitterId = submitterIdResult.SubmitterId!;
var requestMetadata = RequestMetadataReader.Read(context);
var candidateIds = request.Entries.Select(item => item.CandidateId).Distinct().ToArray();
var validCandidates = await db.Candidates
.AsNoTracking()
.Where(item => item.SeasonId == request.SeasonId && candidateIds.Contains(item.Id))
.Select(item => new { item.Id, item.CategoryId })
.ToArrayAsync();
if (validCandidates.Length != candidateIds.Length)
{
return Results.BadRequest(new { message = "One or more selected candidates do not belong to this season." });
}
var candidateCategoryMap = validCandidates.ToDictionary(item => item.Id, item => item.CategoryId);
if (request.Entries.Any(item => candidateCategoryMap[item.CandidateId] != item.CategoryId))
{
return Results.BadRequest(new { message = "A selected candidate does not match the submitted category." });
}
var ballot = await db.VoteBallots
.Include(item => item.Entries)
.FirstOrDefaultAsync(item => item.SeasonId == request.SeasonId && item.SubmittedByTwitchId == submitterId);
var isResubmission = ballot is not null;
if (ballot is null)
{
ballot = new VoteBallot
{
SeasonId = request.SeasonId,
SubmittedByTwitchId = submitterId,
};
await db.VoteBallots.AddAsync(ballot);
}
else
{
db.VoteEntries.RemoveRange(ballot.Entries);
ballot.Entries.Clear();
}
ballot.SubmittedAt = DateTimeOffset.UtcNow;
ballot.Status = "submitted";
ballot.Entries = request.Entries.Select(entry => new VoteEntry
{
CategoryId = entry.CategoryId,
CandidateId = entry.CandidateId,
}).ToList();
var resubmittedBallotRule = await riskRuleService.GetRuleAsync("resubmitted_ballot", context.RequestAborted);
var rapidVoteUpdatesRule = await riskRuleService.GetRuleAsync("rapid_vote_updates", context.RequestAborted);
var recentVoteSubmissions = await db.VoteBallots.CountAsync(item =>
item.SubmittedByTwitchId == submitterId
&& item.SubmittedAt >= DateTimeOffset.UtcNow.AddMinutes(-rapidVoteUpdatesRule.WindowMinutes));
await db.SaveChangesAsync(context.RequestAborted);
var ballotLink = new
{
label = "Voting-Analytics öffnen",
entityType = "vote",
entityId = ballot.Id.ToString(),
to = $"/admin/analytics?query={Uri.EscapeDataString(submitterId)}",
};
if (isResubmission && resubmittedBallotRule.Enabled)
{
await riskFlagService.AddIfMissingAsync(
request.SeasonId,
submitterId,
"vote",
"resubmitted_ballot",
resubmittedBallotRule.Severity,
"Ein User hat sein Ballot erneut gespeichert oder aktualisiert.",
requestMetadata,
new { ballotId = ballot.Id, entryCount = request.Entries.Length, entityLinks = new[] { ballotLink } },
context.RequestAborted);
}
if (rapidVoteUpdatesRule.Enabled && recentVoteSubmissions >= rapidVoteUpdatesRule.Threshold)
{
await riskFlagService.AddIfMissingAsync(
request.SeasonId,
submitterId,
"vote",
"rapid_vote_updates",
rapidVoteUpdatesRule.Severity,
"Mehrere Voting-Aenderungen wurden in kurzer Zeit erkannt.",
requestMetadata,
new { ballotId = ballot.Id, recentVoteSubmissions, threshold = rapidVoteUpdatesRule.Threshold, windowMinutes = rapidVoteUpdatesRule.WindowMinutes, entityLinks = new[] { ballotLink } },
context.RequestAborted);
}
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { ballotId = ballot.Id, entries = ballot.Entries.Count, updated = isResubmission });
}
}