huge commit for advanced stats feature.

broke data out into its own library.
may be breaking changes with existing plugins
This commit is contained in:
RaidMax
2021-03-22 11:09:25 -05:00
parent db2e1deb2f
commit c5375b661b
505 changed files with 13671 additions and 3271 deletions

View File

@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using Stats.Dtos;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace StatsWeb.API
{
[ApiController]
[Route("api/stats")]
public class StatsController : ControllerBase
{
private readonly ILogger _logger;
private readonly IResourceQueryHelper<StatsInfoRequest, StatsInfoResult> _statsQueryHelper;
public StatsController(ILogger<StatsController> logger, IResourceQueryHelper<StatsInfoRequest, StatsInfoResult> statsQueryHelper)
{
_statsQueryHelper = statsQueryHelper;
_logger = logger;
}
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpGet("{clientId}")]
public async Task<IActionResult> ClientStats(int clientId)
{
if (clientId < 1 || !ModelState.IsValid)
{
return BadRequest(new ErrorResponse
{
Messages = new[] { $"Client Id must be between 1 and {int.MaxValue}" }
});
}
var request = new StatsInfoRequest()
{
ClientId = clientId
};
try
{
var result = await _statsQueryHelper.QueryResource(request);
if (result.RetrievedResultCount == 0)
{
return NotFound();
}
return Ok(result.Results);
}
catch (Exception e)
{
_logger.LogWarning(e, "Could not get client stats for client id {clientId}", clientId);
return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse
{
Messages = new[] { e.Message }
});
}
}
}
}

View File

@ -1,5 +1,5 @@
using FluentValidation;
using SharedLibraryCore.Database.Models;
using Data.Models;
using FluentValidation;
using SharedLibraryCore.Dtos;
namespace WebfrontCore.Controllers.API.Validation

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
@ -10,7 +11,6 @@ using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using WebfrontCore.ViewModels;
using static SharedLibraryCore.Database.Models.EFClient;
namespace WebfrontCore.Controllers
{
@ -215,10 +215,10 @@ namespace WebfrontCore.Controllers
Name = "level",
Label = Localization["WEBFRONT_PROFILE_LEVEL"],
Type = "select",
Values = Enum.GetValues(typeof(Permission)).OfType<Permission>()
Values = Enum.GetValues(typeof(EFClient.Permission)).OfType<EFClient.Permission>()
.Where(p => p <= Client.Level)
.Where(p => p != Permission.Banned)
.Where(p => p != Permission.Flagged)
.Where(p => p != EFClient.Permission.Banned)
.Where(p => p != EFClient.Permission.Flagged)
.ToDictionary(p => p.ToString(), p => p.ToLocalizedLevelName())
},
},

View File

@ -5,24 +5,26 @@ using SharedLibraryCore.Dtos;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Plugins.Stats.Config;
using WebfrontCore.ViewComponents;
using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.Database.Models.EFPenalty;
namespace WebfrontCore.Controllers
{
public class ClientController : BaseController
{
private readonly IMetaService _metaService;
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
public ClientController(IManager manager, IMetaService metaService) : base(manager)
public ClientController(IManager manager, IMetaService metaService,
IConfigurationHandler<StatsConfiguration> configurationHandler) : base(manager)
{
_metaService = metaService;
_configurationHandler = configurationHandler;
}
public async Task<IActionResult> ProfileAsync(int id, MetaType? metaFilterType)
@ -42,13 +44,13 @@ namespace WebfrontCore.Controllers
client.SetAdditionalProperty(EFMeta.ClientTag, tag.LinkedMeta.Value);
}
int displayLevelInt = (int)client.Level;
string displayLevel = client.Level.ToLocalizedLevelName();
var displayLevelInt = (int)client.Level;
var displayLevel = client.Level.ToLocalizedLevelName();
if (!Authorized && client.Level.ShouldHideLevel())
{
displayLevelInt = (int)Permission.User;
displayLevel = Permission.User.ToLocalizedLevelName();
displayLevelInt = (int)Data.Models.Client.EFClient.Permission.User;
displayLevel = Data.Models.Client.EFClient.Permission.User.ToLocalizedLevelName();
}
displayLevel = string.IsNullOrEmpty(client.Tag) ? displayLevel : $"{displayLevel} ({client.Tag})";
@ -77,7 +79,7 @@ namespace WebfrontCore.Controllers
.Prepend(client.CurrentAlias.IPAddress.ConvertIPtoString())
.Distinct()
.ToList(),
HasActivePenalty = activePenalties.Any(_penalty => _penalty.Type != PenaltyType.Flag),
HasActivePenalty = activePenalties.Any(_penalty => _penalty.Type != EFPenalty.PenaltyType.Flag),
Online = Manager.GetActiveClients().FirstOrDefault(c => c.ClientId == client.ClientId) != null,
TimeOnline = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
LinkedAccounts = client.LinkedAccounts,
@ -111,6 +113,7 @@ namespace WebfrontCore.Controllers
ViewBag.Title += " " + Localization["WEBFRONT_CLIENT_PROFILE_TITLE"];
ViewBag.Description = $"Client information for {strippedName}";
ViewBag.Keywords = $"IW4MAdmin, client, profile, {strippedName}";
ViewBag.UseNewStats = _configurationHandler.Configuration().EnableAdvancedMetrics;
return View("Profile/Index", clientDto);
}
@ -155,10 +158,10 @@ namespace WebfrontCore.Controllers
foreach (var client in clientsDto)
{
if (!Authorized && ((Permission)client.LevelInt).ShouldHideLevel())
if (!Authorized && ((Data.Models.Client.EFClient.Permission)client.LevelInt).ShouldHideLevel())
{
client.LevelInt = (int)Permission.User;
client.Level = Permission.User.ToLocalizedLevelName();
client.LevelInt = (int)Data.Models.Client.EFClient.Permission.User;
client.Level = Data.Models.Client.EFClient.Permission.User.ToLocalizedLevelName();
}
}

