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 CreateClip( HttpContext context, CreateClipRequest request, AwardsDbContext db, IUserSessionService userSessionService, IRiskFlagService riskFlagService, IRiskRuleService riskRuleService) { if (!TryNormalizeExternalUrl(request.ClipUrl, out var clipUrl)) { return Results.BadRequest(new { message = "A valid http(s) clip link is required." }); } var platform = ResolveClipPlatform(clipUrl); if (platform is null) { return Results.BadRequest(new { message = "Only Twitch or YouTube clip links are supported." }); } var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == request.Year); var clipSeasonResolution = EnsurePublicWriteSeason(season, "nomination"); if (clipSeasonResolution.Result is not null) { return clipSeasonResolution.Result; } season = clipSeasonResolution.Season!; var selectedCandidate = request.CandidateId is int candidateId ? await db.Candidates .AsNoTracking() .FirstOrDefaultAsync(item => item.Id == candidateId && item.SeasonId == season.Id) : null; if (request.CandidateId is not null && selectedCandidate is null) { return Results.BadRequest(new { message = "The selected candidate does not exist for this season." }); } if (request.CategoryId is int requestedCategoryId && selectedCandidate is not null && selectedCandidate.CategoryId != requestedCategoryId) { return Results.BadRequest(new { message = "The selected candidate does not belong to the selected category." }); } var resolvedCategoryId = request.CategoryId ?? selectedCandidate?.CategoryId; var normalizedTitle = request.Title?.Trim() ?? string.Empty; var submittedCreator = request.Creator?.Trim(); var normalizedCreator = string.IsNullOrWhiteSpace(submittedCreator) ? selectedCandidate?.DisplayName ?? string.Empty : submittedCreator; if (normalizedTitle.Length > 160) { return Results.BadRequest(new { message = "Clip titles must stay below 160 characters." }); } if (normalizedCreator.Length > 160) { return Results.BadRequest(new { message = "Creator names must stay below 160 characters." }); } if (resolvedCategoryId is int categoryId) { var categoryExists = selectedCandidate?.CategoryId == categoryId || await db.Categories.AnyAsync(item => item.Id == categoryId && item.SeasonId == season.Id); if (!categoryExists) { return Results.BadRequest(new { message = "The selected category does not exist for this season." }); } } 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 duplicateClipRule = await riskRuleService.GetRuleAsync("duplicate_clip_submission", context.RequestAborted); var rapidClipBurstRule = await riskRuleService.GetRuleAsync("rapid_clip_burst", context.RequestAborted); var recentClipSubmissions = await db.ClipSubmissions.CountAsync(item => item.SubmittedByTwitchId == submitterId && item.CreatedAt >= DateTimeOffset.UtcNow.AddMinutes(-rapidClipBurstRule.WindowMinutes)); var alreadySubmittedClip = await db.ClipSubmissions.AnyAsync(item => item.SeasonId == season.Id && item.SubmittedByTwitchId == submitterId && item.ClipUrl == clipUrl); var clip = new ClipSubmission { SeasonId = season.Id, CategoryId = resolvedCategoryId, CandidateId = selectedCandidate?.Id, SubmittedByTwitchId = submitterId, ClipUrl = clipUrl, Title = normalizedTitle, Creator = normalizedCreator, Platform = platform, Status = "pending", CreatedFromIp = requestMetadata.ClientIp, CreatedAt = DateTimeOffset.UtcNow, }; db.ClipSubmissions.Add(clip); await db.SaveChangesAsync(context.RequestAborted); var clipLink = new { label = "Clip öffnen", entityType = "clip", entityId = clip.Id.ToString(), to = $"/admin/clips?query={Uri.EscapeDataString(clip.Id.ToString())}", }; if (alreadySubmittedClip && duplicateClipRule.Enabled) { await riskFlagService.AddIfMissingAsync( season.Id, submitterId, "clip", "duplicate_clip_submission", duplicateClipRule.Severity, "Ein User hat denselben Clip erneut eingereicht.", requestMetadata, new { clipId = clip.Id, clipUrl, CategoryId = resolvedCategoryId, CandidateId = selectedCandidate?.Id, entityLinks = new[] { clipLink } }, context.RequestAborted); } if (rapidClipBurstRule.Enabled && recentClipSubmissions >= rapidClipBurstRule.Threshold) { await riskFlagService.AddIfMissingAsync( season.Id, submitterId, "clip", "rapid_clip_burst", rapidClipBurstRule.Severity, "Ungewoehnlich viele Clip-Einreichungen in kurzer Zeit erkannt.", requestMetadata, new { clipId = clip.Id, recentClipSubmissions, threshold = rapidClipBurstRule.Threshold, windowMinutes = rapidClipBurstRule.WindowMinutes, entityLinks = new[] { clipLink } }, context.RequestAborted); } await db.SaveChangesAsync(context.RequestAborted); return Results.Ok(new { saved = true, clipId = clip.Id }); } }