IW4M-Admin/SharedLibraryCore/Services/ClientService.cs
RaidMax db11a5f480 upgrade packages, and delete a few unneeded ones
fix search for client resulting in invalid GUID parse
simplify output from dvar not being found
make sure to prompt if not all servers could be reached
2019-05-03 20:13:51 -05:00

531 lines
21 KiB
C#

using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFClient;
namespace SharedLibraryCore.Services
{
public class ClientService : Interfaces.IEntityService<EFClient>
{
public async Task<EFClient> Create(EFClient entity)
{
using (var context = new DatabaseContext())
{
int? linkId = null;
int? aliasId = null;
if (entity.IPAddress != null)
{
var existingAlias = await context.Aliases
.Select(_alias => new { _alias.AliasId, _alias.LinkId, _alias.IPAddress, _alias.Name })
.FirstOrDefaultAsync(_alias => _alias.IPAddress == entity.IPAddress);
if (existingAlias != null)
{
entity.CurrentServer.Logger.WriteDebug($"[create] client with new GUID {entity} has existing link {existingAlias.LinkId}");
linkId = existingAlias.LinkId;
if (existingAlias.Name == entity.Name)
{
entity.CurrentServer.Logger.WriteDebug($"[create] client with new GUID {entity} has existing alias {existingAlias.AliasId}");
aliasId = existingAlias.AliasId;
}
}
}
var client = new EFClient()
{
Level = Permission.User,
FirstConnection = DateTime.UtcNow,
LastConnection = DateTime.UtcNow,
NetworkId = entity.NetworkId
};
context.Clients.Add(client);
// they're just using a new GUID
if (aliasId.HasValue)
{
entity.CurrentServer.Logger.WriteDebug($"[create] setting {entity}'s alias id and linkid to ({aliasId.Value}, {linkId.Value})");
client.CurrentAliasId = aliasId.Value;
client.AliasLinkId = linkId.Value;
}
// link was found but they don't have an exact alias
else if (!aliasId.HasValue && linkId.HasValue)
{
entity.CurrentServer.Logger.WriteDebug($"[create] setting {entity}'s linkid to {linkId.Value}, but creating new alias");
client.AliasLinkId = linkId.Value;
client.CurrentAlias = new EFAlias()
{
Name = entity.Name,
DateAdded = DateTime.UtcNow,
IPAddress = entity.IPAddress,
LinkId = linkId.Value
};
}
// brand new players (supposedly)
else
{
entity.CurrentServer.Logger.WriteDebug($"[create] creating new Link and Alias for {entity}");
var link = new EFAliasLink();
var alias = new EFAlias()
{
Name = entity.Name,
DateAdded = DateTime.UtcNow,
IPAddress = entity.IPAddress,
Link = link
};
link.Children.Add(alias);
client.AliasLink = link;
client.CurrentAlias = alias;
}
await context.SaveChangesAsync();
return client;
}
}
private async Task UpdateAlias(string name, int? ip, EFClient entity, DatabaseContext context)
{
// entity is the tracked db context item
// get all aliases by IP address and LinkId
var iqAliases = context.Aliases
.Include(a => a.Link)
// we only want alias that have the same IP address or share a link
.Where(_alias => _alias.IPAddress == ip || (_alias.LinkId == entity.AliasLinkId));
#if DEBUG == true
var aliasSql = iqAliases.ToSql();
#endif
var aliases = await iqAliases.ToListAsync();
// update each of the aliases where this is no IP but the name is identical
foreach (var alias in aliases.Where(_alias => (_alias.IPAddress == null || _alias.IPAddress == 0)))
{
alias.IPAddress = ip;
}
await context.SaveChangesAsync();
// see if they have a matching IP + Name but new NetworkId
var existingExactAlias = aliases.FirstOrDefault(a => a.Name == name && a.IPAddress == ip);
bool hasExactAliasMatch = existingExactAlias != null;
// if existing alias matches link them
var newAliasLink = existingExactAlias?.Link;
// if no exact matches find the first IP or LinkId that matches
newAliasLink = newAliasLink ?? aliases.FirstOrDefault()?.Link;
// if no matches are found, use our current one ( it will become permanent )
newAliasLink = newAliasLink ?? entity.AliasLink;
bool hasExistingAlias = aliases.Count > 0;
bool isAliasLinkUpdated = newAliasLink.AliasLinkId != entity.AliasLink.AliasLinkId;
// this happens when the link we found is different than the one we create before adding an IP
if (isAliasLinkUpdated)
{
entity.CurrentServer.Logger.WriteDebug($"[updatealias] found a link for {entity} so we are updating link from {entity.AliasLink.AliasLinkId} to {newAliasLink.AliasLinkId}");
var oldAliasLink = entity.AliasLink;
// update all the clients that have the old alias link
await context.Clients
.Where(_client => _client.AliasLinkId == oldAliasLink.AliasLinkId)
.ForEachAsync(_client => _client.AliasLinkId = newAliasLink.AliasLinkId);
entity.AliasLink = newAliasLink;
entity.AliasLinkId = newAliasLink.AliasLinkId;
// update all previous aliases
await context.Aliases
.Where(_alias => _alias.LinkId == oldAliasLink.AliasLinkId)
.ForEachAsync(_alias => _alias.LinkId = newAliasLink.AliasLinkId);
await context.SaveChangesAsync();
// we want to delete the now inactive alias
context.AliasLinks.Remove(oldAliasLink);
await context.SaveChangesAsync();
}
// the existing alias matches ip and name, so we can just ignore the temporary one
if (hasExactAliasMatch)
{
entity.CurrentServer.Logger.WriteDebug($"[updatealias] {entity} has exact alias match");
var oldAlias = entity.CurrentAlias;
entity.CurrentAliasId = existingExactAlias.AliasId;
entity.CurrentAlias = existingExactAlias;
await context.SaveChangesAsync();
// the alias is the same so we can just remove it
if (oldAlias.AliasId != existingExactAlias.AliasId && oldAlias.AliasId > 0)
{
await context.Clients
.Where(_client => _client.CurrentAliasId == oldAlias.AliasId)
.ForEachAsync(_client => _client.CurrentAliasId = existingExactAlias.AliasId);
await context.SaveChangesAsync();
entity.CurrentServer.Logger.WriteDebug($"[updatealias] {entity} has exact alias match, so we're going to try to remove aliasId {oldAlias.AliasId} with linkId {oldAlias.AliasId}");
context.Aliases.Remove(oldAlias);
await context.SaveChangesAsync();
}
}
// theres no exact match, but they've played before with the GUID or IP
else
{
entity.CurrentServer.Logger.WriteDebug($"[updatealias] {entity} is using a new alias");
var newAlias = new EFAlias()
{
DateAdded = DateTime.UtcNow,
IPAddress = ip,
LinkId = newAliasLink.AliasLinkId,
Name = name,
Active = true,
};
entity.CurrentAlias = newAlias;
entity.CurrentAliasId = 0;
await context.SaveChangesAsync();
}
}
/// <summary>
/// updates the permission level of the given target to the given permission level
/// </summary>
/// <param name="newPermission"></param>
/// <param name="temporalClient"></param>
/// <param name="origin"></param>
/// <param name="ctx"></param>
/// <returns></returns>
public async Task UpdateLevel(Permission newPermission, EFClient temporalClient, EFClient origin)
{
using (var ctx = new DatabaseContext())
{
var entity = await ctx.Clients
.Where(_client => _client.ClientId == temporalClient.ClientId)
.FirstAsync();
var oldPermission = entity.Level;
entity.Level = newPermission;
await ctx.SaveChangesAsync();
#if DEBUG == true
temporalClient.CurrentServer.Logger.WriteDebug($"Updated {temporalClient.ClientId} to {newPermission}");
#endif
// if their permission level has been changed to level that needs to be updated on all accounts
if ((oldPermission != newPermission) &&
(newPermission == Permission.Banned ||
newPermission == Permission.Flagged ||
newPermission == Permission.User))
{
var changeSvc = new ChangeHistoryService();
//get all clients that have the same linkId
var iqMatchingClients = ctx.Clients
.Where(_client => _client.AliasLinkId == entity.AliasLinkId);
// make sure we don't select ourselves twice
//.Where(_client => _client.ClientId != temporalClient.ClientId);
// this updates the level for all the clients with the same LinkId
// only if their new level is flagged or banned
await iqMatchingClients.ForEachAsync(_client =>
{
_client.Level = newPermission;
#if DEBUG == true
temporalClient.CurrentServer.Logger.WriteDebug($"Updated linked {_client.ClientId} to {newPermission}");
#endif
});
await ctx.SaveChangesAsync();
}
}
temporalClient.Level = newPermission;
}
public async Task<EFClient> Delete(EFClient entity)
{
using (var context = new DatabaseContext())
{
var client = context.Clients
.Single(e => e.ClientId == entity.ClientId);
entity.Active = false;
context.Entry(entity).State = EntityState.Modified;
await context.SaveChangesAsync();
return entity;
}
}
public async Task<IList<EFClient>> Find(Func<EFClient, bool> e)
{
return await Task.Run(() =>
{
using (var context = new DatabaseContext(true))
{
return context.Clients
.Include(c => c.CurrentAlias)
.Include(c => c.AliasLink.Children)
.Where(e).ToList();
}
});
}
public async Task<EFClient> Get(int entityID)
{
using (var context = new DatabaseContext(true))
{
var iqClient = from client in context.Clients
.Include(c => c.CurrentAlias)
.Include(c => c.AliasLink.Children)
.Include(c => c.Meta)
where client.ClientId == entityID
select new
{
Client = client,
LinkedAccounts = (from linkedClient in context.Clients
where client.AliasLinkId == linkedClient.AliasLinkId
select new
{
linkedClient.ClientId,
linkedClient.NetworkId
})
};
#if DEBUG == true
var clientSql = iqClient.ToSql();
#endif
var foundClient = await iqClient.FirstOrDefaultAsync();
if (foundClient == null)
{
return null;
}
foundClient.Client.LinkedAccounts = new Dictionary<int, long>();
// todo: find out the best way to do this
// I'm doing this here because I don't know the best way to have multiple awaits in the query
foreach (var linked in foundClient.LinkedAccounts)
{
foundClient.Client.LinkedAccounts.Add(linked.ClientId, linked.NetworkId);
}
return foundClient.Client;
}
}
private static readonly Func<DatabaseContext, long, Task<EFClient>> _getUniqueQuery =
EF.CompileAsyncQuery((DatabaseContext context, long networkId) =>
context.Clients
.Include(c => c.CurrentAlias)
.Include(c => c.AliasLink.Children)
.Include(c => c.ReceivedPenalties)
.FirstOrDefault(c => c.NetworkId == networkId)
);
public async Task<EFClient> GetUnique(long entityAttribute)
{
using (var context = new DatabaseContext(true))
{
return await _getUniqueQuery(context, entityAttribute);
}
}
public async Task UpdateAlias(EFClient temporalClient)
{
using (var context = new DatabaseContext())
{
var entity = context.Clients
.Include(c => c.AliasLink)
.Include(c => c.CurrentAlias)
.First(e => e.ClientId == temporalClient.ClientId);
entity.CurrentServer = temporalClient.CurrentServer;
await UpdateAlias(temporalClient.Name, temporalClient.IPAddress, entity, context);
temporalClient.CurrentAlias = entity.CurrentAlias;
temporalClient.CurrentAliasId = entity.CurrentAliasId;
temporalClient.AliasLink = entity.AliasLink;
temporalClient.AliasLinkId = entity.AliasLinkId;
}
}
public async Task<EFClient> Update(EFClient temporalClient)
{
using (var context = new DatabaseContext())
{
// grab the context version of the entity
var entity = context.Clients
.First(client => client.ClientId == temporalClient.ClientId);
entity.LastConnection = temporalClient.LastConnection;
entity.Connections = temporalClient.Connections;
entity.FirstConnection = temporalClient.FirstConnection;
entity.Masked = temporalClient.Masked;
entity.TotalConnectionTime = temporalClient.TotalConnectionTime;
entity.Password = temporalClient.Password;
entity.PasswordSalt = temporalClient.PasswordSalt;
// update in database
await context.SaveChangesAsync();
return entity;
}
}
#region ServiceSpecific
public async Task<IList<EFClient>> GetOwners()
{
using (var context = new DatabaseContext())
{
return await context.Clients
.Where(c => c.Level == Permission.Owner)
.ToListAsync();
}
}
/// <summary>
/// retrieves the number of owners
/// (client level is owner)
/// </summary>
/// <returns></returns>
public async Task<int> GetOwnerCount()
{
using (var ctx = new DatabaseContext(true))
{
return await ctx.Clients.AsNoTracking()
.CountAsync(_client => _client.Level == Permission.Owner);
}
}
public async Task<List<EFClient>> GetPrivilegedClients(bool includeName = true)
{
using (var context = new DatabaseContext(disableTracking: true))
{
var iqClients = from client in context.Clients.AsNoTracking()
where client.Level >= Permission.Trusted
where client.Active
select new EFClient()
{
AliasLinkId = client.AliasLinkId,
CurrentAlias = client.CurrentAlias,
ClientId = client.ClientId,
Level = client.Level,
Password = client.Password,
PasswordSalt = client.PasswordSalt,
NetworkId = client.NetworkId,
LastConnection = client.LastConnection
};
#if DEBUG == true
var clientsSql = iqClients.ToSql();
#endif
return await iqClients.ToListAsync();
}
}
public async Task<IList<PlayerInfo>> FindClientsByIdentifier(string identifier)
{
if (identifier?.Length < 3)
{
return new List<PlayerInfo>();
}
identifier = identifier.ToLower();
using (var context = new DatabaseContext(disableTracking: true))
{
long? networkId = null;
try
{
networkId = identifier.ConvertGuidToLong();
}
catch { }
int? ipAddress = identifier.ConvertToIP();
IQueryable<EFAlias> iqLinkIds = context.Aliases.Where(_alias => _alias.Active);
// we want to query for the IP ADdress
if (ipAddress != null)
{
iqLinkIds = iqLinkIds.Where(_alias => _alias.IPAddress == ipAddress);
}
// want to find them by name (wildcard)
// todo maybe not make it start with wildcard?
else
{
iqLinkIds = iqLinkIds.Where(_alias => EF.Functions.Like(_alias.Name, $"%{identifier}%"));
}
var linkIds = await iqLinkIds
.Select(_alias => _alias.LinkId)
.ToListAsync();
// get all the clients that match the alias link or the network id
var iqClients = context.Clients
.Where(_client => _client.Active);
if (networkId.HasValue)
{
iqClients = iqClients.Where(_client => networkId.Value == _client.NetworkId);
}
else
{
iqClients = iqClients.Where(_client => linkIds.Contains(_client.AliasLinkId));
}
// we want to project our results
var iqClientProjection = iqClients.OrderByDescending(_client => _client.LastConnection)
.Select(_client => new PlayerInfo()
{
Name = _client.CurrentAlias.Name,
LevelInt = (int)_client.Level,
LastConnection = _client.LastConnection,
ClientId = _client.ClientId,
});
#if DEBUG == true
var iqClientsSql = iqClients.ToSql();
#endif
var clients = await iqClientProjection.ToListAsync();
// this is so we don't try to evaluate this in the linq to entities query
foreach (var client in clients)
{
client.Level = ((Permission)client.LevelInt).ToLocalizedLevelName();
}
return clients;
}
}
public async Task<int> GetTotalClientsAsync()
{
using (var context = new DatabaseContext(true))
{
return await context.Clients
.CountAsync();
}
}
#endregion
}
}