protected override ValueTask HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement, DiscordMember member)
 {
     if (DiscordClientProvider.HasAdminRole(member))
     {
         context.Succeed(requirement);
     }
     return(default);
        protected override async ValueTask HandleRequirementAsync(AuthorizationHandlerContext context, CharacterOwnerRequirement requirement, DiscordMember member)
        {
            if (requirement.AllowAdmin && DiscordClientProvider.HasAdminRole(member))
            {
                context.Succeed(requirement);
                return;
            }

            long?characterId = context.Resource switch
            {
                long id => id,
                CharacterDto character => character.Id,
                _ => null
            };

            if (characterId.HasValue)
            {
                var discordId         = (long)member.Id;
                var characterIdString = characterId.Value.ToString();

                if (context.User.HasClaim(AppClaimTypes.Character, characterIdString) ||
                    await _context.UserClaims.AsNoTracking().CountAsync(claim => claim.UserId == discordId && claim.ClaimType == AppClaimTypes.Character && claim.ClaimValue == characterIdString) > 0)
                {
                    context.Succeed(requirement);
                }
            }
        }
    }
 public LootListsController(
     ApplicationDbContext context,
     TimeZoneInfo serverTimeZoneInfo,
     IAuthorizationService authorizationService,
     TelemetryClient telemetry,
     DiscordClientProvider discordClientProvider)
 {
     _context               = context;
     _serverTimeZoneInfo    = serverTimeZoneInfo;
     _authorizationService  = authorizationService;
     _telemetry             = telemetry;
     _discordClientProvider = discordClientProvider;
 }
    protected override async ValueTask HandleRequirementAsync(AuthorizationHandlerContext context, CharacterOwnerRequirement requirement, DiscordMember member)
    {
        if (requirement.AllowAdmin && DiscordClientProvider.HasAdminRole(member))
        {
            context.Succeed(requirement);
            return;
        }

        long characterId;
        long discordId = (long)member.Id;

        switch (context.Resource)
        {
        case long l:
            characterId = l;
            break;

        case CharacterDto dto:
            characterId = dto.Id;
            break;

        case Character character:
            if (character.OwnerId == discordId)
            {
                context.Succeed(requirement);
            }
            return;

        default:
            return;
        }

        if (await _context.Characters.AsNoTracking().CountAsync(c => c.Id == characterId && c.OwnerId == discordId) > 0)
        {
            context.Succeed(requirement);
        }
    }
        protected override async ValueTask HandleRequirementAsync(AuthorizationHandlerContext context, TeamLeaderRequirement requirement, DiscordMember member)
        {
            if (requirement.AllowAdmin && DiscordClientProvider.HasAdminRole(member))
            {
                context.Succeed(requirement);
                return;
            }

            if ((requirement.AllowRaidLeader && DiscordClientProvider.HasRaidLeaderRole(member)) ||
                (requirement.AllowLootMaster && DiscordClientProvider.HasLootMasterRole(member)) ||
                (requirement.AllowRecruiter && DiscordClientProvider.HasRecruiterRole(member)))
            {
                long?teamId = context.Resource switch
                {
                    long id => id,
                    TeamDto team => team.Id,
                    _ => null
                };

                if (teamId.HasValue)
                {
                    var discordId    = (long)member.Id;
                    var teamIdString = teamId.Value.ToString();

                    if (context.User.HasClaim(AppClaimTypes.RaidLeader, teamIdString) ||
                        await _context.UserClaims.AsNoTracking().CountAsync(claim => claim.UserId == discordId && claim.ClaimType == AppClaimTypes.RaidLeader && claim.ClaimValue == teamIdString) > 0)
                    {
                        context.Succeed(requirement);
                    }
                }
                else
                {
                    context.Succeed(requirement);
                }
            }
        }
    }
    protected override async ValueTask HandleRequirementAsync(AuthorizationHandlerContext context, TeamLeaderRequirement requirement, DiscordMember member)
    {
        if (requirement.AllowAdmin && DiscordClientProvider.HasAdminRole(member))
        {
            context.Succeed(requirement);
            return;
        }

        if ((requirement.AllowRaidLeader && DiscordClientProvider.HasRaidLeaderRole(member)) ||
            (requirement.AllowLootMaster && DiscordClientProvider.HasLootMasterRole(member)) ||
            (requirement.AllowRecruiter && DiscordClientProvider.HasRecruiterRole(member)))
        {
            long?teamId = context.Resource switch
            {
                long id => id,
                TeamDto team => team.Id,
                RaidTeam team => team.Id,
                _ => null
            };

            if (teamId.HasValue)
            {
                var discordId = (long)member.Id;

                if (await _context.RaidTeamLeaders.AsNoTracking().CountAsync(rtl => rtl.UserId == discordId && rtl.RaidTeamId == teamId) > 0)
                {
                    context.Succeed(requirement);
                }
            }
            else
            {
                context.Succeed(requirement);
            }
        }
    }
}
 protected DiscordAuthorizationHandler(DiscordClientProvider discordClientProvider)
 {
     DiscordClientProvider = discordClientProvider ?? throw new System.ArgumentNullException(nameof(discordClientProvider));
 }
        public async Task <ActionResult <EncounterDropDto> > PutAssign(long id, [FromBody] AwardDropSubmissionDto dto, [FromServices] TimeZoneInfo realmTimeZoneInfo, [FromServices] IAuthorizationService auth, [FromServices] DiscordClientProvider dcp)
        {
            var now  = realmTimeZoneInfo.TimeZoneNow();
            var drop = await _context.Drops.FindAsync(id);

            if (drop is null)
            {
                return(NotFound());
            }

            var teamId = await _context.Raids
                         .AsNoTracking()
                         .Where(r => r.Id == drop.EncounterKillRaidId)
                         .Select(r => (long?)r.RaidTeamId)
                         .FirstOrDefaultAsync();

            if (!teamId.HasValue)
            {
                return(NotFound());
            }

            var authResult = await auth.AuthorizeAsync(User, teamId, AppPolicies.LootMaster);

            if (!authResult.Succeeded)
            {
                return(Unauthorized());
            }

            if (dto.WinnerId.HasValue && drop.WinnerId.HasValue)
            {
                ModelState.AddModelError(nameof(dto.WinnerId), "Existing winner must be cleared before setting a new winner.");
                return(ValidationProblem());
            }

            drop.AwardedAt = now;
            drop.AwardedBy = User.GetDiscordId();
            var scope = await _context.GetCurrentPriorityScopeAsync();

            var observedDates = await _context.Raids
                                .AsNoTracking()
                                .Where(r => r.RaidTeamId == teamId)
                                .OrderByDescending(r => r.StartedAt)
                                .Select(r => r.StartedAt.Date)
                                .Distinct()
                                .Take(scope.ObservedAttendances)
                                .ToListAsync();

            var presentTeamRaiders = await _context.CharacterEncounterKills
                                     .AsTracking()
                                     .Where(cek => cek.EncounterKillEncounterId == drop.EncounterKillEncounterId && cek.EncounterKillRaidId == drop.EncounterKillRaidId)
                                     .Select(c => new
            {
                Id = c.CharacterId,
                c.Character.TeamId,
                c.Character.MemberStatus,
                Attended = c.Character.Attendances.Where(x => !x.IgnoreAttendance && x.Raid.RaidTeamId == teamId && x.RemovalId == null && observedDates.Contains(x.Raid.StartedAt.Date))
                           .Select(x => x.Raid.StartedAt.Date)
                           .Distinct()
                           .OrderByDescending(x => x)
                           .Take(scope.ObservedAttendances)
                           .Count(),
                Entry = _context.LootListEntries.Where(e => !e.DropId.HasValue && e.LootList.CharacterId == c.CharacterId && (e.ItemId == drop.ItemId || e.Item !.RewardFromId == drop.ItemId))
                        .OrderByDescending(e => e.Rank)
                        .Select(e => new
                {
                    e.Id,
                    e.Rank,
                    e.LootList.Status,
                    Passes = e.Passes.Count(p => p.RemovalId == null)
                })
                        .FirstOrDefault()
            })
 public IdentityProfileService(DiscordClientProvider discordClientProvider, ILogger <DefaultProfileService> logger, ApplicationDbContext context) : base(logger)
 {
     _discordClientProvider = discordClientProvider;
     _context = context;
 }
Exemple #10
0
        public async Task <ActionResult <ApproveOrRejectLootListResponseDto> > PostApproveOrReject(long characterId, byte phase, [FromBody] ApproveOrRejectLootListDto dto, [FromServices] DiscordClientProvider dcp)
        {
            var character = await _context.Characters.FindAsync(characterId);

            if (character is null)
            {
                return(NotFound());
            }

            var list = await _context.CharacterLootLists.FindAsync(characterId, phase);

            if (list is null)
            {
                return(NotFound());
            }

            var team = await _context.RaidTeams.FindAsync(dto.TeamId);

            if (team is null)
            {
                ModelState.AddModelError(nameof(dto.TeamId), "Team does not exist.");
                return(ValidationProblem());
            }

            var auth = await _authorizationService.AuthorizeAsync(User, team.Id, AppPolicies.RaidLeaderOrAdmin);

            if (!auth.Succeeded)
            {
                return(Unauthorized());
            }

            if (!ValidateTimestamp(list, dto.Timestamp))
            {
                return(Problem("Loot list has been changed. Refresh before trying to update the status again."));
            }

            if (list.Status != LootListStatus.Submitted && character.TeamId.HasValue)
            {
                return(Problem("Can't approve or reject a list that isn't in the submitted state."));
            }

            var submissions = await _context.LootListTeamSubmissions
                              .AsTracking()
                              .Where(s => s.LootListCharacterId == list.CharacterId && s.LootListPhase == list.Phase)
                              .ToListAsync();

            var teamSubmission = submissions.Find(s => s.TeamId == dto.TeamId);

            if (teamSubmission is null)
            {
                return(Unauthorized());
            }

            MemberDto?member = null;

            if (dto.Approved)
            {
                _context.LootListTeamSubmissions.RemoveRange(submissions);

                if (character.TeamId.HasValue)
                {
                    if (character.TeamId != dto.TeamId)
                    {
                        return(Problem("That character is not assigned to the specified raid team."));
                    }
                }
                else
                {
                    var idString = character.Id.ToString();
                    var claim    = await _context.UserClaims.AsNoTracking()
                                   .Where(c => c.ClaimType == AppClaimTypes.Character && c.ClaimValue == idString)
                                   .Select(c => new { c.UserId })
                                   .FirstOrDefaultAsync();

                    if (claim is not null)
                    {
                        var otherClaims = await _context.UserClaims.AsNoTracking()
                                          .Where(c => c.ClaimType == AppClaimTypes.Character && c.UserId == claim.UserId)
                                          .Select(c => c.ClaimValue)
                                          .ToListAsync();

                        var characterIds = new List <long>();

                        foreach (var otherClaim in otherClaims)
                        {
                            if (long.TryParse(otherClaim, out var cid))
                            {
                                characterIds.Add(cid);
                            }
                        }

                        var existingCharacterName = await _context.Characters
                                                    .AsNoTracking()
                                                    .Where(c => characterIds.Contains(c.Id) && c.TeamId == team.Id)
                                                    .Select(c => c.Name)
                                                    .FirstOrDefaultAsync();

                        if (existingCharacterName?.Length > 0)
                        {
                            return(Problem($"The owner of this character is already on this team as {existingCharacterName}."));
                        }
                    }

                    character.TeamId       = dto.TeamId;
                    character.MemberStatus = RaidMemberStatus.FullTrial;
                    character.JoinedTeamAt = _serverTimeZoneInfo.TimeZoneNow();
                }

                list.ApprovedBy = User.GetDiscordId();

                if (list.Status != LootListStatus.Locked)
                {
                    list.Status = LootListStatus.Approved;
                }

                await _context.SaveChangesAsync();

                var characterQuery = _context.Characters.AsNoTracking().Where(c => c.Id == character.Id);
                var scope          = await _context.GetCurrentPriorityScopeAsync();

                foreach (var m in await HelperQueries.GetMembersAsync(_context, _serverTimeZoneInfo, characterQuery, scope, team.Id, team.Name, true))
                {
                    member = m;
                    break;
                }
            }
            else
            {
                _context.LootListTeamSubmissions.Remove(teamSubmission);

                if (character.TeamId == team.Id)
                {
                    character.TeamId = null;
                }

                if (submissions.Count == 1)
                {
                    if (list.Status != LootListStatus.Locked)
                    {
                        list.Status = LootListStatus.Editing;
                    }

                    list.ApprovedBy = null;
                }

                await _context.SaveChangesAsync();
            }

            _telemetry.TrackEvent("LootListStatusChanged", User, props =>
            {
                props["CharacterId"] = list.CharacterId.ToString();
                props["Phase"]       = list.Phase.ToString();
                props["Status"]      = list.Status.ToString();
                props["Method"]      = "ApproveOrReject";
            });

            var characterIdString = characterId.ToString();
            var owner             = await _context.UserClaims
                                    .AsNoTracking()
                                    .Where(c => c.ClaimType == AppClaimTypes.Character && c.ClaimValue == characterIdString)
                                    .Select(c => c.UserId)
                                    .FirstOrDefaultAsync();

            if (owner > 0)
            {
                var sb = new StringBuilder("Your application to ")
                         .Append(team.Name)
                         .Append(" for ")
                         .Append(character.Name)
                         .Append(" was ")
                         .Append(dto.Approved ? "approved!" : "rejected.");

                if (dto.Message?.Length > 0)
                {
                    sb.AppendLine()
                    .Append("<@")
                    .Append(User.GetDiscordId())
                    .AppendLine("> said:")
                    .Append("> ")
                    .Append(dto.Message);
                }

                await dcp.SendDmAsync(owner, m => m.WithContent(sb.ToString()));
            }

            return(new ApproveOrRejectLootListResponseDto {
                Timestamp = list.Timestamp, Member = member, LootListStatus = list.Status
            });
        }
Exemple #11
0
        public async Task <ActionResult <TimestampDto> > PostSubmit(long characterId, byte phase, [FromBody] SubmitLootListDto dto, [FromServices] DiscordClientProvider dcp)
        {
            if (dto.SubmitTo.Count == 0)
            {
                ModelState.AddModelError(nameof(dto.SubmitTo), "Loot List must be submitted to at least one raid team.");
                return(ValidationProblem());
            }

            var character = await _context.Characters.FindAsync(characterId);

            if (character is null)
            {
                return(NotFound());
            }

            if (character.Deactivated)
            {
                return(Problem("Character has been deactivated."));
            }

            var list = await _context.CharacterLootLists.FindAsync(characterId, phase);

            if (list is null)
            {
                return(NotFound());
            }

            var auth = await _authorizationService.AuthorizeAsync(User, list.CharacterId, AppPolicies.CharacterOwnerOrAdmin);

            if (!auth.Succeeded)
            {
                return(Unauthorized());
            }

            if (!ValidateTimestamp(list, dto.Timestamp))
            {
                return(Problem("Loot list has been changed. Refresh before trying to update the status again."));
            }

            if (list.Status != LootListStatus.Editing && character.TeamId.HasValue)
            {
                return(Problem("Can't submit a list that is not editable."));
            }

            var teams = await _context.RaidTeams
                        .AsNoTracking()
                        .Where(t => dto.SubmitTo.Contains(t.Id))
                        .Select(t => new { t.Id, t.Name })
                        .ToDictionaryAsync(t => t.Id);

            if (teams.Count != dto.SubmitTo.Count)
            {
                return(Problem("One or more raid teams specified do not exist."));
            }

            var submissions = await _context.LootListTeamSubmissions
                              .AsTracking()
                              .Where(s => s.LootListCharacterId == characterId && s.LootListPhase == list.Phase)
                              .ToListAsync();

            foreach (var id in dto.SubmitTo)
            {
                if (submissions.Find(s => s.TeamId == id) is null)
                {
                    _context.LootListTeamSubmissions.Add(new()
                    {
                        LootListCharacterId = list.CharacterId,
                        LootListPhase       = list.Phase,
                        TeamId = id
                    });
                }
            }

            foreach (var submission in submissions)
            {
                if (!dto.SubmitTo.Contains(submission.TeamId))
                {
                    _context.LootListTeamSubmissions.Remove(submission);
                }
            }

            list.ApprovedBy = null;

            if (list.Status == LootListStatus.Editing)
            {
                list.Status = LootListStatus.Submitted;
            }

            await _context.SaveChangesAsync();

            _telemetry.TrackEvent("LootListStatusChanged", User, props =>
            {
                props["CharacterId"] = list.CharacterId.ToString();
                props["Phase"]       = list.Phase.ToString();
                props["Status"]      = list.Status.ToString();
                props["Method"]      = "Submit";
            });

            const string format = "You have a new application to {0} from {1}. ({2} {3})";

            await foreach (var claim in _context.UserClaims
                           .AsNoTracking()
                           .Where(claim => claim.ClaimType == AppClaimTypes.RaidLeader)
                           .Select(claim => new { claim.UserId, claim.ClaimValue })
                           .AsAsyncEnumerable())
            {
                if (long.TryParse(claim.ClaimValue, out var teamId) &&
                    teams.TryGetValue(teamId, out var team) &&
                    submissions.Find(s => s.TeamId == teamId) is null) // Don't notify when submission status doesn't change.
                {
                    await dcp.SendDmAsync(claim.UserId, m => m.WithContent(string.Format(
                                                                               format,
                                                                               team.Name,
                                                                               character.Name,
                                                                               character.Race.GetDisplayName(),
                                                                               list.MainSpec.GetDisplayName(true))));
                }
            }

            return(new TimestampDto {
                Timestamp = list.Timestamp
            });
        }
 public TeamLeaderPolicyHandler(ApplicationDbContext context, DiscordClientProvider discordClientProvider) : base(discordClientProvider)
 {
     _context = context;
 }
 public AdminPolicyHandler(DiscordClientProvider discordClientProvider) : base(discordClientProvider)
 {
 }
Exemple #14
0
 public DiscordBackgroundService(DiscordClientProvider discordClientProvider, IServiceProvider serviceProvider)
 {
     _discordClientProvider = discordClientProvider;
     _serviceProvider       = serviceProvider;
 }
 public IdentityProfileService(DiscordClientProvider discordClientProvider, ILogger <DefaultProfileService> logger) : base(logger)
 {
     _discordClientProvider = discordClientProvider;
 }
Exemple #16
0
 public MembersController(DiscordClientProvider discordClientProvider)
 {
     _discordClientProvider = discordClientProvider;
 }
 public CharacterOwnerPolicyHandler(ApplicationDbContext context, DiscordClientProvider discordClientProvider) : base(discordClientProvider)
 {
     _context = context;
 }
Exemple #18
0
 public AccountController(SignInManager <AppUser> signInManager, DiscordClientProvider discordClientProvider, ILogger <AccountController> logger)
 {
     _signInManager         = signInManager;
     _discordClientProvider = discordClientProvider;
     _logger = logger;
 }
 public MemberPolicyHandler(DiscordClientProvider discordClientProvider) : base(discordClientProvider)
 {
 }
Exemple #20
0
    public async Task <ActionResult <EncounterDropDto> > PutAssign(long id, [FromBody] AwardDropSubmissionDto dto, [FromServices] DiscordClientProvider dcp)
    {
        var now  = _realmTimeZone.TimeZoneNow();
        var drop = await _context.Drops.FindAsync(id);

        if (drop is null)
        {
            return(NotFound());
        }

        var raid = await _context.Raids
                   .AsNoTracking()
                   .Where(r => r.Id == drop.EncounterKillRaidId)
                   .Select(r => new { r.RaidTeamId, r.LocksAt })
                   .FirstOrDefaultAsync();

        if (raid is null)
        {
            return(NotFound());
        }

        var authResult = await _authorizationService.AuthorizeAsync(User, raid.RaidTeamId, AppPolicies.LootMaster);

        if (!authResult.Succeeded)
        {
            return(Unauthorized());
        }

        if (DateTimeOffset.UtcNow > raid.LocksAt)
        {
            return(Problem("Can't alter a locked raid."));
        }

        if (dto.WinnerId.HasValue && drop.WinnerId.HasValue)
        {
            ModelState.AddModelError(nameof(dto.WinnerId), "Existing winner must be cleared before setting a new winner.");
            return(ValidationProblem());
        }

        drop.AwardedAt = now;
        drop.AwardedBy = User.GetDiscordId();
        var scope = await _context.GetCurrentPriorityScopeAsync();

        var teamId      = raid.RaidTeamId;
        var attendances = await _context.GetAttendanceTableAsync(teamId, scope.ObservedAttendances);

        var donationMatrix = await _context.GetDonationMatrixAsync(d => d.Character.Attendances.Any(a => a.RaidId == drop.EncounterKillRaidId), scope);

        var presentTeamRaiders = await _context.CharacterEncounterKills
                                 .AsTracking()
                                 .Where(cek => cek.EncounterKillEncounterId == drop.EncounterKillEncounterId && cek.EncounterKillRaidId == drop.EncounterKillRaidId && cek.EncounterKillTrashIndex == drop.EncounterKillTrashIndex)
                                 .Select(cek => cek.Character)
                                 .Select(ConvertToDropInfo(drop.ItemId))
                                 .ToListAsync();

        await _context.Entry(drop).Collection(drop => drop.Passes).LoadAsync();

        drop.Passes.Clear();

        if (dto.WinnerId.HasValue)
        {
            var winner = presentTeamRaiders.Find(e => e.Id == dto.WinnerId);

            if (winner is null)
            {
                ModelState.AddModelError(nameof(dto.WinnerId), "Character was not present for the kill.");
                return(ValidationProblem());
            }

            int?winnerPrio = null;

            if (winner.Entry is not null)
            {
                winnerPrio = winner.Entry.Rank;

                var passes = await _context.DropPasses
                             .AsTracking()
                             .Where(p => p.LootListEntryId == winner.Entry.Id && p.RemovalId == null)
                             .ToListAsync();

                Debug.Assert(passes.Count == winner.Entry.Passes);

                var donated = donationMatrix.GetCreditForMonth(winner.Id, now);
                attendances.TryGetValue(winner.Id, out int attended);

                foreach (var bonus in PrioCalculator.GetAllBonuses(scope, attended, winner.MemberStatus, donated, passes.Count, winner.Enchanted, winner.Prepared))
                {
                    winnerPrio = winnerPrio.Value + bonus.Value;
                }

                drop.WinningEntry = await _context.LootListEntries.FindAsync(winner.Entry.Id);

                Debug.Assert(drop.WinningEntry is not null);
                drop.WinningEntry.Drop   = drop;
                drop.WinningEntry.DropId = drop.Id;

                foreach (var pass in passes)
                {
                    pass.WonEntryId = winner.Entry.Id;
                }
            }

            drop.WinnerId = winner.Id;

            foreach (var killer in presentTeamRaiders)
            {
                if (killer.Entry is not null && killer.TeamId == teamId && killer != winner)
                {
                    var thisPrio = (int)killer.Entry.Rank;

                    var donated = donationMatrix.GetCreditForMonth(killer.Id, now);
                    attendances.TryGetValue(killer.Id, out int attended);

                    foreach (var bonus in PrioCalculator.GetAllBonuses(scope, attended, killer.MemberStatus, donated, killer.Entry.Passes, killer.Enchanted, killer.Prepared))
                    {
                        thisPrio += bonus.Value;
                    }

                    _context.DropPasses.Add(new DropPass
                    {
                        Drop             = drop,
                        DropId           = drop.Id,
                        CharacterId      = killer.Id,
                        RelativePriority = thisPrio - (winnerPrio ?? 0),
                        LootListEntryId  = killer.Entry.Id
                    });
                }
            }
        }
        else
        {
            var oldWinningEntry = await _context.LootListEntries
                                  .AsTracking()
                                  .Where(e => e.DropId == drop.Id)
                                  .SingleOrDefaultAsync();

            drop.Winner       = null;
            drop.WinnerId     = null;
            drop.WinningEntry = null;

            if (oldWinningEntry is not null)
            {
                oldWinningEntry.Drop   = null;
                oldWinningEntry.DropId = null;

                await foreach (var pass in _context.DropPasses
                               .AsTracking()
                               .Where(pass => pass.WonEntryId == oldWinningEntry.Id)
                               .AsAsyncEnumerable())
                {
                    pass.WonEntryId = null;
                }
            }
        }

        await _context.SaveChangesAsync();

        var kill = await _context.EncounterKills
                   .AsNoTracking()
                   .Where(kill => kill.EncounterId == drop.EncounterKillEncounterId && kill.RaidId == drop.EncounterKillRaidId && kill.TrashIndex == drop.EncounterKillTrashIndex)
                   .Select(kill => new { kill.DiscordMessageId, kill.KilledAt, kill.RaidId, TeamName = kill.Raid.RaidTeam.Name, EncounterName = kill.Encounter.Name })
                   .FirstAsync();

        var drops = new List <(uint, string, string?)>();

        await foreach (var d in _context.Drops
                       .AsNoTracking()
                       .Where(d => d.EncounterKillEncounterId == drop.EncounterKillEncounterId && d.EncounterKillRaidId == drop.EncounterKillRaidId && d.EncounterKillTrashIndex == drop.EncounterKillTrashIndex)
                       .Select(d => new { d.ItemId, ItemName = d.Item.Name, WinnerName = (string?)d.Winner !.Name })