Compare commits

...

39 Commits

Author SHA1 Message Date
e56f574af4 fix introduced bug :) 2020-10-01 19:06:12 -05:00
513f495304 anticheat tweaks
- reset recoil state on map change
- refactor config
- remove m21 from chest detection
- allow ignored client ids
2020-10-01 19:05:52 -05:00
910faf427b enhance script plugin features
(support service resolver with generic args)
(support requiresTarget for command)
2020-10-01 19:05:38 -05:00
9117440566 merge to pre (#172)
* update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar

* add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x

* implement pm admins command for issue #170

* implement service resolver for script plugins
2020-09-26 18:15:56 -05:00
ad6b6a6465 merge to pre (#169)
* update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar

* add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x
2020-09-21 15:37:31 -05:00
eb8145a168 add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x 2020-09-04 12:58:54 -05:00
a560e05df8 update pipeline file for seperate builds 2020-08-31 12:33:39 -05:00
ac06b41a0b update shared library version 2020-08-31 12:31:40 -05:00
cce6482541 allow tracking of "zombie" clients to support stat tracking in zm 2020-08-31 12:13:20 -05:00
bc7dc3a71a Add XuidString and GuidString to EFClient to allow easier interfacing with mods 2020-08-31 12:03:06 -05:00
2be719d8f9 add website override mapping to tekno parser (_website -> sv_clanWebsite) 2020-08-31 11:58:56 -05:00
8a8dec8bbd remove hard coded paths to make it easier for building in debug mode
auto copy script plugins/localization for local builds
2020-08-26 09:54:56 -05:00
2b3e21d4ba fix most played formatting issue
prevent reverse proxy to 127.0.0.1 from counting as IW4MAdmin client
copy humanizer support lib to output dir
2020-08-21 18:12:00 -05:00
4590d94d7d update bundle minifier package to use .net core one 2020-08-20 13:10:43 -05:00
5842073f91 include "all" meta button on profile
include full humanizer package to library bug in russian translations
2020-08-20 11:08:21 -05:00
c783a04a52 hide chat for password protected servers for issue #162 2020-08-20 10:38:11 -05:00
4735864113 remove some left over warnings from deprecated packages 2020-08-19 14:50:49 -05:00
d70d8fd0ae merge 2020-08-18 20:15:46 -05:00
0dc4e12d61 another attempt to fix display of long client names/temporary t6 getinfo workaround 2020-08-18 20:11:41 -05:00
778e339a61 QOL updates for profile meta
implement filterable meta for issue #158
update translations and use humanizer lib with datetime/timespan for issue #80
2020-08-18 16:35:21 -05:00
1ef2ba5344 fix misaligned kick button with long names on webfront 2020-08-18 16:35:21 -05:00
126f2fcc47 Merge branch '2.4-pr' of https://github.com/RaidMax/IW4M-Admin into 2.4-pr 2020-08-18 16:33:45 -05:00
d5789dac81 Create FUNDING.yml (#161) 2020-08-12 20:48:07 -05:00
25e2438e7f fix misaligned kick button with long names on webfront 2020-08-12 13:46:14 -05:00
19107f9e85 Update README.md 2020-08-11 20:48:13 -05:00
0e44fa10f7 Consolidate README (#156)
* Consolidate README.
We use the wiki now for most information, this just reduces "duplicate" data.
2020-08-06 13:20:35 -05:00
ebb54ebfd7 Add ManualWebFrontURL to readme. (#150)
* Update README.md

* Update project links
2020-08-06 08:49:20 -05:00
03a27d113e Merge pull request #105 from xerxes-at/2.4-pr
Added support for the AC to PlutoT6
2020-08-06 08:48:53 -05:00
22f9e581ed fix dependency injection of comands in webfront preventing ui actions from working 2020-08-06 08:48:14 -05:00
b59504a882 grab gametype from status for T7 2020-08-05 09:43:31 -05:00
ed2b01f229 update action controller to dynamically generate command names in case of overridden names (issue #152) 2020-08-04 17:26:16 -05:00
f040dd5159 fix mislabled dragunov name in live radar 2020-08-01 18:14:29 -05:00
6c00cceb7a update stats plugin to properly use the new configurable broadcast prefix. 2020-08-01 09:58:23 -05:00
04a95aa58a add configurable command and broadcast command prefix for issue #149 2020-07-31 20:40:03 -05:00
6155493181 prevent action on report from activating on privileged clients 2020-07-27 16:22:07 -05:00
297e2c283f Merge branch '2.4-pr' of https://github.com/RaidMax/IW4M-Admin into 2.4-pr 2020-07-27 11:26:37 -05:00
021c0244b4 remove old test project 2020-07-15 10:11:37 -05:00
214d15384d remove discord deprecated discord webhook file, remove game log file as it's being moved to new repo 2020-07-15 10:09:58 -05:00
39fb3b9966 Added support for the AC to PlutoT6
PlutoT6 requires pre-compiled GSC files.
Thats why I include the source and a compiled version. Since we can not create new GSC files but only can replace existing ones I did use this stock GSC to add our code to it.
2020-01-25 19:12:05 +01:00
181 changed files with 4574 additions and 2992 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
ko_fi: raidmax

View File

@ -25,20 +25,20 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1632" /> <PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
<PackageReference Include="RestEase" Version="1.4.10" /> <PackageReference Include="RestEase" Version="1.5.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<ServerGarbageCollection>false</ServerGarbageCollection> <ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation> <TieredCompilation>true</TieredCompilation>
<LangVersion>7.1</LangVersion> <LangVersion>Latest</LangVersion>
<StartupObject></StartupObject> <StartupObject></StartupObject>
</PropertyGroup> </PropertyGroup>

View File

@ -13,6 +13,7 @@ using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using System; using System;
using System.Collections; using System.Collections;
@ -54,7 +55,7 @@ namespace IW4MAdmin.Application
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>(); private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>();
private readonly MetaService _metaService; private readonly IMetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource; private readonly CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>(); private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
@ -65,12 +66,15 @@ namespace IW4MAdmin.Application
private readonly IEnumerable<IRegisterEvent> _customParserEvents; private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly IEventHandler _eventHandler; private readonly IEventHandler _eventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory; private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands, public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration, ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory) IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
@ -81,11 +85,11 @@ namespace IW4MAdmin.Application
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger) }; AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, appConfigHandler.Configuration()) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
_metaService = new MetaService(); _metaService = metaService;
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();
_loggers.Add(0, logger); _loggers.Add(0, logger);
_commands = commands.ToList(); _commands = commands.ToList();
@ -96,6 +100,8 @@ namespace IW4MAdmin.Application
_customParserEvents = customParserEvents; _customParserEvents = customParserEvents;
_eventHandler = eventHandler; _eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory; _scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
Plugins = plugins; Plugins = plugins;
} }
@ -273,12 +279,12 @@ namespace IW4MAdmin.Application
{ {
if (plugin is ScriptPlugin scriptPlugin) if (plugin is ScriptPlugin scriptPlugin)
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory); await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
scriptPlugin.Watcher.Changed += async (sender, e) => scriptPlugin.Watcher.Changed += async (sender, e) =>
{ {
try try
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory); await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
} }
catch (Exception ex) catch (Exception ex)
@ -432,6 +438,11 @@ namespace IW4MAdmin.Application
commandsToAddToConfig.AddRange(unsavedCommands); commandsToAddToConfig.AddRange(unsavedCommands);
} }
// this is because I want to store the command prefix in IW4MAdminSettings, but can't easily
// inject it to all the places that need it
cmdConfig.CommandPrefix = config.CommandPrefix;
cmdConfig.BroadcastCommandPrefix = config.BroadcastCommandPrefix;
foreach (var cmd in commandsToAddToConfig) foreach (var cmd in commandsToAddToConfig)
{ {
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(), cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
@ -440,7 +451,8 @@ namespace IW4MAdmin.Application
Name = cmd.Name, Name = cmd.Name,
Alias = cmd.Alias, Alias = cmd.Alias,
MinimumPermission = cmd.Permission, MinimumPermission = cmd.Permission,
AllowImpersonation = cmd.AllowImpersonation AllowImpersonation = cmd.AllowImpersonation,
SupportedGames = cmd.SupportedGames
}); });
} }
@ -448,133 +460,7 @@ namespace IW4MAdmin.Application
await _commandConfiguration.Save(); await _commandConfiguration.Save();
#endregion #endregion
#region META _metaRegistration.Register();
async Task<List<ProfileMeta>> getProfileMeta(int clientId, int offset, int count, DateTime? startAt)
{
var metaList = new List<ProfileMeta>();
// we don't want to return anything because it means we're trying to retrieve paged meta data
if (count > 1)
{
return metaList;
}
var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = clientId });
if (lastMapMeta != null)
{
metaList.Add(new ProfileMeta()
{
Id = lastMapMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
Value = lastMapMeta.Value,
Show = true,
Type = ProfileMeta.MetaType.Information,
});
}
var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = clientId });
if (lastServerMeta != null)
{
metaList.Add(new ProfileMeta()
{
Id = lastServerMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
Value = lastServerMeta.Value,
Show = true,
Type = ProfileMeta.MetaType.Information
});
}
var client = await GetClientService().Get(clientId);
if (client == null)
{
_logger.WriteWarning($"No client found with id {clientId} when generating profile meta");
return metaList;
}
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_TIME_HOURS"]} {Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"]}",
Value = Math.Round(client.TotalConnectionTime / 3600.0, 1).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Show = true,
Column = 1,
Order = 0,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_FSEEN"],
Value = Utilities.GetTimePassed(client.FirstConnection, false),
Show = true,
Column = 1,
Order = 1,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_LSEEN"],
Value = Utilities.GetTimePassed(client.LastConnection, false),
Show = true,
Column = 1,
Order = 2,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Show = true,
Column = 1,
Order = 3,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_MASKED"],
Value = client.Masked ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"] : Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
Sensitive = true,
Column = 1,
Order = 4,
Type = ProfileMeta.MetaType.Information
});
return metaList;
}
async Task<List<ProfileMeta>> getPenaltyMeta(int clientId, int offset, int count, DateTime? startAt)
{
if (count <= 1)
{
return new List<ProfileMeta>();
}
var penalties = await GetPenaltyService().GetClientPenaltyForMetaAsync(clientId, count, offset, startAt);
return penalties.Select(_penalty => new ProfileMeta()
{
Id = _penalty.Id,
Type = _penalty.PunisherId == clientId ? ProfileMeta.MetaType.Penalized : ProfileMeta.MetaType.ReceivedPenalty,
Value = _penalty,
When = _penalty.TimePunished,
Sensitive = _penalty.Sensitive
})
.ToList();
}
MetaService.AddRuntimeMeta(getProfileMeta);
MetaService.AddRuntimeMeta(getPenaltyMeta);
#endregion
#region CUSTOM_EVENTS #region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events)) foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -729,7 +615,7 @@ namespace IW4MAdmin.Application
public IEventParser GenerateDynamicEventParser(string name) public IEventParser GenerateDynamicEventParser(string name)
{ {
return new DynamicEventParser(_parserRegexFactory, _logger) return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration())
{ {
Name = name Name = name
}; };

View File

@ -0,0 +1,12 @@
param (
[string]$OutputDir = $(throw "-OutputDir is required.")
)
$localizations = @("en-US", "ru-RU", "es-EC", "pt-BR", "de-DE")
foreach($localization in $localizations)
{
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
$filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization
$response = Invoke-WebRequest $url
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
}

View File

@ -26,6 +26,10 @@ if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
move "%PublishDir%\*.dll" "%PublishDir%\Lib\" move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
move "%PublishDir%\*.json" "%PublishDir%\Lib\" move "%PublishDir%\*.json" "%PublishDir%\Lib\"
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes" move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
move "%PublishDir%\ru" "%PublishDir%\Lib\ru"
move "%PublishDir%\de" "%PublishDir%\Lib\de"
move "%PublishDir%\pt" "%PublishDir%\Lib\pt"
move "%PublishDir%\es" "%PublishDir%\Lib\es"
if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs" if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
echo making start scripts echo making start scripts

View File

@ -1,3 +1,6 @@
set SolutionDir=%1 set SolutionDir=%1
set ProjectDir=%2 set ProjectDir=%2
set TargetDir=%3 set TargetDir=%3
echo D | xcopy "%SolutionDir%Plugins\ScriptPlugins\*.js" "%TargetDir%Plugins" /y
powershell -File "%ProjectDir%BuildScripts\DownloadTranslations.ps1" %TargetDir%

View File

@ -1,4 +1,5 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
@ -12,11 +13,13 @@ namespace IW4MAdmin.Application.EventParsers
{ {
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations; private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger) public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
{ {
_customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>(); _customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger; _logger = logger;
_appConfig = appConfig;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory) Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{ {
@ -127,8 +130,7 @@ namespace IW4MAdmin.Application.EventParsers
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
// todo: these need to defined outside of here if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
if (message[0] == '!' || message[0] == '@')
{ {
return new GameEvent() return new GameEvent()
{ {
@ -253,6 +255,7 @@ namespace IW4MAdmin.Application.EventParsers
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting, State = EFClient.ClientState.Connecting,
}, },
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true, IsBlocking = true,
GameTime = gameTime, GameTime = gameTime,

View File

@ -1,4 +1,5 @@
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {
@ -8,7 +9,7 @@ namespace IW4MAdmin.Application.EventParsers
/// </summary> /// </summary>
sealed internal class DynamicEventParser : BaseEventParser sealed internal class DynamicEventParser : BaseEventParser
{ {
public DynamicEventParser(IParserRegexFactory parserRegexFactory, ILogger logger) : base(parserRegexFactory, logger) public DynamicEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig) : base(parserRegexFactory, logger, appConfig)
{ {
} }
} }

View File

@ -13,17 +13,19 @@ namespace IW4MAdmin.Application.Factories
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IRConConnectionFactory _rconConnectionFactory; private readonly IRConConnectionFactory _rconConnectionFactory;
private readonly IGameLogReaderFactory _gameLogReaderFactory; private readonly IGameLogReaderFactory _gameLogReaderFactory;
private readonly IMetaService _metaService;
/// <summary> /// <summary>
/// base constructor /// base constructor
/// </summary> /// </summary>
/// <param name="translationLookup"></param> /// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param> /// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory) public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService)
{ {
_translationLookup = translationLookup; _translationLookup = translationLookup;
_rconConnectionFactory = rconConnectionFactory; _rconConnectionFactory = rconConnectionFactory;
_gameLogReaderFactory = gameLogReaderFactory; _gameLogReaderFactory = gameLogReaderFactory;
_metaService = metaService;
} }
/// <summary> /// <summary>
@ -34,7 +36,7 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager) public Server CreateServer(ServerConfiguration config, IManager manager)
{ {
return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory); return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory, _metaService);
} }
} }
} }

View File

@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Factories
} }
/// <inheritdoc/> /// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction) public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
{ {
var permissionEnum = Enum.Parse<Permission>(permission); var permissionEnum = Enum.Parse<Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument var argsArray = args.Select(_arg => new CommandArgument
@ -34,7 +34,7 @@ namespace IW4MAdmin.Application.Factories
Required = _arg.Item2 Required = _arg.Item2
}).ToArray(); }).ToArray();
return new ScriptCommand(name, alias, description, permissionEnum, argsArray, executeAction, _config, _transLookup); return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction, _config, _transLookup);
} }
} }
} }

View File

@ -7,7 +7,6 @@ using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -26,15 +25,17 @@ namespace IW4MAdmin
private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex; private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex;
public GameLogEventDetection LogEvent; public GameLogEventDetection LogEvent;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IMetaService _metaService;
private const int REPORT_FLAG_COUNT = 4; private const int REPORT_FLAG_COUNT = 4;
private int lastGameTime = 0; private int lastGameTime = 0;
public int Id { get; private set; } public int Id { get; private set; }
public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup, public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup,
IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory) : base(cfg, mgr, connectionFactory, gameLogReaderFactory) IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService) : base(cfg, mgr, connectionFactory, gameLogReaderFactory)
{ {
_translationLookup = lookup; _translationLookup = lookup;
_metaService = metaService;
} }
override public async Task<EFClient> OnClientConnected(EFClient clientFromLog) override public async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -139,7 +140,7 @@ namespace IW4MAdmin
{ {
try try
{ {
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E); C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration());
} }
catch (CommandException e) catch (CommandException e)
@ -475,8 +476,8 @@ namespace IW4MAdmin
Time = DateTime.UtcNow Time = DateTime.UtcNow
}); });
await new MetaService().AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin); await _metaService.AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
await new MetaService().AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin); await _metaService.AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
} }
else if (E.Type == GameEvent.EventType.PreDisconnect) else if (E.Type == GameEvent.EventType.PreDisconnect)
@ -540,14 +541,18 @@ namespace IW4MAdmin
.First(_qm => _qm.Game == GameName) .First(_qm => _qm.Game == GameName)
.Messages[E.Data.Substring(1)]; .Messages[E.Data.Substring(1)];
} }
catch { } catch
{
message = E.Data.Substring(1);
}
} }
ChatHistory.Add(new ChatInfo() ChatHistory.Add(new ChatInfo()
{ {
Name = E.Origin.Name, Name = E.Origin.Name,
Message = message, Message = message,
Time = DateTime.UtcNow Time = DateTime.UtcNow,
IsHidden = !string.IsNullOrEmpty(GamePassword)
}); });
} }
} }
@ -610,13 +615,10 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.Broadcast) if (E.Type == GameEvent.EventType.Broadcast)
{ {
#if DEBUG == false if (!Utilities.IsDevelopment && E.Data != null) // hides broadcast when in development mode
// this is a little ugly but I don't want to change the abstract class
if (E.Data != null)
{ {
await E.Owner.ExecuteCommandAsync(E.Data); await E.Owner.ExecuteCommandAsync(E.Data);
} }
#endif
} }
lock (ChatHistory) lock (ChatHistory)
@ -703,6 +705,7 @@ namespace IW4MAdmin
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients); var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
UpdateMap(statusResponse.Item2); UpdateMap(statusResponse.Item2);
UpdateGametype(statusResponse.Item3);
return new List<EFClient>[] return new List<EFClient>[]
{ {
@ -724,6 +727,14 @@ namespace IW4MAdmin
} }
} }
private void UpdateGametype(string gameType)
{
if (!string.IsNullOrEmpty(gameType))
{
Gametype = gameType;
}
}
private async Task ShutdownInternal() private async Task ShutdownInternal()
{ {
foreach (var client in GetClientsAsList()) foreach (var client in GetClientsAsList())
@ -774,7 +785,7 @@ namespace IW4MAdmin
var polledClients = await PollPlayersAsync(); var polledClients = await PollPlayersAsync();
foreach (var disconnectingClient in polledClients[1]) foreach (var disconnectingClient in polledClients[1].Where(_client => !_client.IsZombieClient /* ignores "fake" zombie clients */))
{ {
disconnectingClient.CurrentServer = this; disconnectingClient.CurrentServer = this;
var e = new GameEvent() var e = new GameEvent()
@ -805,6 +816,7 @@ namespace IW4MAdmin
Origin = client, Origin = client,
Owner = this, Owner = this,
IsBlocking = true, IsBlocking = true,
Extra = client.GetAdditionalProperty<string>("BotGuid"),
Source = GameEvent.EventSource.Status Source = GameEvent.EventSource.Status
}; };
@ -975,6 +987,7 @@ namespace IW4MAdmin
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log"); var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log");
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync"); var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync");
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip"); var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip");
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "");
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName) if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName)
{ {
@ -1008,6 +1021,7 @@ namespace IW4MAdmin
this.FSGame = game.Value; this.FSGame = game.Value;
this.Gametype = gametype; this.Gametype = gametype;
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress; this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
this.GamePassword = gamePassword.Value;
UpdateMap(mapname); UpdateMap(mapname);
if (RconParser.CanGenerateLogPath) if (RconParser.CanGenerateLogPath)
@ -1074,9 +1088,11 @@ namespace IW4MAdmin
Logger.WriteInfo($"Log file is {LogPath}"); Logger.WriteInfo($"Log file is {LogPath}");
_ = Task.Run(() => LogEvent.PollForChanges()); _ = Task.Run(() => LogEvent.PollForChanges());
#if !DEBUG
Broadcast(loc["BROADCAST_ONLINE"]); if (!Utilities.IsDevelopment)
#endif {
Broadcast(loc["BROADCAST_ONLINE"]);
}
} }
public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl) public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl)
@ -1224,6 +1240,7 @@ namespace IW4MAdmin
if (targetClient.IsIngame) if (targetClient.IsIngame)
{ {
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}"); string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}");
Logger.WriteDebug($"Executing tempban kick command for {targetClient}");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
} }
} }