View File

@ -0,0 +1,38 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using Stats.Dtos;
namespace WebfrontCore.Controllers
{
[Route("clientstatistics")]
public class ClientStatisticsController : BaseController
{
private IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> _queryHelper;
private readonly DefaultSettings _defaultConfig;
public ClientStatisticsController(IManager manager,
IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> queryHelper,
IConfigurationHandler<DefaultSettings> configurationHandler) : base(manager)
{
_queryHelper = queryHelper;
_defaultConfig = configurationHandler.Configuration();
}
[HttpGet("{id:int}/advanced")]
public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId)
{
ViewBag.Config = _defaultConfig.GameStrings;
var hitInfo = await _queryHelper.QueryResource(new StatsInfoRequest
{
ClientId = id,
ServerEndpoint = serverId
});
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo.Results.First());
}
}
}

View File

@ -0,0 +1,213 @@
using IW4MAdmin.Plugins.Stats;
using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces;
using Stats.Dtos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Data.Abstractions;
using IW4MAdmin.Plugins.Stats.Config;
namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
{
public class StatsController : BaseController
{
private readonly ILogger _logger;
private readonly IManager _manager;
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper;
private readonly ITranslationLookup _translationLookup;
private readonly IDatabaseContextFactory _contextFactory;
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery,
MessageResponse> resourceQueryHelper, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory,
IConfigurationHandler<StatsConfiguration> configurationHandler) : base(manager)
{
_logger = logger;
_manager = manager;
_chatResourceQueryHelper = resourceQueryHelper;
_translationLookup = translationLookup;
_contextFactory = contextFactory;
_configurationHandler = configurationHandler;
}
[HttpGet]
public IActionResult TopPlayersAsync()
{
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
ViewBag.Servers = _manager.GetServers()
.Select(_server => new ServerInfo() {Name = _server.Hostname, ID = _server.EndPoint});
ViewBag.Localization = _translationLookup;
return View("~/Views/Client/Statistics/Index.cshtml");
}
[HttpGet]
public async Task<IActionResult> GetTopPlayersAsync(int count, int offset, long? serverId = null)
{
// this prevents empty results when we really want aggregate
if (serverId == 0)
{
serverId = null;
}
var server = _manager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
if (server != null)
{
serverId = StatManager.GetIdForServer(server);
}
var results = _configurationHandler.Configuration().EnableAdvancedMetrics
? await Plugin.Manager.GetNewTopStats(offset, count, serverId)
: await Plugin.Manager.GetTopStats(offset, count, serverId);
// this returns an empty result so we know to stale the loader
if (results.Count == 0 && offset > 0)
{
return Ok();
}
ViewBag.UseNewStats = _configurationHandler.Configuration().EnableAdvancedMetrics;
return View("~/Views/Client/Statistics/Components/TopPlayers/_List.cshtml", results);
}
[HttpGet]
public async Task<IActionResult> GetMessageAsync(string serverId, long when)
{
var whenTime = DateTime.FromFileTimeUtc(when);
var whenUpper = whenTime.AddMinutes(5);
var whenLower = whenTime.AddMinutes(-5);
var messages = await _chatResourceQueryHelper.QueryResource(new ChatSearchQuery()
{
ServerId = serverId,
SentBefore = whenUpper,
SentAfter = whenLower
});
return View("~/Views/Client/_MessageContext.cshtml", messages.Results.ToList());
}
[HttpGet("Message/Find")]
public async Task<IActionResult> FindMessage([FromQuery] string query)
{
ViewBag.Localization = _translationLookup;
ViewBag.EnableColorCodes = _manager.GetApplicationSettings().Configuration().EnableColorCodes;
ViewBag.Query = query;
ViewBag.QueryLimit = 100;
ViewBag.Title = _translationLookup["WEBFRONT_STATS_MESSAGES_TITLE"];
ViewBag.Error = null;
ViewBag.IsFluid = true;
ChatSearchQuery searchRequest = null;
try
{
searchRequest = query.ParseSearchInfo(int.MaxValue, 0);
}
catch (ArgumentException e)
{
_logger.LogWarning(e, "Could not parse chat message search query {query}", query);
ViewBag.Error = e;
}
catch (FormatException e)
{
_logger.LogWarning(e, "Could not parse chat message search query filter format {query}", query);
ViewBag.Error = e;
}
var result = searchRequest != null ? await _chatResourceQueryHelper.QueryResource(searchRequest) : null;
return View("~/Views/Client/Message/Find.cshtml", result);
}
[HttpGet("Message/FindNext")]
public async Task<IActionResult> FindNextMessages([FromQuery] string query, [FromQuery] int count,
[FromQuery] int offset)
{
ChatSearchQuery searchRequest;
try
{
searchRequest = query.ParseSearchInfo(count, offset);
}
catch (ArgumentException e)
{
_logger.LogWarning(e, "Could not parse chat message search query {query}", query);
throw;
}
catch (FormatException e)
{
_logger.LogWarning(e, "Could not parse chat message search query filter format {query}", query);
throw;
}
var result = await _chatResourceQueryHelper.QueryResource(searchRequest);
return PartialView("~/Views/Client/Message/_Item.cshtml", result.Results);
}
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int penaltyId)
{
await using var context = _contextFactory.CreateContext(false);
var penalty = await context.Penalties
.Select(_penalty => new
{_penalty.OffenderId, _penalty.PenaltyId, _penalty.When, _penalty.AutomatedOffense})
.FirstOrDefaultAsync(_penalty => _penalty.PenaltyId == penaltyId);
if (penalty == null)
{
return NotFound();
}
// todo: this can be optimized
var iqSnapshotInfo = context.ACSnapshots
.Where(s => s.ClientId == penalty.OffenderId)
.Include(s => s.LastStrainAngle)
.Include(s => s.HitOrigin)
.Include(s => s.HitDestination)
.Include(s => s.CurrentViewAngle)
.Include(s => s.PredictedViewAngles)
.ThenInclude(_angles => _angles.Vector)
.OrderBy(s => s.When)
.ThenBy(s => s.Hits);
var penaltyInfo = await iqSnapshotInfo.ToListAsync();
if (penaltyInfo.Count > 0)
{
return View("~/Views/Client/_PenaltyInfo.cshtml", penaltyInfo);
}
// we want to show anything related to the automated offense
else
{
return View("~/Views/Client/_MessageContext.cshtml", new List<MessageResponse>
{
new MessageResponse()
{
ClientId = penalty.OffenderId,
Message = penalty.AutomatedOffense,
When = penalty.When
}
});
}
}
}
}

