using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; using Data.Abstractions; using Data.Models; using Microsoft.EntityFrameworkCore; using SharedLibraryCore.Configuration; using SharedLibraryCore.Dtos; using SharedLibraryCore.Interfaces; namespace SharedLibraryCore.Services { public class PenaltyService : IEntityService { private readonly ApplicationConfiguration _appConfig; private readonly IDatabaseContextFactory _contextFactory; public PenaltyService(IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig) { _contextFactory = contextFactory; _appConfig = appConfig; } public virtual async Task Create(EFPenalty newEntity) { await using var context = _contextFactory.CreateContext(); var penalty = new EFPenalty { Active = true, OffenderId = newEntity.Offender.ClientId, PunisherId = newEntity.Punisher.ClientId, LinkId = newEntity.Link?.AliasLinkId, Type = newEntity.Type, Expires = newEntity.Expires, Offense = newEntity.Offense, When = DateTime.UtcNow, AutomatedOffense = newEntity.AutomatedOffense ?? newEntity.Punisher.AdministeredPenalties?.FirstOrDefault()?.AutomatedOffense, IsEvadedOffense = newEntity.IsEvadedOffense }; if (LinkedPenalties.Contains(newEntity.Type)) { var penaltyIdentifiers = new EFPenaltyIdentifier { Penalty = penalty, NetworkId = newEntity.Offender.NetworkId, IPv4Address = newEntity.Offender.CurrentAlias.IPAddress }; context.PenaltyIdentifiers.Add(penaltyIdentifiers); } context.Penalties.Add(penalty); await context.SaveChangesAsync(); return newEntity; } public Task Delete(EFPenalty entity) { throw new NotImplementedException(); } public async Task> Find(Func expression) { throw await Task.FromResult(new Exception()); } public Task Get(int entityID) { throw new NotImplementedException(); } public Task GetUnique(long entityProperty) { throw new NotImplementedException(); } public Task Update(EFPenalty entity) { throw new NotImplementedException(); } public async Task> GetRecentPenalties(int count, int offset, EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool ignoreAutomated = true) { await using var context = _contextFactory.CreateContext(false); var iqPenalties = context.Penalties .Where(p => showOnly == EFPenalty.PenaltyType.Any ? p.Type != EFPenalty.PenaltyType.Any : p.Type == showOnly) .Where(_penalty => !ignoreAutomated || _penalty.PunisherId != 1) .OrderByDescending(p => p.When) .Skip(offset) .Take(count) .Select(_penalty => new PenaltyInfo { Id = _penalty.PenaltyId, Offense = _penalty.Offense, AutomatedOffense = _penalty.AutomatedOffense, OffenderId = _penalty.OffenderId, OffenderName = _penalty.Offender.CurrentAlias.Name, PunisherId = _penalty.PunisherId, PunisherName = _penalty.Punisher.CurrentAlias.Name, PunisherLevel = _penalty.Punisher.Level, PenaltyType = _penalty.Type, Expires = _penalty.Expires, TimePunished = _penalty.When, IsEvade = _penalty.IsEvadedOffense }); return await iqPenalties.ToListAsync(); } private static readonly EFPenalty.PenaltyType[] LinkedPenalties = { EFPenalty.PenaltyType.Ban, EFPenalty.PenaltyType.Flag, EFPenalty.PenaltyType.TempBan }; private static readonly Expression> Filter = p => LinkedPenalties.Contains(p.Type) && p.Active && (p.Expires == null || p.Expires > DateTime.UtcNow); private static readonly Expression> FilterById = pi => LinkedPenalties.Contains(pi.Penalty.Type) && pi.Penalty.Active && (pi.Penalty.Expires == null || pi.Penalty.Expires > DateTime.UtcNow); public async Task> GetActivePenaltiesAsync(int linkId, int currentAliasId, long networkId, int? ip = null) { var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId); if (penaltiesByIdentifier.Any()) { return penaltiesByIdentifier; } await using var context = _contextFactory.CreateContext(false); IQueryable iqIpPenalties; if (_appConfig.EnableImplicitAccountLinking) { iqIpPenalties = context.Aliases .Where(a => a.IPAddress != null && a.IPAddress == ip) .SelectMany(a => a.Link.ReceivedPenalties) .Where(Filter); } else { var usedIps = await context.Aliases.AsNoTracking() .Where(alias => (alias.LinkId == linkId || alias.AliasId == currentAliasId) && alias.IPAddress != null) .Select(alias => alias.IPAddress).ToListAsync(); var aliasedIds = await context.Aliases.AsNoTracking().Where(alias => usedIps.Contains(alias.IPAddress)) .Select(alias => alias.LinkId) .ToListAsync(); iqIpPenalties = context.Penalties.AsNoTracking() .Where(penalty => aliasedIds.Contains(penalty.LinkId ?? -1) || penalty.LinkId == linkId) .Where(Filter); } var activeIpPenalties = await iqIpPenalties.ToListAsync(); var activePenalties = activeIpPenalties.Distinct(); // this is a bit more performant in memory (ordering) return activePenalties.OrderByDescending(p => p.When).ToList(); } public async Task> GetActivePenaltiesByIdentifier(int? ip, long networkId) { await using var context = _contextFactory.CreateContext(false); var activePenaltiesIds = context.PenaltyIdentifiers.Where(identifier => identifier.IPv4Address != null && identifier.IPv4Address == ip || identifier.NetworkId == networkId) .Where(FilterById); return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync(); } public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, int? ipAddress = null) { await using var context = _contextFactory.CreateContext(); var now = DateTime.UtcNow; var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId); if (activePenalties.Any()) { var ids = activePenalties.Select(penalty => penalty.PenaltyId); await context.Penalties.Where(penalty => ids.Contains(penalty.PenaltyId)) .ForEachAsync(penalty => { penalty.Active = false; penalty.Expires = now; }); await context.SaveChangesAsync(); return; } var penaltiesByLink = context.Penalties .Where(p => p.LinkId == aliasLinkId) .Where(p => p.Expires > now || p.Expires == null); var penaltiesByIp = context.Penalties .Where(p => p.Offender.CurrentAlias.IPAddress != null && p.Offender.CurrentAlias.IPAddress == null) .Where(p => p.Expires > now || p.Expires == null); await penaltiesByLink.Union(penaltiesByIp).Distinct().ForEachAsync(p => { p.Active = false; p.Expires = now; }); await context.SaveChangesAsync(); } } }