View File

@ -2,16 +2,23 @@
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories; using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Helpers; using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using RestEase; using RestEase;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Repositories; using SharedLibraryCore.Repositories;
using SharedLibraryCore.Services;
using Stats.Dtos;
using StatsWeb;
using System; using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -128,7 +135,11 @@ namespace IW4MAdmin.Application
await ApplicationTask; await ApplicationTask;
} }
catch { } catch (Exception e)
{
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
}
if (ServerManager.IsRestartRequested) if (ServerManager.IsRestartRequested)
{ {
@ -222,7 +233,7 @@ namespace IW4MAdmin.Application
serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection) serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
.AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>) .AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>)
.AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>) .AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>)
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration()) .AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration() ?? new ApplicationConfiguration())
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration()) .AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration())
.AddSingleton<ILogger>(_serviceProvider => defaultLogger) .AddSingleton<ILogger>(_serviceProvider => defaultLogger)
.AddSingleton<IPluginImporter, PluginImporter>() .AddSingleton<IPluginImporter, PluginImporter>()
@ -235,6 +246,14 @@ namespace IW4MAdmin.Application
.AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>() .AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>()
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>() .AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>() .AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddSingleton<IEntityService<EFClient>, ClientService>()
.AddSingleton<IMetaService, MetaService>()
.AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>() .AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton(_serviceProvider => .AddSingleton(_serviceProvider =>
{ {

View File

@ -0,0 +1,64 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
namespace IW4MAdmin.Application.Meta
{
/// <summary>
/// implementation of IResourceQueryHelper
/// query helper that retrieves administered penalties for provided client id
/// </summary>
public class AdministeredPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public AdministeredPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
}
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId)
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When);
var penalties = await iqPenalties
.Take(query.Count)
.Select(_penalty => new AdministeredPenaltyResponse()
{
PenaltyId = _penalty.PenaltyId,
Offense = _penalty.Offense,
AutomatedOffense = _penalty.AutomatedOffense,
ClientId = _penalty.OffenderId,
OffenderName = _penalty.Offender.CurrentAlias.Name,
OffenderClientId = _penalty.Offender.ClientId,
PunisherClientId = _penalty.PunisherId,
PunisherName = _penalty.Punisher.CurrentAlias.Name,
PenaltyType = _penalty.Type,
When = _penalty.When,
ExpirationDate = _penalty.Expires,
IsLinked = _penalty.OffenderId != query.ClientId,
IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag
})
.ToListAsync();
return new ResourceQueryHelperResult<AdministeredPenaltyResponse>
{
// todo: might need to do count at some point
RetrievedResultCount = penalties.Count,
Results = penalties
};
}
}
}

View File

@ -0,0 +1,165 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Meta
{
public class MetaRegistration : IMetaRegistration
{
private readonly ILogger _logger;
private ITranslationLookup _transLookup;
private readonly IMetaService _metaService;
private readonly IEntityService<EFClient> _clientEntityService;
private readonly IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> _receivedPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper;
public MetaRegistration(ILogger logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper)
{
_logger = logger;
_transLookup = transLookup;
_metaService = metaService;
_clientEntityService = clientEntityService;
_receivedPenaltyHelper = receivedPenaltyHelper;
_administeredPenaltyHelper = administeredPenaltyHelper;
_updatedAliasHelper = updatedAliasHelper;
}
public void Register()
{
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, GetProfileMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty, GetReceivedPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized, GetAdministeredPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate, GetUpdatedAliasMeta);
}
private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request)
{
var metaList = new List<InformationResponse>();
var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = request.ClientId });
if (lastMapMeta != null)
{
metaList.Add(new InformationResponse()
{
ClientId = request.ClientId,
MetaId = lastMapMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
Value = lastMapMeta.Value,
ShouldDisplay = true,
Type = MetaType.Information,
Column = 1,
Order = 6
});
}
var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = request.ClientId });
if (lastServerMeta != null)
{
metaList.Add(new InformationResponse()
{
ClientId = request.ClientId,
MetaId = lastServerMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
Value = lastServerMeta.Value,
ShouldDisplay = true,
Type = MetaType.Information,
Column = 0,
Order = 6
});
}
var client = await _clientEntityService.Get(request.ClientId);
if (client == null)
{
_logger.WriteWarning($"No client found with id {request.ClientId} when generating profile meta");
return metaList;
}
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"],
Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Column = 1,
Order = 0,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"],
Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Column = 1,
Order = 1,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"],
Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Column = 1,
Order = 2,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
ShouldDisplay = true,
Column = 1,
Order = 3,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_MASKED"],
Value = client.Masked ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"] : Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
IsSensitive = true,
Column = 1,
Order = 4,
Type = MetaType.Information
});
return metaList;
}
private async Task<IEnumerable<ReceivedPenaltyResponse>> GetReceivedPenaltiesMeta(ClientPaginationRequest request)
{
var penalties = await _receivedPenaltyHelper.QueryResource(request);
return penalties.Results;
}
private async Task<IEnumerable<AdministeredPenaltyResponse>> GetAdministeredPenaltiesMeta(ClientPaginationRequest request)
{
var penalties = await _administeredPenaltyHelper.QueryResource(request);
return penalties.Results;
}
private async Task<IEnumerable<UpdatedAliasResponse>> GetUpdatedAliasMeta(ClientPaginationRequest request)
{
var aliases = await _updatedAliasHelper.QueryResource(request);
return aliases.Results;
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
namespace IW4MAdmin.Application.Meta
{
/// <summary>
/// implementation of IResourceQueryHelper
/// used to pull in penalties applied to a given client id
/// </summary>
public class ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public ReceivedPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
}
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{
var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
using var ctx = _contextFactory.CreateContext(enableTracking: false);
var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId)
.Select(_client => _client.AliasLinkId)
.FirstOrDefaultAsync();
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => _penalty.OffenderId == query.ClientId || (linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId))
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When);
var penalties = await iqPenalties
.Take(query.Count)
.Select(_penalty => new ReceivedPenaltyResponse()
{
PenaltyId = _penalty.PenaltyId,
ClientId = query.ClientId,
Offense = _penalty.Offense,
AutomatedOffense = _penalty.AutomatedOffense,
OffenderClientId = _penalty.OffenderId,
OffenderName = _penalty.Offender.CurrentAlias.Name,
PunisherClientId = _penalty.PunisherId,
PunisherName = _penalty.Punisher.CurrentAlias.Name,
PenaltyType = _penalty.Type,
When = _penalty.When,
ExpirationDate = _penalty.Expires,
IsLinked = _penalty.OffenderId != query.ClientId,
IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag
})
.ToListAsync();
return new ResourceQueryHelperResult<ReceivedPenaltyResponse>
{
// todo: maybe actually count
RetrievedResultCount = penalties.Count,
Results = penalties
};
}
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System.Linq;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Meta
{
/// <summary>
/// implementation if IResrouceQueryHerlp
/// used to pull alias changes for given client id
/// </summary>
public class UpdatedAliasResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public UpdatedAliasResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_contextFactory = contextFactory;
}
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
var iqAliasUpdates = ctx.Aliases
.Where(_alias => _alias.LinkId == linkId)
.Where(_alias => _alias.DateAdded < query.Before)
.Where(_alias => _alias.IPAddress != null)
.OrderByDescending(_alias => _alias.DateAdded)
.Select(_alias => new UpdatedAliasResponse
{
MetaId = _alias.AliasId,
Name = _alias.Name,
IPAddress = _alias.IPAddress.ConvertIPtoString(),
When = _alias.DateAdded,
Type = MetaType.AliasUpdate,
IsSensitive = true
});
var result = (await iqAliasUpdates
.Take(query.Count)
.ToListAsync())
.Distinct();
return new ResourceQueryHelperResult<UpdatedAliasResponse>
{
Results = result, // we can potentially have duplicates
RetrievedResultCount = result.Count()
};
}
}
}

View File

@ -0,0 +1,187 @@
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IMetaService
/// used to add and retrieve runtime and persistent meta
/// </summary>
public class MetaService : IMetaService
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public MetaService(ILogger logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
}
public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client)
{
// this seems to happen if the client disconnects before they've had time to authenticate and be added
if (client.ClientId < 1)
{
return;
}
using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
.Where(_meta => _meta.ClientId == client.ClientId)
.FirstOrDefaultAsync();
if (existingMeta != null)
{
existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow;
}
else
{
ctx.EFMeta.Add(new EFMeta()
{
ClientId = client.ClientId,
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue
});
}
await ctx.SaveChangesAsync();
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
.Where(_meta => _meta.ClientId == client.ClientId)
.Select(_meta => new EFMeta()
{
MetaId = _meta.MetaId,
Key = _meta.Key,
ClientId = _meta.ClientId,
Value = _meta.Value
})
.FirstOrDefaultAsync();
}
public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta
{
if (!_metaActions.ContainsKey(metaKey))
{
_metaActions.Add(metaKey, new List<dynamic>() { metaAction });
}
else
{
_metaActions[metaKey].Add(metaAction);
}
}
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request)
{
var meta = new List<IClientMeta>();
foreach (var (type, actions) in _metaActions)
{
// information is not listed chronologically
if (type != MetaType.Information)
{
var metaItems = await actions[0](request);
meta.AddRange(metaItems);
}
}
return meta.OrderByDescending(_meta => _meta.When)
.Take(request.Count)
.ToList();
}
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta
{
IEnumerable<T> meta;
if (metaType == MetaType.Information)
{
var allMeta = new List<T>();
foreach (var individualMetaRegistration in _metaActions[metaType])
{
allMeta.AddRange(await individualMetaRegistration(request));
}
return ProcessInformationMeta(allMeta);
}
else
{
meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
}
return meta;
}
private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta
{
var table = new List<List<T>>();
var metaWithColumn = meta
.Where(_meta => _meta.Column != null);
var columnGrouping = metaWithColumn
.GroupBy(_meta => _meta.Column);
var metaToSort = meta.Except(metaWithColumn).ToList();
foreach (var metaItem in columnGrouping)
{
table.Add(new List<T>(metaItem));
}
while (metaToSort.Count > 0)
{
var sortingMeta = metaToSort.First();
int indexOfSmallestColumn()
{
int index = 0;
int smallestColumnSize = int.MaxValue;
for (int i = 0; i < table.Count; i++)
{
if (table[i].Count < smallestColumnSize)
{
smallestColumnSize = table[i].Count;
index = i;
}
}
return index;
}
int columnIndex = indexOfSmallestColumn();
sortingMeta.Column = columnIndex;
sortingMeta.Order = columnGrouping
.First(_group => _group.Key == columnIndex)
.Count();
table[columnIndex].Add(sortingMeta);
metaToSort.Remove(sortingMeta);
}
return meta;
}
}
}

View File

@ -15,7 +15,7 @@ namespace IW4MAdmin.Application.Misc
{ {
private readonly Action<GameEvent> _executeAction; private readonly Action<GameEvent> _executeAction;
public ScriptCommand(string name, string alias, string description, Permission permission, public ScriptCommand(string name, string alias, string description, bool isTargetRequired, Permission permission,
CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout) CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout)
: base(config, layout) : base(config, layout)
{ {
@ -24,6 +24,7 @@ namespace IW4MAdmin.Application.Misc
Name = name; Name = name;
Alias = alias; Alias = alias;
Description = description; Description = description;
RequiresTarget = isTargetRequired;
Permission = permission; Permission = permission;
Arguments = args; Arguments = args;
} }

View File

@ -61,7 +61,7 @@ namespace IW4MAdmin.Application.Misc
_onProcessing.Dispose(); _onProcessing.Dispose();
} }
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory) public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory, IScriptPluginServiceResolver serviceResolver)
{ {
await _onProcessing.WaitAsync(); await _onProcessing.WaitAsync();
@ -114,6 +114,7 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine.Execute(script); _scriptEngine.Execute(script);
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization); _scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject(); dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
Author = pluginObject.author; Author = pluginObject.author;
@ -164,6 +165,11 @@ namespace IW4MAdmin.Application.Misc
successfullyLoaded = true; successfullyLoaded = true;
} }
catch (JavaScriptException ex)
{
throw new PluginException($"An error occured while initializing script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName };
}
catch catch
{ {
throw; throw;
@ -246,6 +252,7 @@ namespace IW4MAdmin.Application.Misc
string alias = dynamicCommand.alias; string alias = dynamicCommand.alias;
string description = dynamicCommand.description; string description = dynamicCommand.description;
string permission = dynamicCommand.permission; string permission = dynamicCommand.permission;
bool targetRequired = false;
List<(string, bool)> args = new List<(string, bool)>(); List<(string, bool)> args = new List<(string, bool)>();
dynamic arguments = null; dynamic arguments = null;
@ -260,6 +267,16 @@ namespace IW4MAdmin.Application.Misc
// arguments are optional // arguments are optional
} }
try
{
targetRequired = dynamicCommand.targetRequired;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
if (arguments != null) if (arguments != null)
{ {
foreach (var arg in dynamicCommand.arguments) foreach (var arg in dynamicCommand.arguments)
@ -284,7 +301,7 @@ namespace IW4MAdmin.Application.Misc
} }
} }
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, args, execute)); commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, targetRequired, args, execute));
} }
return commandList; return commandList;

View File

@ -0,0 +1,48 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IScriptPluginServiceResolver
/// </summary>
public class ScriptPluginServiceResolver : IScriptPluginServiceResolver
{
private readonly IServiceProvider _serviceProvider;
public ScriptPluginServiceResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public object ResolveService(string serviceName)
{
var serviceType = DetermineRootType(serviceName);
return _serviceProvider.GetService(serviceType);
}
public object ResolveService(string serviceName, string[] genericParameters)
{
var serviceType = DetermineRootType(serviceName, genericParameters.Length);
var genericTypes = genericParameters.Select(_genericTypeParam => DetermineRootType(_genericTypeParam));
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
return _serviceProvider.GetService(resolvedServiceType);
}
private Type DetermineRootType(string serviceName, int genericParamCount = 0)
{
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes());
string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
if (serviceType == null)
{
throw new InvalidOperationException($"No object type '{serviceName}' defined in loaded assemblies");
}
return serviceType;
}
}
}

View File

@ -52,6 +52,7 @@ namespace IW4MAdmin.Application.RconParsers
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *"; Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *";
Configuration.GametypeStatus.Pattern = "";
Configuration.MapStatus.Pattern = @"map: (([a-z]|_|\d)+)"; Configuration.MapStatus.Pattern = @"map: (([a-z]|_|\d)+)";
Configuration.MapStatus.AddMapping(ParserRegex.GroupType.RConStatusMap, 1); Configuration.MapStatus.AddMapping(ParserRegex.GroupType.RConStatusMap, 1);
@ -114,7 +115,7 @@ namespace IW4MAdmin.Application.RconParsers
}; };
} }
public virtual async Task<(List<EFClient>, string)> GetStatusAsync(IRConConnection connection) public virtual async Task<(List<EFClient>, string, string)> GetStatusAsync(IRConConnection connection)
{ {
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
#if DEBUG #if DEBUG
@ -123,7 +124,7 @@ namespace IW4MAdmin.Application.RconParsers
Console.WriteLine(line); Console.WriteLine(line);
} }
#endif #endif
return (ClientsFromStatus(response), MapFromStatus(response)); return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response));
} }
private string MapFromStatus(string[] response) private string MapFromStatus(string[] response)
@ -141,6 +142,26 @@ namespace IW4MAdmin.Application.RconParsers
return map; return map;
} }
private string GameTypeFromStatus(string[] response)
{
if (string.IsNullOrWhiteSpace(Configuration.GametypeStatus.Pattern))
{
return null;
}
string gametype = null;
foreach (var line in response)
{
var regex = Regex.Match(line, Configuration.GametypeStatus.Pattern);
if (regex.Success)
{
gametype = regex.Groups[Configuration.GametypeStatus.GroupMapping[ParserRegex.GroupType.RConStatusGametype]].ToString();
}
}
return gametype;
}
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
{ {
string dvarString = (dvarValue is string str) string dvarString = (dvarValue is string str)
@ -182,10 +203,11 @@ namespace IW4MAdmin.Application.RconParsers
long networkId; long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
try try
{ {
string networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() ? networkId = networkIdString.IsBotGuid() ?
name.GenerateGuidFromString() : name.GenerateGuidFromString() :
@ -213,6 +235,8 @@ namespace IW4MAdmin.Application.RconParsers
State = EFClient.ClientState.Connecting State = EFClient.ClientState.Connecting
}; };
client.SetAdditionalProperty("BotGuid", networkIdString);
StatusPlayers.Add(client); StatusPlayers.Add(client);
} }
} }

View File

@ -14,6 +14,7 @@ namespace IW4MAdmin.Application.RconParsers
public CommandPrefix CommandPrefixes { get; set; } public CommandPrefix CommandPrefixes { get; set; }
public ParserRegex Status { get; set; } public ParserRegex Status { get; set; }
public ParserRegex MapStatus { get; set; } public ParserRegex MapStatus { get; set; }
public ParserRegex GametypeStatus { get; set; }
public ParserRegex Dvar { get; set; } public ParserRegex Dvar { get; set; }
public ParserRegex StatusHeader { get; set; } public ParserRegex StatusHeader { get; set; }
public string ServerNotRunningResponse { get; set; } public string ServerNotRunningResponse { get; set; }
@ -26,6 +27,7 @@ namespace IW4MAdmin.Application.RconParsers
{ {
Status = parserRegexFactory.CreateParserRegex(); Status = parserRegexFactory.CreateParserRegex();
MapStatus = parserRegexFactory.CreateParserRegex(); MapStatus = parserRegexFactory.CreateParserRegex();
GametypeStatus = parserRegexFactory.CreateParserRegex();
Dvar = parserRegexFactory.CreateParserRegex(); Dvar = parserRegexFactory.CreateParserRegex();
StatusHeader = parserRegexFactory.CreateParserRegex(); StatusHeader = parserRegexFactory.CreateParserRegex();
} }

View File