View File

@ -6,6 +6,7 @@ using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
namespace WebfrontCore.Controllers
{

View File

@ -1,10 +1,15 @@
using Microsoft.AspNetCore.Diagnostics;
using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -15,11 +20,23 @@ namespace WebfrontCore.Controllers
{
private readonly ITranslationLookup _translationLookup;
private readonly ILogger _logger;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private const string ServerStatKey = nameof(ServerStatKey);
public HomeController(ILogger<HomeController> logger, IManager manager, ITranslationLookup translationLookup) : base(manager)
public HomeController(ILogger<HomeController> logger, IManager manager, ITranslationLookup translationLookup,
IDataValueCache<EFClient, (int, int)> serverStatsCache) : base(manager)
{
_logger = logger;
_translationLookup = translationLookup;
_serverStatsCache = serverStatsCache;
_serverStatsCache.SetCacheItem(async set =>
{
var count = await set.CountAsync();
var startOfPeriod = DateTime.UtcNow.AddHours(-24);
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod);
return (count, recentCount);
}, ServerStatKey);
}
public async Task<IActionResult> Index(Game? game = null)
@ -28,14 +45,15 @@ namespace WebfrontCore.Controllers
ViewBag.Title = Localization["WEBFRONT_HOME_TITLE"];
ViewBag.Keywords = Localization["WEBFRONT_KEWORDS_HOME"];
var servers = Manager.GetServers().Where(_server => !game.HasValue ? true : _server.GameName == game);
var servers = Manager.GetServers().Where(_server => !game.HasValue || _server.GameName == game);
var (count, recentCount) = await _serverStatsCache.GetCacheItem(ServerStatKey);
var model = new IW4MAdminInfo()
{
TotalAvailableClientSlots = servers.Sum(_server => _server.MaxClients),
TotalOccupiedClientSlots = servers.SelectMany(_server => _server.GetClientsAsList()).Count(),
TotalClientCount = await Manager.GetClientService().GetTotalClientsAsync(),
RecentClientCount = await Manager.GetClientService().GetRecentClientCount(),
TotalClientCount = count,
RecentClientCount = recentCount,
Game = game,
ActiveServerGames = Manager.GetServers().Select(_server => _server.GameName).Distinct().ToArray()
};
@ -46,7 +64,8 @@ namespace WebfrontCore.Controllers
public IActionResult Error()
{
var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
_logger.LogError("[Webfront] {path} {message} {@exception}", exceptionFeature.Path, exceptionFeature.Error.Message, exceptionFeature.Error);
_logger.LogError("[Webfront] {path} {message} {@exception}", exceptionFeature.Path,
exceptionFeature.Error.Message, exceptionFeature.Error);
ViewBag.Description = Localization["WEBFRONT_ERROR_DESC"];
ViewBag.Title = Localization["WEBFRONT_ERROR_TITLE"];
return View(exceptionFeature.Error);
@ -71,15 +90,16 @@ namespace WebfrontCore.Controllers
.GroupBy(_cmd =>
{
// we need the plugin type the command is defined in
var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type => _type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type));
return pluginType == null ?
_translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"] :
var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type =>
_type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type));
return pluginType == null ? _translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"] :
pluginType.Name == "ScriptPlugin" ? _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"] :
Manager.Plugins.First(_plugin => _plugin.GetType() == pluginType).Name; // for now we're just returning the name of the plugin, maybe later we'll include more info
Manager.Plugins.First(_plugin => _plugin.GetType() == pluginType)
.Name; // for now we're just returning the name of the plugin, maybe later we'll include more info
})
.Select(_grp => (_grp.Key, _grp.AsEnumerable()));
return View(commands);
}
}
}
}

