IW4M-Admin/SharedLibraryCore/Services/ClientService.cs
RaidMax 161b27e2f2 fix alias command sending message to origin instead of target
(hopefully) fix an issue with banned players causing exception if they create events before they are kicked out
fix issues with sometimes wrong error message for timeout
show most recent IP address at top of alias list
optimization to some sql queries
2019-11-15 14:50:20 -06:00

734 lines
29 KiB
C#

using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
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 existingAliases = await context.Aliases
.Select(_alias => new { _alias.AliasId, _alias.LinkId, _alias.IPAddress, _alias.Name })
.Where(_alias => _alias.IPAddress == entity.IPAddress)
.ToListAsync();
if (existingAliases.Count > 0)
{
linkId = existingAliases.First().LinkId;
entity.CurrentServer.Logger.WriteDebug($"[create] client with new GUID {entity} has existing link {linkId}");
var existingExactAlias = existingAliases.FirstOrDefault(_alias => _alias.Name == entity.Name);
if (existingExactAlias != null)
{
entity.CurrentServer.Logger.WriteDebug($"[create] client with new GUID {entity} has existing alias {existingExactAlias.AliasId}");
aliasId = existingExactAlias.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,
SearchableName = entity.Name.StripColors().ToLower(),
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,
SearchableName = entity.Name.StripColors().ToLower(),
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.CurrentServer.Manager.GetLogger(0).WriteDebug($"Begin update alias for {entity}");
// 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));
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;
//}
//// remove any possible duplicates after updating
//foreach (var aliasGroup in aliases.GroupBy(_alias => new { _alias.IPAddress, _alias.Name })
// .Where(_group => _group.Count() > 1))
//{
// var oldestDuplicateAlias = aliasGroup.OrderBy(_alias => _alias.DateAdded).First();
// entity.CurrentServer.Manager.GetLogger(0).WriteDebug($"Oldest duplicate is {oldestDuplicateAlias.AliasId}");
// await context.Clients.Where(_client => aliasGroup.Select(_grp => _grp.AliasId).Contains(_client.CurrentAliasId))
// .ForEachAsync(_client => _client.CurrentAliasId = oldestDuplicateAlias.AliasId);
// var duplicateAliases = aliasGroup.Where(_alias => _alias.AliasId != oldestDuplicateAlias.AliasId);
// context.RemoveRange(duplicateAliases);
// await context.SaveChangesAsync();
// entity.CurrentServer.Manager.GetLogger(0).WriteDebug($"Removed duplicate aliases {string.Join(",", duplicateAliases.Select(_alias => _alias.AliasId))}");
//}
// 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.OrderBy(_alias => _alias.LinkId).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;
await context.SaveChangesAsync();
// 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);
// we also need to update all the penalties or they get deleted
// scenario
// link1 joins with ip1
// link2 joins with ip2,
// link2 receives penalty
// link2 joins with ip1
// pre existing link for link2 detected
// link2 is deleted
// link2 penalties are orphaned
await context.Penalties
.Where(_penalty => _penalty.LinkId == oldAliasLink.AliasLinkId)
.ForEachAsync(_penalty => _penalty.LinkId = 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();
if (context.Entry(oldAlias).State != EntityState.Deleted)
{
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,
SearchableName = name.StripColors().ToLower(),
Active = true,
};
entity.CurrentAlias = newAlias;
entity.CurrentAliasId = 0;
await context.SaveChangesAsync();
}
entity.CurrentServer.Manager.GetLogger(0).WriteDebug($"End update alias for {entity}");
}
/// <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
var linkedPermissionSet = new[] { Permission.Banned, Permission.Flagged };
// if their permission level has been changed to level that needs to be updated on all accounts
if (linkedPermissionSet.Contains(newPermission) || linkedPermissionSet.Contains(oldPermission))
{
//get all clients that have the same linkId
var iqMatchingClients = ctx.Clients
.Where(_client => _client.AliasLinkId == entity.AliasLinkId);
// 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 Task<IList<EFClient>> Find(Func<EFClient, bool> e)
{
throw new NotImplementedException();
}
public async Task<EFClient> Get(int entityId)
{
// todo: this needs to be optimized for large linked accounts
using (var context = new DatabaseContext(true))
{
var client = context.Clients
.Select(_client => new EFClient()
{
ClientId = _client.ClientId,
AliasLinkId = _client.AliasLinkId,
Level = _client.Level,
Connections = _client.Connections,
FirstConnection = _client.FirstConnection,
LastConnection = _client.LastConnection,
Masked = _client.Masked,
NetworkId = _client.NetworkId,
CurrentAlias = new EFAlias()
{
Name = _client.CurrentAlias.Name,
IPAddress = _client.CurrentAlias.IPAddress
}
})
.FirstOrDefault(_client => _client.ClientId == entityId);
client.AliasLink = new EFAliasLink()
{
Children = await context.Aliases
.Where(_alias => _alias.LinkId == client.AliasLinkId)
.Select(_alias => new EFAlias()
{
Name = _alias.Name,
IPAddress = _alias.IPAddress
}).ToListAsync()
};
if (client == null)
{
return null;
}
var foundClient = new
{
Client = client,
LinkedAccounts = await context.Clients.Where(_client => _client.AliasLinkId == client.AliasLinkId)
.Select(_linkedClient => new
{
_linkedClient.ClientId,
_linkedClient.NetworkId
})
.ToListAsync()
};
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)
.Select(_client => new EFClient()
{
ClientId = _client.ClientId,
AliasLinkId = _client.AliasLinkId,
Level = _client.Level,
Connections = _client.Connections,
FirstConnection = _client.FirstConnection,
LastConnection = _client.LastConnection,
Masked = _client.Masked,
NetworkId = _client.NetworkId
})
.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);
if (temporalClient.LastConnection > entity.LastConnection)
{
entity.LastConnection = temporalClient.LastConnection;
}
if (temporalClient.Connections > entity.Connections)
{
entity.Connections = temporalClient.Connections;
}
entity.Masked = temporalClient.Masked;
if (temporalClient.TotalConnectionTime > entity.TotalConnectionTime)
{
entity.TotalConnectionTime = temporalClient.TotalConnectionTime;
}
if (temporalClient.Password != null)
{
entity.Password = temporalClient.Password;
}
if (temporalClient.PasswordSalt != null)
{
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
.CountAsync(_client => _client.Level == Permission.Owner);
}
}
public async Task<EFClient> GetClientForLogin(int clientId)
{
using (var ctx = new DatabaseContext(true))
{
return await ctx.Clients
.Select(_client => new EFClient()
{
NetworkId = _client.NetworkId,
ClientId = _client.ClientId,
CurrentAlias = new EFAlias()
{
Name = _client.CurrentAlias.Name
},
Password = _client.Password,
PasswordSalt = _client.PasswordSalt,
Level = _client.Level
})
.FirstAsync(_client => _client.ClientId == clientId);
}
}
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>();
}
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)
else
{
iqLinkIds = iqLinkIds.Where(_alias => EF.Functions.Like((_alias.SearchableName ?? _alias.Name.ToLower()), $"%{identifier.ToLower()}%"));
}
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);
iqClients = iqClients.Where(_client => networkId == _client.NetworkId || 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();
}
}
/// <summary>
/// Returns the number of clients seen today
/// </summary>
/// <returns></returns>
public async Task<int> GetRecentClientCount()
{
using (var context = new DatabaseContext(true))
{
var startOfPeriod = DateTime.UtcNow.AddHours(-24);
var iqQuery = context.Clients.Where(_client => _client.LastConnection >= startOfPeriod);
#if DEBUG
string sql = iqQuery.ToSql();
#endif
return await iqQuery.CountAsync();
}
}
/// <summary>
/// gets the 10 most recently added clients to IW4MAdmin
/// </summary>
/// <returns></returns>
public async Task<IList<PlayerInfo>> GetRecentClients()
{
var startOfPeriod = DateTime.UtcNow.AddHours(-24);
using (var context = new DatabaseContext(true))
{
var iqClients = context.Clients
.Where(_client => _client.CurrentAlias.IPAddress != null)
.Where(_client => _client.FirstConnection >= startOfPeriod)
.OrderByDescending(_client => _client.FirstConnection)
.Select(_client => new PlayerInfo()
{
ClientId = _client.ClientId,
Name = _client.CurrentAlias.Name,
IPAddress = _client.CurrentAlias.IPAddress.ConvertIPtoString(),
LastConnection = _client.FirstConnection
});
#if DEBUG
var sql = iqClients.ToSql();
#endif
return await iqClients.ToListAsync();
}
}
#endregion
/// <summary>
/// retrieves the number of times the given client id has been reported
/// </summary>
/// <param name="clientId">client id to search for report counts of</param>
/// <returns></returns>
public async Task<int> GetClientReportCount(int clientId)
{
using (var ctx = new DatabaseContext(true))
{
return await ctx.Penalties
.Where(_penalty => _penalty.Active)
.Where(_penalty => _penalty.OffenderId == clientId)
.Where(_penalty => _penalty.Type == EFPenalty.PenaltyType.Report)
.CountAsync();
}
}
/// <summary>
/// indicates if the given clientid has been autoflagged
/// </summary>
/// <param name="clientId"></param>
/// <returns></returns>
public async Task<bool> IsAutoFlagged(int clientId)
{
using (var ctx = new DatabaseContext(true))
{
var now = DateTime.UtcNow;
return await ctx.Penalties
.Where(_penalty => _penalty.Active)
.Where(_penalty => _penalty.OffenderId == clientId)
.Where(_penalty => _penalty.Type == EFPenalty.PenaltyType.Flag)
.Where(_penalty => _penalty.PunisherId == 1)
.Where(_penalty => _penalty.Expires == null || _penalty.Expires > now)
.AnyAsync();
}
}
/// <summary>
/// Unlinks shared GUID account into its own separate account
/// </summary>
/// <param name="clientId"></param>
/// <returns></returns>
public async Task UnlinkClient(int clientId)
{
using (var ctx = new DatabaseContext())
{
var newLink = new EFAliasLink() { Active = true };
ctx.AliasLinks.Add(newLink);
await ctx.SaveChangesAsync();
var client = await ctx.Clients.Include(_client => _client.CurrentAlias)
.FirstAsync(_client => _client.ClientId == clientId);
client.AliasLinkId = newLink.AliasLinkId;
client.Level = Permission.User;
await ctx.Aliases.Where(_alias => _alias.IPAddress == client.IPAddress)
.ForEachAsync(_alias => _alias.LinkId = newLink.AliasLinkId);
await ctx.SaveChangesAsync();
}
}
}
}