@ -1,214 +0,0 @@
import requests
import time
import json
import collections
import os
# the following classes model the discord webhook api parameters
class WebhookAuthor():
def __init__(self, name=None, url=None, icon_url=None):
if name:
self.name = name
if url:
self.url = url
if icon_url:
self.icon_url = icon_url
class WebhookField():
def __init__(self, name=None, value=None, inline=False):
if name:
self.name = name
if value:
self.value = value
if inline:
self.inline = inline
class WebhookEmbed():
def __init__(self):
self.author = ''
self.title = ''
self.url = ''
self.description = ''
self.color = 0
self.fields = []
self.thumbnail = {}
class WebhookParams():
def __init__(self, username=None, avatar_url=None, content=None):
self.username = ''
self.avatar_url = ''
self.content = ''
self.embeds = []
# quick way to convert all the objects to a nice json object
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
# gets the relative link to a user's profile
def get_client_profile(profile_id):
return u'{}/Client/ProfileAsync/{}'.format(base_url, profile_id)
def get_client_profile_markdown(client_name, profile_id):
return u'[{}]({})'.format(client_name, get_client_profile(profile_id))
#todo: exception handling for opening the file
if os.getenv("DEBUG"):
config_file_name = 'config.dev.json'
else:
config_file_name = 'config.json'
with open(config_file_name) as json_config_file:
json_config = json.load(json_config_file)
# this should be an URL to an IP or FQN to an IW4MAdmin instance
# ie http://127.0.0.1 or http://IW4MAdmin.com
base_url = json_config['IW4MAdminUrl']
end_point = '/api/event'
request_url = base_url + end_point
# this should be the full discord webhook url
# ie https://discordapp.com/api/webhooks/<id>/<token>
discord_webhook_notification_url = json_config['DiscordWebhookNotificationUrl']
discord_webhook_information_url = json_config['DiscordWebhookInformationUrl']
# this should be the numerical id of the discord group
# 12345678912345678
notify_role_ids = json_config['NotifyRoleIds']
def get_new_events():
events = []
response = requests.get(request_url)
data = response.json()
should_notify = False
for event in data:
# commonly used event info items
event_type = event['eventType']['name']
server_name = event['ownerEntity']['name']
if event['originEntity']:
origin_client_name = event['originEntity']['name']
origin_client_id = int(event['originEntity']['id'])
if event['targetEntity']:
target_client_name = event['targetEntity']['name'] or ''
target_client_id = int(event['targetEntity']['id']) or 0
webhook_item = WebhookParams()
webhook_item_embed = WebhookEmbed()
#todo: the following don't need to be generated every time, as it says the same
webhook_item.username = 'IW4MAdmin'
webhook_item.avatar_url = 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png'
webhook_item_embed.color = 31436
webhook_item_embed.url = base_url
webhook_item_embed.thumbnail = { 'url' : 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png' }
webhook_item.embeds.append(webhook_item_embed)
# the server should be visible on all event types
server_field = WebhookField('Server', server_name)
webhook_item_embed.fields.append(server_field)
role_ids_string = ''
for id in notify_role_ids:
role_ids_string += '\r\n<@&{}>\r\n'.format(id)
if event_type == 'Report':
report_reason = event['extraInfo']
report_reason_field = WebhookField('Reason', report_reason)
reported_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
reported_field = WebhookField('Reported Player',get_client_profile_markdown(target_client_name, target_client_id))
# add each fields to the embed
webhook_item_embed.title = 'Player Reported'
webhook_item_embed.fields.append(reported_field)
webhook_item_embed.fields.append(reported_by_field)
webhook_item_embed.fields.append(report_reason_field)
should_notify = True
elif event_type == 'Ban':
ban_reason = event['extraInfo']
ban_reason_field = WebhookField('Reason', ban_reason)
banned_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
banned_field = WebhookField('Banned Player', get_client_profile_markdown(target_client_name, target_client_id))
# add each fields to the embed
webhook_item_embed.title = 'Player Banned'
webhook_item_embed.fields.append(banned_field)
webhook_item_embed.fields.append(banned_by_field)
webhook_item_embed.fields.append(ban_reason_field)
should_notify = True
elif event_type == 'Connect':
connected_field = WebhookField('Connected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
webhook_item_embed.title = 'Player Connected'
webhook_item_embed.fields.append(connected_field)
elif event_type == 'Disconnect':
disconnected_field = WebhookField('Disconnected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
webhook_item_embed.title = 'Player Disconnected'
webhook_item_embed.fields.append(disconnected_field)
elif event_type == 'Say':
say_client_field = WebhookField('Player', get_client_profile_markdown(origin_client_name, origin_client_id))
message_field = WebhookField('Message', event['extraInfo'])
webhook_item_embed.title = 'Message From Player'
webhook_item_embed.fields.append(say_client_field)
webhook_item_embed.fields.append(message_field)
#if event_type == 'ScriptKill' or event_type == 'Kill':
# kill_str = '{} killed {}'.format(get_client_profile_markdown(origin_client_name, origin_client_id),
# get_client_profile_markdown(target_client_name, target_client_id))
# killed_field = WebhookField('Kill Information', kill_str)
# webhook_item_embed.title = 'Player Killed'
# webhook_item_embed.fields.append(killed_field)
#todo: handle other events
else:
continue
#make sure there's at least one group to notify
if len(notify_role_ids) > 0:
# unfortunately only the content can be used to to notify members in groups
#embed content shows the role but doesn't notify
webhook_item.content = role_ids_string
events.append({'item' : webhook_item, 'notify' : should_notify})
return events
# sends the data to the webhook location
def execute_webhook(data):
for event in data:
event_json = event['item'].to_json()
url = None
if event['notify']:
url = discord_webhook_notification_url
else:
if len(discord_webhook_information_url) > 0:
url = discord_webhook_information_url
if url :
response = requests.post(url,
data=event_json,
headers={'Content-type' : 'application/json'})
# grabs new events and executes the webhook fo each valid event
def run():
failed_count = 1
print('starting polling for events')
while True:
try:
new_events = get_new_events()
execute_webhook(new_events)
except Exception as e:
print('failed to get new events ({})'.format(failed_count))
print(e)
failed_count += 1
time.sleep(5)
if __name__ == "__main__":
run()

View File

@ -1,99 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>15a81d6e-7502-46ce-8530-0647a380b5f4</ProjectGuid>
<ProjectHome>.</ProjectHome>
<StartupFile>DiscordWebhook.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<Name>DiscordWebhook</Name>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<RootNamespace>DiscordWebhook</RootNamespace>
<InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
<IsWindowsApplication>False</IsWindowsApplication>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<Environment>DEBUG=True</Environment>
<PublishUrl>C:\Projects\IW4M-Admin\Publish\WindowsPrerelease\DiscordWebhook</PublishUrl>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
<OutputPath>bin\Prerelease\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="DiscordWebhook.py" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.6</Version>
<Description>env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<ItemGroup>
<Content Include="config.json">
<Publish>True</Publish>
</Content>
<Content Include="requirements.txt">
<Publish>True</Publish>
</Content>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Uncomment the CoreCompile target to enable the Build command in
Visual Studio and specify your pre- and post-build commands in
the BeforeBuild and AfterBuild targets below. -->
<!--<Target Name="CoreCompile" />-->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<AutoAssignPort>True</AutoAssignPort>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -1,6 +0,0 @@
{
"IW4MAdminUrl": "",
"DiscordWebhookNotificationUrl": "",
"DiscordWebhookInformationUrl": "",
"NotifyRoleIds": []
}

View File

@ -1,7 +0,0 @@
certifi>=2018.4.16
chardet>=3.0.4
idna>=2.7
pip>=18.0
requests>=2.19.1
setuptools>=39.0.1
urllib3>=1.23

View File

@ -0,0 +1,283 @@
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include common_scripts\utility;
init()
{
level.clientid = 0;
level thread onplayerconnect();
level thread IW4MA_init();
}
onplayerconnect()
{
for ( ;; )
{
level waittill( "connecting", player );
player.clientid = level.clientid;
level.clientid++;
}
}
IW4MA_init()
{
SetDvarIfUninitialized( "sv_customcallbacks", true );
SetDvarIfUninitialized( "sv_framewaittime", 0.05 );
SetDvarIfUninitialized( "sv_additionalwaittime", 0.1 );
SetDvarIfUninitialized( "sv_maxstoredframes", 12 );
SetDvarIfUninitialized( "sv_printradarupdates", 0 );
SetDvarIfUninitialized( "sv_printradar_updateinterval", 500 );
SetDvarIfUninitialized( "sv_iw4madmin_url", "http://127.0.0.1:1624" );
level thread IW4MA_onPlayerConnect();
if (getDvarInt("sv_printradarupdates") == 1)
{
level thread runRadarUpdates();
}
level waittill( "prematch_over" );
level.callbackPlayerKilled = ::Callback_PlayerKilled;
level.callbackPlayerDamage = ::Callback_PlayerDamage;
level.callbackPlayerDisconnect = ::Callback_PlayerDisconnect;
}
//Does not exist in T6
SetDvarIfUninitialized(dvar, val)
{
curval = getDvar(dvar);
if (curval == "")
SetDvar(dvar,val);
}
IW4MA_onPlayerConnect( player )
{
for( ;; )
{
level waittill( "connected", player );
player thread waitForFrameThread();
//player thread waitForAttack();
}
}
//Does not work in T6
/*waitForAttack()
{
self endon( "disconnect" );
self.lastAttackTime = 0;
for( ;; )
{
self notifyOnPlayerCommand( "player_shot", "+attack" );
self waittill( "player_shot" );
self.lastAttackTime = getTime();
}
}*/
runRadarUpdates()
{
interval = int(getDvar("sv_printradar_updateinterval"));
for ( ;; )
{
for ( i = 0; i <= 17; i++ )
{
player = level.players[i];
if ( isDefined( player ) )
{
payload = player.guid + ";" + player.origin + ";" + player getPlayerAngles() + ";" + player.team + ";" + player.kills + ";" + player.deaths + ";" + player.score + ";" + player GetCurrentWeapon() + ";" + player.health + ";" + isAlive(player) + ";" + player.timePlayed["total"];
logPrint( "LiveRadar;" + payload + "\n" );
}
}
wait( interval / 1000 );
}
}
hitLocationToBone( hitloc )
{
switch( hitloc )
{
case "helmet":
return "j_helmet";
case "head":
return "j_head";
case "neck":
return "j_neck";
case "torso_upper":
return "j_spineupper";
case "torso_lower":
return "j_spinelower";
case "right_arm_upper":
return "j_shoulder_ri";
case "left_arm_upper":
return "j_shoulder_le";
case "right_arm_lower":
return "j_elbow_ri";
case "left_arm_lower":
return "j_elbow_le";
case "right_hand":
return "j_wrist_ri";
case "left_hand":
return "j_wrist_le";
case "right_leg_upper":
return "j_hip_ri";
case "left_leg_upper":
return "j_hip_le";
case "right_leg_lower":
return "j_knee_ri";
case "left_leg_lower":
return "j_knee_le";
case "right_foot":
return "j_ankle_ri";
case "left_foot":
return "j_ankle_le";
default:
return "tag_origin";
}
}
waitForFrameThread()
{
self endon( "disconnect" );
self.currentAnglePosition = 0;
self.anglePositions = [];
for (i = 0; i < getDvarInt( "sv_maxstoredframes" ); i++)
{
self.anglePositions[i] = self getPlayerAngles();
}
for( ;; )
{
self.anglePositions[self.currentAnglePosition] = self getPlayerAngles();
wait( getDvarFloat( "sv_framewaittime" ) );
self.currentAnglePosition = (self.currentAnglePosition + 1) % getDvarInt( "sv_maxstoredframes" );
}
}
waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
{
currentIndex = self.currentAnglePosition;
wait( 0.05 * afterFrameCount );
self.angleSnapshot = [];
for( j = 0; j < self.anglePositions.size; j++ )
{
self.angleSnapshot[j] = self.anglePositions[j];
}
anglesStr = "";
collectedFrames = 0;
i = currentIndex - beforeFrameCount;
while (collectedFrames < beforeFrameCount)
{
fixedIndex = i;
if (i < 0)
{
fixedIndex = self.angleSnapshot.size - abs(i);
}
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
collectedFrames++;
i++;
}
if (i == currentIndex)
{
anglesStr += self.angleSnapshot[i] + ":";
i++;
}
collectedFrames = 0;
while (collectedFrames < afterFrameCount)
{
fixedIndex = i;
if (i > self.angleSnapshot.size - 1)
{
fixedIndex = i % self.angleSnapshot.size;
}
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
collectedFrames++;
i++;
}
lastAttack = 100;//int(getTime()) - int(self.lastAttackTime);
isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
}
vectorScale( vector, scale )
{
return ( vector[0] * scale, vector[1] * scale, vector[2] * scale );
}
Process_Hit( type, attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon )
{
if (sMeansOfDeath == "MOD_FALLING" || !isPlayer(attacker))
{
return;
}
victim = self;
_attacker = attacker;
if ( !isPlayer( attacker ) && isDefined( attacker.owner ) )
{
_attacker = attacker.owner;
}
else if( !isPlayer( attacker ) && sMeansOfDeath == "MOD_FALLING" )
{
_attacker = victim;
}
location = victim GetTagOrigin( hitLocationToBone( sHitLoc ) );
isKillstreakKill = false;
if(!isPlayer(attacker))
{
isKillstreakKill = true;
}
if(maps/mp/killstreaks/_killstreaks::iskillstreakweapon(sWeapon))
{
isKillstreakKill = true;
}
logLine = "Script" + type + ";" + _attacker.guid + ";" + victim.guid + ";" + _attacker GetTagOrigin("tag_eye") + ";" + location + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + ";" + _attacker getPlayerAngles() + ";" + int(gettime()) + ";" + isKillstreakKill + ";" + _attacker playerADS() + ";0;0";
attacker thread waitForAdditionalAngles( logLine, 2, 2 );
}
Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex )
{
if ( level.teamBased && isDefined( attacker ) && ( self != attacker ) && isDefined( attacker.team ) && ( self.pers[ "team" ] == attacker.team ) )
{
return;
}
if ( self.health - iDamage > 0 )
{
self Process_Hit( "Damage", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
}
self [[maps/mp/gametypes/_globallogic_player::callback_playerdamage]]( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex );
}
Callback_PlayerKilled(eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration)
{
Process_Hit( "Kill", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
self [[maps/mp/gametypes/_globallogic_player::callback_playerkilled]]( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration );
}
Callback_PlayerDisconnect()
{
level notify( "disconnected", self );
self [[maps/mp/gametypes/_globallogic_player::callback_playerdisconnect]]();
}

View File

@ -1,118 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>42efda12-10d3-4c40-a210-9483520116bc</ProjectGuid>
<ProjectHome>.</ProjectHome>
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
<StartupFile>runserver.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl>
<OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>GameLogServer</Name>
<RootNamespace>GameLogServer</RootNamespace>
<InterpreterId>MSBuild|game_log_server_env|$(MSBuildProjectFullPath)</InterpreterId>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<Environment>DEBUG=True</Environment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
<OutputPath>bin\Prerelease\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="GameLogServer\log_reader.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="GameLogServer\restart_resource.py" />
<Compile Include="GameLogServer\server.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="runserver.py" />
<Compile Include="GameLogServer\__init__.py" />
<Compile Include="GameLogServer\log_resource.py" />
</ItemGroup>
<ItemGroup>
<Folder Include="GameLogServer\" />
</ItemGroup>
<ItemGroup>
<None Include="Stable.pubxml" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.6</Version>
<Description>env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
<Interpreter Include="game_log_server_env\">
<Id>game_log_server_env</Id>
<Version>3.8</Version>
<Description>game_log_server_env (Python 3.8 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<ItemGroup>
<Content Include="requirements.txt" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Specify pre- and post-build commands in the BeforeBuild and
AfterBuild targets below. -->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<AutoAssignPort>True</AutoAssignPort>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -1,9 +0,0 @@
"""
The flask application package.
"""
from flask import Flask
from flask_restful import Api
app = Flask(__name__)
api = Api(app)

View File

@ -1,118 +0,0 @@
import re
import os
import time
import random
import string
class LogReader(object):
def __init__(self):
self.log_file_sizes = {}
# (if the time between checks is greater, ignore ) - in seconds
self.max_file_time_change = 30
def read_file(self, path, retrieval_key):
# this removes old entries that are no longer valid
try:
self._clear_old_logs()
except Exception as e:
print('could not clear old logs')
print(e)
if os.name != 'nt':
path = re.sub(r'^[A-Z]\:', '', path)
path = re.sub(r'\\+', '/', path)
# prevent traversing directories
if re.search('r^.+\.\.\\.+$', path):
return self._generate_bad_response()
# must be a valid log path and log file
if not re.search(r'^.+[\\|\/](.+)[\\|\/].+.log$', path):
return self._generate_bad_response()
# get the new file size
new_file_size = self.file_length(path)
# the log size was unable to be read (probably the wrong path)
if new_file_size < 0:
return self._generate_bad_response()
next_retrieval_key = self._generate_key()
# this is the first time the key has been requested, so we need to the next one
if retrieval_key not in self.log_file_sizes or int(time.time() - self.log_file_sizes[retrieval_key]['read']) > self.max_file_time_change:
print('retrieval key "%s" does not exist or is outdated' % retrieval_key)
last_log_info = {
'size' : new_file_size,
'previous_key' : None
}
else:
last_log_info = self.log_file_sizes[retrieval_key]
print('next key is %s' % next_retrieval_key)
expired_key = last_log_info['previous_key']
print('expired key is %s' % expired_key)
# grab the previous value
last_size = last_log_info['size']
file_size_difference = new_file_size - last_size
#print('generating info for next key %s' % next_retrieval_key)
# update the new size
self.log_file_sizes[next_retrieval_key] = {
'size' : new_file_size,
'read': time.time(),
'next_key': next_retrieval_key,
'previous_key': retrieval_key
}
if expired_key in self.log_file_sizes:
print('deleting expired key %s' % expired_key)
del self.log_file_sizes[expired_key]
#print('reading %i bytes starting at %i' % (file_size_difference, last_size))
new_log_content = self.get_file_lines(path, last_size, file_size_difference)
return {
'content': new_log_content,
'next_key': next_retrieval_key
}
def get_file_lines(self, path, start_position, length_to_read):
try:
file_handle = open(path, 'rb')
file_handle.seek(start_position)
file_data = file_handle.read(length_to_read)
file_handle.close()
# using ignore errors omits the pesky 0xb2 bytes we're reading in for some reason
return file_data.decode('utf-8', errors='ignore')
except Exception as e:
print('could not read the log file at {0}, wanted to read {1} bytes'.format(path, length_to_read))
print(e)
return False
def _clear_old_logs(self):
expired_logs = [path for path in self.log_file_sizes if int(time.time() - self.log_file_sizes[path]['read']) > self.max_file_time_change]
for key in expired_logs:
print('removing expired log with key {0}'.format(key))
del self.log_file_sizes[key]
def _generate_bad_response(self):
return {
'content': None,
'next_key': None
}
def _generate_key(self):
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
def file_length(self, path):
try:
return os.stat(path).st_size
except Exception as e:
print('could not get the size of the log file at {0}'.format(path))
print(e)
return -1
reader = LogReader()

View File

@ -1,16 +0,0 @@
from flask_restful import Resource
from GameLogServer.log_reader import reader
from base64 import urlsafe_b64decode
class LogResource(Resource):
def get(self, path, retrieval_key):
path = urlsafe_b64decode(path).decode('utf-8')
log_info = reader.read_file(path, retrieval_key)
content = log_info['content']
return {
'success' : content is not None,
'length': 0 if content is None else len(content),
'data': content,
'next_key': log_info['next_key']
}

View File

@ -1,29 +0,0 @@
#from flask_restful import Resource
#from flask import request
#import requests
#import os
#import subprocess
#import re
#def get_pid_of_server_windows(port):
# process = subprocess.Popen('netstat -aon', shell=True, stdout=subprocess.PIPE)
# output = process.communicate()[0]
# matches = re.search(' *(UDP) +([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):'+ str(port) + ' +[^\w]*([0-9]+)', output.decode('utf-8'))
# if matches is not None:
# return matches.group(3)
# else:
# return 0
#class RestartResource(Resource):
# def get(self):
# try:
# response = requests.get('http://' + request.remote_addr + ':1624/api/restartapproved')
# if response.status_code == 200:
# pid = get_pid_of_server_windows(response.json()['port'])
# subprocess.check_output("Taskkill /PID %s /F" % pid)
# else:
# return {}, 400
# except Exception as e:
# print(e)
# return {}, 500
# return {}, 200

View File

@ -1,14 +0,0 @@
from flask import Flask
from flask_restful import Api
from .log_resource import LogResource
#from .restart_resource import RestartResource
import logging
app = Flask(__name__)
def init():
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
api = Api(app)
api.add_resource(LogResource, '/log/<string:path>/<string:retrieval_key>')
#api.add_resource(RestartResource, '/restart')

View File

@ -1,11 +0,0 @@
aniso8601==8.0.0
click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
pip==20.1
pytz==2020.1
setuptools==46.4.0
six==1.14.0
Werkzeug==1.0.1

View File

@ -1,15 +0,0 @@
"""
This script runs the GameLogServer application using a development server.
"""
from os import environ
from GameLogServer.server import app, init
if __name__ == '__main__':
HOST = environ.get('SERVER_HOST', '0.0.0.0')
try:
PORT = int(environ.get('SERVER_PORT', '1625'))
except ValueError:
PORT = 5555
init()
app.run('0.0.0.0', PORT, debug=False)

View File

@ -8,8 +8,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
azure-pipelines.yml = azure-pipelines.yml
PostPublish.ps1 = PostPublish.ps1 PostPublish.ps1 = PostPublish.ps1
pre-release-pipeline.yml = pre-release-pipeline.yml
README.md = README.md README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd RunPublishPre.cmd = RunPublishPre.cmd
RunPublishRelease.cmd = RunPublishRelease.cmd RunPublishRelease.cmd = RunPublishRelease.cmd
@ -30,8 +30,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProfanityDeterment", "Plugi
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Login.csproj", "{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Login.csproj", "{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
@ -49,8 +47,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection EndProjectSection
EndProject EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "GameLogServer", "GameLogServer\GameLogServer.pyproj", "{42EFDA12-10D3-4C40-A210-9483520116BC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\StatsWeb\StatsWeb.csproj", "{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\StatsWeb\StatsWeb.csproj", "{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}"
@ -247,28 +243,6 @@ Global
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x64.Build.0 = Release|Any CPU {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x64.Build.0 = Release|Any CPU
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.ActiveCfg = Release|Any CPU {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.ActiveCfg = Release|Any CPU
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.Build.0 = Release|Any CPU {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.Build.0 = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|x64.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|x64.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|x86.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|x86.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|x64.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|x86.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x64.ActiveCfg = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x64.Build.0 = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.ActiveCfg = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.Build.0 = Debug|Any CPU {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -293,28 +267,6 @@ Global
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU {6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU {776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -419,7 +371,6 @@ Global
{179140D3-97AA-4CB4-8BF6-A0C73CA75701} = {26E8B310-269E-46D4-A612-24601F16065F} {179140D3-97AA-4CB4-8BF6-A0C73CA75701} = {26E8B310-269E-46D4-A612-24601F16065F}
{958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F} {958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F}
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F} {D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F} {6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F} {3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
{A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F} {A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F}

View File

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -74,14 +74,14 @@ namespace LiveRadar.Web.Controllers
[Route("Radar/Update")] [Route("Radar/Update")]
public IActionResult Update(string payload) public IActionResult Update(string payload)
{ {
var radarUpdate = RadarEvent.Parse(payload); /*var radarUpdate = RadarEvent.Parse(payload);
var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid); var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
if (client != null) if (client != null)
{ {
radarUpdate.Name = client.Name.StripColors(); radarUpdate.Name = client.Name.StripColors();
client.SetAdditionalProperty("LiveRadar", radarUpdate); client.SetAdditionalProperty("LiveRadar", radarUpdate);
} }*/
return Ok(); return Ok();
} }

View File

@ -0,0 +1,33 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using EventGeneratorCallback = System.ValueTuple<string, string,
System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration,
SharedLibraryCore.GameEvent,
SharedLibraryCore.GameEvent>>;
namespace LiveRadar.Events
{
public class Script : IRegisterEvent
{
private const string EVENT_LIVERADAR = "LiveRadar";
private EventGeneratorCallback LiveRadar()
{
return (EVENT_LIVERADAR, EVENT_LIVERADAR, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) =>
{
string[] lineSplit = eventLine.Split(";");
autoEvent.Type = GameEvent.EventType.Other;
autoEvent.Subtype = EVENT_LIVERADAR;
autoEvent.Origin = new EFClient() { NetworkId = 0 };
autoEvent.Extra = lineSplit[1]; // guid
return autoEvent;
}
);
}
public IEnumerable<EventGeneratorCallback> Events => new[] { LiveRadar() };
}
}

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -3,6 +3,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -17,12 +18,14 @@ namespace LiveRadar
public string Author => "RaidMax"; public string Author => "RaidMax";
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler; private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
private readonly Dictionary<string, long> _botGuidLookups;
private bool addedPage; private bool addedPage;
private readonly object lockObject = new object(); private readonly object lockObject = new object();
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
{ {
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration"); _configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
_botGuidLookups = new Dictionary<string, long>();
} }
public Task OnEventAsync(GameEvent E, Server S) public Task OnEventAsync(GameEvent E, Server S)
@ -41,28 +44,45 @@ namespace LiveRadar
} }
} }
if (E.Type == GameEvent.EventType.Unknown) if (E.Type == GameEvent.EventType.PreConnect && E.Origin.IsBot)
{ {
if (E.Data?.StartsWith("LiveRadar") ?? false) string botKey = $"BotGuid_{E.Extra}";
lock (lockObject)
{ {
try if (!_botGuidLookups.ContainsKey(botKey))
{ {
var radarUpdate = RadarEvent.Parse(E.Data); _botGuidLookups.Add(botKey, E.Origin.NetworkId);
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid); }
}
}
if (client != null) if (E.Type == GameEvent.EventType.Other && E.Subtype == "LiveRadar")
{ {
radarUpdate.Name = client.Name.StripColors(); try
client.SetAdditionalProperty("LiveRadar", radarUpdate); {
} string botKey = $"BotGuid_{E.Extra}";
long generatedBotGuid;
lock (lockObject)
{
generatedBotGuid = _botGuidLookups.ContainsKey(botKey) ? _botGuidLookups[botKey] : 0;
} }
catch (Exception e) var radarUpdate = RadarEvent.Parse(E.Data, generatedBotGuid);
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
if (client != null)
{ {
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}"); radarUpdate.Name = client.Name.StripColors();
S.Logger.WriteDebug(e.GetExceptionInfo()); client.SetAdditionalProperty("LiveRadar", radarUpdate);
} }
} }
catch (Exception e)
{
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}");
S.Logger.WriteDebug(e.GetExceptionInfo());
}
} }
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -1,9 +1,7 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
namespace LiveRadar namespace LiveRadar
{ {
@ -39,13 +37,13 @@ namespace LiveRadar
return false; return false;
} }
public static RadarEvent Parse(string input) public static RadarEvent Parse(string input, long generatedBotGuid)
{ {
var items = input.Split(';').Skip(1).ToList(); var items = input.Split(';').Skip(1).ToList();
var parsedEvent = new RadarEvent() var parsedEvent = new RadarEvent()
{ {
Guid = items[0].ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber), Guid = generatedBotGuid,
Location = Vector3.Parse(items[1]), Location = Vector3.Parse(items[1]),
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(), ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
Team = items[3], Team = items[3],

View File

@ -81,7 +81,7 @@
weapons["wa2000"] = "wa2000"; weapons["wa2000"] = "wa2000";
weapons["m21"] = "m14ebr"; weapons["m21"] = "m14ebr";
weapons["cheytac"] = "cheytac"; weapons["cheytac"] = "cheytac";
weapons["dragunov"] = "hud_dragunovsvd"; weapons["dragunov"] = "dragunovsvd";
weapons["beretta"] = "m9beretta"; weapons["beretta"] = "m9beretta";
weapons["usp"] = "usp_45"; weapons["usp"] = "usp_45";

View File

@ -23,7 +23,7 @@
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -7,6 +7,7 @@ let plugin = {
maxReportCount: 5, // how many reports before action is taken maxReportCount: 5, // how many reports before action is taken
tempBanDurationMinutes: 60, // how long to temporarily ban the player tempBanDurationMinutes: 60, // how long to temporarily ban the player
eventTypes: { 'report': 103 }, eventTypes: { 'report': 103 },
permissionTypes: { 'trusted': 2 },
onEventAsync: function (gameEvent, server) { onEventAsync: function (gameEvent, server) {
if (!this.enabled) { if (!this.enabled) {
@ -14,7 +15,8 @@ let plugin = {
} }
if (gameEvent.Type === this.eventTypes['report']) { if (gameEvent.Type === this.eventTypes['report']) {
if (!gameEvent.Target.IsIngame) { if (!gameEvent.Target.IsIngame || gameEvent.Target.Level >= this.permissionTypes['trusted']) {
server.Logger.WriteInfo(`Ignoring report for client (id) ${gameEvent.Target.ClientId} because they are privileged or not ingame`);
return; return;
} }
@ -46,4 +48,4 @@ let plugin = {
onTickAsync: function (server) { onTickAsync: function (server) {
} }
}; };

View File

@ -20,6 +20,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick_for_reason {0} "{1}"'; rconParser.Configuration.CommandPrefixes.Ban = 'clientkick_for_reason {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick_for_reason {0} "{1}"'; rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick_for_reason {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xffrcon {0} get {1}'; rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xffrcon {0} get {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; // adding this in here temporarily until getInfo is fixed in new T6 version
rconParser.Configuration.Dvar.Pattern = '^(.+) is "(.+)?"$'; rconParser.Configuration.Dvar.Pattern = '^(.+) is "(.+)?"$';
rconParser.Configuration.Dvar.AddMapping(106, 1); rconParser.Configuration.Dvar.AddMapping(106, 1);

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 0.2, version: 0.3,
name: 'Black Ops 3 Parser', name: 'Black Ops 3 Parser',
isParser: true, isParser: true,
@ -23,6 +23,8 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xff\x00{0} {1}'; rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xff\x00{0} {1}';
rconParser.Configuration.CommandPrefixes.RConSetDvar = '\xff\xff\xff\xff\x00{0} set {1}'; rconParser.Configuration.CommandPrefixes.RConSetDvar = '\xff\xff\xff\xff\x00{0} set {1}';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff\x01'; rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff\x01';
rconParser.Configuration.GametypeStatus.Pattern = 'Gametype: (.+)';
rconParser.Configuration.MapStatus.Pattern = 'Map: (.+)';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; // disables this, because it's useless on T7 rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; // disables this, because it's useless on T7
rconParser.Configuration.ServerNotRunningResponse = 'this is here to prevent a hiberating server from being detected as not running'; rconParser.Configuration.ServerNotRunningResponse = 'this is here to prevent a hiberating server from being detected as not running';
@ -34,6 +36,7 @@ var plugin = {
rconParser.Configuration.DefaultDvarValues.Add('fs_game', ''); rconParser.Configuration.DefaultDvarValues.Add('fs_game', '');
rconParser.Configuration.Status.AddMapping(105, 6); // ip address rconParser.Configuration.Status.AddMapping(105, 6); // ip address
rconParser.Configuration.GametypeStatus.AddMapping(112, 1); // gametype
rconParser.Version = '[local] ship win64 CODBUILD8-764 (3421987) Mon Dec 16 10:44:20 2019 10d27bef'; rconParser.Version = '[local] ship win64 CODBUILD8-764 (3421987) Mon Dec 16 10:44:20 2019 10d27bef';
rconParser.GameName = 8; // BO3 rconParser.GameName = 8; // BO3
rconParser.CanGenerateLogPath = false; rconParser.CanGenerateLogPath = false;
@ -49,4 +52,4 @@ var plugin = {
onTickAsync: function (server) { onTickAsync: function (server) {
} }
}; };

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 0.6, version: 0.7,
name: 'Tekno MW3 Parser', name: 'Tekno MW3 Parser',
isParser: true, isParser: true,
@ -29,6 +29,7 @@ var plugin = {
rconParser.Configuration.Dvar.Pattern = '^(.*)$'; rconParser.Configuration.Dvar.Pattern = '^(.*)$';
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1'); rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
rconParser.Configuration.OverrideDvarNameMapping.Add('_website', 'sv_clanWebsite');
rconParser.Version = 'IW5 MP 1.4 build 382 latest Thu Jan 19 2012 11:09:49AM win-x86'; rconParser.Version = 'IW5 MP 1.4 build 382 latest Thu Jan 19 2012 11:09:49AM win-x86';
rconParser.GameName = 3; // IW5 rconParser.GameName = 3; // IW5

View File

@ -7,6 +7,8 @@ let commands = [{
alias: "pp", alias: "pp",
// required // required
permission: "User", permission: "User",
// optional (defaults to false)
targetRequired: false,
// optional // optional
arguments: [{ arguments: [{
name: "times to ping", name: "times to ping",
@ -44,6 +46,8 @@ let plugin = {
}, },
onLoadAsync: function (manager) { onLoadAsync: function (manager) {
this.logger = _serviceResolver.ResolveService("ILogger");
this.logger.WriteDebug("sample plugin loaded");
}, },
onUnloadAsync: function () { onUnloadAsync: function () {

View File

@ -20,7 +20,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Offset, Offset,
Strain, Strain,
Recoil, Recoil,
Snap Snap,
Button
}; };
public ChangeTracking<EFACSnapshot> Tracker { get; private set; } public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
@ -38,11 +39,12 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
ILogger Log; ILogger Log;
Strain Strain; Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow; readonly DateTime ConnectionTime = DateTime.UtcNow;
private double sessionAverageRecoilAmount; private double mapAverageRecoilAmount;
private double sessionAverageSnapAmount; private double sessionAverageSnapAmount;
private int sessionSnapHits; private int sessionSnapHits;
private EFClientKill lastHit; private EFClientKill lastHit;
private int validRecoilHitCount; private int validRecoilHitCount;
private int validButtonHitCount;
private class HitInfo private class HitInfo
{ {
@ -282,18 +284,30 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#region RECOIL #region RECOIL
float hitRecoilAverage = 0; float hitRecoilAverage = 0;
if (!Plugin.Config.Configuration().RecoilessWeapons.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex))) bool shouldIgnoreDetection = false;
try
{
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Recoil]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (!shouldIgnoreDetection)
{ {
validRecoilHitCount++; validRecoilHitCount++;
hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1); hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1);
sessionAverageRecoilAmount = (sessionAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount; mapAverageRecoilAmount = (mapAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount;
if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && sessionAverageRecoilAmount == 0) if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && mapAverageRecoilAmount == 0)
{ {
results.Add(new DetectionPenaltyResult() results.Add(new DetectionPenaltyResult()
{ {
ClientPenalty = EFPenalty.PenaltyType.Ban, ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = sessionAverageRecoilAmount, Value = mapAverageRecoilAmount,
HitCount = HitCount, HitCount = HitCount,
Type = DetectionType.Recoil Type = DetectionType.Recoil
}); });
@ -301,6 +315,37 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
} }
#endregion #endregion
#region BUTTON
try
{
shouldIgnoreDetection = false;
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Button]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (!shouldIgnoreDetection)
{
validButtonHitCount++;
}
double lastDiff = hit.TimeOffset - hit.TimeSinceLastAttack;
if (validButtonHitCount > 0 && lastDiff <= 0)
{
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = lastDiff,
HitCount = HitCount,
Type = DetectionType.Button
});
}
#endregion
#region SESSION_RATIOS #region SESSION_RATIOS
if (Kills >= Thresholds.LowSampleMinKills) if (Kills >= Thresholds.LowSampleMinKills)
{ {
@ -384,7 +429,19 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#region CHEST_ABDOMEN_RATIO_SESSION #region CHEST_ABDOMEN_RATIO_SESSION
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count; int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count;
if (chestHits >= Thresholds.MediumSampleMinKills) try
{
shouldIgnoreDetection = false; // reset previous value
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Chest]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (chestHits >= Thresholds.MediumSampleMinKills && !shouldIgnoreDetection)
{ {
double marginOfError = Thresholds.GetMarginOfError(chestHits); double marginOfError = Thresholds.GetMarginOfError(chestHits);
double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills)); double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
@ -466,5 +523,11 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
return results; return results;
} }
public void OnMapChange()
{
mapAverageRecoilAmount = 0;
validRecoilHitCount = 0;
}
} }
} }

View File

@ -17,6 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
class MostKillsCommand : Command class MostKillsCommand : Command
{ {
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly CommandConfiguration _config;
public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory) : base(config, translationLookup) public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{ {
@ -26,12 +27,13 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Permission = EFClient.Permission.User; Permission = EFClient.Permission.User;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_config = config;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(), _contextFactory, _translationLookup); var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(), _contextFactory, _translationLookup);
if (!E.Message.IsBroadcastCommand()) if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
foreach (var stat in mostKills) foreach (var stat in mostKills)
{ {

View File

@ -22,7 +22,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
List<string> mostPlayed = new List<string>() List<string> mostPlayed = new List<string>()
{ {
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--" $"^5--{translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
}; };
using (var db = new DatabaseContext(true)) using (var db = new DatabaseContext(true))
@ -51,14 +51,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var iqList = await iqStats.ToListAsync(); var iqList = await iqStats.ToListAsync();
mostPlayed.AddRange(iqList.Select(stats => mostPlayed.AddRange(iqList.Select(stats => translationLookup["COMMANDS_MOST_PLAYED_FORMAT"].FormatExt(stats.Name, stats.Kills, (DateTime.UtcNow - DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime)).HumanizeForCurrentCulture())));
$"^3{stats.Name}^7 - ^5{stats.Kills} ^7{translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{Utilities.GetTimePassed(DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime), false)} ^7{translationLookup["WEBFRONT_PROFILE_PLAYER"].ToLower()}"));
} }
return mostPlayed; return mostPlayed;
} }
private readonly CommandConfiguration _config;
public MostPlayedCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) public MostPlayedCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "mostplayed"; Name = "mostplayed";
@ -66,12 +67,14 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Alias = "mp"; Alias = "mp";
Permission = EFClient.Permission.User; Permission = EFClient.Permission.User;
RequiresTarget = false; RequiresTarget = false;
_config = config;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var topStats = await GetMostPlayed(E.Owner, _translationLookup); var topStats = await GetMostPlayed(E.Owner, _translationLookup);
if (!E.Message.IsBroadcastCommand()) if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
foreach (var stat in topStats) foreach (var stat in topStats)
{ {

View File

@ -65,6 +65,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
return topStatsText; return topStatsText;
} }
private readonly CommandConfiguration _config;
public TopStats(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) public TopStats(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "topstats"; Name = "topstats";
@ -72,12 +74,14 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Alias = "ts"; Alias = "ts";
Permission = EFClient.Permission.User; Permission = EFClient.Permission.User;
RequiresTarget = false; RequiresTarget = false;
_config = config;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var topStats = await GetTopStats(E.Owner, _translationLookup); var topStats = await GetTopStats(E.Owner, _translationLookup);
if (!E.Message.IsBroadcastCommand()) if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
foreach (var stat in topStats) foreach (var stat in topStats)
{ {

View File

@ -30,8 +30,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Required = false Required = false
} }
}; };
_config = config;
} }
private readonly CommandConfiguration _config;
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
string statLine; string statLine;
@ -90,7 +94,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
statLine = $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}"; statLine = $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
} }
if (E.Message.IsBroadcastCommand()) if (E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
string name = E.Target == null ? E.Origin.Name : E.Target.Name; string name = E.Target == null ? E.Origin.Name : E.Target.Name;
E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name)); E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
using static SharedLibraryCore.Server;
namespace Stats.Config
{
public class AnticheatConfiguration
{
public bool Enable { get; set; }
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } = new Dictionary<long, DetectionType[]>();
public IList<long> IgnoredClientIds { get; set; } = new List<long>();
public IDictionary<Game, IDictionary<DetectionType, string[]>> IgnoredDetectionSpecification{ get; set; } = new Dictionary<Game, IDictionary<DetectionType, string[]>>
{
{
Game.IW4, new Dictionary<DetectionType, string[]>
{
{ DetectionType.Chest, new[] { "m21.+" } },
{ DetectionType.Recoil, new[] { "ranger.*_mp", "model1887.*_mp", ".+shotgun.*_mp" } },
{ DetectionType.Button, new[] { ".*akimbo.*" } }
}
}
};
}
}

View File

@ -1,6 +1,7 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using Stats.Config; using Stats.Config;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection; using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
@ -8,21 +9,41 @@ namespace IW4MAdmin.Plugins.Stats.Config
{ {
public class StatsConfiguration : IBaseConfiguration public class StatsConfiguration : IBaseConfiguration
{ {
public bool EnableAntiCheat { get; set; } [Obsolete]
public bool? EnableAntiCheat { get; set; }
public List<StreakMessageConfiguration> KillstreakMessages { get; set; } public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; } public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
public List<string> RecoilessWeapons { get; set; }
public int TopPlayersMinPlayTime { get; set; } public int TopPlayersMinPlayTime { get; set; }
public bool StoreClientKills { get; set; } public bool StoreClientKills { get; set; }
public int MostKillsMaxInactivityDays { get; set; } = 30; public int MostKillsMaxInactivityDays { get; set; } = 30;
public int MostKillsClientLimit { get; set; } = 5; public int MostKillsClientLimit { get; set; } = 5;
public IDictionary<DetectionType, DistributionConfiguration> DetectionDistributions { get; set; } [Obsolete]
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; }
public AnticheatConfiguration AnticheatConfiguration { get; set; } = new AnticheatConfiguration();
#pragma warning disable CS0612 // Type or member is obsolete
public void ApplyMigration()
{
if (ServerDetectionTypes != null)
{
AnticheatConfiguration.ServerDetectionTypes = ServerDetectionTypes;
}
ServerDetectionTypes = null;
if (EnableAntiCheat != null)
{
AnticheatConfiguration.Enable = EnableAntiCheat.Value;
}
EnableAntiCheat = null;
}
#pragma warning restore CS0612 // Type or member is obsolete
public string Name() => "StatsPluginSettings"; public string Name() => "StatsPluginSettings";
public IBaseConfiguration Generate() public IBaseConfiguration Generate()
{ {
EnableAntiCheat = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]); AnticheatConfiguration.Enable = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]);
KillstreakMessages = new List<StreakMessageConfiguration>() KillstreakMessages = new List<StreakMessageConfiguration>()
{ {
new StreakMessageConfiguration(){ new StreakMessageConfiguration(){
@ -57,16 +78,9 @@ namespace IW4MAdmin.Plugins.Stats.Config
}, },
}; };
RecoilessWeapons = new List<string>()
{
"ranger.*_mp",
"model1887.*_mp",
".+shotgun.*_mp"
};
TopPlayersMinPlayTime = 3600 * 3; TopPlayersMinPlayTime = 3600 * 3;
StoreClientKills = false; StoreClientKills = false;
return this; return this;
} }
} }

View File

@ -1,9 +1,9 @@
using SharedLibraryCore.Dtos; using SharedLibraryCore.QueryHelper;
using System; using System;
namespace StatsWeb.Dtos namespace Stats.Dtos
{ {
public class ChatSearchQuery : PaginationInfo public class ChatSearchQuery : ClientPaginationRequest
{ {
/// <summary> /// <summary>
/// specifies the partial content of the message to search for /// specifies the partial content of the message to search for
@ -18,7 +18,7 @@ namespace StatsWeb.Dtos
/// <summary> /// <summary>
/// identifier for the client /// identifier for the client
/// </summary> /// </summary>
public int? ClientId { get; set; } public new int? ClientId { get; set; }
/// <summary> /// <summary>
/// only look for messages sent after this date /// only look for messages sent after this date
@ -29,5 +29,10 @@ namespace StatsWeb.Dtos
/// only look for messages sent before this date0 /// only look for messages sent before this date0
/// </summary> /// </summary>
public DateTime SentBefore { get; set; } = DateTime.UtcNow; public DateTime SentBefore { get; set; } = DateTime.UtcNow;
/// <summary>
/// indicates if the chat is on the meta page
/// </summary>
public bool IsProfileMeta { get; set; }
} }
} }

View File

@ -156,7 +156,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Deaths = s.Deaths, Deaths = s.Deaths,
Kills = s.Kills, Kills = s.Kills,
KDR = Math.Round(s.KDR, 2), KDR = Math.Round(s.KDR, 2),
LastSeen = Utilities.GetTimePassed(clientRatingsDict[s.ClientId].LastConnection, false), LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection).HumanizeForCurrentCulture(),
Name = clientRatingsDict[s.ClientId].Name, Name = clientRatingsDict[s.ClientId].Name,
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2), Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking, RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
@ -248,6 +248,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true; ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true;
ctx.SaveChanges(); ctx.SaveChanges();
} }
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true;
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword);
ctx.SaveChanges();
} }
// check to see if the stats have ever been initialized // check to see if the stats have ever been initialized
@ -477,7 +481,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
IsKill = !isDamage, IsKill = !isDamage,
AnglesList = snapshotAngles, AnglesList = snapshotAngles,
IsAlive = isAlive == "1", IsAlive = isAlive == "1",
TimeSinceLastAttack = long.Parse(lastAttackTime) TimeSinceLastAttack = long.Parse(lastAttackTime),
GameName = attacker.CurrentServer.GameName
}; };
if (hit.HitLoc == IW4Info.HitLocation.shield) if (hit.HitLoc == IW4Info.HitLocation.shield)
@ -535,7 +540,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId) if (Plugin.Config.Configuration().AnticheatConfiguration.Enable && !attacker.IsBot && attacker.ClientId != victim.ClientId)
{ {
clientDetection.TrackedHits.Add(hit); clientDetection.TrackedHits.Add(hit);
@ -551,10 +556,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (oldestHit.IsAlive) if (oldestHit.IsAlive)
{ {
var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint); var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker);
#if !DEBUG
await ApplyPenalty(result, attacker); if (!Utilities.IsDevelopment)
#endif {
await ApplyPenalty(result, attacker);
}
if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any) if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
{ {
@ -590,10 +597,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, long serverId) private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, EFClient client)
{ {
// allow disabling of certain detection types // allow disabling of certain detection types
results = results.Where(_result => ShouldUseDetection(serverId, _result.Type)); results = results.Where(_result => ShouldUseDetection(client.CurrentServer, _result.Type, client.ClientId));
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ?? return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ?? results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
new DetectionPenaltyResult() new DetectionPenaltyResult()
@ -613,21 +620,31 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
private bool ShouldUseDetection(long serverId, DetectionType detectionType) private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{ {
var detectionTypes = Plugin.Config.Configuration().ServerDetectionTypes; var detectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds;
if (detectionTypes == null) if (ignoredClients.Contains(clientId))
{ {
return true; return false;
} }
if (!detectionTypes.ContainsKey(serverId))
try
{ {
return true; if (detectionTypes[server.EndPoint].Contains(detectionType))
{
return false;
}
} }
return detectionTypes[serverId].Contains(detectionType); catch (KeyNotFoundException)
{
}
return true;
} }
async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker) async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker)
@ -663,6 +680,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" : $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"; $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
penaltyClient.AdministeredPenalties = new List<EFPenalty>()
{
new EFPenalty()
{
AutomatedOffense = flagReason
}
};
await attacker.Flag(flagReason, penaltyClient, new TimeSpan(168, 0, 0)).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken); await attacker.Flag(flagReason, penaltyClient, new TimeSpan(168, 0, 0)).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
break; break;
} }
@ -1127,10 +1152,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetKillstreaks(Server sv) public void ResetKillstreaks(Server sv)
{ {
foreach (var stat in sv.GetClientsAsList() foreach (var session in sv.GetClientsAsList()
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))) .Select(_client => new
{
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
}))
{ {
stat?.StartNewSession(); session.stat?.StartNewSession();
session.detection?.OnMapChange();
} }
} }
@ -1220,6 +1250,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 886229536; return 886229536;
} }
// todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(server.IP, server.Port); long id = HashCode.Combine(server.IP, server.Port);
id = id < 0 ? Math.Abs(id) : id; id = id < 0 ? Math.Abs(id) : id;
long? serverId; long? serverId;