View File

@ -1,13 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Database;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFPenalty;
using Data.Abstractions;
using Data.Models;
namespace WebfrontCore.Controllers
{
@ -20,7 +20,7 @@ namespace WebfrontCore.Controllers
_contextFactory = contextFactory;
}
public IActionResult List(PenaltyType showOnly = PenaltyType.Any, bool hideAutomatedPenalties = true)
public IActionResult List(EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool hideAutomatedPenalties = true)
{
ViewBag.Description = Localization["WEBFRONT_DESCRIPTION_PENALTIES"];
ViewBag.Title = Localization["WEBFRONT_PENALTY_TITLE"];
@ -30,7 +30,7 @@ namespace WebfrontCore.Controllers
return View(showOnly);
}
public async Task<IActionResult> ListAsync(int offset = 0, PenaltyType showOnly = PenaltyType.Any, bool hideAutomatedPenalties = true)
public async Task<IActionResult> ListAsync(int offset = 0, EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool hideAutomatedPenalties = true)
{
return await Task.FromResult(View("_List", new ViewModels.PenaltyFilterInfo()
{
@ -52,7 +52,7 @@ namespace WebfrontCore.Controllers
await using var ctx = _contextFactory.CreateContext(false);
var iqPenalties = ctx.Penalties
.AsNoTracking()
.Where(p => p.Type == PenaltyType.Ban && p.Active)
.Where(p => p.Type == EFPenalty.PenaltyType.Ban && p.Active)
.OrderByDescending(_penalty => _penalty.When)
.Select(p => new PenaltyInfo()
{