View File

@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Collections.Generic; using System.Collections.Generic;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Plugins.Stats.Models namespace IW4MAdmin.Plugins.Stats.Models
{ {
@ -44,6 +45,8 @@ namespace IW4MAdmin.Plugins.Stats.Models
public float AdsPercent { get; set; } public float AdsPercent { get; set; }
[NotMapped] [NotMapped]
public List<Vector3> AnglesList { get; set; } public List<Vector3> AnglesList { get; set; }
[NotMapped]
public Game GameName { get; set; }
/// <summary> /// <summary>
/// Indicates if the attacker was alive after last captured angle /// Indicates if the attacker was alive after last captured angle

View File

@ -16,5 +16,6 @@ namespace IW4MAdmin.Plugins.Stats.Models
public string EndPoint { get; set; } public string EndPoint { get; set; }
public Game? GameName { get; set; } public Game? GameName { get; set; }
public string HostName { get; set; } public string HostName { get; set; }
public bool IsPasswordProtected { get; set; }
} }
} }

View File

@ -5,10 +5,11 @@ using Microsoft.EntityFrameworkCore;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Database; using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services; using SharedLibraryCore.QueryHelper;
using Stats.Dtos;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -33,13 +34,17 @@ namespace IW4MAdmin.Plugins.Stats
#endif #endif
private readonly IDatabaseContextFactory _databaseContextFactory; private readonly IDatabaseContextFactory _databaseContextFactory;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IMetaService _metaService;
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatQueryHelper;
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory, public Plugin(IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
ITranslationLookup translationLookup) ITranslationLookup translationLookup, IMetaService metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper)
{ {
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings"); Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_databaseContextFactory = databaseContextFactory; _databaseContextFactory = databaseContextFactory;
_translationLookup = translationLookup; _translationLookup = translationLookup;
_metaService = metaService;
_chatQueryHelper = chatQueryHelper;
} }
public async Task OnEventAsync(GameEvent E, Server S) public async Task OnEventAsync(GameEvent E, Server S)
@ -177,8 +182,9 @@ namespace IW4MAdmin.Plugins.Stats
if (Config.Configuration() == null) if (Config.Configuration() == null)
{ {
Config.Set((StatsConfiguration)new StatsConfiguration().Generate()); Config.Set((StatsConfiguration)new StatsConfiguration().Generate());
await Config.Save();
} }
Config.Configuration().ApplyMigration();
await Config.Save();
// register the topstats page // register the topstats page
// todo:generate the URL/Location instead of hardcoding // todo:generate the URL/Location instead of hardcoding
@ -188,17 +194,14 @@ namespace IW4MAdmin.Plugins.Stats
"/Stats/TopPlayersAsync"); "/Stats/TopPlayersAsync");
// meta data info // meta data info
async Task<List<ProfileMeta>> getStats(int clientId, int offset, int count, DateTime? startAt) async Task<IEnumerable<InformationResponse>> getStats(ClientPaginationRequest request)
{ {
if (count > 1)
{
return new List<ProfileMeta>();
}
IList<EFClientStatistics> clientStats; IList<EFClientStatistics> clientStats;
using (var ctx = new DatabaseContext(disableTracking: true)) int messageCount = 0;
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
{ {
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == clientId).ToListAsync(); clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
} }
int kills = clientStats.Sum(c => c.Kills); int kills = clientStats.Sum(c => c.Kills);
@ -209,73 +212,76 @@ namespace IW4MAdmin.Plugins.Stats
double performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2); double performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
double spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Where(c => c.SPM > 0).Count(), 1); double spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Where(c => c.SPM > 0).Count(), 1);
return new List<ProfileMeta>() return new List<InformationResponse>()
{ {
new ProfileMeta() new InformationResponse()
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"], Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
Value = "#" + (await Manager.GetClientOverallRanking(clientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 0, Order = 0,
Type = ProfileMeta.MetaType.Information Type = MetaType.Information
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"], Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
Value = kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 1, Order = 1,
Type = ProfileMeta.MetaType.Information Type = MetaType.Information
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"], Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"],
Value = deaths.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = deaths.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 2, Order = 2,
Type = ProfileMeta.MetaType.Information Type = MetaType.Information
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"], Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"],
Value = kdr.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = kdr.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 3, Order = 3,
Type = ProfileMeta.MetaType.Information Type = MetaType.Information
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_PERFORMANCE"], Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PERFORMANCE"],
Value = performance.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = performance.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 4, Order = 4,
Type = ProfileMeta.MetaType.Information Type = MetaType.Information
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"], Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"],
Value = spm.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = spm.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 5, Order = 5,
Type = ProfileMeta.MetaType.Information Type = MetaType.Information
},
new InformationResponse()
{
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
Value = messageCount.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 1,
Order = 4,
Type = MetaType.Information
} }
}; };
} }
async Task<List<ProfileMeta>> getAnticheatInfo(int clientId, int offset, int count, DateTime? startAt) async Task<IEnumerable<InformationResponse>> getAnticheatInfo(ClientPaginationRequest request)
{ {
if (count > 1)
{
return new List<ProfileMeta>();
}
IList<EFClientStatistics> clientStats; IList<EFClientStatistics> clientStats;
using (var ctx = new DatabaseContext(disableTracking: true)) using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
{ {
clientStats = await ctx.Set<EFClientStatistics>() clientStats = await ctx.Set<EFClientStatistics>()
.Include(c => c.HitLocations) .Include(c => c.HitLocations)
.Where(c => c.ClientId == clientId) .Where(c => c.ClientId == request.ClientId)
.ToListAsync(); .ToListAsync();
} }
@ -310,159 +316,103 @@ namespace IW4MAdmin.Plugins.Stats
averageSnapValue = clientStats.Any(_stats => _stats.AverageSnapValue > 0) ? clientStats.Where(_stats => _stats.AverageSnapValue > 0).Average(_stat => _stat.AverageSnapValue) : 0; averageSnapValue = clientStats.Any(_stats => _stats.AverageSnapValue > 0) ? clientStats.Where(_stats => _stats.AverageSnapValue > 0).Average(_stat => _stat.AverageSnapValue) : 0;
} }
return new List<ProfileMeta>() return new List<InformationResponse>()
{ {
new ProfileMeta() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 1", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 1",
Value = chestRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%', Value = chestRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
Type = ProfileMeta.MetaType.Information, Type = MetaType.Information,
Column = 2, Column = 2,
Order = 0, Order = 0,
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"],
Sensitive = true IsSensitive = true
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 2", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 2",
Value = abdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%', Value = abdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
Type = ProfileMeta.MetaType.Information, Type = MetaType.Information,
Column = 2, Column = 2,
Order = 1, Order = 1,
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"],
Sensitive = true IsSensitive = true
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 3", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 3",
Value = chestAbdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%', Value = chestAbdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
Type = ProfileMeta.MetaType.Information, Type = MetaType.Information,
Column = 2, Column = 2,
Order = 2, Order = 2,
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"],
Sensitive = true IsSensitive = true
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 4", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 4",
Value = headRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%', Value = headRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
Type = ProfileMeta.MetaType.Information, Type = MetaType.Information,
Column = 2, Column = 2,
Order = 3, Order = 3,
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"],
Sensitive = true IsSensitive = true
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 5", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 5",
// todo: make sure this is wrapped somewhere else // todo: make sure this is wrapped somewhere else
Value = $"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°", Value = $"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°",
Type = ProfileMeta.MetaType.Information, Type = MetaType.Information,
Column = 2, Column = 2,
Order = 4, Order = 4,
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"],
Sensitive = true IsSensitive = true
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 6", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 6",
Value = Math.Round(maxStrain, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = Math.Round(maxStrain, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Type = ProfileMeta.MetaType.Information, Type = MetaType.Information,
Column = 2, Column = 2,
Order = 5, Order = 5,
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
Sensitive = true IsSensitive = true
}, },
new ProfileMeta() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 7", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 7",
Value = Math.Round(averageSnapValue, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = Math.Round(averageSnapValue, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Type = ProfileMeta.MetaType.Information, Type = MetaType.Information,
Column = 2, Column = 2,
Order = 6, Order = 6,
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"],
Sensitive = true IsSensitive = true
} }
}; };
} }
async Task<List<ProfileMeta>> getMessages(int clientId, int offset, int count, DateTime? startAt) async Task<IEnumerable<MessageResponse>> getMessages(ClientPaginationRequest request)
{ {
if (count <= 1) var query = new ChatSearchQuery()
{ {
using (var ctx = new DatabaseContext(true)) ClientId = request.ClientId,
{ Before = request.Before,
return new List<ProfileMeta> SentBefore = request.Before ?? DateTime.UtcNow,
{ Count = request.Count,
new ProfileMeta() IsProfileMeta = true
{ };
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
Value = (await ctx.Set<EFClientMessage>()
.CountAsync(_message => _message.ClientId == clientId))
.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 1,
Order= 4,
Type = ProfileMeta.MetaType.Information
}
};
}
}
List<ProfileMeta> messageMeta; return (await _chatQueryHelper.QueryResource(query)).Results;
using (var ctx = new DatabaseContext(disableTracking: true))
{
var messages = ctx.Set<EFClientMessage>()
.Where(m => m.ClientId == clientId)
.Where(_message => _message.TimeSent < startAt)
.OrderByDescending(_message => _message.TimeSent)
.Skip(offset)
.Take(count);
messageMeta = await messages.Select(m => new ProfileMeta()
{
Key = null,
Value = new { m.Message, m.Server.GameName },
When = m.TimeSent,
Extra = m.ServerId.ToString(),
Type = ProfileMeta.MetaType.ChatMessage
}).ToListAsync();
foreach (var message in messageMeta)
{
if ((message.Value.Message as string).IsQuickMessage())
{
try
{
var quickMessages = ServerManager.GetApplicationSettings().Configuration()
.QuickMessages
.First(_qm => _qm.Game == message.Value.GameName);
message.Value = quickMessages.Messages[(message.Value.Message as string).Substring(1)];
message.Type = ProfileMeta.MetaType.QuickMessage;
}
catch
{
message.Value = message.Value.Message;
}
}
else
{
message.Value = message.Value.Message;
}
}
}
return messageMeta;
} }
if (Config.Configuration().EnableAntiCheat) if (Config.Configuration().AnticheatConfiguration.Enable)
{ {
MetaService.AddRuntimeMeta(getAnticheatInfo); _metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getAnticheatInfo);
} }
MetaService.AddRuntimeMeta(getStats); _metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getStats);
MetaService.AddRuntimeMeta(getMessages); _metaService.AddRuntimeMeta<ClientPaginationRequest, MessageResponse>(MetaType.ChatMessage, getMessages);
async Task<string> totalKills(Server server) async Task<string> totalKills(Server server)
{ {
@ -547,6 +497,6 @@ namespace IW4MAdmin.Plugins.Stats
/// </summary> /// </summary>
/// <param name="s"></param> /// <param name="s"></param>
/// <returns></returns> /// <returns></returns>
private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().EnableAntiCheat && s.GameName == Server.Game.IW5; private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().AnticheatConfiguration.Enable && s.GameName == Server.Game.IW5;
} }
} }

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -1,349 +0,0 @@
using IW4MAdmin.Application;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Tests
{
[Collection("ManagerCollection")]
public class ClientTests
{
private readonly ApplicationManager _manager;
const int TestTimeout = 10000;
public ClientTests(ManagerFixture fixture)
{
_manager = fixture.Manager;
}
[Fact]
public void SetAdditionalPropertyShouldSucceed()
{
var client = new EFClient();
int newProp = 5;
client.SetAdditionalProperty("NewProp", newProp);
}
[Fact]
public void GetAdditionalPropertyShouldSucceed()
{
var client = new EFClient();
int newProp = 5;
client.SetAdditionalProperty("NewProp", newProp);
Assert.True(client.GetAdditionalProperty<int>("NewProp") == 5, "added property does not match retrieved property");
}
[Fact]
public void BanEvasionShouldLink()
{
var server = _manager.Servers[0];
var waiter = new ManualResetEventSlim();
_manager.GetApplicationSettings().Configuration().RConPollRate = 5000;
while (!server.IsInitialized)
{
Thread.Sleep(100);
}
var e = new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Owner = server,
Origin = new EFClient()
{
NetworkId = 1337,
ClientNumber = 0,
CurrentAlias = new EFAlias()
{
Name = "Ban Me",
IPAddress = 1337
}
}
};
_manager.AddEvent(e);
e.Complete();
e = new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Owner = server,
Origin = new EFClient()
{
NetworkId = 1338,
ClientNumber = 1,
CurrentAlias = new EFAlias()
{
Name = "Ban Me",
IPAddress = null
}
}
};
_manager.AddEvent(e);
e.Complete();
e = new GameEvent()
{
Type = GameEvent.EventType.Update,
Owner = server,
Origin = new EFClient()
{
NetworkId = 1338,
ClientNumber = 1,
CurrentAlias = new EFAlias()
{
Name = "Ban Me",
IPAddress = 1337
}
}
};
_manager.AddEvent(e);
e.Complete();
}
[Fact]
public void WarnClientShouldSucceed()
{
var onJoined = new ManualResetEventSlim();
var server = _manager.Servers[0];
while (!server.IsInitialized)
{
Thread.Sleep(100);
}
//_manager.OnServerEvent += (sender, eventArgs) =>
//{
// if (eventArgs.Event.Type == GameEvent.EventType.Connect)
// {
// onJoined.Set();
// }
//};
server.EmulateClientJoinLog();
onJoined.Wait();
var client = server.Clients[0];
var warnEvent = client.Warn("test warn", Utilities.IW4MAdminClient(server));
warnEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
Assert.False(warnEvent.Failed);
warnEvent = client.Warn("test warn", new EFClient() { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
warnEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
Assert.True(warnEvent.FailReason == GameEvent.EventFailReason.Permission &&
client.Warnings == 1, "warning was applied without proper permissions");
// warn clear
var warnClearEvent = client.WarnClear(new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
warnClearEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
Assert.True(warnClearEvent.FailReason == GameEvent.EventFailReason.Permission &&
client.Warnings == 1, "warning was removed without proper permissions");
warnClearEvent = client.WarnClear(Utilities.IW4MAdminClient(server));
warnClearEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
Assert.True(!warnClearEvent.Failed && client.Warnings == 0, "warning was not cleared");
}
[Fact]
public void ReportClientShouldSucceed()
{
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
Assert.False(client == null, "no client found to report");
// fail
var player = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer };
player.SetAdditionalProperty("_reportCount", 3);
var reportEvent = client.Report("test report", player);
reportEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Throttle &
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 0, $"too many reports were applied [{reportEvent.FailReason.ToString()}]");
// succeed
reportEvent = client.Report("test report", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
reportEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
Assert.True(!reportEvent.Failed &&
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"report was not applied [{reportEvent.FailReason.ToString()}]");
// fail
reportEvent = client.Report("test report", new EFClient() { ClientId = 1, NetworkId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Permission &&
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1,
$"report was applied without proper permission [{reportEvent.FailReason.ToString()},{ client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId)}]");
// fail
reportEvent = client.Report("test report", client);
Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Invalid &&
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"report was applied to self");
// fail
reportEvent = client.Report("test report", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Exception &&
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"duplicate report was applied");
}
[Fact]
public void FlagClientShouldSucceed()
{
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
Assert.False(client == null, "no client found to flag");
var flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
flagEvent.Complete();
// succeed
Assert.True(!flagEvent.Failed &&
client.Level == EFClient.Permission.Flagged, $"player is not flagged [{flagEvent.FailReason.ToString()}]");
Assert.False(client.ReceivedPenalties.FirstOrDefault(p => p.Offense == "test flag") == null, "flag was not applied");
flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
flagEvent.Complete();
// fail
Assert.True(client.ReceivedPenalties.Count == 1, "flag was applied without permisions");
flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
flagEvent.Complete();
// fail
Assert.True(client.ReceivedPenalties.Count == 1, "duplicate flag was applied");
var unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
unflagEvent.Complete();
// fail
Assert.False(client.Level == EFClient.Permission.User, "user was unflagged without permissions");
unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
unflagEvent.Complete();
// succeed
Assert.True(client.Level == EFClient.Permission.User, "user was not unflagged");
unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
unflagEvent.Complete();
// succeed
Assert.True(unflagEvent.FailReason == GameEvent.EventFailReason.Invalid, "user was not flagged");
}
[Fact]
void KickClientShouldSucceed()
{
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
Assert.False(client == null, "no client found to kick");
var kickEvent = client.Kick("test kick", new EFClient() { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
kickEvent.Complete();
Assert.True(kickEvent.FailReason == GameEvent.EventFailReason.Permission, "client was kicked without permission");
kickEvent = client.Kick("test kick", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
kickEvent.Complete();
Assert.True(_manager.Servers.First().GetClientsAsList().FirstOrDefault(c => c.NetworkId == client.NetworkId) == null, "client was not kicked");
}
[Fact]
void TempBanClientShouldSucceed()
{
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
Assert.False(client == null, "no client found to tempban");
/* var tbCommand = new TempBanCommand();
tbCommand.ExecuteAsync(new GameEvent()
{
Origin = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer },
Target = client,
Data = "5days test tempban",
Type = GameEvent.EventType.Command,
Owner = client.CurrentServer
}).Wait();
Assert.True(_manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == EFPenalty.PenaltyType.TempBan) == 1,
"tempban was not added");*/
}
[Fact]
void BanUnbanClientShouldSucceed()
{
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
Assert.False(client == null, "no client found to ban");
/*
var banCommand = new BanCommand();
banCommand.ExecuteAsync(new GameEvent()
{
Origin = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer },
Target = client,
Data = "test ban",
Type = GameEvent.EventType.Command,
Owner = client.CurrentServer
}).Wait();
Assert.True(_manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == EFPenalty.PenaltyType.Ban) == 1,
"ban was not added");
var unbanCommand = new UnbanCommand();
unbanCommand.ExecuteAsync(new GameEvent()
{
Origin = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer },
//Target = Manager.GetClientService().Find(c => c.NetworkId == client.NetworkId).Result.First(),
Data = "test unban",
Type = GameEvent.EventType.Command,
Owner = client.CurrentServer
}).Wait();
Assert.True(_manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == EFPenalty.PenaltyType.Ban) == 0,
"ban was not removed");*/
}
}
}

View File

@ -1,63 +0,0 @@
using IW4MAdmin.Application;
using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Tests
{
public class ManagerFixture : IDisposable
{
public ApplicationManager Manager { get; private set; }
public ManagerFixture()
{
string logFile = @"X:\IW4MAdmin\Plugins\Tests\bin\Debug\netcoreapp2.2\test_mp.log";
File.WriteAllText(logFile, Environment.NewLine);
Manager = null;
var config = new ApplicationConfiguration
{
Servers = new[]
{
new ServerConfiguration()
{
IPAddress = "127.0.0.1",
Password = "test",
Port = 28960,
RConParserVersion = "test",
EventParserVersion = "IW4x (v0.6.0)",
ManualLogPath = logFile
}
},
RConPollRate = int.MaxValue
};
Manager.ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("test");
Manager.ConfigHandler.Set(config);
Manager.Init().Wait();
Task.Run(() => Manager.Start());
}
public void Dispose()
{
Manager.Stop();
}
}
[CollectionDefinition("ManagerCollection")]
public class ManagerCollection : ICollectionFixture<ManagerFixture>
{
}
}

View File

@ -1,63 +0,0 @@
using IW4MAdmin.Application;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Xunit;
namespace Tests
{
[Collection("ManagerCollection")]
public class ManagerTests
{
readonly ApplicationManager Manager;
public ManagerTests(ManagerFixture fixture)
{
Manager = fixture.Manager;
}
[Fact]
public void AreCommandNamesUnique()
{
bool test = Manager.GetCommands().Count == Manager.GetCommands().Select(c => c.Name).Distinct().Count();
Assert.True(test, "command names are not unique");
}
[Fact]
public void AreCommandAliasesUnique()
{
var mgr = Manager;
bool test = mgr.GetCommands().Count == mgr.GetCommands().Select(c => c.Alias).Distinct().Count();
foreach (var duplicate in mgr.GetCommands().GroupBy(_cmd => _cmd.Alias).Where(_grp => _grp.Count() > 1).Select(_grp => new { Command = _grp.First().Name, Alias = _grp.Key }))
{
Debug.WriteLine($"{duplicate.Command}: {duplicate.Alias}");
}
Assert.True(test, "command aliases are not unique");
}
[Fact]
public void PrintCommands()
{
var sb = new StringBuilder();
sb.AppendLine("|Name |Alias|Description |Requires Target|Syntax |Required Level|");
sb.AppendLine("|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|");
foreach (var command in Manager.GetCommands().OrderByDescending(c => c.Permission).ThenBy(c => c.Name))
{
sb.AppendLine($"|{command.Name}|{command.Alias}|{command.Description}|{((Command)command).RequiresTarget}|{((Command)command).Syntax.Substring(8).EscapeMarkdown()}|{command.Permission}|");
}
Assert.True(false, sb.ToString());
}
}
}

View File

@ -1,52 +0,0 @@
using IW4MAdmin.Application;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using Xunit;
namespace Tests
{
[Collection("ManagerCollection")]
public class PluginTests
{
readonly ApplicationManager Manager;
public PluginTests(ManagerFixture fixture)
{
Manager = fixture.Manager;
}
[Fact]
public void ClientSayObjectionalWordShouldWarn()
{
var e = new GameEvent()
{
Type = GameEvent.EventType.Connect,
Origin = new EFClient()
{
Name = $"Player1",
NetworkId = 1,
ClientNumber = 1
},
Owner = Manager.GetServers()[0]
};
Manager.AddEvent(e);
e.Complete();
var client = Manager.GetServers()[0].Clients[0];
e = new GameEvent()
{
Type = GameEvent.EventType.Say,
Origin = client,
Data = "nigger",
Owner = e.Owner
};
Manager.AddEvent(e);
e.Complete();
Assert.True(client.Warnings == 1, "client wasn't warned for objectional language");
}
}
}

View File

@ -1,113 +0,0 @@
using IW4MAdmin.Application;
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using Xunit;
namespace Tests
{
[Collection("ManagerCollection")]
public class ServerTests
{
private readonly ApplicationManager _manager;
public ServerTests(ManagerFixture fixture)
{
_manager = fixture.Manager;
}
[Fact]
public void AddAndRemoveClientViaLog()
{
var resetEvent = new ManualResetEventSlim();
var server = _manager.Servers[0];
var currentClientCount = server.ClientNum;
int eventsProcessed = 0;
/*_manager.OnServerEvent += (sender, eventArgs) =>
{
if (eventArgs.Event.Type == GameEvent.EventType.Connect)
{
eventArgs.Event.Complete();
Assert.False(eventArgs.Event.Failed, "connect event was not processed");
Assert.True(server.ClientNum == currentClientCount + 1, "client count was not incremented");
eventsProcessed++;
resetEvent.Set();
}
if (eventArgs.Event.Type == GameEvent.EventType.Disconnect)
{
eventArgs.Event.Complete();
Assert.False(eventArgs.Event.Failed, "disconnect event was not processed");
Assert.True(server.ClientNum == currentClientCount, "client count was not decremented");
eventsProcessed++;
resetEvent.Set();
}
};*/
server.EmulateClientJoinLog();
resetEvent.Wait(15000);
resetEvent.Reset();
Assert.Equal(1, eventsProcessed);
server.EmulateClientQuitLog();
resetEvent.Wait(15000);
Assert.Equal(2, eventsProcessed);
}
[Fact]
public void AddAndRemoveClientViaRcon()
{
var resetEvent = new ManualResetEventSlim();
var server = _manager.Servers[0];
var currentClientCount = server.ClientNum;
int eventsProcessed = 0;
_manager.GetApplicationSettings().Configuration().RConPollRate = 5000;
/*_manager.OnServerEvent += (sender, eventArgs) =>
{
if (eventArgs.Event.Type == GameEvent.EventType.Connect)
{
eventArgs.Event.Complete();
Assert.False(eventArgs.Event.Failed, "connect event was not processed");
Assert.True(server.ClientNum == currentClientCount + 1, "client count was not incremented");
eventsProcessed++;
resetEvent.Set();
}
if (eventArgs.Event.Type == GameEvent.EventType.Disconnect)
{
eventArgs.Event.Complete();
Assert.False(eventArgs.Event.Failed, "disconnect event was not processed");
Assert.True(server.ClientNum == currentClientCount, "client count was not decremented");
eventsProcessed++;
resetEvent.Set();
}
};*/
(server.RconParser as TestRconParser).FakeClientCount = 1;
resetEvent.Wait(15000);
resetEvent.Reset();
Assert.Equal(1, eventsProcessed);
(server.RconParser as TestRconParser).FakeClientCount = 0;
resetEvent.Wait(15000);
Assert.Equal(2, eventsProcessed);
_manager.GetApplicationSettings().Configuration().RConPollRate = int.MaxValue;
}
}
}

View File

@ -1,24 +0,0 @@
using IW4MAdmin.Application;
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Tests
{
internal static class TestHelpers
{
internal static void EmulateClientJoinLog(this Server svr)
{
long guid = svr.ClientNum + 1;
File.AppendAllText(svr.LogPath, $"0:00 J;{guid};{svr.ClientNum};test_client_{svr.ClientNum}\r\n");
}
internal static void EmulateClientQuitLog(this Server svr)
{
long guid = Math.Max(1, svr.ClientNum);
File.AppendAllText(svr.LogPath, $"0:00 Q;{guid};{svr.ClientNum};test_client_{svr.ClientNum}\r\n");
}
}
}

View File

@ -1,44 +0,0 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Tests
{
class TestRconParser : IW4MAdmin.Application.RconParsers.BaseRConParser
{
public TestRconParser(IParserRegexFactory f) : base(f)
{
}
public int FakeClientCount { get; set; }
public List<EFClient> FakeClients { get; set; } = new List<EFClient>();
public override string Version => "test";
public override async Task<(List<EFClient>, string)> GetStatusAsync(IRConConnection connection)
{
var clientList = new List<EFClient>();
for (int i = 0; i < FakeClientCount; i++)
{
clientList.Add(new EFClient()
{
ClientNumber = i,
NetworkId = i + 1,
CurrentAlias = new EFAlias()
{
Name = $"test_bot_{i}",
IPAddress = i + 1
}
});
}
return clientList.Count > 0 ? (clientList, "mp_rust") : (FakeClients, "mp_rust");
}
}
}

View File

@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<ApplicationIcon />
<StartupObject />
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>TRACE;DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Application\Application.csproj" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
</Project>

View File

@ -1,9 +1,13 @@
using IW4MAdmin.Plugins.Stats.Models; using IW4MAdmin.Plugins.Stats.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using StatsWeb.Dtos; using Stats.Dtos;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,31 +16,44 @@ namespace StatsWeb
/// <summary> /// <summary>
/// implementation of IResourceQueryHelper /// implementation of IResourceQueryHelper
/// </summary> /// </summary>
public class ChatResourceQueryHelper : IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> public class ChatResourceQueryHelper : IResourceQueryHelper<ChatSearchQuery, MessageResponse>
{ {
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
private List<EFServer> serverCache;
public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory) public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
{ {
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
_appConfig = appConfig;
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<ResourceQueryHelperResult<ChatSearchResult>> QueryResource(ChatSearchQuery query) public async Task<ResourceQueryHelperResult<MessageResponse>> QueryResource(ChatSearchQuery query)
{ {
if (query == null) if (query == null)
{ {
throw new ArgumentException("Query must be specified"); throw new ArgumentException("Query must be specified");
} }
var result = new ResourceQueryHelperResult<ChatSearchResult>(); var result = new ResourceQueryHelperResult<MessageResponse>();
using var context = _contextFactory.CreateContext(enableTracking: false); using var context = _contextFactory.CreateContext(enableTracking: false);
if (serverCache == null)
{
serverCache = await context.Set<EFServer>().ToListAsync();
}
if (int.TryParse(query.ServerId, out int serverId))
{
query.ServerId = serverCache.FirstOrDefault(_server => _server.ServerId == serverId)?.EndPoint ?? query.ServerId;
}
var iqMessages = context.Set<EFClientMessage>() var iqMessages = context.Set<EFClientMessage>()
.Where(_message => _message.TimeSent >= query.SentAfter) .Where(_message => _message.TimeSent >= query.SentAfter)
.Where(_message => _message.TimeSent <= query.SentBefore); .Where(_message => _message.TimeSent < query.SentBefore);
if (query.ClientId != null) if (query.ClientId != null)
{ {
@ -50,27 +67,29 @@ namespace StatsWeb
if (!string.IsNullOrEmpty(query.MessageContains)) if (!string.IsNullOrEmpty(query.MessageContains))
{ {
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message, $"%{query.MessageContains}%")); iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
} }
var iqResponse = iqMessages var iqResponse = iqMessages
.Select(_message => new ChatSearchResult .Select(_message => new MessageResponse
{ {
ClientId = _message.ClientId, ClientId = _message.ClientId,
ClientName = _message.Client.CurrentAlias.Name, ClientName = query.IsProfileMeta ? "" : _message.Client.CurrentAlias.Name,
Date = _message.TimeSent, ServerId = _message.ServerId,
When = _message.TimeSent,
Message = _message.Message, Message = _message.Message,
ServerName = _message.Server.HostName ServerName = query.IsProfileMeta ? "" : _message.Server.HostName,
GameName = _message.Server.GameName == null ? Server.Game.IW4 : _message.Server.GameName.Value
}); });
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending) if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending)
{ {
iqResponse = iqResponse.OrderByDescending(_message => _message.Date); iqResponse = iqResponse.OrderByDescending(_message => _message.When);
} }
else else
{ {
iqResponse = iqResponse.OrderBy(_message => _message.Date); iqResponse = iqResponse.OrderBy(_message => _message.When);
} }
var resultList = await iqResponse var resultList = await iqResponse
@ -78,6 +97,27 @@ namespace StatsWeb
.Take(query.Count) .Take(query.Count)
.ToListAsync(); .ToListAsync();
foreach (var message in resultList)
{
message.IsHidden = serverCache.Any(server => server.ServerId == message.ServerId && server.IsPasswordProtected);
if (message.Message.IsQuickMessage())
{
try
{
var quickMessages = _appConfig
.QuickMessages
.First(_qm => _qm.Game == message.GameName);
message.Message = quickMessages.Messages[message.Message.Substring(1)];
message.IsQuickMessage = true;
}
catch
{
message.Message = message.Message.Substring(1);
}
}
}
result.TotalResultCount = await iqResponse.CountAsync(); result.TotalResultCount = await iqResponse.CountAsync();
result.Results = resultList; result.Results = resultList;
result.RetrievedResultCount = resultList.Count; result.RetrievedResultCount = resultList.Count;

View File

@ -5,8 +5,9 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using StatsWeb.Dtos; using Stats.Dtos;
using StatsWeb.Extensions; using StatsWeb.Extensions;
using System; using System;
using System.Linq; using System.Linq;
@ -18,10 +19,10 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IManager _manager; private readonly IManager _manager;
private readonly IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> _chatResourceQueryHelper; private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> resourceQueryHelper, public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, MessageResponse> resourceQueryHelper,
ITranslationLookup translationLookup) : base(manager) ITranslationLookup translationLookup) : base(manager)
{ {
_logger = logger; _logger = logger;
@ -36,6 +37,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"]; ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"]; ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
ViewBag.Servers = _manager.GetServers().Select(_server => new ServerInfo() { Name = _server.Hostname, ID = _server.EndPoint }); ViewBag.Servers = _manager.GetServers().Select(_server => new ServerInfo() { Name = _server.Hostname, ID = _server.EndPoint });
ViewBag.Localization = _translationLookup;
return View("Index"); return View("Index");
} }
@ -71,51 +73,24 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetMessageAsync(int serverId, long when) public async Task<IActionResult> GetMessageAsync(string serverId, long when)
{ {
var whenTime = DateTime.FromFileTimeUtc(when); var whenTime = DateTime.FromFileTimeUtc(when);
var whenUpper = whenTime.AddMinutes(5); var whenUpper = whenTime.AddMinutes(5);
var whenLower = whenTime.AddMinutes(-5); var whenLower = whenTime.AddMinutes(-5);
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true)) var messages = await _chatResourceQueryHelper.QueryResource(new ChatSearchQuery()
{ {
var iqMessages = from message in ctx.Set<Stats.Models.EFClientMessage>() ServerId = serverId,
where message.ServerId == serverId SentBefore = whenUpper,
where message.TimeSent >= whenLower SentAfter = whenLower
where message.TimeSent <= whenUpper });
select new ChatInfo()
{
ClientId = message.ClientId,
Message = message.Message,
Name = message.Client.CurrentAlias.Name,
Time = message.TimeSent,
ServerGame = message.Server.GameName ?? Server.Game.IW4
};
var messages = await iqMessages.ToListAsync(); return View("_MessageContext", messages.Results);
foreach (var message in messages)
{
if (message.Message.IsQuickMessage())
{
try
{
var quickMessages = _manager.GetApplicationSettings().Configuration()
.QuickMessages
.First(_qm => _qm.Game == message.ServerGame);
message.Message = quickMessages.Messages[message.Message.Substring(1)];
message.IsQuickMessage = true;
}
catch { }
}
}
return View("_MessageContext", messages);
}
} }
[HttpGet("Message/Find")] [HttpGet("Message/Find")]
public async Task<IActionResult> FindMessage([FromQuery]string query) public async Task<IActionResult> FindMessage([FromQuery] string query)
{ {
ViewBag.Localization = _translationLookup; ViewBag.Localization = _translationLookup;
ViewBag.EnableColorCodes = _manager.GetApplicationSettings().Configuration().EnableColorCodes; ViewBag.EnableColorCodes = _manager.GetApplicationSettings().Configuration().EnableColorCodes;
@ -150,7 +125,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
} }
[HttpGet("Message/FindNext")] [HttpGet("Message/FindNext")]
public async Task<IActionResult> FindNextMessages([FromQuery]string query, [FromQuery]int count, [FromQuery]int offset) public async Task<IActionResult> FindNextMessages([FromQuery] string query, [FromQuery] int count, [FromQuery] int offset)
{ {
ChatSearchQuery searchRequest; ChatSearchQuery searchRequest;

View File

@ -1,32 +0,0 @@
using System;
namespace StatsWeb.Dtos
{
public class ChatSearchResult
{
/// <summary>
/// name of the client
/// </summary>
public string ClientName { get; set; }
/// <summary>
/// client id
/// </summary>
public int ClientId { get; set; }
/// <summary>
/// hostname of the server
/// </summary>
public string ServerName { get; set; }
/// <summary>
/// chat message
/// </summary>
public string Message { get; set; }
/// <summary>
/// date the chat occured on
/// </summary>
public DateTime Date { get; set; }
}
}

View File

@ -1,5 +1,5 @@
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using StatsWeb.Dtos; using Stats.Dtos;
using System; using System;
using System.Linq; using System.Linq;

View File

@ -14,7 +14,7 @@
<RunPostBuildEvent>Always</RunPostBuildEvent> <RunPostBuildEvent>Always</RunPostBuildEvent>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,6 +1,6 @@
<ul class="nav nav-tabs border-top border-bottom nav-fill row" role="tablist" id="stats_top_players"> <ul class="nav nav-tabs border-top border-bottom nav-fill row" role="tablist" id="stats_top_players">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active top-players-link" href="#server_0" role="tab" data-toggle="tab" aria-selected="true" data-serverid="0">All Servers</a> <a class="nav-link active top-players-link" href="#server_0" role="tab" data-toggle="tab" aria-selected="true" data-serverid="0">@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</a>
</li> </li>
@foreach (var server in ViewBag.Servers) @foreach (var server in ViewBag.Servers)

View File

@ -1,4 +1,5 @@
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<StatsWeb.Dtos.ChatSearchResult> @using SharedLibraryCore.Dtos.Meta.Responses
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<MessageResponse>
@if (ViewBag.Error != null) @if (ViewBag.Error != null)
{ {

View File

@ -1,4 +1,5 @@
@model IEnumerable<StatsWeb.Dtos.ChatSearchResult> @using SharedLibraryCore.Dtos.Meta.Responses
@model IEnumerable<MessageResponse>
@foreach (var message in Model) @foreach (var message in Model)
{ {
@ -10,13 +11,20 @@
</a> </a>
</td> </td>
<td class="text-light w-50 text-break"> <td class="text-light w-50 text-break">
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code> @if (message.IsHidden && !ViewBag.Authorized)
{
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
}
else
{
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
}
</td> </td>
<td class="text-light"> <td class="text-light">
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code> <color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
</td> </td>
<td class="text-right text-light"> <td class="text-right text-light">
@message.Date @message.When
</td> </td>
</tr> </tr>
@ -33,7 +41,14 @@
<tr class="d-table-row d-lg-none bg-dark"> <tr class="d-table-row d-lg-none bg-dark">
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th> <th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
<td class="text-light"> <td class="text-light">
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code> @if (message.IsHidden && !ViewBag.Authorized)
{
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
}
else
{
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
}
</td> </td>
</tr> </tr>
@ -47,7 +62,7 @@
<tr class="d-table-row d-lg-none bg-dark"> <tr class="d-table-row d-lg-none bg-dark">
<th scope="row" class="bg-primary" style="border-bottom: 1px solid #222">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th> <th scope="row" class="bg-primary" style="border-bottom: 1px solid #222">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
<td class="text-light mb-2 border-bottom"> <td class="text-light mb-2 border-bottom">
@message.Date @message.When
</td> </td>
</tr> </tr>
} }

View File

@ -1,20 +1,21 @@
@model IEnumerable<SharedLibraryCore.Dtos.ChatInfo> @using SharedLibraryCore.Dtos.Meta.Responses
@model IEnumerable<MessageResponse>
@{ @{
Layout = null; Layout = null;
} }
<div class="client-message-context"> <div class="client-message-context">
<h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().Time.ToString()</h5> <h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().When.ToString()</h5>
<div class="bg-dark p-3 mb-2 border-bottom"> <div class="bg-dark p-3 mb-2 border-bottom">
@foreach (var message in Model) @foreach (var message in Model)
{ {
<span class="text-white"> <span class="text-white">
<color-code value="@message.Name" allow="ViewBag.EnableColorCodes"></color-code> <color-code value="@message.ClientName" allow="ViewBag.EnableColorCodes"></color-code>
</span> </span>
<span> <span>
&mdash; &mdash;
<span class="@(message.IsQuickMessage ? "font-italic" : "")"> <span class="@(message.IsQuickMessage ? "font-italic" : "")">
<color-code value="@message.Message" allow="ViewBag.EnableColorCodes"></color-code> <color-code value="@(message.IsHidden ? message.HiddenMessage : message.Message)" allow="ViewBag.EnableColorCodes"></color-code>
</span> </span>
</span> </span>
<br /> <br />

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

437
README.md
View File

@ -1,26 +1,25 @@
# IW4MAdmin # IW4MAdmin [![GitHub license](https://img.shields.io/github/license/RaidMax/IW4M-Admin)](https://github.com/RaidMax/IW4M-Admin/blob/2.4-pr/LICENSE) [![GitHub stars](https://img.shields.io/github/stars/RaidMax/IW4M-Admin)](https://github.com/RaidMax/IW4M-Admin/stargazers)
### Quick Start Guide [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/J3J821KUJ)
### Version 2.4
_______ ## About
### About **IW4MAdmin** is an administration tool for [IW4x](https://iw4x.org/), [Pluto T6](https://forum.plutonium.pw/category/6/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/14/plutonium-iw5), [CoD4x](https://cod4x.me/), [TeknoMW3](https://github.com/Musta1337/TeknoMW3), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
**IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/6/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/14/plutonium-iw5), [CoD4x](https://cod4x.me/), [TeknoMW3](https://www.teknomw3.pw/), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
### Download ### Download
Latest binary builds are always available at: Latest binary builds are always available at:
- [GitHub](https://github.com/RaidMax/IW4M-Admin/releases) - [GitHub](https://github.com/RaidMax/IW4M-Admin/releases)
- [RaidMax](https://raidmax.org/IW4MAdmin) - [RaidMax](https://raidmax.org/IW4MAdmin)
---
### Setup ## Setup
**IW4MAdmin** requires minimal effort to get up and running. **IW4MAdmin** requires minimal effort to get up and running.
#### Prerequisites ### Prerequisites
* [.NET Core 3.1.x Runtime](https://www.microsoft.com/net/download) *or newer* * [.NET Core 3.1.x Runtime](https://www.microsoft.com/net/download) *or newer*
* [Direct Download (Windows)](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-aspnetcore-3.1.4-windows-hosting-bundle-installer) * [Direct Download (Windows)](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-aspnetcore-3.1.4-windows-hosting-bundle-installer)
* [Package Installation (Linux)](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1910) * [Package Installation (Linux)](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1910)
#### Installation ### Installation
1. Install .NET Core Runtime 1. Install .NET Core Runtime
2. Extract `IW4MAdmin-<version>.zip` 2. Extract `IW4MAdmin-<version>.zip`
#### Launching ### Launching
Windows Windows
1. Run `StartIW4MAdmin.cmd` 1. Run `StartIW4MAdmin.cmd`
2. Configure **IW4MAdmin** 2. Configure **IW4MAdmin**
@ -41,421 +40,9 @@ Linux
_Your configuration and database will be saved_ _Your configuration and database will be saved_
--- ## Help
### Help
Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3) Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3)
If you come across an issue, bug, or feature request please post an [issue](https://github.com/RaidMax/IW4M-Admin/issues) If you come across an issue, bug, or feature request please post an [issue](https://github.com/RaidMax/IW4M-Admin/issues)
___
### Configuration
#### Initial Configuration
When **IW4MAdmin** is launched for the _first time_, you will be prompted to setup your configuration.
`Enable webfront` #### Explore the [wiki](https://github.com/RaidMax/IW4M-Admin/wiki) to find more information.
* Enables you to monitor and control your server(s) through a web interface
* Default &mdash; `http://0.0.0.0:1624`
`Enable multiple owners`
* Enables more than one client to be promoted to level of `Owner`
* Default &mdash; `false`
`Enable stepped privilege hierarchy`
* Allows privileged clients to promote other clients to the level below their current level
* Default &mdash; `false`
`Enable custom say name`
* Shows a prefix to every message send by **IW4MAdmin** -- `[Admin] message`
* _This feature requires you specify a custom say name_
* _This feature only works on games that support the `sv_sayName` dvar_
* Default &mdash; `false`
`Enable social link`
* Shows a link to your community's social media/website on the webfront
* Default &mdash; `false`
`Use Custom Encoding Parser`
* Allows alternative encodings to be used for parsing game information and events
* **Russian users should use this and then specify** `windows-1251` **as the encoding string**
* Default &mdash; `false`
#### Server Configuration
After initial configuration is finished, you will be prompted to configure your servers for **IW4MAdmin**.
`Enter server IP Address`
* For almost all scenarios `127.0.0.1` is sufficient
* Default &mdash; `n/a`
`Enter server port`
* The port that your server is listening on (can be obtained via `net_port`)
* Default &mdash; `n/a`
`Enter server RCon password`
* The *\(R\)emote (Con)sole* password set in your server configuration (can be obtained via `rcon_password`)
* Default &mdash; `n/a`
`Enter number of reserved slots`
* The number of client slots reserved for privileged players (unavailable for regular users to occupy)
* For example, if you enter **2** reserved slots on an **18** slot server, you will have **16** publicly available slots
* Default &mdash; `0`
#### Advanced Configuration
If you wish to further customize your experience of **IW4MAdmin**, the following configuration file(s) will allow you to changes core options using any text-editor.
#### `IW4MAdminSettings.json`-- this file is created after initial setup
* This file uses the [JSON](https://en.wikipedia.org/wiki/JSON#JSON_sample) specification, so please validate your configuration before running **IW4MAdmin**
`WebfrontBindUrl`
* Specifies the address and port the webfront will listen on.
* The value can be an [IP Address](https://en.wikipedia.org/wiki/IP_address):port or [Domain Name](https://en.wikipedia.org/wiki/Domain_name):port
* Example http://gameserver.com:8080
* Default &mdash; `http://0.0.0.0:1624` (indicates that it will listen on all IP Addresses available to the default interface)
`CustomLocale`
* Specifies a [locale name](https://msdn.microsoft.com/en-us/library/39cwe7zf.aspx) to use instead of system default
* Locale must be from the `Equivalent Locale Name` column
* Default &mdash; `windows-1252`
`ConnectionString`
* Specifies the [connection string](https://www.connectionstrings.com/mysql/) to a MySQL server that is used instead of SQLite
* Default &mdash; `null`
`DatabaseProvider`
* Specifies the database provider **IW4MAdmin** should use
* Possible values &mdash; `sqlite`, `mysql`, `postgresql`
* Default &mdash; `sqlite`
`Ignore Bots`
* Disables bots from being registered and tracked by **IW4MAdmin**
`RConPollRate`
* Specifies (in milliseconds) how often to poll each server for updates
* Default &mdash; `5000`
`Servers`
* Specifies the list of servers **IW4MAdmin** will monitor
* Default &mdash; `[]`
* `IPAddress`
* Specifies the IP Address of the particular server
* Default &mdash; `n/a`
* `Port`
* Specifies the port of the particular server
* Default &mdash; `n/a`
* `Password`
* Specifies the `rcon_password` of the particular server
* Default &mdash; `n/a`
* `ManualLogPath`
* Specifies the log path to be used instead of the automatically generated one
* To use the `GameLogServer`, this should be set to the http address that the `GameLogServer` is listening on
* Example &mdash; http://gamelogserver.com/
* Default &mdash; `null`
* `AutoMessages`
* Specifies the list of messages that are broadcasted to the particular server
* Default &mdash; `[]`
* `Rules`
* Specifies the list of rules that apply to the particular server
* Default &mdash; `[]`
* `ReservedSlotNumber`
* Specifies the number of client slots to reserve for privileged users
* Default &mdash; `0`
* `GameLogServerUrl`
* Specifies the HTTP Url for the Game Log Server
* Default &mdash; `null`
`AutoMessagePeriod`
* Specifies (in seconds) how often messages should be broadcasted to each server
* Default &mdash; `60`
`AutoMessages`
* Specifies the list of messages that are broadcasted to **all** servers
* Specially formatted **tokens** can be used to broadcast dynamic information
* `{{TOTALPLAYERS}}` &mdash; displays how many players have connected
* `{{TOPSTATS}}` &mdash; displays the top 5 players on the server based on performance
* `{{MOSTPLAYED}}` &mdash; displays the top 5 players based on number of kills
* `{{TOTALPLAYTIME}}` &mdash; displays the cumulative play time (in man-hours) on all monitored servers
* `{{VERSION}}` &mdash; displays the version of **IW4MAdmin**
* `{{ADMINS}}` &mdash; displays the currently connected and *unmasked* privileged users online
* `{{NEXTMAP}}` &mdash; displays the next map and gametype in rotation
`GlobalRules`
* Specifies the list of rules that apply to **all** servers`
`Maps`
* Specifies the list of maps for each supported game
* `Name`
* Specifies the name of the map as returned by the game (usually the file name sans the file extension)
* `Alias`
* Specifies the display name of the map (as seen while loading in)
___
### Commands
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ---------------|
|prune|pa|demote any trusted clients that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|quit|q|quit IW4MAdmin|False|!q |Owner|
|rcon|rcon|send rcon command to server|False|!rcon \<commands\>|Owner|
|ban|b|permanently ban a client from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|unban|ub|unban client by client id|True|!ub \<client id\> \<reason\>|SeniorAdmin|
|find|f|find client in database|False|!f \<player\>|Administrator|
|killserver|kill|kill the game server|False|!kill |Administrator|
|map|m|change to specified map|False|!m \<map\>|Administrator|
|maprotate|mr|cycle to the next map in rotation|False|!mr |Administrator|
|plugins|p|view all loaded plugins|False|!p |Administrator|
|tempban|tb|temporarily ban a client for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Administrator|
|alias|known|get past aliases and ips of a client|True|!known \<player\>|Moderator|
|baninfo|bi|get information about a ban for a client|True|!bi \<player\>|Moderator|
|fastrestart|fr|fast restart current map|False|!fr |Moderator|
|flag|fp|flag a suspicious client and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|kick|k|kick a client by name|True|!k \<player\> \<reason\>|Moderator|
|list|l|list active clients|False|!l |Moderator|
|mask|hide|hide your presence as a privileged client|False|!hide |Moderator|
|reports|reps|get or clear recent reports|False|!reps \<optional clear\>|Moderator|
|say|s|broadcast message to all clients|False|!s \<message\>|Moderator|
|setlevel|sl|set client to specified privilege level|True|!sl \<player\> \<level\>|Moderator|
|setpassword|sp|set your authentication password|False|!sp \<password\>|Moderator|
|unflag|uf|Remove flag for client|True|!uf \<player\>|Moderator|
|uptime|up|get current application running time|False|!up |Moderator|
|usage|us|get application memory usage|False|!us |Moderator|
|warn|w|warn client for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|warnclear|wc|remove all warnings for a client|True|!wc \<player\>|Trusted|
|admins|a|list currently connected privileged clients|False|!a |User|
|getexternalip|ip|view your external IP address|False|!ip |User|
|help|h|list all available commands|False|!h \<optional commands\>|User|
|nextmap|nm|view next map in rotation|False|!nm |User|
|owner|iamgod|claim ownership of the server|False|!iamgod |User|
|ping|pi|get client's latency|False|!pi \<optional player\>|User|
|privatemessage|pm|send message to other client|True|!pm \<player\> \<message\>|User|
|report|rep|report a client for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|rules|r|list server rules|False|!r |User|
|setgravatar|sg|set gravatar for webfront profile|False|!sg \<gravatar email\>|User|
|whoami|who|give information about yourself|False|!who |User|
_These commands include all shipped plugin commands._
---
#### Player Identification
All players are identified 5 separate ways
1. `npID/GUID/XUID` - The ID corresponding to the player's hardware or forum account
2. `IP` - The player's IP Address
3. `Client ID` - The internal reference to a player, generated by **IW4MAdmin**
4. `Name` - The visible player name as it appears in game
5. `Client Number` - The slot the client occupies on a server. (The number ranges between 0 and the max number of clients allowed on the server)
For most commands players are identified by their `Name`
However, if they are currently offline, or their name contains un-typable characters, their `Client ID` must be used
The `Client ID` is specified by prefixing a player's reference number with `@`.
For example, `@123` would reference the player with a `Client ID` of 123.
**All commands that require a `target` look at the `first argument` for a form of player identification**
---
#### Additional Command Examples
`setlevel`
- _shortcut_ - `sl`
- _Parameter 1_ - Player to modify level of
- _Parameter 2_ - Level to set the player to ```[ User, Trusted, Moderator, Administrator, SeniorAdmin, Owner ]```
- _Example_ - `!setlevel Player1 SeniorAdmin`, `!sl @123 Moderator`
- **NOTE** - An `owner` cannot set another player's level to `owner` unless the configuration option is enabled during setup
`ban`
- _Shortcut_ - `b`
- _Parameter 1_ - Player to ban
- _Parameter 2_ - Reason for ban
- _Example_ - `!ban Player1 caught cheating`, `!b @123 GUID Spoofing`
`tempban`
- _Shortcut_ - `tb`
- _Parameter 1_ - Player to ban
- _Parameter 2_ - Ban length (minutes|hours|days|weeks|years)
- _Parameter 3_ - Reason for ban
- _Example_ - `!tempban Player1 3w racism`, `!tb @123 8h Abusive behaivor`
`reports`
- _Shortcut_ - `reps`
- _Optional Parameter 1_ - `clear` (erases reports for current server)
___
### Plugins
#### Welcome
- This plugin uses geo-location data to welcome a player based on their country of origin
- All privileged users ( Trusted or higher ) receive a specialized welcome message as well
- Welcome messages can be customized in `WelcomePluginSettings.json`
#### Stats
- This plugin calculates basic player performance, skill approximation, and kill/death ratio
- Skill is an number derived from an algorithmic processing of a player's Kill Death Ratio (KDR) and Score per Minute (SPM).
- Elo Rating is based off of the number of encounters a player wins.
- Performance is the average of Skill + Elo Rating
**Commands added by this plugin**
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
- To qualify for top stats, a client must have played for at least `3 hours` and connected within the past `15 days`.
#### Login
- This plugin deters GUID spoofing by requiring privileged users to login with their password before executing commands
- A password must be set using the `setpassword` command before logging in
**Commands added by this plugin**
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|login|l|login using password|False|!l \<password\>|Trusted|
#### Profanity Determent
- This plugin warns and kicks players for using profanity
- Profane words and warning message can be specified in `ProfanityDetermentSettings.json`
- If a client's name contains a word listed in the settings, they will immediately be kicked
#### IW4 Script Commands
- This plugin provides additional integration to IW4x
- In order to take advantage of it, copy the `userraw` folder into your IW4x server directory
#### VPN Detection [Script Plugin]
- This plugin detects if a client is using a VPN and kicks them if they are
- To disable this plugin, delete `Plugins\VPNDetection.js`
- Adding **Client IDs** to the `vpnExceptionIds` array will prevent a client from being kicked.
#### Shared GUID Kicker [Script Plugin]
- This plugin kicks users using a specific GUID
- GUID `F4D2C30B712AC6E3` on IW4x was packed into a torrent version of the game.
___
### Webfront
`Home`
* Shows an overview of the monitored server(s)
`Penalties`
* Shows a chronological ordered list of client penalties (scrolling down loads older penalties)
`Admins`
* Shows a list of privileged clients
`Login`
* Allows privileged users to login using their `Client ID` and password set via `setpassword`
* `ClientID` is a number that can be found by using `!find <client name>` or find the client on the webfront and copy the ID following `ProfileAsync/`
`Profile`
* Shows a client's information and history
`Web Console`
* Allows logged in privileged users to execute commands as if they are in-
`Search`
* Query clients and messages
Advanced filters can be constructed to search for resources using the following filter table.
| Filter | Description | Format | Example |
|-----------|--------------------------------------------------------|-----------------------|---------------------|
| before | include items occurring on or before the provided date | YYYY-MM-DD hh:mm:ss (UTC inferred) | 2020-05-21 23:00:00 |
| after | include items occurring on or after the provided date | YYYY-MM-DD hh:mm:ss (UTC inferred) | 2015-01-01 |
| server | include items matching the server id | ip:port | 127.0.0.1:28960 |
| client | include items matching the client id | integer | 8947 |
| contains | include items containing this substring | string | hack |
| sort | display results in this order | ascending\|descending | descending |
Any number of filters can be combined in any order.
Example &mdash; `chat|before 2020-05-21|after 2020-05-01|server 127.0.0.1:28960|client 444|contains cheating|sort descending`
---
### Game Log Server
The game log server provides a way to remotely host your server's log over a http rest-ful api.
This feature is useful if you plan on running IW4MAdmin on a different machine than the game server.
#### Requirements
- [Python 3.8.x](https://www.python.org/downloads/) or newer
#### Installation
1. With Python 3.x installed, open up a terminal/command prompt window in the `GameLogServer` folder and execute:
```console
pip install -r requirements.txt
```
If this fails, you can alternatively try installing with:
```console
python -m pip install -r requirements.txt
```
2. Allow TCP port 1625 through firewall
* [Windows Instructions](https://www.tomshardware.com/news/how-to-open-firewall-ports-in-windows-10,36451.html)
* [Linux Instructions (iptables)](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-basic-iptables-firewall-on-centos-6#open-up-ports-for-selected-services)
#### Launching
With Python 3 installed, open a terminal/command prompt window open in the `GameServerLog` folder and execute:
```console
python runserver.py
```
The Game Log Server window will need to remain running/open as long as **IW4MAdmin** is running
#### Configuring
* Update your `IW4MAdminSettings.json` by changing the value of `GameLogServerUrl` to "http://<remote_server_ip>:1625"
* Example &mdash; `"GameLogServerUrl": "http://192.168.1.123:1625",`
---
### Extending Plugins
#### NuGet Package
The NuGet package for **IW4MAdmin's** "Shared Library" can be obtained from the [NuGet Gallery](https://www.nuget.org/packages/RaidMax.IW4MAdmin.SharedLibraryCore)
Referencing this package will give you the ability to write plugins against **IW4MAdmin's** core library.
#### Code
**IW4MAdmin's** functionality can be extended by writing additional plugins in C#.
Each class library must implement the `IPlugin` interface.
See the existing [plugins](https://github.com/RaidMax/IW4M-Admin/tree/master/Plugins) for examples.
#### JavaScript
**IW4MAdmin** functionality can also be extended using JavaScript.
The JavaScript parser supports [ECMA 5.1](https://ecma-international.org/ecma-262/5.1/) standards.
#### Plugin Object Template
In order to be properly parsed by the JavaScript engine, every plugin must conform to the following template.
```js
var plugin = {
author: 'YourHandle',
version: 1.0,
name: 'Sample JavaScript Plugin',
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};
```
#### Required Properties
- `author` &mdash; [string] Author of the plugin (usually your name or online name/alias)
- `version` &mdash; [float] Version number of your plugin (useful if you release several different versions)
- `name` &mdash; [string] Name of your plugin (be descriptive!)
- `onEventAsync` &mdash; [function] Handler executed when an event occurs
- `gameEvent` &mdash; [parameter object] Object containing event type, origin, target, and other info (see the GameEvent class declaration)
- `server` &mdash; [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
- `onLoadAsync` &mdash; [function] Handler executed when the plugin is loaded by code
- `manager` &mdash; [parameter object] Object reference to the application manager (see the IManager interface definition)
- `onUnloadAsync` &mdash; [function] Handler executed when the plugin is unloaded by code (see live reloading)
- `onTickAsync` &mdash; [function] Handler executed approximately once per second by code *(unimplemented as of version 2.\*)*
- `server` &mdash; [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
### Live Reloading
Thanks to JavaScript's flexibility and parsability, the plugin importer scans the plugins folder and reloads the JavaScript plugins on demand as they're modified. This allows faster development/testing/debugging.
---
### Misc
#### Anti-cheat
This is an [IW4x](https://iw4xcachep26muba.onion.link/) only feature (wider game support planned), that uses analytics to detect aimbots and aim-assist tools.
To utilize anti-cheat, enable it during setup **and** copy `_customcallbacks.gsc` from `userraw` into your `IW4x Server\userraw\scripts` folder.
The anti-cheat feature is a work in progress and as such will be constantly tweaked and may not be 100% accurate, however the goal is to deter as many cheaters as possible from IW4x.
#### Database Storage
By default, all **IW4MAdmin** information is stored in `Database.db`.
Should you need to reset your database, this file can simply be deleted.
Additionally, this file should be preserved during updates to retain client information.
Setting the `ConnectionString` and `DatabaseProvider` properties in `IW4MAdminSettings.json`
will allow **IW4MAdmin** to use alternate methods for database storage

View File

@ -57,6 +57,13 @@ namespace SharedLibraryCore
ViewBag.Version = Manager.Version; ViewBag.Version = Manager.Version;
ViewBag.IsFluid = false; ViewBag.IsFluid = false;
ViewBag.EnableColorCodes = Manager.GetApplicationSettings().Configuration().EnableColorCodes; ViewBag.EnableColorCodes = Manager.GetApplicationSettings().Configuration().EnableColorCodes;
Client ??= new EFClient()
{
ClientId = -1,
Level = EFClient.Permission.User,
CurrentAlias = new EFAlias() { Name = "Webfront Guest" }
};
} }
protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple) protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple)
@ -72,13 +79,6 @@ namespace SharedLibraryCore
public override void OnActionExecuting(ActionExecutingContext context) public override void OnActionExecuting(ActionExecutingContext context)
{ {
Client = Client ?? new EFClient()
{
ClientId = -1,
Level = EFClient.Permission.User,
CurrentAlias = new EFAlias() { Name = "Webfront Guest" }
};
if (!HttpContext.Connection.RemoteIpAddress.GetAddressBytes().SequenceEqual(LocalHost)) if (!HttpContext.Connection.RemoteIpAddress.GetAddressBytes().SequenceEqual(LocalHost))
{ {
try try
@ -108,7 +108,7 @@ namespace SharedLibraryCore
} }
// give the local host full access // give the local host full access
else else if (!HttpContext.Request.Headers.ContainsKey("X-Forwarded-For"))
{ {
Client.ClientId = 1; Client.ClientId = 1;
Client.Level = EFClient.Permission.Console; Client.Level = EFClient.Permission.Console;

View File

@ -5,6 +5,7 @@ using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore namespace SharedLibraryCore
{ {
@ -13,7 +14,7 @@ namespace SharedLibraryCore
/// </summary> /// </summary>
public abstract class Command : IManagerCommand public abstract class Command : IManagerCommand
{ {
private readonly CommandConfiguration _config; protected readonly CommandConfiguration _config;
protected readonly ITranslationLookup _translationLookup; protected readonly ITranslationLookup _translationLookup;
protected ILogger logger; protected ILogger logger;
@ -59,7 +60,7 @@ namespace SharedLibraryCore
/// <summary> /// <summary>
/// Helper property to provide the syntax of the command /// Helper property to provide the syntax of the command
/// </summary> /// </summary>
public string Syntax => $"{_translationLookup["COMMAND_HELP_SYNTAX"]} !{Alias} {string.Join(" ", Arguments.Select(a => $"<{(a.Required ? "" : _translationLookup["COMMAND_HELP_OPTIONAL"] + " ")}{a.Name}>"))}"; public string Syntax => $"{_translationLookup["COMMAND_HELP_SYNTAX"]} {_config.CommandPrefix}{Alias} {string.Join(" ", Arguments.Select(a => $"<{(a.Required ? "" : _translationLookup["COMMAND_HELP_OPTIONAL"] + " ")}{a.Name}>"))}";
/// <summary> /// <summary>
/// Alternate name for this command to be executed by /// Alternate name for this command to be executed by
@ -113,6 +114,25 @@ namespace SharedLibraryCore
} }
private EFClient.Permission permission; private EFClient.Permission permission;
public Game[] SupportedGames
{
get => supportedGames;
protected set
{
try
{
var savedGames = _config?.Commands[GetType().Name].SupportedGames;
supportedGames = savedGames?.Length != 0 ? savedGames : value;
}
catch (KeyNotFoundException)
{
supportedGames = value;
}
}
}
private Game[] supportedGames;
/// <summary> /// <summary>
/// Argument list for the command /// Argument list for the command
@ -123,5 +143,7 @@ namespace SharedLibraryCore
/// indicates if this command allows impersonation (run as) /// indicates if this command allows impersonation (run as)
/// </summary> /// </summary>
public bool AllowImpersonation { get; set; } public bool AllowImpersonation { get; set; }
public bool IsBroadcast { get; set; }
} }
} }

View File

@ -1,4 +1,5 @@
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -10,12 +11,14 @@ namespace SharedLibraryCore.Commands
{ {
public class CommandProcessing public class CommandProcessing
{ {
public static async Task<Command> ValidateCommand(GameEvent E) public static async Task<Command> ValidateCommand(GameEvent E, ApplicationConfiguration appConfig)
{ {
var loc = Utilities.CurrentLocalization.LocalizationIndex; var loc = Utilities.CurrentLocalization.LocalizationIndex;
var Manager = E.Owner.Manager; var Manager = E.Owner.Manager;
bool isBroadcast = E.Data.StartsWith(appConfig.BroadcastCommandPrefix);
int prefixLength = isBroadcast ? appConfig.BroadcastCommandPrefix.Length : appConfig.CommandPrefix.Length;
string CommandString = E.Data.Substring(1, E.Data.Length - 1).Split(' ')[0]; string CommandString = E.Data.Substring(prefixLength, E.Data.Length - prefixLength).Split(' ')[0];
E.Message = E.Data; E.Message = E.Data;
Command C = null; Command C = null;
@ -34,6 +37,8 @@ namespace SharedLibraryCore.Commands
throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\""); throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\"");
} }
C.IsBroadcast = isBroadcast;
if (!C.AllowImpersonation && E.ImpersonationOrigin != null) if (!C.AllowImpersonation && E.ImpersonationOrigin != null)
{ {
E.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]); E.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);

View File

@ -53,7 +53,6 @@ namespace SharedLibraryCore.Commands
public override Task ExecuteAsync(GameEvent E) public override Task ExecuteAsync(GameEvent E)
{ {
MetaService.Clear();
E.Owner.Manager.Restart(); E.Owner.Manager.Restart();
E.Origin.Tell(_translationLookup["COMMANDS_RESTART_SUCCESS"]); E.Origin.Tell(_translationLookup["COMMANDS_RESTART_SUCCESS"]);
return Task.CompletedTask; return Task.CompletedTask;
@ -292,7 +291,7 @@ namespace SharedLibraryCore.Commands
switch ((await E.Target.TempBan(tempbanReason, length, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason) switch ((await E.Target.TempBan(tempbanReason, length, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
{ {
case GameEvent.EventFailReason.None: case GameEvent.EventFailReason.None:
E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.TimeSpanText())); E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.HumanizeForCurrentCulture()));
break; break;
case GameEvent.EventFailReason.Exception: case GameEvent.EventFailReason.Exception:
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
@ -798,7 +797,7 @@ namespace SharedLibraryCore.Commands
{ {
foreach (string line in OnlineAdmins(E.Owner, _translationLookup).Split(Environment.NewLine)) foreach (string line in OnlineAdmins(E.Owner, _translationLookup).Split(Environment.NewLine))
{ {
var _ = E.Message.IsBroadcastCommand() ? E.Owner.Broadcast(line) : E.Origin.Tell(line); var _ = E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix) ? E.Owner.Broadcast(line) : E.Origin.Tell(line);
} }
return Task.CompletedTask; return Task.CompletedTask;
@ -886,14 +885,9 @@ namespace SharedLibraryCore.Commands
return; return;
} }
foreach (var P in db_players) foreach (var client in db_players)
{ {
// they're not going by another alias E.Origin.Tell(_translationLookup["COMMANDS_FIND_FORMAT"].FormatExt(client.Name, client.ClientId, Utilities.ConvertLevelToColor((Permission)client.LevelInt, client.Level), client.IPAddress, client.LastConnectionText));
// /*P.AliasLink.Children.FirstOrDefault(a => a.Name.ToLower().Contains(E.Data.ToLower()))?.Name*/
string msg = P.Name.ToLower().Contains(E.Data.ToLower()) ?
$"[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor((Permission)P.LevelInt, P.Level)}^7] - {P.IPAddress} | last seen {Utilities.GetTimePassed(P.LastConnection)}" :
$"()->[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor((Permission)P.LevelInt, P.Level)}^7] - {P.IPAddress} | last seen {Utilities.GetTimePassed(P.LastConnection)}";
E.Origin.Tell(msg);
} }
} }
} }
@ -917,7 +911,7 @@ namespace SharedLibraryCore.Commands
if (E.Owner.Manager.GetApplicationSettings().Configuration().GlobalRules?.Length < 1 && if (E.Owner.Manager.GetApplicationSettings().Configuration().GlobalRules?.Length < 1 &&
E.Owner.ServerConfig.Rules?.Length < 1) E.Owner.ServerConfig.Rules?.Length < 1)
{ {
var _ = E.Message.IsBroadcastCommand() ? var _ = E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix) ?
E.Owner.Broadcast(_translationLookup["COMMANDS_RULES_NONE"]) : E.Owner.Broadcast(_translationLookup["COMMANDS_RULES_NONE"]) :
E.Origin.Tell(_translationLookup["COMMANDS_RULES_NONE"]); E.Origin.Tell(_translationLookup["COMMANDS_RULES_NONE"]);
} }
@ -933,7 +927,7 @@ namespace SharedLibraryCore.Commands
foreach (string r in rules) foreach (string r in rules)
{ {
var _ = E.Message.IsBroadcastCommand() ? E.Owner.Broadcast($"- {r}") : E.Origin.Tell($"- {r}"); var _ = E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix) ? E.Owner.Broadcast($"- {r}") : E.Origin.Tell($"- {r}");
} }
} }
@ -1251,7 +1245,7 @@ namespace SharedLibraryCore.Commands
else else
{ {
string remainingTime = (penalty.Expires.Value - DateTime.UtcNow).TimeSpanText(); string remainingTime = (penalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture();
E.Origin.Tell(_translationLookup["COMMANDS_BANINFO_TB_SUCCESS"].FormatExt(E.Target.Name, penalty.Offense, remainingTime)); E.Origin.Tell(_translationLookup["COMMANDS_BANINFO_TB_SUCCESS"].FormatExt(E.Target.Name, penalty.Offense, remainingTime));
} }
} }
@ -1535,7 +1529,9 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class SetGravatarCommand : Command public class SetGravatarCommand : Command
{ {
public SetGravatarCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) private readonly IMetaService _metaService;
public SetGravatarCommand(CommandConfiguration config, ITranslationLookup translationLookup, IMetaService metaService) : base(config, translationLookup)
{ {
Name = "setgravatar"; Name = "setgravatar";
Description = _translationLookup["COMMANDS_GRAVATAR_DESC"]; Description = _translationLookup["COMMANDS_GRAVATAR_DESC"];
@ -1550,17 +1546,17 @@ namespace SharedLibraryCore.Commands
Required = true Required = true
} }
}; };
_metaService = metaService;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var metaSvc = new MetaService();
using (var md5 = MD5.Create()) using (var md5 = MD5.Create())
{ {
string gravatarEmail = string.Concat(md5.ComputeHash(E.Data.ToLower().Select(d => Convert.ToByte(d)).ToArray()) string gravatarEmail = string.Concat(md5.ComputeHash(E.Data.ToLower().Select(d => Convert.ToByte(d)).ToArray())
.Select(h => h.ToString("x2"))); .Select(h => h.ToString("x2")));
await metaSvc.AddPersistentMeta("GravatarEmail", gravatarEmail, E.Origin); await _metaService.AddPersistentMeta("GravatarEmail", gravatarEmail, E.Origin);
} }
E.Origin.Tell(_translationLookup["COMMANDS_GRAVATAR_SUCCESS_NEW"]); E.Origin.Tell(_translationLookup["COMMANDS_GRAVATAR_SUCCESS_NEW"]);

View File

@ -0,0 +1,37 @@
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading.Tasks;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Commands
{
public class PrivateMessageAdminsCommand : Command
{
public PrivateMessageAdminsCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup)
{
Name = "privatemessageadmin";
Description = lookup["COMMANDS_PMADMINS_DESC"];
Alias = "pma";
Permission = EFClient.Permission.Moderator;
SupportedGames = new[] { Game.IW4, Game.IW5 };
}
public override Task ExecuteAsync(GameEvent E)
{
bool isGameSupported = _config.Commands[nameof(PrivateMessageAdminsCommand)].SupportedGames.Length > 0 &&
_config.Commands[nameof(PrivateMessageAdminsCommand)].SupportedGames.Contains(E.Owner.GameName);
if (!isGameSupported)
{
E.Origin.Tell(_translationLookup["COMMANDS_GAME_NOT_SUPPORTED"].FormatExt(nameof(PrivateMessageAdminsCommand)));
return Task.CompletedTask;
}
E.Owner.ToAdmins(E.Data);
return Task.CompletedTask;
}
}
}

View File

@ -66,6 +66,12 @@ namespace SharedLibraryCore.Configuration
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
public string CustomLocale { get; set; } public string CustomLocale { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_COMMAND_PREFIX")]
public string CommandPrefix { get; set; } = "!";
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_BROADCAST_COMMAND_PREFIX")]
public string BroadcastCommandPrefix { get; set; } = "@";
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_DB_PROVIDER")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_DB_PROVIDER")]
public string DatabaseProvider { get; set; } = "sqlite"; public string DatabaseProvider { get; set; } = "sqlite";
[ConfigurationOptional] [ConfigurationOptional]

View File

@ -1,6 +1,7 @@
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace SharedLibraryCore.Configuration namespace SharedLibraryCore.Configuration
{ {
@ -14,6 +15,18 @@ namespace SharedLibraryCore.Configuration
/// </summary> /// </summary>
public Dictionary<string, CommandProperties> Commands { get; set; } = new Dictionary<string, CommandProperties>(); public Dictionary<string, CommandProperties> Commands { get; set; } = new Dictionary<string, CommandProperties>();
/// <summary>
/// prefix indicated the chat message is a command
/// </summary>
[JsonIgnore]
public string CommandPrefix { get; set; }
/// <summary>
/// prefix indicating that the chat message is a broadcast command
/// </summary>
[JsonIgnore]
public string BroadcastCommandPrefix { get; set; }
public IBaseConfiguration Generate() public IBaseConfiguration Generate()
{ {
throw new NotImplementedException(); throw new NotImplementedException();

View File

@ -1,6 +1,7 @@
using Newtonsoft.Json.Converters; using Newtonsoft.Json;
using System.Text.Json.Serialization; using Newtonsoft.Json.Converters;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Configuration namespace SharedLibraryCore.Configuration
{ {
@ -29,5 +30,11 @@ namespace SharedLibraryCore.Configuration
/// Indicates if the command can be run by another user (impersonation) /// Indicates if the command can be run by another user (impersonation)
/// </summary> /// </summary>
public bool AllowImpersonation { get; set; } public bool AllowImpersonation { get; set; }
/// <summary>
/// Specifies the games supporting the functionality of the command
/// </summary>
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
public Game[] SupportedGames { get; set; } = new Game[0];
} }
} }

View File

@ -64,13 +64,19 @@ namespace SharedLibraryCore.Configuration.Validation
RuleFor(_app => _app.GlobalRules) RuleFor(_app => _app.GlobalRules)
.NotNull(); .NotNull();
RuleForEach(_app => _app.Servers)
.NotEmpty()
.SetValidator(new ServerConfigurationValidator());
RuleFor(_app => _app.MasterUrl) RuleFor(_app => _app.MasterUrl)
.NotNull() .NotNull()
.Must(_url => _url != null && _url.Scheme == Uri.UriSchemeHttp); .Must(_url => _url != null && _url.Scheme == Uri.UriSchemeHttp);
RuleFor(_app => _app.CommandPrefix)
.NotEmpty();
RuleFor(_app => _app.BroadcastCommandPrefix)
.NotEmpty();
RuleForEach(_app => _app.Servers)
.NotEmpty()
.SetValidator(new ServerConfigurationValidator());
} }
} }
} }

View File

@ -26,7 +26,6 @@ namespace SharedLibraryCore.Database
static string _ConnectionString; static string _ConnectionString;
static string _provider; static string _provider;
private static readonly string _migrationPluginDirectory = @"X:\IW4MAdmin\BUILD\Plugins";
private static readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder => private static readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder =>
{ {
builder.AddConsole() builder.AddConsole()
@ -72,7 +71,7 @@ namespace SharedLibraryCore.Database
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
//optionsBuilder.UseLoggerFactory(_loggerFactory) //optionsBuilder.UseLoggerFactory(_loggerFactory)
// .EnableSensitiveDataLogging(); // .EnableSensitiveDataLogging();
if (string.IsNullOrEmpty(_ConnectionString)) if (string.IsNullOrEmpty(_ConnectionString))
{ {
@ -198,11 +197,14 @@ namespace SharedLibraryCore.Database
// adapted from // adapted from
// https://aleemkhan.wordpress.com/2013/02/28/dynamically-adding-dbset-properties-in-dbcontext-for-entity-framework-code-first/ // https://aleemkhan.wordpress.com/2013/02/28/dynamically-adding-dbset-properties-in-dbcontext-for-entity-framework-code-first/
#if DEBUG
string pluginDir = _migrationPluginDirectory;
#else
string pluginDir = Path.Join(Utilities.OperatingDirectory, "Plugins"); string pluginDir = Path.Join(Utilities.OperatingDirectory, "Plugins");
#endif
if (Utilities.IsDevelopment)
{
pluginDir = Path.Join(Utilities.OperatingDirectory, "..", "..", "..", "..", "BUILD", "Plugins");
}
IEnumerable<string> directoryFiles = Directory.GetFiles(pluginDir).Where(f => f.EndsWith(".dll")); IEnumerable<string> directoryFiles = Directory.GetFiles(pluginDir).Where(f => f.EndsWith(".dll"));
foreach (string dllPath in directoryFiles) foreach (string dllPath in directoryFiles)

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Dtos namespace SharedLibraryCore.Dtos
@ -11,5 +12,7 @@ namespace SharedLibraryCore.Dtos
public string Name { get; set; } public string Name { get; set; }
public Game ServerGame { get; set; } public Game ServerGame { get; set; }
public bool IsQuickMessage { get; set; } public bool IsQuickMessage { get; set; }
public bool IsHidden { get; set; }
public string HiddenMessage => string.Concat(Enumerable.Repeat('●', Message.Length));
} }
} }

View File

@ -1,6 +1,6 @@
namespace SharedLibraryCore.Dtos namespace SharedLibraryCore.Dtos
{ {
public class FindClientRequest : PaginationInfo public class FindClientRequest : PaginationRequest
{ {
/// <summary> /// <summary>
/// name of client /// name of client

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Dtos.Meta.Requests
{
public class BaseClientMetaRequest : PaginationRequest
{
public int ClientId { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Dtos.Meta.Requests
{
public class ReceivedPenaltyRequest : BaseClientMetaRequest
{
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Dtos.Meta.Responses
{
public class AdministeredPenaltyResponse : ReceivedPenaltyResponse
{
}
}

View File

@ -0,0 +1,19 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Dtos.Meta.Responses
{
public class BaseMetaResponse : IClientMeta, IClientMetaResponse
{
public int MetaId { get; set; }
public int ClientId { get; set; }
public MetaType Type { get; set; }
public DateTime When { get; set; }
public bool IsSensitive { get; set; }
public bool ShouldDisplay { get; set; }
public int? Column { get; set; }
public int? Order { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More