Compare commits
68 Commits
2022.03.29
...
2022.06.02
Author | SHA1 | Date | |
---|---|---|---|
a38789adb9 | |||
e459b2fcde | |||
26853a0005 | |||
ee14306db9 | |||
169105e849 | |||
7c10e0e3de | |||
2f7eb07e39 | |||
0f9f4f597b | |||
880f9333d9 | |||
31da5d352e | |||
83a469cae3 | |||
1f13f9122c | |||
dd8c4f438f | |||
2230036d45 | |||
1700b7da91 | |||
fab97ccad4 | |||
0bf0d033f7 | |||
2bbabcb9e8 | |||
1995dbd080 | |||
a3b94b50e3 | |||
bc38b36e4a | |||
e346aa037e | |||
389c687420 | |||
074e36413e | |||
104d9bdc4c | |||
c51d28937b | |||
ffa8a46feb | |||
91c46dbdd4 | |||
ff0d22c142 | |||
3ad4aa2196 | |||
d462892467 | |||
cabedb6f0b | |||
5b7f5160b2 | |||
0a8e415af8 | |||
7b3ddd58c6 | |||
ed1032415e | |||
35b43e7438 | |||
284c2e9726 | |||
fd049edb3f | |||
4884abee76 | |||
1df76b6ac3 | |||
4c42a1d511 | |||
27635a6dd3 | |||
0175425708 | |||
5e12bf60b5 | |||
2f10ca8599 | |||
62ec18309e | |||
87361bf3d7 | |||
dc45136077 | |||
21b0a7998d | |||
20b8f0b99a | |||
314ff96e71 | |||
d7c4f5452c | |||
3cb50635e5 | |||
4fbe0ee0ed | |||
4023ca37d4 | |||
425ec2621d | |||
15c3ca53e2 | |||
6097ca504c | |||
70cd01eafb | |||
a2d5e37c6f | |||
19bd47d0f4 | |||
dc97956bc3 | |||
89fdc00f9b | |||
039a05d9ad | |||
25fb5fdc14 | |||
1e67f6e86c | |||
7dbdf87728 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -224,7 +224,6 @@ bootstrap-custom.min.css
|
||||
bootstrap-custom.css
|
||||
**/Master/static
|
||||
**/Master/dev_env
|
||||
/WebfrontCore/Views/Plugins/*
|
||||
/WebfrontCore/wwwroot/**/dds
|
||||
/WebfrontCore/wwwroot/images/radar/*
|
||||
|
||||
|
@ -24,7 +24,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jint" Version="3.0.0-beta-2037" />
|
||||
<PackageReference Include="Jint" Version="3.0.0-beta-2038" />
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@ -63,6 +64,9 @@
|
||||
<None Update="Configuration\LoggingConfiguration.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\GeoLite2-Country.mmdb">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
|
@ -431,6 +431,7 @@ namespace IW4MAdmin
|
||||
Clients[E.Origin.ClientNumber] = E.Origin;
|
||||
try
|
||||
{
|
||||
E.Origin.GameName = (Reference.Game?)GameName;
|
||||
E.Origin = await OnClientConnected(E.Origin);
|
||||
E.Target = E.Origin;
|
||||
}
|
||||
@ -508,7 +509,8 @@ namespace IW4MAdmin
|
||||
{
|
||||
Origin = E.Origin,
|
||||
Target = E.Target,
|
||||
Reason = E.Data
|
||||
Reason = E.Data,
|
||||
ReportedOn = DateTime.UtcNow
|
||||
});
|
||||
|
||||
var newReport = new EFPenalty()
|
||||
@ -571,10 +573,8 @@ namespace IW4MAdmin
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await _metaService.SetPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin.ClientId,
|
||||
Manager.CancellationToken);
|
||||
await _metaService.SetPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin.ClientId,
|
||||
Manager.CancellationToken);
|
||||
await _metaService.SetPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin.ClientId);
|
||||
await _metaService.SetPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin.ClientId);
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.PreDisconnect)
|
||||
@ -645,7 +645,7 @@ namespace IW4MAdmin
|
||||
}
|
||||
}
|
||||
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
ChatHistory.Add(new ChatInfo
|
||||
{
|
||||
Name = E.Origin.Name,
|
||||
Message = message,
|
||||
@ -792,8 +792,16 @@ namespace IW4MAdmin
|
||||
/// <returns></returns>
|
||||
async Task<List<EFClient>[]> PollPlayersAsync()
|
||||
{
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var currentClients = GetClientsAsList();
|
||||
var statusResponse = await this.GetStatusAsync(Manager.CancellationToken);
|
||||
var statusResponse = await this.GetStatusAsync(tokenSource.Token);
|
||||
|
||||
if (statusResponse is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var polledClients = statusResponse.Clients.AsEnumerable();
|
||||
|
||||
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
|
||||
@ -907,9 +915,8 @@ namespace IW4MAdmin
|
||||
}
|
||||
}
|
||||
|
||||
DateTime start = DateTime.Now;
|
||||
DateTime playerCountStart = DateTime.Now;
|
||||
DateTime lastCount = DateTime.Now;
|
||||
private DateTime _lastMessageSent = DateTime.Now;
|
||||
private DateTime _lastPlayerCount = DateTime.Now;
|
||||
|
||||
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
||||
{
|
||||
@ -923,14 +930,21 @@ namespace IW4MAdmin
|
||||
|
||||
try
|
||||
{
|
||||
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue && Utilities.IsDevelopment)
|
||||
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue &&
|
||||
Utilities.IsDevelopment)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var polledClients = await PollPlayersAsync();
|
||||
|
||||
foreach (var disconnectingClient in polledClients[1].Where(_client => !_client.IsZombieClient /* ignores "fake" zombie clients */))
|
||||
if (polledClients is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var disconnectingClient in polledClients[1]
|
||||
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
|
||||
{
|
||||
disconnectingClient.CurrentServer = this;
|
||||
var e = new GameEvent()
|
||||
@ -946,23 +960,20 @@ namespace IW4MAdmin
|
||||
}
|
||||
|
||||
// this are our new connecting clients
|
||||
foreach (var client in polledClients[0])
|
||||
foreach (var client in polledClients[0].Where(client =>
|
||||
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
|
||||
{
|
||||
// note: this prevents players in ZMBI state from being registered with no name
|
||||
if (string.IsNullOrEmpty(client.Name) || (client.Ping == 999 && !client.IsBot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
client.CurrentServer = this;
|
||||
var e = new GameEvent()
|
||||
client.GameName = (Reference.Game?)GameName;
|
||||
|
||||
var e = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.PreConnect,
|
||||
Origin = client,
|
||||
Owner = this,
|
||||
IsBlocking = true,
|
||||
Extra = client.GetAdditionalProperty<string>("BotGuid"),
|
||||
Source = GameEvent.EventSource.Status
|
||||
Source = GameEvent.EventSource.Status,
|
||||
};
|
||||
|
||||
Manager.AddEvent(e);
|
||||
@ -973,19 +984,19 @@ namespace IW4MAdmin
|
||||
foreach (var client in polledClients[2])
|
||||
{
|
||||
client.CurrentServer = this;
|
||||
var e = new GameEvent()
|
||||
var gameEvent = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.Update,
|
||||
Origin = client,
|
||||
Owner = this
|
||||
};
|
||||
|
||||
Manager.AddEvent(e);
|
||||
Manager.AddEvent(gameEvent);
|
||||
}
|
||||
|
||||
if (Throttled)
|
||||
{
|
||||
var _event = new GameEvent()
|
||||
var gameEvent = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.ConnectionRestored,
|
||||
Owner = this,
|
||||
@ -993,72 +1004,52 @@ namespace IW4MAdmin
|
||||
Target = Utilities.IW4MAdminClient(this)
|
||||
};
|
||||
|
||||
Manager.AddEvent(_event);
|
||||
Manager.AddEvent(gameEvent);
|
||||
}
|
||||
|
||||
LastPoll = DateTime.Now;
|
||||
}
|
||||
|
||||
catch (NetworkException e)
|
||||
catch (NetworkException ex)
|
||||
{
|
||||
if (!Throttled)
|
||||
if (Throttled)
|
||||
{
|
||||
var _event = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.ConnectionLost,
|
||||
Owner = this,
|
||||
Origin = Utilities.IW4MAdminClient(this),
|
||||
Target = Utilities.IW4MAdminClient(this),
|
||||
Extra = e,
|
||||
Data = ConnectionErrors.ToString()
|
||||
};
|
||||
|
||||
Manager.AddEvent(_event);
|
||||
return true;
|
||||
}
|
||||
|
||||
var gameEvent = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.ConnectionLost,
|
||||
Owner = this,
|
||||
Origin = Utilities.IW4MAdminClient(this),
|
||||
Target = Utilities.IW4MAdminClient(this),
|
||||
Extra = ex,
|
||||
Data = ConnectionErrors.ToString()
|
||||
};
|
||||
|
||||
Manager.AddEvent(gameEvent);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RunServerCollection();
|
||||
}
|
||||
|
||||
if (DateTime.Now - _lastMessageSent <=
|
||||
TimeSpan.FromSeconds(Manager.GetApplicationSettings().Configuration().AutoMessagePeriod) ||
|
||||
BroadcastMessages.Count <= 0 || ClientNum <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
LastMessage = DateTime.Now - start;
|
||||
lastCount = DateTime.Now;
|
||||
|
||||
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
|
||||
// update the player history
|
||||
if (lastCount - playerCountStart >= appConfig.ServerDataCollectionInterval)
|
||||
{
|
||||
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
|
||||
appConfig.ServerDataCollectionInterval.TotalMinutes);
|
||||
|
||||
while (ClientHistory.ClientCounts.Count > maxItems)
|
||||
{
|
||||
ClientHistory.ClientCounts.RemoveAt(0);
|
||||
}
|
||||
|
||||
ClientHistory.ClientCounts.Add(new ClientCountSnapshot
|
||||
{
|
||||
ClientCount = ClientNum,
|
||||
ConnectionInterrupted = Throttled,
|
||||
Time = DateTime.UtcNow,
|
||||
Map = CurrentMap.Name
|
||||
});
|
||||
playerCountStart = DateTime.Now;
|
||||
}
|
||||
|
||||
// send out broadcast messages
|
||||
if (LastMessage.TotalSeconds > Manager.GetApplicationSettings().Configuration().AutoMessagePeriod
|
||||
&& BroadcastMessages.Count > 0
|
||||
&& ClientNum > 0)
|
||||
{
|
||||
string[] messages = (await this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage])).Split(Environment.NewLine);
|
||||
var messages =
|
||||
(await this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage])).Split(
|
||||
Environment.NewLine);
|
||||
await BroadcastAsync(messages, token: Manager.CancellationToken);
|
||||
|
||||
foreach (string message in messages)
|
||||
{
|
||||
Broadcast(message);
|
||||
}
|
||||
|
||||
NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1;
|
||||
start = DateTime.Now;
|
||||
}
|
||||
NextMessage = NextMessage == BroadcastMessages.Count - 1 ? 0 : NextMessage + 1;
|
||||
_lastMessageSent = DateTime.Now;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -1070,26 +1061,56 @@ namespace IW4MAdmin
|
||||
}
|
||||
|
||||
// this one is ok
|
||||
catch (Exception e) when(e is ServerException || e is RConException)
|
||||
catch (Exception e) when (e is ServerException || e is RConException)
|
||||
{
|
||||
using(LogContext.PushProperty("Server", ToString()))
|
||||
using (LogContext.PushProperty("Server", ToString()))
|
||||
{
|
||||
ServerLogger.LogWarning(e, "Undesirable exception occured during processing updates");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
using(LogContext.PushProperty("Server", ToString()))
|
||||
using (LogContext.PushProperty("Server", ToString()))
|
||||
{
|
||||
ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
|
||||
}
|
||||
|
||||
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RunServerCollection()
|
||||
{
|
||||
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
|
||||
|
||||
if (DateTime.Now - _lastPlayerCount < appConfig?.ServerDataCollectionInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
|
||||
appConfig.ServerDataCollectionInterval.TotalMinutes);
|
||||
|
||||
while (ClientHistory.ClientCounts.Count > maxItems)
|
||||
{
|
||||
ClientHistory.ClientCounts.RemoveAt(0);
|
||||
}
|
||||
|
||||
ClientHistory.ClientCounts.Add(new ClientCountSnapshot
|
||||
{
|
||||
ClientCount = ClientNum,
|
||||
ConnectionInterrupted = Throttled,
|
||||
Time = DateTime.UtcNow,
|
||||
Map = CurrentMap.Name
|
||||
});
|
||||
|
||||
_lastPlayerCount = DateTime.Now;
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
try
|
||||
|
@ -447,6 +447,7 @@ namespace IW4MAdmin.Application
|
||||
.AddSingleton<IServerDataViewer, ServerDataViewer>()
|
||||
.AddSingleton<IServerDataCollector, ServerDataCollector>()
|
||||
.AddSingleton<IEventPublisher, EventPublisher>()
|
||||
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
|
||||
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
|
||||
.AddSingleton(translationLookup)
|
||||
.AddDatabaseContextOptions(appConfig);
|
||||
|
@ -83,7 +83,6 @@ namespace IW4MAdmin.Application.Meta
|
||||
Value = lastMapMeta.Value,
|
||||
ShouldDisplay = true,
|
||||
Type = MetaType.Information,
|
||||
Column = 1,
|
||||
Order = 6
|
||||
});
|
||||
}
|
||||
@ -101,8 +100,7 @@ namespace IW4MAdmin.Application.Meta
|
||||
Value = lastServerMeta.Value,
|
||||
ShouldDisplay = true,
|
||||
Type = MetaType.Information,
|
||||
Column = 0,
|
||||
Order = 6
|
||||
Order = 7
|
||||
});
|
||||
}
|
||||
|
||||
@ -120,8 +118,7 @@ namespace IW4MAdmin.Application.Meta
|
||||
Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"],
|
||||
Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(),
|
||||
ShouldDisplay = true,
|
||||
Column = 1,
|
||||
Order = 0,
|
||||
Order = 8,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
@ -131,8 +128,7 @@ namespace IW4MAdmin.Application.Meta
|
||||
Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"],
|
||||
Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(),
|
||||
ShouldDisplay = true,
|
||||
Column = 1,
|
||||
Order = 1,
|
||||
Order = 9,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
@ -142,8 +138,7 @@ namespace IW4MAdmin.Application.Meta
|
||||
Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"],
|
||||
Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
|
||||
ShouldDisplay = true,
|
||||
Column = 1,
|
||||
Order = 2,
|
||||
Order = 10,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
@ -154,8 +149,7 @@ namespace IW4MAdmin.Application.Meta
|
||||
Value = client.Connections.ToString("#,##0",
|
||||
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
ShouldDisplay = true,
|
||||
Column = 1,
|
||||
Order = 3,
|
||||
Order = 11,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
@ -167,8 +161,7 @@ namespace IW4MAdmin.Application.Meta
|
||||
? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"]
|
||||
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
|
||||
IsSensitive = true,
|
||||
Column = 1,
|
||||
Order = 4,
|
||||
Order = 12,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
|
12
Application/Misc/AsyncResult.cs
Normal file
12
Application/Misc/AsyncResult.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc;
|
||||
|
||||
public class AsyncResult : IAsyncResult
|
||||
{
|
||||
public object AsyncState { get; set; }
|
||||
public WaitHandle AsyncWaitHandle { get; set; }
|
||||
public bool CompletedSynchronously { get; set; }
|
||||
public bool IsCompleted { get; set; }
|
||||
}
|
13
Application/Misc/GeoLocationResult.cs
Normal file
13
Application/Misc/GeoLocationResult.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc;
|
||||
|
||||
public class GeoLocationResult : IGeoLocationResult
|
||||
{
|
||||
public string Country { get; set; }
|
||||
public string CountryCode { get; set; }
|
||||
public string Region { get; set; }
|
||||
public string ASN { get; set; }
|
||||
public string Timezone { get; set; }
|
||||
public string Organization { get; set; }
|
||||
}
|
40
Application/Misc/GeoLocationService.cs
Normal file
40
Application/Misc/GeoLocationService.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using MaxMind.GeoIP2;
|
||||
using MaxMind.GeoIP2.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc;
|
||||
|
||||
public class GeoLocationService : IGeoLocationService
|
||||
{
|
||||
private readonly string _sourceAddress;
|
||||
|
||||
public GeoLocationService(string sourceAddress)
|
||||
{
|
||||
_sourceAddress = sourceAddress;
|
||||
}
|
||||
|
||||
public Task<IGeoLocationResult> Locate(string address)
|
||||
{
|
||||
CountryResponse country = null;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new DatabaseReader(_sourceAddress);
|
||||
reader.TryCountry(address, out country);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
var response = new GeoLocationResult
|
||||
{
|
||||
Country = country?.Country.Name ?? "Unknown",
|
||||
CountryCode = country?.Country.IsoCode ?? ""
|
||||
};
|
||||
|
||||
return Task.FromResult((IGeoLocationResult)response);
|
||||
}
|
||||
}
|
@ -207,7 +207,7 @@ public class MetaServiceV2 : IMetaServiceV2
|
||||
|
||||
if (metaValue is null)
|
||||
{
|
||||
_logger.LogWarning("No meta exists for key {Key}, clientId {ClientId}", metaKey, clientId);
|
||||
_logger.LogDebug("No meta exists for key {Key}, clientId {ClientId}", metaKey, clientId);
|
||||
return default;
|
||||
}
|
||||
|
||||
@ -446,54 +446,7 @@ public class MetaServiceV2 : IMetaServiceV2
|
||||
|
||||
private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta
|
||||
{
|
||||
var metaList = meta.ToList();
|
||||
var metaWithColumn = metaList
|
||||
.Where(m => m.Column != null)
|
||||
.ToList();
|
||||
|
||||
var columnGrouping = metaWithColumn
|
||||
.GroupBy(m => m.Column)
|
||||
.ToList();
|
||||
|
||||
var metaToSort = metaList.Except(metaWithColumn).ToList();
|
||||
|
||||
var table = columnGrouping.Select(metaItem => new List<T>(metaItem)).ToList();
|
||||
|
||||
while (metaToSort.Count > 0)
|
||||
{
|
||||
var sortingMeta = metaToSort.First();
|
||||
|
||||
int IndexOfSmallestColumn()
|
||||
{
|
||||
var index = 0;
|
||||
var smallestColumnSize = int.MaxValue;
|
||||
for (var i = 0; i < table.Count; i++)
|
||||
{
|
||||
if (table[i].Count >= smallestColumnSize)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
smallestColumnSize = table[i].Count;
|
||||
index = i;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
var columnIndex = IndexOfSmallestColumn();
|
||||
|
||||
sortingMeta.Column = columnIndex;
|
||||
sortingMeta.Order = columnGrouping
|
||||
.First(group => group.Key == columnIndex)
|
||||
.Count();
|
||||
|
||||
table[columnIndex].Add(sortingMeta);
|
||||
|
||||
metaToSort.Remove(sortingMeta);
|
||||
}
|
||||
|
||||
return metaList;
|
||||
return meta;
|
||||
}
|
||||
|
||||
private static bool ValidArgs(string key, int clientId) => !string.IsNullOrWhiteSpace(key) && clientId > 0;
|
||||
|
@ -276,8 +276,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
_logger.LogDebug("OnLoad executing for {Name}", Name);
|
||||
_scriptEngine.SetValue("_manager", manager);
|
||||
_scriptEngine.SetValue("getDvar", GetDvarAsync);
|
||||
_scriptEngine.SetValue("setDvar", SetDvarAsync);
|
||||
_scriptEngine.SetValue("getDvar", BeginGetDvar);
|
||||
_scriptEngine.SetValue("setDvar", BeginSetDvar);
|
||||
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
|
||||
|
||||
return Task.CompletedTask;
|
||||
@ -343,7 +343,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
/// <param name="commands">commands value from jint parser</param>
|
||||
/// <param name="scriptCommandFactory">factory to create the command from</param>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
|
||||
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands,
|
||||
IScriptCommandFactory scriptCommandFactory)
|
||||
{
|
||||
var commandList = new List<IManagerCommand>();
|
||||
|
||||
@ -410,7 +411,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
_scriptEngine.SetValue("_event", gameEvent);
|
||||
var jsEventObject = _scriptEngine.Evaluate("_event");
|
||||
|
||||
|
||||
dynamicCommand.execute.Target.Invoke(_scriptEngine, jsEventObject);
|
||||
}
|
||||
|
||||
@ -424,7 +425,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
throw new PluginException("A runtime error occured while executing action for script plugin");
|
||||
}
|
||||
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
using (LogContext.PushProperty("Server", gameEvent.Owner?.ToString()))
|
||||
@ -454,83 +455,71 @@ namespace IW4MAdmin.Application.Misc
|
||||
return commandList;
|
||||
}
|
||||
|
||||
private void GetDvarAsync(Server server, string dvarName, Delegate onCompleted)
|
||||
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
|
||||
{
|
||||
Task.Run<Task>(async () =>
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
|
||||
|
||||
server.BeginGetDvar(dvarName, result =>
|
||||
{
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
string result = null;
|
||||
var success = true;
|
||||
var shouldRelease = false;
|
||||
try
|
||||
{
|
||||
result = (await server.GetDvarAsync<string>(dvarName, token: tokenSource.Token)).Value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
success = false;
|
||||
}
|
||||
_onProcessing.Wait(tokenSource.Token);
|
||||
shouldRelease = true;
|
||||
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
|
||||
|
||||
await _onProcessing.WaitAsync();
|
||||
try
|
||||
{
|
||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
||||
new[]
|
||||
{
|
||||
JsValue.FromObject(_scriptEngine, server),
|
||||
JsValue.FromObject(_scriptEngine, dvarName),
|
||||
JsValue.FromObject(_scriptEngine, result),
|
||||
JsValue.FromObject(_scriptEngine, success),
|
||||
});
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
if (_onProcessing.CurrentCount == 0)
|
||||
{
|
||||
_onProcessing.Release();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
private void SetDvarAsync(Server server, string dvarName, string dvarValue, Delegate onCompleted)
|
||||
{
|
||||
Task.Run<Task>(async () =>
|
||||
{
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var success = true;
|
||||
|
||||
try
|
||||
{
|
||||
await server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token);
|
||||
}
|
||||
catch
|
||||
{
|
||||
success = false;
|
||||
}
|
||||
|
||||
await _onProcessing.WaitAsync();
|
||||
try
|
||||
{
|
||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
||||
new[]
|
||||
{
|
||||
JsValue.FromObject(_scriptEngine, server),
|
||||
JsValue.FromObject(_scriptEngine, dvarName),
|
||||
JsValue.FromObject(_scriptEngine, dvarValue),
|
||||
JsValue.FromObject(_scriptEngine, dvarName),
|
||||
JsValue.FromObject(_scriptEngine, value),
|
||||
JsValue.FromObject(_scriptEngine, success)
|
||||
});
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
if (_onProcessing.CurrentCount == 0)
|
||||
if (_onProcessing.CurrentCount == 0 && shouldRelease)
|
||||
{
|
||||
_onProcessing.Release();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, tokenSource.Token);
|
||||
}
|
||||
|
||||
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
|
||||
{
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
|
||||
|
||||
server.BeginSetDvar(dvarName, dvarValue, result =>
|
||||
{
|
||||
var shouldRelease = false;
|
||||
try
|
||||
{
|
||||
_onProcessing.Wait(tokenSource.Token);
|
||||
shouldRelease = true;
|
||||
var success = (bool)result.AsyncState;
|
||||
|
||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
||||
new[]
|
||||
{
|
||||
JsValue.FromObject(_scriptEngine, server),
|
||||
JsValue.FromObject(_scriptEngine, dvarName),
|
||||
JsValue.FromObject(_scriptEngine, dvarValue),
|
||||
JsValue.FromObject(_scriptEngine, success)
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onProcessing.CurrentCount == 0 && shouldRelease)
|
||||
{
|
||||
_onProcessing.Release();
|
||||
}
|
||||
}
|
||||
}, tokenSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static SharedLibraryCore.Server;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
@ -80,6 +81,7 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
|
||||
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default)
|
||||
{
|
||||
command = command.FormatMessageForEngine(Configuration?.ColorCodeMapping);
|
||||
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
|
||||
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
|
||||
}
|
||||
@ -140,6 +142,30 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
};
|
||||
}
|
||||
|
||||
public void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default)
|
||||
{
|
||||
GetDvarAsync<string>(connection, dvarName, token: token).ContinueWith(action =>
|
||||
{
|
||||
if (action.Exception is null)
|
||||
{
|
||||
callback?.Invoke(new AsyncResult
|
||||
{
|
||||
IsCompleted = true,
|
||||
AsyncState = (true, action.Result.Value)
|
||||
});
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
callback?.Invoke(new AsyncResult
|
||||
{
|
||||
IsCompleted = true,
|
||||
AsyncState = (false, (string)null)
|
||||
});
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
|
||||
{
|
||||
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
|
||||
@ -195,6 +221,31 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0;
|
||||
}
|
||||
|
||||
public void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
SetDvarAsync(connection, dvarName, dvarValue, token).ContinueWith(action =>
|
||||
{
|
||||
if (action.Exception is null)
|
||||
{
|
||||
callback?.Invoke(new AsyncResult
|
||||
{
|
||||
IsCompleted = true,
|
||||
AsyncState = true
|
||||
});
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
callback?.Invoke(new AsyncResult
|
||||
{
|
||||
IsCompleted = true,
|
||||
AsyncState = false
|
||||
});
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
private List<EFClient> ClientsFromStatus(string[] Status)
|
||||
{
|
||||
List<EFClient> StatusPlayers = new List<EFClient>();
|
||||
|
BIN
Application/Resources/GeoLite2-Country.mmdb
Normal file
BIN
Application/Resources/GeoLite2-Country.mmdb
Normal file
Binary file not shown.
@ -120,6 +120,9 @@ namespace Data.Context
|
||||
ent.Property(_alias => _alias.SearchableName).HasMaxLength(24);
|
||||
ent.HasIndex(_alias => _alias.SearchableName);
|
||||
ent.HasIndex(_alias => new {_alias.Name, _alias.IPAddress});
|
||||
ent.Property(alias => alias.SearchableIPAddress)
|
||||
.HasComputedColumnSql(@"((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", stored: true);
|
||||
ent.HasIndex(alias => alias.SearchableIPAddress);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EFMeta>(ent =>
|
||||
|
1631
Data/Migrations/MySql/20220404151444_AddSearchableIPToEFAlias.Designer.cs
generated
Normal file
1631
Data/Migrations/MySql/20220404151444_AddSearchableIPToEFAlias.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.MySql
|
||||
{
|
||||
public partial class AddSearchableIPToEFAlias : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SearchableIPAddress",
|
||||
table: "EFAlias",
|
||||
type: "longtext",
|
||||
nullable: true,
|
||||
computedColumnSql: "CONCAT((IPAddress & 255), \".\", ((IPAddress >> 8) & 255), \".\", ((IPAddress >> 16) & 255), \".\", ((IPAddress >> 24) & 255))",
|
||||
stored: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SearchableIPAddress",
|
||||
table: "EFAlias");
|
||||
}
|
||||
}
|
||||
}
|
1633
Data/Migrations/MySql/20220404192417_AddIndexToSearchableIPToEFAlias.Designer.cs
generated
Normal file
1633
Data/Migrations/MySql/20220404192417_AddIndexToSearchableIPToEFAlias.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.MySql
|
||||
{
|
||||
public partial class AddIndexToSearchableIPToEFAlias : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFAlias_SearchableIPAddress",
|
||||
table: "EFAlias",
|
||||
column: "SearchableIPAddress");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFAlias_SearchableIPAddress",
|
||||
table: "EFAlias");
|
||||
}
|
||||
}
|
||||
}
|
1636
Data/Migrations/MySql/20220422202702_AddGameToEFClient.Designer.cs
generated
Normal file
1636
Data/Migrations/MySql/20220422202702_AddGameToEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Data/Migrations/MySql/20220422202702_AddGameToEFClient.cs
Normal file
25
Data/Migrations/MySql/20220422202702_AddGameToEFClient.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.MySql
|
||||
{
|
||||
public partial class AddGameToEFClient : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GameName",
|
||||
table: "EFClients");
|
||||
}
|
||||
}
|
||||
}
|
@ -64,6 +64,9 @@ namespace Data.Migrations.MySql
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@ -804,6 +807,11 @@ namespace Data.Migrations.MySql
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("varchar(24)");
|
||||
|
||||
b.Property<string>("SearchableIPAddress")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("varchar(255)")
|
||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||
|
||||
b.Property<string>("SearchableName")
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("varchar(24)");
|
||||
@ -816,6 +824,8 @@ namespace Data.Migrations.MySql
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("SearchableIPAddress");
|
||||
|
||||
b.HasIndex("SearchableName");
|
||||
|
||||
b.HasIndex("Name", "IPAddress");
|
||||
|
1688
Data/Migrations/Postgresql/20220404185627_AddSearchableIPToEFAlias.Designer.cs
generated
Normal file
1688
Data/Migrations/Postgresql/20220404185627_AddSearchableIPToEFAlias.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Postgresql
|
||||
{
|
||||
public partial class AddSearchableIPToEFAlias : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SearchableIPAddress",
|
||||
table: "EFAlias",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
computedColumnSql: "CASE WHEN \"IPAddress\" IS NULL THEN 'NULL'::text ELSE (\"IPAddress\" & 255)::text || '.'::text || ((\"IPAddress\" >> 8) & 255)::text || '.'::text || ((\"IPAddress\" >> 16) & 255)::text || '.'::text || ((\"IPAddress\" >> 24) & 255)::text END",
|
||||
stored: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SearchableIPAddress",
|
||||
table: "EFAlias");
|
||||
}
|
||||
}
|
||||
}
|
1690
Data/Migrations/Postgresql/20220404192553_AddIndexToSearchableIPToEFAlias.Designer.cs
generated
Normal file
1690
Data/Migrations/Postgresql/20220404192553_AddIndexToSearchableIPToEFAlias.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Postgresql
|
||||
{
|
||||
public partial class AddIndexToSearchableIPToEFAlias : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFAlias_SearchableIPAddress",
|
||||
table: "EFAlias",
|
||||
column: "SearchableIPAddress");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFAlias_SearchableIPAddress",
|
||||
table: "EFAlias");
|
||||
}
|
||||
}
|
||||
}
|
1693
Data/Migrations/Postgresql/20220422203121_AddGameToEFClient.Designer.cs
generated
Normal file
1693
Data/Migrations/Postgresql/20220422203121_AddGameToEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Postgresql
|
||||
{
|
||||
public partial class AddGameToEFClient : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GameName",
|
||||
table: "EFClients");
|
||||
}
|
||||
}
|
||||
}
|
@ -71,6 +71,9 @@ namespace Data.Migrations.Postgresql
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
@ -843,6 +846,11 @@ namespace Data.Migrations.Postgresql
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("character varying(24)");
|
||||
|
||||
b.Property<string>("SearchableIPAddress")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("text")
|
||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||
|
||||
b.Property<string>("SearchableName")
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("character varying(24)");
|
||||
@ -855,6 +863,8 @@ namespace Data.Migrations.Postgresql
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("SearchableIPAddress");
|
||||
|
||||
b.HasIndex("SearchableName");
|
||||
|
||||
b.HasIndex("Name", "IPAddress");
|
||||
|
1629
Data/Migrations/Sqlite/20220402211115_AddSearchableIPToEFAlias.Designer.cs
generated
Normal file
1629
Data/Migrations/Sqlite/20220402211115_AddSearchableIPToEFAlias.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class AddSearchableIPToEFAlias : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SearchableIPAddress",
|
||||
table: "EFAlias",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
computedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SearchableIPAddress",
|
||||
table: "EFAlias");
|
||||
}
|
||||
}
|
||||
}
|
1631
Data/Migrations/Sqlite/20220404192319_AddIndexToSearchableIPToEFAlias.Designer.cs
generated
Normal file
1631
Data/Migrations/Sqlite/20220404192319_AddIndexToSearchableIPToEFAlias.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class AddIndexToSearchableIPToEFAlias : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFAlias_SearchableIPAddress",
|
||||
table: "EFAlias",
|
||||
column: "SearchableIPAddress");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFAlias_SearchableIPAddress",
|
||||
table: "EFAlias");
|
||||
}
|
||||
}
|
||||
}
|
1634
Data/Migrations/Sqlite/20220422202315_AddGameToEFClient.Designer.cs
generated
Normal file
1634
Data/Migrations/Sqlite/20220422202315_AddGameToEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Data/Migrations/Sqlite/20220422202315_AddGameToEFClient.cs
Normal file
25
Data/Migrations/Sqlite/20220422202315_AddGameToEFClient.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class AddGameToEFClient : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GameName",
|
||||
table: "EFClients");
|
||||
}
|
||||
}
|
||||
}
|
@ -62,6 +62,9 @@ namespace Data.Migrations.Sqlite
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -802,6 +805,11 @@ namespace Data.Migrations.Sqlite
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SearchableIPAddress")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||
|
||||
b.Property<string>("SearchableName")
|
||||
.HasMaxLength(24)
|
||||
.HasColumnType("TEXT");
|
||||
@ -814,6 +822,8 @@ namespace Data.Migrations.Sqlite
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("SearchableIPAddress");
|
||||
|
||||
b.HasIndex("SearchableName");
|
||||
|
||||
b.HasIndex("Name", "IPAddress");
|
||||
|
@ -63,6 +63,7 @@ namespace Data.Models.Client
|
||||
public DateTime FirstConnection { get; set; }
|
||||
[Required]
|
||||
public DateTime LastConnection { get; set; }
|
||||
public Reference.Game? GameName { get; set; } = Reference.Game.UKN;
|
||||
public bool Masked { get; set; }
|
||||
[Required]
|
||||
public int AliasLinkId { get; set; }
|
||||
|
@ -19,6 +19,7 @@ namespace Data.Models
|
||||
public string SearchableName { get; set; }
|
||||
[Required]
|
||||
public int? IPAddress { get; set; }
|
||||
public string SearchableIPAddress { get; set; }
|
||||
[Required]
|
||||
public DateTime DateAdded { get; set; }
|
||||
|
||||
|
@ -53,7 +53,9 @@ steps:
|
||||
script: |
|
||||
Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)'
|
||||
md -Force lib\open-iconic\font\css
|
||||
wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap.scss
|
||||
wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap-override.scss
|
||||
cd lib\open-iconic\font\css
|
||||
(Get-Content open-iconic-bootstrap-override.scss).replace('../fonts/', '/font/') | Set-Content open-iconic-bootstrap-override.scss
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot'
|
||||
|
||||
@ -75,6 +77,7 @@ steps:
|
||||
Write-Host 'Unzipping download'
|
||||
Expand-Archive -LiteralPath $(Build.Repository.LocalPath)\dotnet-bundle.zip -DestinationPath $(Build.Repository.LocalPath)
|
||||
Write-Host 'Executing dotnet-bundle'
|
||||
$(Build.Repository.LocalPath)\dotnet-bundle.exe clean $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json
|
||||
$(Build.Repository.LocalPath)\dotnet-bundle.exe $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore'
|
||||
|
16
GameFiles/README.MD
Normal file
16
GameFiles/README.MD
Normal file
@ -0,0 +1,16 @@
|
||||
# Game Interface
|
||||
|
||||
Allows integration of IW4M-Admin to GSC, mainly used for special commands that need to use GSC in order to work.
|
||||
But can also be used to read / write metadata from / to a profile and to get the player permission level.
|
||||
|
||||
|
||||
## Installation Plutonium IW5
|
||||
|
||||
|
||||
Move `_integration.gsc` to `%localappdata%\Plutonium\storage\iw5\scripts\`
|
||||
|
||||
|
||||
## Installation IW4x
|
||||
|
||||
|
||||
Move `_integration.gsc` to `IW4x/userraw/scripts`, `IW4x` being the root folder of your game server.
|
@ -1,7 +1,6 @@
|
||||
#include common_scripts\utility;
|
||||
#include maps\mp\_utility;
|
||||
#include maps\mp\gametypes\_hud_util;
|
||||
#include maps\mp\gametypes\_playerlogic;
|
||||
|
||||
init()
|
||||
{
|
||||
@ -12,6 +11,7 @@ init()
|
||||
level.eventBus.failKey = "fail";
|
||||
level.eventBus.timeoutKey = "timeout";
|
||||
level.eventBus.timeout = 30;
|
||||
level.eventBus.gamename = getDvar( "gamename" ); // We want to do a few small detail different on IW5 compared to IW4, nothing where 2 files would make sense.
|
||||
|
||||
level.clientDataKey = "clientData";
|
||||
|
||||
@ -22,6 +22,8 @@ init()
|
||||
level.eventTypes.setClientDataRequested = "SetClientDataRequested";
|
||||
level.eventTypes.setClientDataCompleted = "SetClientDataCompleted";
|
||||
level.eventTypes.executeCommandRequested = "ExecuteCommandRequested";
|
||||
|
||||
level.iw4adminIntegrationDebug = false;
|
||||
|
||||
SetDvarIfUninitialized( level.eventBus.inVar, "" );
|
||||
SetDvarIfUninitialized( level.eventBus.outVar, "" );
|
||||
@ -33,18 +35,26 @@ init()
|
||||
level.eventCallbacks[level.eventTypes.clientDataReceived] = ::OnClientDataReceived;
|
||||
level.eventCallbacks[level.eventTypes.executeCommandRequested] = ::OnExecuteCommand;
|
||||
level.eventCallbacks[level.eventTypes.setClientDataCompleted] = ::OnSetClientDataCompleted;
|
||||
|
||||
level.clientCommandCallbacks = [];
|
||||
level.clientCommandRusAsTarget = [];
|
||||
|
||||
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeGameMethods();
|
||||
RegisterClientCommands();
|
||||
|
||||
// start long running tasks
|
||||
level thread MonitorClientEvents();
|
||||
level thread MonitorBus();
|
||||
level thread OnPlayerConnect();
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////
|
||||
// Client Methods
|
||||
//////////////////////////////////
|
||||
@ -59,6 +69,12 @@ OnPlayerConnect()
|
||||
|
||||
level.iw4adminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
|
||||
|
||||
if ( isDefined(player.pers["isBot"]) && player.pers["isBot"] )
|
||||
{
|
||||
// we don't want to track bots
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !isDefined( player.pers[level.clientDataKey] ) )
|
||||
{
|
||||
player.pers[level.clientDataKey] = spawnstruct();
|
||||
@ -101,26 +117,26 @@ OnPlayerDisconnect()
|
||||
|
||||
OnPlayerJoinedTeam()
|
||||
{
|
||||
self endon( "disconnect" );
|
||||
self endon( "disconnect" );
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
self waittill( "joined_team" );
|
||||
for( ;; )
|
||||
{
|
||||
self waittill( "joined_team" );
|
||||
// join spec and join team occur at the same moment - out of order logging would be problematic
|
||||
wait( 0.25 );
|
||||
LogPrint( GenerateJoinTeamString( false ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnPlayerJoinedSpectators()
|
||||
{
|
||||
self endon( "disconnect" );
|
||||
self endon( "disconnect" );
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
for( ;; )
|
||||
{
|
||||
self waittill( "joined_spectators" );
|
||||
LogPrint( GenerateJoinTeamString( true ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnGameEnded()
|
||||
@ -201,7 +217,7 @@ MonitorClientEvents()
|
||||
|
||||
if ( level.iw4adminIntegrationDebug == 1 )
|
||||
{
|
||||
self IPrintLn( "Processing Event " + client.event.type + "-" + client.event.subtype );
|
||||
IPrintLn( "Processing Event " + client.event.type + "-" + client.event.subtype );
|
||||
}
|
||||
|
||||
eventHandler = level.eventCallbacks[client.event.type];
|
||||
@ -219,6 +235,53 @@ MonitorClientEvents()
|
||||
// Helper Methods
|
||||
//////////////////////////////////
|
||||
|
||||
RegisterClientCommands()
|
||||
{
|
||||
AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
|
||||
AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
|
||||
AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
|
||||
AddClientCommand( "Hide", false, ::HideImpl );
|
||||
AddClientCommand( "Unhide", false, ::UnhideImpl );
|
||||
AddClientCommand( "Alert", true, ::AlertImpl );
|
||||
AddClientCommand( "Goto", false, ::GotoImpl );
|
||||
AddClientCommand( "Kill", true, ::KillImpl );
|
||||
AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
|
||||
AddClientCommand( "NightMode", false, ::NightModeImpl ); //This really should be a level command
|
||||
AddClientCommand( "LockControls", true, ::LockControlsImpl );
|
||||
AddClientCommand( "UnlockControls", true, ::UnlockControlsImpl );
|
||||
AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
|
||||
AddClientCommand( "NoClip", false, ::NoClipImpl );
|
||||
AddClientCommand( "NoClipOff", false, ::NoClipOffImpl );
|
||||
}
|
||||
|
||||
InitializeGameMethods()
|
||||
{
|
||||
level.overrideMethods = [];
|
||||
level.overrideMethods["god"] = ::_god;
|
||||
level.overrideMethods["noclip"] = ::UnsupportedFunc;
|
||||
|
||||
if ( isDefined( ::God ) )
|
||||
{
|
||||
level.overrideMethods["god"] = ::God;
|
||||
}
|
||||
|
||||
if ( isDefined( ::NoClip ) )
|
||||
{
|
||||
level.overrideMethods["noclip"] = ::NoClip;
|
||||
}
|
||||
|
||||
if ( level.eventBus.gamename == "IW5" )
|
||||
{ //PlutoIW5 only allows Godmode and NoClip if cheats are on..
|
||||
level.overrideMethods["god"] = ::IW5_God;
|
||||
level.overrideMethods["noclip"] = ::IW5_NoClip;
|
||||
}
|
||||
}
|
||||
|
||||
UnsupportedFunc()
|
||||
{
|
||||
self IPrintLnBold( "Function is not supported!" );
|
||||
}
|
||||
|
||||
RequestClientMeta( metaKey )
|
||||
{
|
||||
getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey );
|
||||
@ -482,14 +545,65 @@ NotifyClientEvent( eventInfo )
|
||||
if ( level.iw4adminIntegrationDebug == 1 )
|
||||
{
|
||||
IPrintLn( "NotifyClientEvent->" + event.data );
|
||||
if( int( eventInfo[3] ) != -1 && !isDefined( origin ) )
|
||||
{
|
||||
IPrintLn( "origin is null but the slot id is " + int( eventInfo[3] ) );
|
||||
}
|
||||
if( int( eventInfo[4] ) != -1 && !isDefined( target ) )
|
||||
{
|
||||
IPrintLn( "target is null but the slot id is " + int( eventInfo[4] ) );
|
||||
}
|
||||
}
|
||||
|
||||
client = event.origin;
|
||||
if( isDefined( target ) )
|
||||
{
|
||||
client = event.target;
|
||||
}
|
||||
else if( isDefined( origin ) )
|
||||
{
|
||||
client = event.origin;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( level.iw4adminIntegrationDebug == 1 )
|
||||
{
|
||||
IPrintLn( "Neither origin or target are set but we are a Client Event, aborting" );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
client.event = event;
|
||||
|
||||
level notify( level.eventTypes.localClientEvent, client );
|
||||
}
|
||||
|
||||
GetPlayerFromClientNum( clientNum )
|
||||
{
|
||||
if ( clientNum < 0 )
|
||||
return undefined;
|
||||
|
||||
for ( i = 0; i < level.players.size; i++ )
|
||||
{
|
||||
if ( level.players[i] getEntityNumber() == clientNum )
|
||||
{
|
||||
return level.players[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
|
||||
{
|
||||
if ( isDefined( level.clientCommandCallbacks[commandName] ) && isDefined( shouldOverwrite ) && !shouldOverwrite ) {
|
||||
|
||||
return;
|
||||
}
|
||||
level.clientCommandCallbacks[commandName] = callback;
|
||||
level.clientCommandRusAsTarget[commandName] = shouldRunAsTarget == true; //might speed up things later in case someone gives us a string or number instead of a boolean
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////
|
||||
// Event Handlers
|
||||
/////////////////////////////////
|
||||
@ -536,45 +650,18 @@ OnExecuteCommand( event )
|
||||
data = ParseDataString( event.data );
|
||||
response = "";
|
||||
|
||||
switch ( event.subtype )
|
||||
command = level.clientCommandCallbacks[event.subtype];
|
||||
runAsTarget = level.clientCommandRusAsTarget[event.subtype];
|
||||
executionContextEntity = event.origin;
|
||||
if ( runAsTarget ) {
|
||||
executionContextEntity = event.target;
|
||||
}
|
||||
if ( isDefined( command ) ) {
|
||||
response = executionContextEntity [[command]]( event, data );
|
||||
}
|
||||
else if ( level.iw4adminIntegrationDebug == 1 )
|
||||
{
|
||||
case "GiveWeapon":
|
||||
response = event.target GiveWeaponImpl( data );
|
||||
break;
|
||||
case "TakeWeapons":
|
||||
response = event.target TakeWeaponsImpl();
|
||||
break;
|
||||
case "SwitchTeams":
|
||||
response = event.target TeamSwitchImpl();
|
||||
break;
|
||||
case "Hide":
|
||||
response = self HideImpl();
|
||||
break;
|
||||
case "Unhide":
|
||||
response = self UnhideImpl();
|
||||
break;
|
||||
case "Alert":
|
||||
response = event.target AlertImpl( data );
|
||||
break;
|
||||
case "Goto":
|
||||
if ( IsDefined( event.target ) )
|
||||
{
|
||||
response = self GotoPlayerImpl( event.target );
|
||||
}
|
||||
else
|
||||
{
|
||||
response = self GotoImpl( data );
|
||||
}
|
||||
break;
|
||||
case "Kill":
|
||||
response = event.target KillImpl();
|
||||
break;
|
||||
case "NightMode":
|
||||
NightModeImpl();
|
||||
break;
|
||||
case "SetSpectator":
|
||||
response = event.target SetSpectatorImpl();
|
||||
break;
|
||||
IPrintLn( "Unkown Client command->" + event.subtype);
|
||||
}
|
||||
|
||||
// send back the response to the origin, but only if they're not the target
|
||||
@ -597,7 +684,7 @@ OnSetClientDataCompleted( event )
|
||||
// Command Implementations
|
||||
/////////////////////////////////
|
||||
|
||||
GiveWeaponImpl( data )
|
||||
GiveWeaponImpl( event, data )
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
@ -628,7 +715,7 @@ TeamSwitchImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + "^7 is not alive";
|
||||
return self + "^7 is not alive";
|
||||
}
|
||||
|
||||
team = level.allies;
|
||||
@ -645,6 +732,93 @@ TeamSwitchImpl()
|
||||
return self.name + "^7 switched to " + self.team;
|
||||
}
|
||||
|
||||
LockControlsImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + "^7 is not alive";
|
||||
}
|
||||
|
||||
|
||||
self freezeControls( true );
|
||||
self call [[level.overrideMethods["god"]]]( true );
|
||||
self Hide();
|
||||
|
||||
info = [];
|
||||
info[ "alertType" ] = "Alert!";
|
||||
info[ "message" ] = "You have been frozen!";
|
||||
|
||||
self AlertImpl( undefined, info );
|
||||
|
||||
return self.name + "\'s controls are locked";
|
||||
}
|
||||
|
||||
UnlockControlsImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + "^7 is not alive";
|
||||
}
|
||||
|
||||
self freezeControls( false );
|
||||
self call [[level.overrideMethods["god"]]]( false );
|
||||
self Show();
|
||||
|
||||
return self.name + "\'s controls are unlocked";
|
||||
}
|
||||
|
||||
NoClipImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
self IPrintLnBold( "You are not alive" );
|
||||
// Due to bug when sometimes disabling noclip game thinks you're dead
|
||||
// removing the return and allowing them to go back into noclip is probably better.
|
||||
//return;
|
||||
}
|
||||
|
||||
self SetClientDvar( "sv_cheats", 1 );
|
||||
self SetClientDvar( "cg_thirdperson", 1 );
|
||||
self SetClientDvar( "sv_cheats", 0 );
|
||||
|
||||
self call [[level.overrideMethods["god"]]]( true );
|
||||
self call [[level.overrideMethods["noclip"]]]( true );
|
||||
self Hide();
|
||||
|
||||
self.isNoClipped = true;
|
||||
|
||||
self IPrintLnBold( "NoClip enabled" );
|
||||
}
|
||||
|
||||
NoClipOffImpl()
|
||||
{
|
||||
if ( !IsDefined( self.isNoClipped ) || !self.isNoClipped )
|
||||
{
|
||||
self IPrintLnBold( "You are not no-clipped" );
|
||||
return;
|
||||
}
|
||||
|
||||
self SetClientDvar( "sv_cheats", 1 );
|
||||
self SetClientDvar( "cg_thirdperson", 0 );
|
||||
self SetClientDvar( "sv_cheats", 0 );
|
||||
|
||||
self call [[level.overrideMethods["god"]]]( false );
|
||||
self call [[level.overrideMethods["noclip"]]]( false );
|
||||
self Show();
|
||||
|
||||
self IPrintLnBold( "NoClip disabled" );
|
||||
|
||||
if ( !IsAlive( self ) && self.isNoClipped )
|
||||
{
|
||||
// Sometimes you will bug exiting noclip where the game thinks you're dead
|
||||
// but you're not. You will retain godmode but be able to run around and kill people.
|
||||
// So, it's important to let the user know.
|
||||
self IPrintLnBold( "^1You are bugged! ^4Swap team." );
|
||||
}
|
||||
|
||||
self.isNoClipped = false;
|
||||
}
|
||||
|
||||
HideImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
@ -657,18 +831,17 @@ HideImpl()
|
||||
self SetClientDvar( "cg_thirdperson", 1 );
|
||||
self SetClientDvar( "sv_cheats", 0 );
|
||||
|
||||
if ( !IsDefined( self.savedHealth ) || self.health < 1000 )
|
||||
if ( !IsDefined( self.savedHealth ) || self.health < 1000 )
|
||||
{
|
||||
self.savedHealth = self.health;
|
||||
self.savedMaxHealth = self.maxhealth;
|
||||
}
|
||||
|
||||
self.maxhealth = 99999;
|
||||
self.health = 99999;
|
||||
self.isHidden = true;
|
||||
|
||||
self call [[level.overrideMethods["god"]]]( true );
|
||||
self Hide();
|
||||
|
||||
self.isHidden = true;
|
||||
|
||||
self IPrintLnBold( "You are now ^5hidden ^7from other players" );
|
||||
}
|
||||
|
||||
@ -690,21 +863,38 @@ UnhideImpl()
|
||||
self SetClientDvar( "cg_thirdperson", 0 );
|
||||
self SetClientDvar( "sv_cheats", 0 );
|
||||
|
||||
self.health = self.savedHealth;
|
||||
self.maxhealth = self.savedMaxHealth;
|
||||
self.isHidden = false;
|
||||
|
||||
self call [[level.overrideMethods["god"]]]( false );
|
||||
self Show();
|
||||
|
||||
self.isHidden = false;
|
||||
|
||||
self IPrintLnBold( "You are now ^5visible ^7to other players" );
|
||||
}
|
||||
|
||||
AlertImpl( data )
|
||||
AlertImpl( event, data )
|
||||
{
|
||||
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
|
||||
if ( level.eventBus.gamename == "IW4" ) {
|
||||
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
|
||||
}
|
||||
if ( level.eventBus.gamename == "IW5" ) { //IW5's notification are a bit different...
|
||||
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
|
||||
}
|
||||
return "Sent alert to " + self.name;
|
||||
}
|
||||
|
||||
GotoImpl( data )
|
||||
GotoImpl( event, data )
|
||||
{
|
||||
if ( IsDefined( event.target ) )
|
||||
{
|
||||
return self GotoPlayerImpl( event.target );
|
||||
}
|
||||
else
|
||||
{
|
||||
return self GotoCoordImpl( data );
|
||||
}
|
||||
}
|
||||
|
||||
GotoCoordImpl( data )
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
@ -729,6 +919,18 @@ GotoPlayerImpl( target )
|
||||
self IPrintLnBold( "Moved to " + target.name );
|
||||
}
|
||||
|
||||
PlayerToMeImpl( event )
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + " is not alive";
|
||||
}
|
||||
|
||||
self SetOrigin( event.origin GetOrigin() );
|
||||
return "Moved here " + self.name;
|
||||
}
|
||||
|
||||
|
||||
KillImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
@ -797,3 +999,48 @@ SetSpectatorImpl()
|
||||
|
||||
return self.name + " has been moved to spectator";
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Function Overrides
|
||||
//////////////////////////////////
|
||||
|
||||
_god( isEnabled )
|
||||
{
|
||||
if ( isEnabled == true )
|
||||
{
|
||||
if ( !IsDefined( self.savedHealth ) || self.health < 1000 )
|
||||
{
|
||||
self.savedHealth = self.health;
|
||||
self.savedMaxHealth = self.maxhealth;
|
||||
}
|
||||
|
||||
self.maxhealth = 99999;
|
||||
self.health = 99999;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if ( !IsDefined( self.savedHealth ) || !IsDefined( self.savedMaxHealth ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.health = self.savedHealth;
|
||||
self.maxhealth = self.savedMaxHealth;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
IW5_God()
|
||||
{
|
||||
SetDvar( "sv_cheats", 1 );
|
||||
self God();
|
||||
SetDvar( "sv_cheats", 0 );
|
||||
}
|
||||
|
||||
IW5_NoClip()
|
||||
{
|
||||
SetDvar( "sv_cheats", 1 );
|
||||
self NoClip();
|
||||
SetDvar( "sv_cheats", 0 );
|
||||
}
|
@ -13,7 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
version.txt = version.txt
|
||||
DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1
|
||||
DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh
|
||||
GameFiles\IW4x\userraw\scripts\_integration.gsc = GameFiles\IW4x\userraw\scripts\_integration.gsc
|
||||
GameFiles\_integration.gsc = GameFiles\_integration.gsc
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}"
|
||||
@ -51,6 +51,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
|
||||
Plugins\ScriptPlugins\ParserCSGOSM.js = Plugins\ScriptPlugins\ParserCSGOSM.js
|
||||
Plugins\ScriptPlugins\ParserPlutoniumT4COZM.js = Plugins\ScriptPlugins\ParserPlutoniumT4COZM.js
|
||||
Plugins\ScriptPlugins\GameInterface.js = Plugins\ScriptPlugins\GameInterface.js
|
||||
Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js
|
||||
Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js
|
||||
Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
|
||||
|
@ -33,7 +33,8 @@ namespace Integrations.Cod
|
||||
private readonly Encoding _gameEncoding;
|
||||
private readonly int _retryAttempts;
|
||||
|
||||
public CodRConConnection(IPEndPoint ipEndpoint, string password, ILogger<CodRConConnection> log, Encoding gameEncoding, int retryAttempts)
|
||||
public CodRConConnection(IPEndPoint ipEndpoint, string password, ILogger<CodRConConnection> log,
|
||||
Encoding gameEncoding, int retryAttempts)
|
||||
{
|
||||
RConPassword = password;
|
||||
_gameEncoding = gameEncoding;
|
||||
@ -66,21 +67,31 @@ namespace Integrations.Cod
|
||||
}
|
||||
finally
|
||||
{
|
||||
using (LogContext.PushProperty("Server", Endpoint.ToString()))
|
||||
{
|
||||
_log.LogDebug("Releasing OnComplete {Count}", ActiveQueries[Endpoint].OnComplete.CurrentCount);
|
||||
}
|
||||
|
||||
if (ActiveQueries[Endpoint].OnComplete.CurrentCount == 0)
|
||||
{
|
||||
ActiveQueries[Endpoint].OnComplete.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string[]> SendQueryAsyncInternal(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default)
|
||||
|
||||
private async Task<string[]> SendQueryAsyncInternal(StaticHelpers.QueryType type, string parameters = "",
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (!ActiveQueries.ContainsKey(Endpoint))
|
||||
{
|
||||
ActiveQueries.TryAdd(Endpoint, new ConnectionState());
|
||||
}
|
||||
|
||||
var connectionState = ActiveQueries[Endpoint];
|
||||
if (!ActiveQueries.TryGetValue(Endpoint, out var connectionState))
|
||||
{
|
||||
_log.LogError("Could not retrieve connection state");
|
||||
throw new InvalidOperationException("Could not get connection state");
|
||||
}
|
||||
|
||||
_log.LogDebug("Waiting for semaphore to be released [{Endpoint}]", Endpoint);
|
||||
|
||||
@ -91,6 +102,8 @@ namespace Integrations.Cod
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_log.LogDebug("OnComplete did not complete before timeout {Count}",
|
||||
connectionState.OnComplete.CurrentCount);
|
||||
throw new RConException("Timed out waiting for access to rcon socket");
|
||||
}
|
||||
|
||||
@ -100,16 +113,20 @@ namespace Integrations.Cod
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_config.FloodProtectInterval - (int)timeSinceLastQuery, token);
|
||||
var delay = _config.FloodProtectInterval - (int)timeSinceLastQuery;
|
||||
_log.LogDebug("Delaying for {Delay}ms", delay);
|
||||
await Task.Delay(delay, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_log.LogDebug("Waiting for flood protect did not complete before timeout timeout {Count}",
|
||||
connectionState.OnComplete.CurrentCount);
|
||||
throw new RConException("Timed out waiting for flood protect to expire");
|
||||
}
|
||||
}
|
||||
|
||||
_log.LogDebug("Semaphore has been released [{Endpoint}]", Endpoint);
|
||||
_log.LogDebug("Query {@QueryInfo}", new { endpoint=Endpoint.ToString(), type, parameters });
|
||||
_log.LogDebug("Query {@QueryInfo}", new { endpoint = Endpoint.ToString(), type, parameters });
|
||||
|
||||
byte[] payload = null;
|
||||
var waitForResponse = _config.WaitForResponse;
|
||||
@ -163,7 +180,6 @@ namespace Integrations.Cod
|
||||
// e.g: emoji -> windows-1252
|
||||
catch (OverflowException ex)
|
||||
{
|
||||
|
||||
using (LogContext.PushProperty("Server", Endpoint.ToString()))
|
||||
{
|
||||
_log.LogError(ex, "Could not convert RCon data payload to desired encoding {Encoding} {Params}",
|
||||
@ -181,8 +197,8 @@ namespace Integrations.Cod
|
||||
using (LogContext.PushProperty("Server", Endpoint.ToString()))
|
||||
{
|
||||
_log.LogInformation(
|
||||
"Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts) with parameters {Payload}",
|
||||
connectionState.ConnectionAttempts,
|
||||
"Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts) with parameters {Payload}",
|
||||
connectionState.ConnectionAttempts,
|
||||
_retryAttempts, parameters);
|
||||
}
|
||||
}
|
||||
@ -201,6 +217,7 @@ namespace Integrations.Cod
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_log.LogDebug("OnSent did not complete in time");
|
||||
throw new RConException("Timed out waiting for access to RCon send socket");
|
||||
}
|
||||
|
||||
@ -211,14 +228,13 @@ namespace Integrations.Cod
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw new RConException("Timed out waiting for access to RCon receive socket");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_log.LogDebug("OnReceived did not complete in time");
|
||||
if (connectionState.OnSentData.CurrentCount == 0)
|
||||
{
|
||||
connectionState.OnSentData.Release();
|
||||
}
|
||||
|
||||
throw new RConException("Timed out waiting for access to RCon receive socket");
|
||||
}
|
||||
|
||||
connectionState.SendEventArgs.UserToken = new ConnectionUserToken
|
||||
@ -242,6 +258,7 @@ namespace Integrations.Cod
|
||||
|
||||
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
|
||||
{
|
||||
_log.LogDebug("0 bytes received from rcon request");
|
||||
throw new RConException("Expected response but got 0 bytes back");
|
||||
}
|
||||
|
||||
@ -252,6 +269,7 @@ namespace Integrations.Cod
|
||||
{
|
||||
// if we timed out due to the cancellation token,
|
||||
// we don't want to count that as an attempt
|
||||
_log.LogDebug("OperationCanceledException when waiting for payload send to complete");
|
||||
connectionState.ConnectionAttempts = 0;
|
||||
}
|
||||
catch
|
||||
@ -265,7 +283,8 @@ namespace Integrations.Cod
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
_log.LogDebug("OperationCancelled while waiting for retry");
|
||||
throw;
|
||||
}
|
||||
|
||||
goto retrySend;
|
||||
@ -311,11 +330,13 @@ namespace Integrations.Cod
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
|
||||
ReassembleSegmentedStatus(response) : RecombineMessages(response);
|
||||
var responseString = type == StaticHelpers.QueryType.COMMAND_STATUS
|
||||
? ReassembleSegmentedStatus(response)
|
||||
: RecombineMessages(response);
|
||||
|
||||
// note: not all games respond if the password is wrong or not set
|
||||
if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
|
||||
if (responseString.Contains("Invalid password", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
responseString.Contains("rconpassword"))
|
||||
{
|
||||
throw new RConException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]);
|
||||
}
|
||||
@ -327,11 +348,14 @@ namespace Integrations.Cod
|
||||
|
||||
if (responseString.Contains(_config.ServerNotRunningResponse))
|
||||
{
|
||||
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString()));
|
||||
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"]
|
||||
.FormatExt(Endpoint.ToString()));
|
||||
}
|
||||
|
||||
var responseHeaderMatch = Regex.Match(responseString, _config.CommandPrefixes.RConResponse).Value;
|
||||
var headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? _config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch);
|
||||
var headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO
|
||||
? _config.CommandPrefixes.RconGetInfoResponseHeader
|
||||
: responseHeaderMatch);
|
||||
|
||||
if (headerSplit.Length != 2)
|
||||
{
|
||||
@ -369,7 +393,8 @@ namespace Integrations.Cod
|
||||
|
||||
else
|
||||
{
|
||||
splitStatusStrings.Add(responseString.Replace(_config.CommandPrefixes.RConResponse, "").TrimEnd('\0'));
|
||||
splitStatusStrings.Add(responseString.Replace(_config.CommandPrefixes.RConResponse, "")
|
||||
.TrimEnd('\0'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,8 +421,10 @@ namespace Integrations.Cod
|
||||
{
|
||||
message = message.Replace(_config.CommandPrefixes.RConResponse, "");
|
||||
}
|
||||
|
||||
builder.Append(message);
|
||||
}
|
||||
|
||||
builder.Append('\n');
|
||||
return builder.ToString();
|
||||
}
|
||||
@ -410,6 +437,7 @@ namespace Integrations.Cod
|
||||
|
||||
if (rconSocket is null)
|
||||
{
|
||||
_log.LogDebug("Invalid state");
|
||||
throw new InvalidOperationException("State is not valid for socket operation");
|
||||
}
|
||||
|
||||
@ -419,6 +447,7 @@ namespace Integrations.Cod
|
||||
// setup the event handlers only once because we're reusing the event args
|
||||
connectionState.SendEventArgs.Completed += OnDataSent;
|
||||
connectionState.ReceiveEventArgs.Completed += OnDataReceived;
|
||||
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
|
||||
connectionState.SendEventArgs.RemoteEndPoint = Endpoint;
|
||||
connectionState.ReceiveEventArgs.RemoteEndPoint = Endpoint;
|
||||
connectionState.ReceiveEventArgs.DisconnectReuseSocket = true;
|
||||
@ -435,15 +464,15 @@ namespace Integrations.Cod
|
||||
// the send has not been completed asynchronously
|
||||
// this really shouldn't ever happen because it's UDP
|
||||
var complete = await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(4), token);
|
||||
|
||||
|
||||
if (!complete)
|
||||
{
|
||||
using(LogContext.PushProperty("Server", Endpoint.ToString()))
|
||||
using (LogContext.PushProperty("Server", Endpoint.ToString()))
|
||||
{
|
||||
_log.LogWarning("Socket timed out while sending RCon data on attempt {Attempt}",
|
||||
connectionState.ConnectionAttempts);
|
||||
}
|
||||
|
||||
|
||||
rconSocket.Close();
|
||||
throw new NetworkException("Timed out sending RCon data", rconSocket);
|
||||
}
|
||||
@ -461,7 +490,8 @@ namespace Integrations.Cod
|
||||
|
||||
if (receiveDataPending)
|
||||
{
|
||||
_log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}", connectionState.ConnectionAttempts);
|
||||
_log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}",
|
||||
connectionState.ConnectionAttempts);
|
||||
|
||||
var completed = false;
|
||||
|
||||
@ -493,6 +523,7 @@ namespace Integrations.Cod
|
||||
}
|
||||
|
||||
rconSocket.Close();
|
||||
_log.LogDebug("OnDataReceived did not complete in allocated time");
|
||||
throw new NetworkException("Timed out receiving RCon response", rconSocket);
|
||||
}
|
||||
}
|
||||
@ -521,13 +552,14 @@ namespace Integrations.Cod
|
||||
|
||||
private void OnDataReceived(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
_log.LogDebug("Read {BytesTransferred} bytes from {Endpoint}", e.BytesTransferred, e.RemoteEndPoint?.ToString());
|
||||
_log.LogDebug("Read {BytesTransferred} bytes from {Endpoint}", e.BytesTransferred,
|
||||
e.RemoteEndPoint?.ToString());
|
||||
|
||||
// this occurs when we close the socket
|
||||
if (e.BytesTransferred == 0)
|
||||
{
|
||||
_log.LogDebug("No bytes were transmitted so the connection was probably closed");
|
||||
|
||||
|
||||
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
|
||||
|
||||
try
|
||||
@ -545,7 +577,7 @@ namespace Integrations.Cod
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var state = ActiveQueries[Endpoint];
|
||||
var cancellationRequested = ((ConnectionUserToken)e.UserToken)?.CancellationToken.IsCancellationRequested ??
|
||||
false;
|
||||
@ -566,12 +598,12 @@ namespace Integrations.Cod
|
||||
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
|
||||
// this thread is not notified because it's an event
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
state.BytesReadPerSegment.Add(e.BytesTransferred);
|
||||
|
||||
|
||||
// I don't even want to know why this works for getting more data from Cod4x
|
||||
// but I'm leaving it in here as long as it doesn't break anything.
|
||||
// it's very stupid...
|
||||
@ -582,7 +614,7 @@ namespace Integrations.Cod
|
||||
var totalBytesTransferred = e.BytesTransferred;
|
||||
_log.LogDebug("{Total} total bytes transferred with {Available} bytes remaining", totalBytesTransferred,
|
||||
sock.Available);
|
||||
|
||||
|
||||
// we still have available data so the payload was segmented
|
||||
while (sock.Available > 0)
|
||||
{
|
||||
@ -596,17 +628,17 @@ namespace Integrations.Cod
|
||||
bufferSpaceAvailable);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, totalBytesTransferred, sock.Available);
|
||||
if (sock.ReceiveAsync(state.ReceiveEventArgs))
|
||||
{
|
||||
_log.LogDebug("Remaining bytes are async");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
_log.LogDebug("Read {BytesTransferred} synchronous bytes from {Endpoint}",
|
||||
state.ReceiveEventArgs.BytesTransferred, e.RemoteEndPoint?.ToString());
|
||||
|
||||
|
||||
// we need to increment this here because the callback isn't executed if there's no pending IO
|
||||
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
|
||||
totalBytesTransferred += state.ReceiveEventArgs.BytesTransferred;
|
||||
@ -638,8 +670,9 @@ namespace Integrations.Cod
|
||||
|
||||
private void OnDataSent(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
_log.LogDebug("Sent {ByteCount} bytes to {Endpoint}", e.Buffer?.Length, e.ConnectSocket?.RemoteEndPoint?.ToString());
|
||||
|
||||
_log.LogDebug("Sent {ByteCount} bytes to {Endpoint}", e.Buffer?.Length,
|
||||
e.ConnectSocket?.RemoteEndPoint?.ToString());
|
||||
|
||||
var semaphore = ActiveQueries[Endpoint].OnSentData;
|
||||
try
|
||||
{
|
||||
|
@ -20,7 +20,7 @@ namespace Integrations.Cod
|
||||
public int ConnectionAttempts { get; set; }
|
||||
private const int BufferSize = 16384;
|
||||
public readonly byte[] ReceiveBuffer = new byte[BufferSize];
|
||||
public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1);
|
||||
public readonly SemaphoreSlim OnComplete = new(1, 1);
|
||||
public readonly SemaphoreSlim OnSentData = new(1, 1);
|
||||
public readonly SemaphoreSlim OnReceivedData = new (1, 1);
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace AutomessageFeed
|
||||
{
|
||||
@ -11,16 +10,6 @@ namespace AutomessageFeed
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
EnableFeed = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_ENABLE"]);
|
||||
|
||||
if (EnableFeed)
|
||||
{
|
||||
FeedUrl = Utilities.PromptString(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_URL"]);
|
||||
MaxFeedItems = Utilities.PromptInt(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_MAXITEMS"],
|
||||
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_MAXITEMS_DESC"],
|
||||
0, int.MaxValue, 0);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,8 @@ namespace LiveRadar.Web.Controllers
|
||||
private static LiveRadarConfiguration _config;
|
||||
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
|
||||
|
||||
public RadarController(IManager manager, IConfigurationHandlerFactory configurationHandlerFactory) : base(manager)
|
||||
public RadarController(IManager manager, IConfigurationHandlerFactory configurationHandlerFactory) :
|
||||
base(manager)
|
||||
{
|
||||
_manager = manager;
|
||||
_configurationHandler =
|
||||
@ -23,29 +24,33 @@ namespace LiveRadar.Web.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Radar/{serverId}")]
|
||||
public IActionResult Index(long? serverId = null)
|
||||
[Route("Radar/{serverId?}")]
|
||||
public IActionResult Index(string serverId = null)
|
||||
{
|
||||
ViewBag.IsFluid = true;
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"];
|
||||
ViewBag.ActiveServerId = serverId ?? _manager.GetServers().FirstOrDefault()?.EndPoint;
|
||||
ViewBag.Servers = _manager.GetServers()
|
||||
.Where(_server => _server.GameName == Server.Game.IW4)
|
||||
.Select(_server => new ServerInfo()
|
||||
var servers = _manager.GetServers()
|
||||
.Where(server => server.GameName == Server.Game.IW4)
|
||||
.Select(server => new ServerInfo
|
||||
{
|
||||
Name = _server.Hostname,
|
||||
ID = _server.EndPoint
|
||||
Name = server.Hostname,
|
||||
IPAddress = server.IP,
|
||||
Port = server.Port
|
||||
});
|
||||
|
||||
return View();
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"];
|
||||
ViewBag.SelectedServerId = string.IsNullOrEmpty(serverId) ? servers.FirstOrDefault()?.Endpoint : serverId;
|
||||
|
||||
// ReSharper disable once Mvc.ViewNotResolved
|
||||
return View("~/Views/Plugins/LiveRadar/Radar/Index.cshtml", servers);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Radar/{serverId}/Map")]
|
||||
public async Task<IActionResult> Map(long? serverId = null)
|
||||
public async Task<IActionResult> Map(string serverId = null)
|
||||
{
|
||||
var server = serverId == null ? _manager.GetServers().FirstOrDefault() : _manager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
|
||||
|
||||
var server = serverId == null
|
||||
? _manager.GetServers().FirstOrDefault()
|
||||
: _manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
|
||||
|
||||
if (server == null)
|
||||
{
|
||||
return NotFound();
|
||||
@ -56,15 +61,15 @@ namespace LiveRadar.Web.Controllers
|
||||
await _configurationHandler.BuildAsync();
|
||||
_config = _configurationHandler.Configuration() ?? new LiveRadarConfiguration();
|
||||
}
|
||||
|
||||
var map = _config.Maps.FirstOrDefault(_map => _map.Name == server.CurrentMap.Name);
|
||||
|
||||
var map = _config.Maps.FirstOrDefault(map => map.Name == server.CurrentMap.Name);
|
||||
|
||||
if (map == null)
|
||||
{
|
||||
// occurs if we don't recognize the map
|
||||
return StatusCode(StatusCodes.Status422UnprocessableEntity);
|
||||
}
|
||||
|
||||
|
||||
map.Alias = server.CurrentMap.Alias;
|
||||
return Json(map);
|
||||
}
|
||||
@ -72,27 +77,21 @@ namespace LiveRadar.Web.Controllers
|
||||
[HttpGet]
|
||||
[Route("Radar/{serverId}/Data")]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Data(long? serverId = null)
|
||||
public IActionResult Data(string serverId = null)
|
||||
{
|
||||
var server = serverId == null ? _manager.GetServers()[0] : _manager.GetServers().First(_server => _server.EndPoint == serverId);
|
||||
var radarInfo = server.GetClientsAsList().Select(_client => _client.GetAdditionalProperty<RadarEvent>("LiveRadar")).ToList();
|
||||
return Json(radarInfo);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Radar/Update")]
|
||||
public IActionResult Update(string payload)
|
||||
{
|
||||
/*var radarUpdate = RadarEvent.Parse(payload);
|
||||
var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
|
||||
|
||||
if (client != null)
|
||||
var server = serverId == null
|
||||
? _manager.GetServers().FirstOrDefault()
|
||||
: _manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
|
||||
|
||||
if (server == null)
|
||||
{
|
||||
radarUpdate.Name = client.Name.StripColors();
|
||||
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||
}*/
|
||||
|
||||
return Ok();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var radarInfo = server.GetClientsAsList()
|
||||
.Select(client => client.GetAdditionalProperty<RadarEvent>("LiveRadar")).ToList();
|
||||
|
||||
return Json(radarInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,13 +15,6 @@
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Views\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
@ -46,7 +46,7 @@ namespace LiveRadar
|
||||
S.CustomCallback &&
|
||||
!addedPage)
|
||||
{
|
||||
E.Owner.Manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar/All");
|
||||
E.Owner.Manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar");
|
||||
addedPage = true;
|
||||
}
|
||||
}
|
||||
@ -77,7 +77,15 @@ namespace LiveRadar
|
||||
|
||||
lock (lockObject)
|
||||
{
|
||||
generatedBotGuid = _botGuidLookups.ContainsKey(botKey)
|
||||
var hasBotKey = _botGuidLookups.ContainsKey(botKey);
|
||||
|
||||
if (!hasBotKey && ((string)E.Extra).IsBotGuid())
|
||||
{
|
||||
// edge case where the bot guid has not been registered yet
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
generatedBotGuid = hasBotKey
|
||||
? _botGuidLookups[botKey]
|
||||
: (E.Extra.ToString() ?? "0").ConvertGuidToLong(NumberStyles.HexNumber);
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
@model IEnumerable<long>
|
||||
|
||||
<style>
|
||||
.progress {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.player-stat-icon {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
background-size: 1.5rem 1.5rem;
|
||||
}
|
||||
</style>
|
||||
<div class="row p-0 ml-auto mr-auto mb-4">
|
||||
<div class="col-12 col-xl-10 p-0 ml-auto mr-auto p-0 pl-lg-3 pr-lg-3 ">
|
||||
<ul class="nav nav-tabs border-top border-bottom nav-fill" role="tablist">
|
||||
@foreach (SharedLibraryCore.Dtos.ServerInfo server in ViewBag.Servers)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a asp-controller="Radar" asp-action="Index" asp-route-serverId="@server.ID" class="nav-link @(server.ID == ViewBag.ActiveServerId ? "active": "")" aria-selected="@(server.ID == ViewBag.ActiveServerId ? "true": "false")"><color-code value="@server.Name"></color-code></a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-0 ml-auto mr-auto col-12 col-xl-10">
|
||||
<div class="p-0 pl-lg-3 pr-lg-3 m-0 col-lg-3 col-12 text-lg-right text-center player-data-left" style="opacity: 0;">
|
||||
</div>
|
||||
<div class="pl-0 pr-0 pl-lg-3 pr-lg-3 col-lg-6 col-12 pb-4">
|
||||
<div id="map_name" class="h4 text-center pb-2 pt-2 mb-0 bg-primary">—</div>
|
||||
<div id="map_list" style="background-size:cover; padding-bottom: 100% !important;">
|
||||
<canvas id="map_canvas" style="position:absolute;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-0 pl-lg-3 pr-lg-3 m-0 col-lg-3 col-12 text-lg-left text-center player-data-right" style="opacity: 0;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- images used by canvas -->
|
||||
<img class="hide" id="hud_death" src="~/images/radar/death.png" />
|
||||
|
||||
@section scripts {
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/liveradar.js" defer="defer"></script>
|
||||
</environment>
|
||||
|
||||
<script type="text/javascript">
|
||||
const radarDataUrl = '@Url.Action("Data", "Radar", new { serverId = ViewBag.ActiveServerId })';
|
||||
const mapDataUrl = '@Url.Action("Map", "Radar", new { serverId = ViewBag.ActiveServerId })';
|
||||
</script>
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
@using SharedLibraryCore
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, SharedLibraryCore
|
@ -9,7 +9,6 @@ namespace IW4MAdmin.Plugins.Login
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
RequirePrivilegedClientLogin = Utilities.PromptBool("Require privileged client login");
|
||||
return this;
|
||||
}
|
||||
|
||||
|
48
Plugins/ScriptPlugins/BanBroadcasting.js
Normal file
48
Plugins/ScriptPlugins/BanBroadcasting.js
Normal file
@ -0,0 +1,48 @@
|
||||
const broadcastMessage = (server, message) => {
|
||||
server.Manager.GetServers().forEach(s => {
|
||||
s.Broadcast(message);
|
||||
});
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
author: 'Amos',
|
||||
version: 1.0,
|
||||
name: 'Broadcast Bans',
|
||||
|
||||
onEventAsync: function (gameEvent, server) {
|
||||
if (!this.enableBroadcastBans) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameEvent.TypeName === 'Ban') {
|
||||
let penalty = undefined;
|
||||
gameEvent.Origin.AdministeredPenalties?.forEach(p => {
|
||||
penalty = p.AutomatedOffense;
|
||||
})
|
||||
|
||||
if (gameEvent.Origin.ClientId === 1 && penalty !== undefined) {
|
||||
let localization = _localization.LocalizationIndex['PLUGINS_BROADCAST_BAN_ACMESSAGE'].replace('{{targetClient}}', gameEvent.Target.CleanedName);
|
||||
broadcastMessage(server, localization);
|
||||
} else {
|
||||
let localization = _localization.LocalizationIndex['PLUGINS_BROADCAST_BAN_MESSAGE'].replace('{{targetClient}}', gameEvent.Target.CleanedName);
|
||||
broadcastMessage(server, localization);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onLoadAsync: function (manager) {
|
||||
this.configHandler = _configHandler;
|
||||
this.enableBroadcastBans = this.configHandler.GetValue('EnableBroadcastBans');
|
||||
|
||||
if (this.enableBroadcastBans === undefined) {
|
||||
this.enableBroadcastBans = false;
|
||||
this.configHandler.SetValue('EnableBroadcastBans', this.enableBroadcastBans);
|
||||
}
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
},
|
||||
|
||||
onTickAsync: function (server) {
|
||||
}
|
||||
};
|
@ -85,7 +85,7 @@ let commands = [{
|
||||
name: 'weapon name',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -103,7 +103,7 @@ let commands = [{
|
||||
name: 'player',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -121,7 +121,7 @@ let commands = [{
|
||||
name: 'player',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -129,6 +129,72 @@ let commands = [{
|
||||
sendScriptCommand(gameEvent.Owner, 'SwitchTeams', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lockcontrols',
|
||||
description: 'locks target player\'s controls',
|
||||
alias: 'lc',
|
||||
permission: 'Administrator',
|
||||
targetRequired: true,
|
||||
arguments: [{
|
||||
name: 'player',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'LockControls', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'unlockcontrols',
|
||||
description: 'unlocks target player\'s controls',
|
||||
alias: 'ulc',
|
||||
permission: 'Administrator',
|
||||
targetRequired: true,
|
||||
arguments: [{
|
||||
name: 'player',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'UnlockControls', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'noclip',
|
||||
description: 'enable noclip on yourself ingame',
|
||||
alias: 'nc',
|
||||
permission: 'SeniorAdmin',
|
||||
targetRequired: false,
|
||||
arguments: [],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'NoClip', gameEvent.Origin, gameEvent.Origin, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'noclipoff',
|
||||
description: 'disable noclip on yourself ingame',
|
||||
alias: 'nco',
|
||||
permission: 'SeniorAdmin',
|
||||
targetRequired: false,
|
||||
arguments: [],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'NoClipOff', gameEvent.Origin, gameEvent.Origin, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'hide',
|
||||
description: 'hide yourself ingame',
|
||||
@ -136,7 +202,7 @@ let commands = [{
|
||||
permission: 'SeniorAdmin',
|
||||
targetRequired: false,
|
||||
arguments: [],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -151,7 +217,7 @@ let commands = [{
|
||||
permission: 'SeniorAdmin',
|
||||
targetRequired: false,
|
||||
arguments: [],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -173,7 +239,7 @@ let commands = [{
|
||||
name: 'message',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -194,7 +260,7 @@ let commands = [{
|
||||
name: 'player',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -202,6 +268,24 @@ let commands = [{
|
||||
sendScriptCommand(gameEvent.Owner, 'Goto', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'playertome',
|
||||
description: 'teleport a player to you',
|
||||
alias: 'p2m',
|
||||
permission: 'SeniorAdmin',
|
||||
targetRequired: true,
|
||||
arguments: [{
|
||||
name: 'player',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'PlayerToMe', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'goto',
|
||||
description: 'teleport to a position',
|
||||
@ -220,7 +304,7 @@ let commands = [{
|
||||
name: 'z',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -244,7 +328,7 @@ let commands = [{
|
||||
name: 'player',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -259,7 +343,7 @@ let commands = [{
|
||||
permission: 'SeniorAdmin',
|
||||
targetRequired: false,
|
||||
arguments: [],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -277,7 +361,7 @@ let commands = [{
|
||||
name: 'player',
|
||||
required: true
|
||||
}],
|
||||
supportedGames: ['IW4'],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
return;
|
||||
@ -482,14 +566,14 @@ const pollForEvents = server => {
|
||||
if (!state.waitingOnOutput) {
|
||||
if (state.queuedMessages.length === 0) {
|
||||
logger.WriteDebug('No messages in queue');
|
||||
return;``
|
||||
return;
|
||||
}
|
||||
|
||||
state.waitingOnOutput = true;
|
||||
const nextMessage = state.queuedMessages.splice(0, 1);
|
||||
setDvar(server, outDvar, nextMessage, onSetDvar);
|
||||
}
|
||||
|
||||
|
||||
if (state.waitingOnOutput) {
|
||||
getDvar(server, outDvar, onReceivedDvar);
|
||||
}
|
||||
|
42
Plugins/ScriptPlugins/ParserH1MOD.js
Normal file
42
Plugins/ScriptPlugins/ParserH1MOD.js
Normal file
@ -0,0 +1,42 @@
|
||||
var rconParser;
|
||||
var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'fed',
|
||||
version: 0.1,
|
||||
name: 'H1-Mod Parser',
|
||||
isParser: true,
|
||||
|
||||
onEventAsync: function(gameEvent, server) {},
|
||||
|
||||
onLoadAsync: function(manager) {
|
||||
rconParser = manager.GenerateDynamicRConParser(this.name);
|
||||
eventParser = manager.GenerateDynamicEventParser(this.name);
|
||||
|
||||
rconParser.Configuration.CommandPrefixes.Kick = 'kickClient {0} "{1}"';
|
||||
rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"';
|
||||
rconParser.Configuration.CommandPrefixes.TempBan = 'kickClient {0} "{1}"';
|
||||
rconParser.Configuration.CommandPrefixes.Tell = 'tellraw {0} "{1}"';
|
||||
rconParser.Configuration.CommandPrefixes.Say = 'sayraw "{0}"';
|
||||
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint';
|
||||
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"';
|
||||
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
|
||||
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
|
||||
rconParser.Configuration.Status.AddMapping(102, 4);
|
||||
rconParser.Configuration.Status.AddMapping(103, 5);
|
||||
rconParser.Configuration.Status.AddMapping(104, 6);
|
||||
rconParser.Configuration.WaitForResponse = false;
|
||||
rconParser.Configuration.DefaultRConPort = 27016;
|
||||
|
||||
eventParser.Configuration.GameDirectory = '';
|
||||
|
||||
rconParser.Version = 'H1 MP 1.15 build 1251288 Tue Jul 23 13:38:30 2019 win64';
|
||||
rconParser.GameName = 11; // H1
|
||||
eventParser.Version = 'H1 MP 1.15 build 1251288 Tue Jul 23 13:38:30 2019 win64';
|
||||
eventParser.GameName = 11; // H1
|
||||
},
|
||||
|
||||
onUnloadAsync: function() {},
|
||||
|
||||
onTickAsync: function(server) {}
|
||||
};
|
115
Plugins/ScriptPlugins/SubnetBan.js
Normal file
115
Plugins/ScriptPlugins/SubnetBan.js
Normal file
@ -0,0 +1,115 @@
|
||||
const cidrRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
|
||||
const validCIDR = input => cidrRegex.test(input);
|
||||
let subnetList = [];
|
||||
|
||||
const commands = [{
|
||||
name: "bansubnet",
|
||||
description: "bans an IPv4 subnet",
|
||||
alias: "bs",
|
||||
permission: "SeniorAdmin",
|
||||
targetRequired: false,
|
||||
arguments: [{
|
||||
name: "subnet in IPv4 CIDR notation",
|
||||
required: true
|
||||
}],
|
||||
|
||||
execute: (gameEvent) => {
|
||||
const input = String(gameEvent.Data).trim();
|
||||
|
||||
if (!validCIDR(input)) {
|
||||
gameEvent.Origin.Tell('Invalid CIDR input');
|
||||
return;
|
||||
}
|
||||
|
||||
subnetList.push(input);
|
||||
_configHandler.SetValue('SubnetBanList', subnetList);
|
||||
|
||||
gameEvent.Origin.Tell(`Added ${input} to subnet banlist`);
|
||||
}
|
||||
}];
|
||||
|
||||
convertIPtoLong = ip => {
|
||||
let components = String(ip).match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (components) {
|
||||
let ipLong = 0;
|
||||
let power = 1;
|
||||
for (let i = 4; i >= 1; i -= 1) {
|
||||
ipLong += power * parseInt(components[i]);
|
||||
power *= 256;
|
||||
}
|
||||
return ipLong;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
isInSubnet = (ip, subnet) => {
|
||||
const mask = subnet.match(/^(.*?)\/(\d{1,2})$/);
|
||||
|
||||
if (!mask) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baseIP = convertIPtoLong(mask[1]);
|
||||
const longIP = convertIPtoLong(ip);
|
||||
|
||||
if (mask && baseIP >= 0) {
|
||||
const freedom = Math.pow(2, 32 - parseInt(mask[2]));
|
||||
return (longIP > baseIP) && (longIP < baseIP + freedom - 1);
|
||||
} else return false;
|
||||
};
|
||||
|
||||
isSubnetBanned = (ip, list) => {
|
||||
const matchingSubnets = list.filter(subnet => isInSubnet(ip, subnet));
|
||||
return matchingSubnets.length !== 0;
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 1.0,
|
||||
name: 'Subnet Banlist Plugin',
|
||||
manager: null,
|
||||
logger: null,
|
||||
banMessage: '',
|
||||
|
||||
onEventAsync: (gameEvent, server) => {
|
||||
if (gameEvent.TypeName === 'Join') {
|
||||
if (!isSubnetBanned(gameEvent.Origin.IPAddressString, subnetList, this.logger)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.WriteInfo(`Kicking ${gameEvent.Origin} because they are subnet banned.`);
|
||||
gameEvent.Origin.Kick(this.banMessage, _IW4MAdminClient);
|
||||
}
|
||||
},
|
||||
onLoadAsync: manager => {
|
||||
this.manager = manager;
|
||||
this.logger = manager.GetLogger(0);
|
||||
this.configHandler = _configHandler;
|
||||
this.subnetList = [];
|
||||
|
||||
const list = this.configHandler.GetValue('SubnetBanList');
|
||||
if (list !== undefined) {
|
||||
list.forEach(element => {
|
||||
const ban = String(element);
|
||||
subnetList.push(ban)
|
||||
});
|
||||
this.logger.WriteInfo(`Loaded ${list.length} banned subnets`);
|
||||
} else {
|
||||
this.configHandler.SetValue('SubnetBanList', []);
|
||||
}
|
||||
|
||||
this.banMessage = this.configHandler.GetValue('BanMessage');
|
||||
|
||||
if (this.banMessage === undefined) {
|
||||
this.banMessage = 'You are not allowed to join this server.';
|
||||
this.configHandler.SetValue('BanMessage', this.banMessage);
|
||||
}
|
||||
},
|
||||
|
||||
onUnloadAsync: () => {
|
||||
},
|
||||
|
||||
onTickAsync: server => {
|
||||
}
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
let vpnExceptionIds = [];
|
||||
const commands = [{
|
||||
name: "whitelistvpn",
|
||||
description: "whitelists a player's client id from VPN detection",
|
||||
@ -5,12 +6,12 @@ const commands = [{
|
||||
permission: "SeniorAdmin",
|
||||
targetRequired: true,
|
||||
arguments: [{
|
||||
name: "players",
|
||||
name: "player",
|
||||
required: true
|
||||
}],
|
||||
execute: (gameEvent) => {
|
||||
plugin.vpnExceptionIds.push(gameEvent.Target.ClientId);
|
||||
plugin.configHandler.SetValue('vpnExceptionIds', plugin.vpnExceptionIds);
|
||||
vpnExceptionIds.push(gameEvent.Target.ClientId);
|
||||
plugin.configHandler.SetValue('vpnExceptionIds', vpnExceptionIds);
|
||||
|
||||
gameEvent.Origin.Tell(`Successfully whitelisted ${gameEvent.Target.Name}`);
|
||||
}
|
||||
@ -22,13 +23,13 @@ const plugin = {
|
||||
name: 'VPN Detection Plugin',
|
||||
manager: null,
|
||||
logger: null,
|
||||
vpnExceptionIds: [],
|
||||
|
||||
|
||||
checkForVpn: function (origin) {
|
||||
let exempt = false;
|
||||
// prevent players that are exempt from being kicked
|
||||
this.vpnExceptionIds.forEach(function (id) {
|
||||
if (id === origin.ClientId) {
|
||||
vpnExceptionIds.forEach(function (id) {
|
||||
if (id == origin.ClientId) { // when loaded from the config the "id" type is not the same as the ClientId type
|
||||
exempt = true;
|
||||
return false;
|
||||
}
|
||||
@ -79,8 +80,8 @@ const plugin = {
|
||||
this.logger = manager.GetLogger(0);
|
||||
|
||||
this.configHandler = _configHandler;
|
||||
this.configHandler.GetValue('vpnExceptionIds').forEach(element => this.vpnExceptionIds.push(element));
|
||||
this.logger.WriteInfo(`Loaded ${this.vpnExceptionIds.length} ids into whitelist`);
|
||||
this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(element));
|
||||
this.logger.WriteInfo(`Loaded ${vpnExceptionIds.length} ids into whitelist`);
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
@ -7,7 +9,15 @@ namespace Stats.Config
|
||||
public class AnticheatConfiguration
|
||||
{
|
||||
public bool Enable { get; set; }
|
||||
[Obsolete]
|
||||
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } = new Dictionary<long, DetectionType[]>();
|
||||
|
||||
public IDictionary<Game, DetectionType[]> GameDetectionTypes { get; set; } =
|
||||
new Dictionary<Game, DetectionType[]>()
|
||||
{
|
||||
{ Game.IW4, Enum.GetValues(typeof(DetectionType)).Cast<DetectionType>().ToArray() },
|
||||
{ Game.T6, new[] { DetectionType.Offset, DetectionType.Snap, DetectionType.Strain } }
|
||||
};
|
||||
public IList<long> IgnoredClientIds { get; set; } = new List<long>();
|
||||
public IDictionary<Game, IDictionary<DetectionType, string[]>> IgnoredDetectionSpecification{ get; set; } = new Dictionary<Game, IDictionary<DetectionType, string[]>>
|
||||
{
|
||||
|
@ -193,4 +193,4 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
throw new ArgumentException("No filters specified for chat search");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -633,32 +633,44 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return;
|
||||
}
|
||||
|
||||
var hit = new EFClientKill()
|
||||
EFClientKill hit;
|
||||
try
|
||||
{
|
||||
Active = true,
|
||||
AttackerId = attacker.ClientId,
|
||||
VictimId = victim.ClientId,
|
||||
ServerId = serverId,
|
||||
DeathOrigin = vDeathOrigin,
|
||||
KillOrigin = vKillOrigin,
|
||||
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
||||
Damage = int.Parse(damage),
|
||||
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
WeaponReference = weapon,
|
||||
ViewAngles = vViewAngles,
|
||||
TimeOffset = long.Parse(offset),
|
||||
When = time,
|
||||
IsKillstreakKill = isKillstreakKill[0] != '0',
|
||||
AdsPercent = float.Parse(Ads, System.Globalization.CultureInfo.InvariantCulture),
|
||||
Fraction = double.Parse(fraction, System.Globalization.CultureInfo.InvariantCulture),
|
||||
VisibilityPercentage = double.Parse(visibilityPercentage,
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
IsKill = !isDamage,
|
||||
AnglesList = snapshotAngles,
|
||||
IsAlive = isAlive == "1",
|
||||
TimeSinceLastAttack = long.Parse(lastAttackTime),
|
||||
GameName = (int) attacker.CurrentServer.GameName
|
||||
};
|
||||
hit = new EFClientKill
|
||||
{
|
||||
Active = true,
|
||||
AttackerId = attacker.ClientId,
|
||||
VictimId = victim.ClientId,
|
||||
ServerId = serverId,
|
||||
DeathOrigin = vDeathOrigin,
|
||||
KillOrigin = vKillOrigin,
|
||||
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
||||
Damage = int.Parse(damage),
|
||||
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
WeaponReference = weapon,
|
||||
ViewAngles = vViewAngles,
|
||||
TimeOffset = long.Parse(offset),
|
||||
When = time,
|
||||
IsKillstreakKill = isKillstreakKill[0] != '0',
|
||||
AdsPercent = float.Parse(Ads, System.Globalization.CultureInfo.InvariantCulture),
|
||||
Fraction = double.Parse(fraction, System.Globalization.CultureInfo.InvariantCulture),
|
||||
VisibilityPercentage = double.Parse(visibilityPercentage,
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
IsKill = !isDamage,
|
||||
AnglesList = snapshotAngles,
|
||||
IsAlive = isAlive == "1",
|
||||
TimeSinceLastAttack = long.Parse(lastAttackTime),
|
||||
GameName = (int) attacker.CurrentServer.GameName
|
||||
};
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}",
|
||||
damage, offset, lastAttackTime);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
hit.SetAdditionalProperty("HitLocationReference", hitLoc);
|
||||
|
||||
@ -769,7 +781,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Could not save hit or anti-cheat info {@attacker} {@victim} {server}", attacker,
|
||||
_log.LogError(ex, "Could not save hit or anti-cheat info {Attacker} {Victim} {Server}", attacker,
|
||||
victim, serverId);
|
||||
}
|
||||
|
||||
@ -806,7 +818,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
|
||||
{
|
||||
var detectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
|
||||
var serverDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
|
||||
var gameDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.GameDetectionTypes;
|
||||
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds;
|
||||
|
||||
if (ignoredClients.Contains(clientId))
|
||||
@ -814,10 +827,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
if (!detectionTypes[server.EndPoint].Contains(detectionType))
|
||||
if (!serverDetectionTypes[server.EndPoint].Contains(detectionType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -826,6 +838,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!gameDetectionTypes[server.GameName].Contains(detectionType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -186,7 +186,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
manager.GetPageList()
|
||||
.Pages.Add(
|
||||
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"],
|
||||
"/Stats/TopPlayersAsync");
|
||||
"/Stats/TopPlayers");
|
||||
|
||||
// meta data info
|
||||
async Task<IEnumerable<InformationResponse>> GetStats(ClientPaginationRequest request, CancellationToken token = default)
|
||||
@ -301,8 +301,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 1",
|
||||
Value = chestRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 0,
|
||||
Order = 100,
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"],
|
||||
IsSensitive = true
|
||||
},
|
||||
@ -311,8 +310,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 2",
|
||||
Value = abdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 1,
|
||||
Order = 101,
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"],
|
||||
IsSensitive = true
|
||||
},
|
||||
@ -321,8 +319,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 3",
|
||||
Value = chestAbdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 2,
|
||||
Order = 102,
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"],
|
||||
IsSensitive = true
|
||||
},
|
||||
@ -331,8 +328,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 4",
|
||||
Value = headRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 3,
|
||||
Order = 103,
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"],
|
||||
IsSensitive = true
|
||||
},
|
||||
@ -342,8 +338,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
// todo: make sure this is wrapped somewhere else
|
||||
Value = $"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°",
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 4,
|
||||
Order = 104,
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"],
|
||||
IsSensitive = true
|
||||
},
|
||||
@ -352,8 +347,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 6",
|
||||
Value = Math.Round(maxStrain, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 5,
|
||||
Order = 105,
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
|
||||
IsSensitive = true
|
||||
},
|
||||
@ -362,8 +356,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 7",
|
||||
Value = Math.Round(averageSnapValue, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 6,
|
||||
Order = 106,
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"],
|
||||
IsSensitive = true
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ namespace SharedLibraryCore
|
||||
private static string SocialTitle;
|
||||
protected readonly DatabaseContext Context;
|
||||
protected List<Page> Pages;
|
||||
protected List<string> PermissionsSet;
|
||||
|
||||
public BaseController(IManager manager)
|
||||
{
|
||||
@ -43,7 +44,6 @@ namespace SharedLibraryCore
|
||||
SocialTitle = AppConfig.SocialLinkTitle;
|
||||
}
|
||||
|
||||
|
||||
Pages = Manager.GetPageList().Pages
|
||||
.Select(page => new Page
|
||||
{
|
||||
@ -130,11 +130,16 @@ namespace SharedLibraryCore
|
||||
new Claim(ClaimTypes.NameIdentifier, Client.CurrentAlias.Name),
|
||||
new Claim(ClaimTypes.Role, Client.Level.ToString()),
|
||||
new Claim(ClaimTypes.Sid, Client.ClientId.ToString()),
|
||||
new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X"))
|
||||
new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")),
|
||||
};
|
||||
var claimsIdentity = new ClaimsIdentity(claims, "login");
|
||||
SignInAsync(new ClaimsPrincipal(claimsIdentity)).Wait();
|
||||
}
|
||||
|
||||
if (AppConfig.PermissionSets.ContainsKey(Client.Level.ToString()))
|
||||
{
|
||||
PermissionsSet = AppConfig.PermissionSets[Client.Level.ToString()];
|
||||
}
|
||||
|
||||
var communityName = AppConfig.CommunityInformation?.Name;
|
||||
var shouldUseCommunityName = !string.IsNullOrWhiteSpace(communityName)
|
||||
@ -156,8 +161,16 @@ namespace SharedLibraryCore
|
||||
ViewBag.EnablePrivilegedUserPrivacy = AppConfig.EnablePrivilegedUserPrivacy;
|
||||
ViewBag.Configuration = AppConfig;
|
||||
ViewBag.ScriptInjection = AppConfig.Webfront?.ScriptInjection;
|
||||
ViewBag.CommunityInformation = AppConfig.CommunityInformation;
|
||||
ViewBag.ClientCount = Manager.GetServers().Sum(server => server.ClientNum);
|
||||
ViewBag.AdminCount = Manager.GetServers().Sum(server =>
|
||||
server.GetClientsAsList()
|
||||
.Count(client => client.Level >= Data.Models.Client.EFClient.Permission.Trusted));
|
||||
ViewBag.ReportCount = Manager.GetServers().Sum(server =>
|
||||
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
|
||||
ViewBag.PermissionsSet = PermissionsSet;
|
||||
|
||||
base.OnActionExecuting(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ namespace SharedLibraryCore.Configuration
|
||||
public bool EnableWebfrontConnectionWhitelist { get; set; }
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_WHITELIST_LIST")]
|
||||
public string[] WebfrontConnectionWhitelist { get; set; } = new string[0];
|
||||
public string[] WebfrontConnectionWhitelist { get; set; } = Array.Empty<string>();
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
|
||||
[ConfigurationLinked("CustomLocale")]
|
||||
@ -115,20 +115,20 @@ namespace SharedLibraryCore.Configuration
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENABLE_COLOR_CODES")]
|
||||
public bool EnableColorCodes { get; set; }
|
||||
|
||||
|
||||
[ConfigurationIgnore] public string IngameAccentColorKey { get; set; } = "Cyan";
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_AUTOMESSAGE_PERIOD")]
|
||||
public int AutoMessagePeriod { get; set; }
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_AUTOMESSAGES")]
|
||||
public string[] AutoMessages { get; set; } = new string[0];
|
||||
public string[] AutoMessages { get; set; } = Array.Empty<string>();
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_GLOBAL_RULES")]
|
||||
public string[] GlobalRules { get; set; } = new string[0];
|
||||
public string[] GlobalRules { get; set; } = Array.Empty<string>();
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_DISALLOWED_NAMES")]
|
||||
public string[] DisallowedClientNames { get; set; } = new string[0];
|
||||
public string[] DisallowedClientNames { get; set; } = Array.Empty<string>();
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_MAP_CHANGE_DELAY")]
|
||||
public int MapChangeDelaySeconds { get; set; } = 5;
|
||||
@ -144,9 +144,19 @@ namespace SharedLibraryCore.Configuration
|
||||
TimeSpan.FromDays(30)
|
||||
};
|
||||
|
||||
public Dictionary<string, List<string>> PermissionSets { get; set; } = new()
|
||||
{
|
||||
{ Permission.Trusted.ToString(), new List<string> { "*" } },
|
||||
{ Permission.Moderator.ToString(), new List<string> { "*" } },
|
||||
{ Permission.Administrator.ToString(), new List<string> { "*" } },
|
||||
{ Permission.SeniorAdmin.ToString(), new List<string> { "*" } },
|
||||
{ Permission.Owner.ToString(), new List<string> { "*" } },
|
||||
{ Permission.Console.ToString(), new List<string> { "*" } }
|
||||
};
|
||||
|
||||
[ConfigurationIgnore]
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
|
||||
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new Dictionary<string, string>
|
||||
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()
|
||||
{ { "afk", "Away from keyboard" }, { "ci", "Connection interrupted. Reconnect" } };
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENABLE_PRIVILEGED_USER_PRIVACY")]
|
||||
@ -188,7 +198,7 @@ namespace SharedLibraryCore.Configuration
|
||||
: ManualWebfrontUrl;
|
||||
|
||||
[ConfigurationIgnore] public bool IgnoreServerConnectionLost { get; set; }
|
||||
[ConfigurationIgnore] public Uri MasterUrl { get; set; } = new Uri("http://api.raidmax.org:5000");
|
||||
[ConfigurationIgnore] public Uri MasterUrl { get; set; } = new("http://api.raidmax.org:5000");
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
@ -197,22 +207,7 @@ namespace SharedLibraryCore.Configuration
|
||||
|
||||
EnableWebFront = loc["SETUP_ENABLE_WEBFRONT"].PromptBool();
|
||||
EnableMultipleOwners = loc["SETUP_ENABLE_MULTIOWN"].PromptBool();
|
||||
EnableSteppedHierarchy = loc["SETUP_ENABLE_STEPPEDPRIV"].PromptBool();
|
||||
EnableCustomSayName = loc["SETUP_ENABLE_CUSTOMSAY"].PromptBool();
|
||||
|
||||
var useCustomParserEncoding = loc["SETUP_USE_CUSTOMENCODING"].PromptBool();
|
||||
if (useCustomParserEncoding)
|
||||
{
|
||||
CustomParserEncoding = loc["SETUP_ENCODING_STRING"].PromptString();
|
||||
}
|
||||
|
||||
WebfrontBindUrl = "http://0.0.0.0:1624";
|
||||
|
||||
if (EnableCustomSayName)
|
||||
{
|
||||
CustomSayName = loc["SETUP_SAY_NAME"].PromptString();
|
||||
}
|
||||
|
||||
EnableSocialLink = loc["SETUP_DISPLAY_SOCIAL"].PromptBool();
|
||||
|
||||
if (EnableSocialLink)
|
||||
|
@ -11,11 +11,8 @@ namespace SharedLibraryCore.Dtos
|
||||
|
||||
public class ClientCountSnapshot
|
||||
{
|
||||
private const int UpdateInterval = 5;
|
||||
public DateTime Time { get; set; }
|
||||
public string TimeString => new DateTime(Time.Year, Time.Month, Time.Day, Time.Hour,
|
||||
Math.Min(59, UpdateInterval * (int)Math.Round(Time.Minute / (float)UpdateInterval)), 0)
|
||||
.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
public string TimeString => Time.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
public int ClientCount { get; set; }
|
||||
public bool ConnectionInterrupted { get;set; }
|
||||
public string Map { get; set; }
|
||||
|
@ -1,12 +1,15 @@
|
||||
using Data.Models.Client;
|
||||
using System;
|
||||
using Data.Models.Client;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
public class ClientInfo
|
||||
public class ClientInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public int LinkId { get; set; }
|
||||
public EFClient.Permission Level { get; set; }
|
||||
public DateTime LastConnection { get; set; }
|
||||
public bool IsMasked { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace SharedLibraryCore.Dtos
|
||||
public int Offset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// how many itesm to take
|
||||
/// how many items to take
|
||||
/// </summary>
|
||||
public int Count { get; set; } = 100;
|
||||
|
||||
@ -35,4 +35,4 @@ namespace SharedLibraryCore.Dtos
|
||||
Ascending,
|
||||
Descending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ namespace SharedLibraryCore.Dtos
|
||||
public int LevelInt { get; set; }
|
||||
public string IPAddress { get; set; }
|
||||
public long NetworkId { get; set; }
|
||||
public List<string> Aliases { get; set; }
|
||||
public List<string> IPs { get; set; }
|
||||
public List<(string, DateTime)> Aliases { get; set; }
|
||||
public List<(string, DateTime)> IPs { get; set; }
|
||||
public bool HasActivePenalty { get; set; }
|
||||
public string ActivePenaltyType { get; set; }
|
||||
public bool Authenticated { get; set; }
|
||||
@ -29,5 +29,8 @@ namespace SharedLibraryCore.Dtos
|
||||
public IDictionary<int, long> LinkedAccounts { get; set; }
|
||||
public MetaType? MetaFilterType { get; set; }
|
||||
public double? ZScore { get; set; }
|
||||
public string ConnectProtocolUrl { get;set; }
|
||||
public string CurrentServerName { get; set; }
|
||||
public IGeoLocationResult GeoLocationInfo { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,13 @@ namespace SharedLibraryCore.Dtos
|
||||
public int MaxClients { get; set; }
|
||||
public List<ChatInfo> ChatHistory { get; set; }
|
||||
public List<PlayerInfo> Players { get; set; }
|
||||
public List<Report> Reports { get; set; }
|
||||
public ClientHistoryInfo ClientHistory { get; set; }
|
||||
public long ID { get; set; }
|
||||
public bool Online { get; set; }
|
||||
public string ConnectProtocolUrl { get; set; }
|
||||
public string IPAddress { get; set; }
|
||||
public string ExternalIPAddress { get; set; }
|
||||
public bool IsPasswordProtected { get; set; }
|
||||
public string Endpoint => $"{IPAddress}:{Port}";
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using System;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
|
||||
namespace SharedLibraryCore.Helpers
|
||||
{
|
||||
@ -7,5 +8,6 @@ namespace SharedLibraryCore.Helpers
|
||||
public EFClient Target { get; set; }
|
||||
public EFClient Origin { get; set; }
|
||||
public string Reason { get; set; }
|
||||
public DateTime ReportedOn { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
SharedLibraryCore/Interfaces/IGeoLocationResult.cs
Normal file
11
SharedLibraryCore/Interfaces/IGeoLocationResult.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace SharedLibraryCore.Interfaces;
|
||||
|
||||
public interface IGeoLocationResult
|
||||
{
|
||||
string Country { get; set; }
|
||||
string CountryCode { get; set; }
|
||||
string Region { get; set; }
|
||||
string ASN { get; set; }
|
||||
string Timezone { get; set; }
|
||||
string Organization { get; set; }
|
||||
}
|
8
SharedLibraryCore/Interfaces/IGeoLocationService.cs
Normal file
8
SharedLibraryCore/Interfaces/IGeoLocationService.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces;
|
||||
|
||||
public interface IGeoLocationService
|
||||
{
|
||||
Task<IGeoLocationResult> Locate(string address);
|
||||
}
|
@ -55,6 +55,8 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default);
|
||||
|
||||
void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// set value of DVAR by name
|
||||
@ -65,6 +67,8 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default);
|
||||
|
||||
void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// executes a console command on the server
|
||||
|
@ -626,6 +626,8 @@ namespace SharedLibraryCore.Database.Models
|
||||
Utilities.DefaultLogger.LogInformation("Client {client} is joining the game from {source}", ToString(),
|
||||
ipAddress.HasValue ? "Status" : "Log");
|
||||
|
||||
GameName = (Reference.Game)CurrentServer.GameName;
|
||||
|
||||
if (ipAddress != null)
|
||||
{
|
||||
IPAddress = ipAddress;
|
||||
|
@ -117,7 +117,7 @@ namespace SharedLibraryCore
|
||||
|
||||
public int ClientNum
|
||||
{
|
||||
get { return Clients.ToArray().Count(p => p != null && !p.IsBot); }
|
||||
get { return Clients.ToArray().Count(p => p != null && Utilities.IsDevelopment || (!p?.IsBot ?? false)); }
|
||||
}
|
||||
|
||||
public int MaxClients { get; protected set; }
|
||||
@ -222,7 +222,7 @@ namespace SharedLibraryCore
|
||||
public GameEvent Broadcast(string message, EFClient sender = null)
|
||||
{
|
||||
var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Say ?? "",
|
||||
$"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping)}");
|
||||
$"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message}");
|
||||
ServerLogger.LogDebug("All-> {Message}",
|
||||
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors());
|
||||
|
||||
@ -270,8 +270,6 @@ namespace SharedLibraryCore
|
||||
/// <param name="targetClient">EFClient to send message to</param>
|
||||
protected async Task Tell(string message, EFClient targetClient)
|
||||
{
|
||||
var engineMessage = message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping);
|
||||
|
||||
if (!Utilities.IsDevelopment)
|
||||
{
|
||||
var temporalClientId = targetClient.GetAdditionalProperty<string>("ConnectionClientId");
|
||||
@ -280,7 +278,7 @@ namespace SharedLibraryCore
|
||||
|
||||
var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Tell,
|
||||
clientNumber,
|
||||
$"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{engineMessage}");
|
||||
$"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message}");
|
||||
if (targetClient.ClientNumber > -1 && message.Length > 0 &&
|
||||
targetClient.Level != Data.Models.Client.EFClient.Permission.Console)
|
||||
{
|
||||
@ -296,13 +294,14 @@ namespace SharedLibraryCore
|
||||
if (targetClient.Level == Data.Models.Client.EFClient.Permission.Console)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping)
|
||||
.StripColors();
|
||||
using (LogContext.PushProperty("Server", ToString()))
|
||||
{
|
||||
ServerLogger.LogInformation("Command output received: {Message}",
|
||||
engineMessage.StripColors());
|
||||
ServerLogger.LogInformation("Command output received: {Message}", cleanMessage);
|
||||
}
|
||||
|
||||
Console.WriteLine(engineMessage.StripColors());
|
||||
Console.WriteLine(cleanMessage);
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
}
|
||||
}
|
||||
|
@ -47,13 +47,15 @@ namespace SharedLibraryCore.Services
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IGeoLocationService _geoLocationService;
|
||||
|
||||
public ClientService(ILogger<ClientService> logger, IDatabaseContextFactory databaseContextFactory,
|
||||
ApplicationConfiguration appConfig)
|
||||
ApplicationConfiguration appConfig, IGeoLocationService geoLocationService)
|
||||
{
|
||||
_contextFactory = databaseContextFactory;
|
||||
_logger = logger;
|
||||
_appConfig = appConfig;
|
||||
_geoLocationService = geoLocationService;
|
||||
}
|
||||
|
||||
public async Task<EFClient> Create(EFClient entity)
|
||||
@ -101,7 +103,8 @@ namespace SharedLibraryCore.Services
|
||||
Level = Permission.User,
|
||||
FirstConnection = DateTime.UtcNow,
|
||||
LastConnection = DateTime.UtcNow,
|
||||
NetworkId = entity.NetworkId
|
||||
NetworkId = entity.NetworkId,
|
||||
GameName = (Reference.Game)entity.CurrentServer.GameName
|
||||
};
|
||||
|
||||
_logger.LogDebug("[create] adding {entity} to context", entity.ToString());
|
||||
@ -281,6 +284,8 @@ namespace SharedLibraryCore.Services
|
||||
entity.PasswordSalt = temporalClient.PasswordSalt;
|
||||
}
|
||||
|
||||
entity.GameName ??= temporalClient.GameName;
|
||||
|
||||
// update in database
|
||||
await context.SaveChangesAsync();
|
||||
return entity.ToPartialClient();
|
||||
@ -355,7 +360,8 @@ namespace SharedLibraryCore.Services
|
||||
Level = Permission.User,
|
||||
FirstConnection = DateTime.UtcNow,
|
||||
LastConnection = DateTime.UtcNow,
|
||||
NetworkId = entity.NetworkId
|
||||
NetworkId = entity.NetworkId,
|
||||
GameName = (Reference.Game)entity.CurrentServer.GameName
|
||||
};
|
||||
|
||||
if (existingAlias == null)
|
||||
@ -782,7 +788,8 @@ namespace SharedLibraryCore.Services
|
||||
Password = client.Password,
|
||||
PasswordSalt = client.PasswordSalt,
|
||||
NetworkId = client.NetworkId,
|
||||
LastConnection = client.LastConnection
|
||||
LastConnection = client.LastConnection,
|
||||
Masked = client.Masked
|
||||
};
|
||||
|
||||
return await iqClients.ToListAsync();
|
||||
@ -791,7 +798,7 @@ namespace SharedLibraryCore.Services
|
||||
public async Task<IList<PlayerInfo>> FindClientsByIdentifier(string identifier)
|
||||
{
|
||||
var trimmedIdentifier = identifier?.Trim();
|
||||
if (trimmedIdentifier?.Length < _appConfig.MinimumNameLength)
|
||||
if (trimmedIdentifier == null || trimmedIdentifier.Length < _appConfig.MinimumNameLength)
|
||||
{
|
||||
return new List<PlayerInfo>();
|
||||
}
|
||||
@ -812,7 +819,7 @@ namespace SharedLibraryCore.Services
|
||||
var iqLinkIds = context.Aliases.Where(_alias => _alias.Active);
|
||||
|
||||
// we want to query for the IP Address
|
||||
if (ipAddress != null)
|
||||
if (ipAddress != null && trimmedIdentifier.Split('.').Length == 3)
|
||||
{
|
||||
iqLinkIds = iqLinkIds.Where(_alias => _alias.IPAddress == ipAddress);
|
||||
}
|
||||
@ -821,7 +828,7 @@ namespace SharedLibraryCore.Services
|
||||
else
|
||||
{
|
||||
iqLinkIds = iqLinkIds.Where(_alias => EF.Functions.Like(_alias.SearchableName ?? _alias.Name.ToLower(),
|
||||
$"%{trimmedIdentifier.ToLower()}%"));
|
||||
$"%{trimmedIdentifier.ToLower()}%") || EF.Functions.Like(_alias.SearchableIPAddress, $"{trimmedIdentifier}%"));
|
||||
}
|
||||
|
||||
var linkIds = await iqLinkIds
|
||||
@ -858,7 +865,7 @@ namespace SharedLibraryCore.Services
|
||||
LastConnection = _client.LastConnection,
|
||||
ClientId = _client.ClientId,
|
||||
IPAddress = _client.CurrentAlias.IPAddress.HasValue
|
||||
? _client.CurrentAlias.IPAddress.Value.ToString()
|
||||
? _client.CurrentAlias.SearchableIPAddress
|
||||
: ""
|
||||
});
|
||||
|
||||
@ -895,24 +902,32 @@ namespace SharedLibraryCore.Services
|
||||
/// gets the 10 most recently added clients to IW4MAdmin
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<PlayerInfo>> GetRecentClients()
|
||||
public async Task<IList<PlayerInfo>> GetRecentClients(PaginationRequest request)
|
||||
{
|
||||
var startOfPeriod = DateTime.UtcNow.AddHours(-24);
|
||||
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
var iqClients = context.Clients
|
||||
.Where(_client => _client.CurrentAlias.IPAddress != null)
|
||||
.Where(_client => _client.FirstConnection >= startOfPeriod)
|
||||
.OrderByDescending(_client => _client.FirstConnection)
|
||||
.Select(_client => new PlayerInfo
|
||||
.Where(client => client.CurrentAlias.IPAddress != null)
|
||||
.Where(client => client.FirstConnection >= startOfPeriod)
|
||||
.OrderByDescending(client => client.FirstConnection)
|
||||
.Select(client => new PlayerInfo
|
||||
{
|
||||
ClientId = _client.ClientId,
|
||||
Name = _client.CurrentAlias.Name,
|
||||
IPAddress = _client.CurrentAlias.IPAddress.ConvertIPtoString(),
|
||||
LastConnection = _client.FirstConnection
|
||||
});
|
||||
ClientId = client.ClientId,
|
||||
Name = client.CurrentAlias.Name,
|
||||
IPAddress = client.CurrentAlias.IPAddress.ConvertIPtoString(),
|
||||
LastConnection = client.FirstConnection
|
||||
})
|
||||
.Skip(request.Offset)
|
||||
.Take(request.Count);
|
||||
|
||||
return await iqClients.ToListAsync();
|
||||
var clientList = await iqClients.ToListAsync();
|
||||
foreach (var client in clientList)
|
||||
{
|
||||
client.GeoLocationInfo = await _geoLocationService.Locate(client.IPAddress);
|
||||
}
|
||||
|
||||
return clientList;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -154,6 +154,7 @@ namespace SharedLibraryCore
|
||||
}
|
||||
|
||||
str = Regex.Replace(str, @"(\^+((?![a-z]|[A-Z]).){0,1})+", "");
|
||||
str = Regex.Replace(str, @"\(Color::(.{1,16})\)", "");
|
||||
return str;
|
||||
}
|
||||
|
||||
@ -183,7 +184,7 @@ namespace SharedLibraryCore
|
||||
output = output.Replace(match.Value, mapping.TryGetValue(key, out var code) ? code : "");
|
||||
}
|
||||
|
||||
return output.FixIW4ForwardSlash() + mapping[ColorCodes.White.ToString()];
|
||||
return output.FixIW4ForwardSlash();
|
||||
}
|
||||
|
||||
private static readonly IList<string> _zmGameTypes = new[] { "zclassic", "zstandard", "zcleansed", "zgrief" };
|
||||
@ -525,6 +526,49 @@ namespace SharedLibraryCore
|
||||
return new TimeSpan(1, 0, 0);
|
||||
}
|
||||
|
||||
public static bool HasPermission<TEntity, TPermission>(this IEnumerable<string> permissionsSet, TEntity entity,
|
||||
TPermission permission) where TEntity : Enum where TPermission : Enum
|
||||
{
|
||||
if (permissionsSet == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var requiredPermission = $"{entity.ToString()}.{permission.ToString()}";
|
||||
var hasAllPermissions = permissionsSet.Any(p => p.Equals("*"));
|
||||
var permissionCheckResult = permissionsSet.Select(p =>
|
||||
{
|
||||
if (p.Equals(requiredPermission, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (p.Equals($"-{requiredPermission}", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool?)null;
|
||||
}).ToList();
|
||||
|
||||
var permissionNegated = permissionCheckResult.Any(result => result.HasValue && !result.Value);
|
||||
|
||||
if (permissionNegated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasAllPermissions || permissionCheckResult.Any(result => result.HasValue && result.Value);
|
||||
}
|
||||
|
||||
public static bool HasPermission<TEntity, TPermission>(this ApplicationConfiguration appConfig,
|
||||
Permission permissionLevel, TEntity entity,
|
||||
TPermission permission) where TEntity : Enum where TPermission : Enum
|
||||
{
|
||||
return appConfig.PermissionSets.ContainsKey(permissionLevel.ToString()) &&
|
||||
HasPermission(appConfig.PermissionSets[permissionLevel.ToString()], entity, permission);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns a list of penalty types that should be shown across all profiles
|
||||
/// </summary>
|
||||
@ -729,6 +773,11 @@ namespace SharedLibraryCore
|
||||
{
|
||||
return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token);
|
||||
}
|
||||
|
||||
public static void BeginGetDvar(this Server server, string dvarName, AsyncCallback callback, CancellationToken token = default)
|
||||
{
|
||||
server.RconParser.BeginGetDvar(server.RemoteConnection, dvarName, callback, token);
|
||||
}
|
||||
|
||||
public static async Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName,
|
||||
T fallbackValue = default)
|
||||
@ -764,6 +813,12 @@ namespace SharedLibraryCore
|
||||
{
|
||||
await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token);
|
||||
}
|
||||
|
||||
public static void BeginSetDvar(this Server server, string dvarName, object dvarValue,
|
||||
AsyncCallback callback, CancellationToken token = default)
|
||||
{
|
||||
server.RconParser.BeginSetDvar(server.RemoteConnection, dvarName, dvarValue, callback, token);
|
||||
}
|
||||
|
||||
public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue)
|
||||
{
|
||||
@ -780,9 +835,17 @@ namespace SharedLibraryCore
|
||||
return await ExecuteCommandAsync(server, commandName, default);
|
||||
}
|
||||
|
||||
public static Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token)
|
||||
public static async Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token)
|
||||
{
|
||||
return server.RconParser.GetStatusAsync(server.RemoteConnection, token);
|
||||
try
|
||||
{
|
||||
return await server.RconParser.GetStatusAsync(server.RemoteConnection, token);
|
||||
}
|
||||
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -80,7 +80,7 @@ namespace WebfrontCore.Controllers.API
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> LoginAsync([FromRoute] int clientId,
|
||||
public async Task<IActionResult> Login([FromRoute] int clientId,
|
||||
[FromBody, Required] PasswordRequest request)
|
||||
{
|
||||
if (clientId == 0)
|
||||
@ -145,7 +145,7 @@ namespace WebfrontCore.Controllers.API
|
||||
[HttpPost("{clientId:int}/logout")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> LogoutAsync()
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
if (Authorized)
|
||||
{
|
||||
@ -170,4 +170,4 @@ namespace WebfrontCore.Controllers.API
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
WebfrontCore/Controllers/API/PenaltyController.cs
Normal file
25
WebfrontCore/Controllers/API/PenaltyController.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using WebfrontCore.QueryHelpers.Models;
|
||||
|
||||
namespace WebfrontCore.Controllers.API;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
public class PenaltyController : BaseController
|
||||
{
|
||||
private readonly IResourceQueryHelper<BanInfoRequest, BanInfo> _banInfoQueryHelper;
|
||||
|
||||
public PenaltyController(IManager manager, IResourceQueryHelper<BanInfoRequest, BanInfo> banInfoQueryHelper) : base(manager)
|
||||
{
|
||||
_banInfoQueryHelper = banInfoQueryHelper;
|
||||
}
|
||||
|
||||
[HttpGet("BanInfo/{clientName}")]
|
||||
public async Task<IActionResult> BanInfo(BanInfoRequest request)
|
||||
{
|
||||
var result = await _banInfoQueryHelper.QueryResource(request);
|
||||
return Json(result);
|
||||
}
|
||||
}
|
@ -19,11 +19,11 @@ namespace WebfrontCore.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> LoginAsync(int clientId, string password)
|
||||
public async Task<IActionResult> Login(int clientId, string password)
|
||||
{
|
||||
if (clientId == 0 || string.IsNullOrEmpty(password))
|
||||
{
|
||||
return Unauthorized();
|
||||
return Unauthorized("Invalid credentials");
|
||||
}
|
||||
|
||||
try
|
||||
@ -63,20 +63,20 @@ namespace WebfrontCore.Controllers
|
||||
: HttpContext.Connection.RemoteIpAddress.ToString()
|
||||
});
|
||||
|
||||
return Ok();
|
||||
return Ok($"Welcome {privilegedClient.Name}. You are now logged in");
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception)
|
||||
{
|
||||
return Unauthorized();
|
||||
return Unauthorized("Could not validate credentials");
|
||||
}
|
||||
|
||||
return Unauthorized();
|
||||
return Unauthorized("Invalid credentials");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> LogoutAsync()
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
if (Authorized)
|
||||
{
|
||||
|
@ -9,7 +9,9 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using WebfrontCore.Permissions;
|
||||
using WebfrontCore.ViewModels;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
@ -68,25 +70,25 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public IActionResult BanForm()
|
||||
{
|
||||
var info = new ActionInfo()
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_BAN_NAME"],
|
||||
Name = "Ban",
|
||||
Inputs = new List<InputInfo>()
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "Reason",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
|
||||
},
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "PresetReason",
|
||||
Type = "select",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_PRESET_REASON"],
|
||||
Values = GetPresetPenaltyReasons()
|
||||
},
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "Duration",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_DURATION"],
|
||||
@ -133,7 +135,7 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
var server = Manager.GetServers().First();
|
||||
|
||||
return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = server.EndPoint,
|
||||
command
|
||||
@ -142,13 +144,13 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public IActionResult UnbanForm()
|
||||
{
|
||||
var info = new ActionInfo()
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
|
||||
Name = "Unban",
|
||||
Inputs = new List<InputInfo>()
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "Reason",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
|
||||
@ -161,57 +163,59 @@ namespace WebfrontCore.Controllers
|
||||
return View("_ActionForm", info);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> UnbanAsync(int targetId, string Reason)
|
||||
public async Task<IActionResult> UnbanAsync(int targetId, string reason)
|
||||
{
|
||||
var server = Manager.GetServers().First();
|
||||
|
||||
return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = server.EndPoint,
|
||||
command = $"{_appConfig.CommandPrefix}{_unbanCommandName} @{targetId} {Reason}"
|
||||
command = $"{_appConfig.CommandPrefix}{_unbanCommandName} @{targetId} {reason}"
|
||||
}));
|
||||
}
|
||||
|
||||
public IActionResult LoginForm()
|
||||
{
|
||||
var login = new ActionInfo()
|
||||
var login = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_LOGIN_NAME"],
|
||||
Name = "Login",
|
||||
Inputs = new List<InputInfo>()
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "clientId",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_ID"]
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_ID"],
|
||||
Required = true
|
||||
},
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "Password",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_PASSWORD"],
|
||||
Type = "password",
|
||||
Required = true
|
||||
}
|
||||
},
|
||||
Action = "LoginAsync"
|
||||
Action = "Login"
|
||||
};
|
||||
|
||||
return View("_ActionForm", login);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> LoginAsync(int clientId, string password)
|
||||
public async Task<IActionResult> Login(int clientId, string password)
|
||||
{
|
||||
return await Task.FromResult(RedirectToAction("LoginAsync", "Account", new {clientId, password}));
|
||||
return await Task.FromResult(RedirectToAction("Login", "Account", new {clientId, password}));
|
||||
}
|
||||
|
||||
public IActionResult EditForm()
|
||||
{
|
||||
var info = new ActionInfo()
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_EDIT"],
|
||||
Name = "Edit",
|
||||
Inputs = new List<InputInfo>()
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "level",
|
||||
Label = Localization["WEBFRONT_PROFILE_LEVEL"],
|
||||
@ -234,7 +238,7 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
var server = Manager.GetServers().First();
|
||||
|
||||
return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = server.EndPoint,
|
||||
command = $"{_appConfig.CommandPrefix}{_setLevelCommandName} @{targetId} {level}"
|
||||
@ -243,7 +247,7 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public IActionResult GenerateLoginTokenForm()
|
||||
{
|
||||
var info = new ActionInfo()
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_GENERATE_TOKEN"],
|
||||
Name = "GenerateLoginToken",
|
||||
@ -266,19 +270,19 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public IActionResult ChatForm(long id)
|
||||
{
|
||||
var info = new ActionInfo()
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_SUBMIT_MESSAGE"],
|
||||
Name = "Chat",
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "message",
|
||||
Type = "text",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]
|
||||
},
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "id",
|
||||
Value = id.ToString(),
|
||||
@ -293,7 +297,7 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public async Task<IActionResult> ChatAsync(long id, string message)
|
||||
{
|
||||
var server = Manager.GetServers().First(_server => _server.EndPoint == id);
|
||||
var server = Manager.GetServers().First(server => server.EndPoint == id);
|
||||
|
||||
server.ChatHistory.Add(new SharedLibraryCore.Dtos.ChatInfo()
|
||||
{
|
||||
@ -304,33 +308,55 @@ namespace WebfrontCore.Controllers
|
||||
Time = DateTime.Now
|
||||
});
|
||||
|
||||
return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = server.EndPoint,
|
||||
command = $"{_appConfig.CommandPrefix}{_sayCommandName} {message}"
|
||||
}));
|
||||
}
|
||||
|
||||
public async Task<IActionResult> RecentClientsForm()
|
||||
public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
|
||||
{
|
||||
var clients = await Manager.GetClientService().GetRecentClients();
|
||||
return View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
|
||||
ViewBag.First = request.Offset == 0;
|
||||
|
||||
if (request.Count > 20)
|
||||
{
|
||||
request.Count = 20;
|
||||
}
|
||||
|
||||
var clients = await Manager.GetClientService().GetRecentClients(request);
|
||||
|
||||
return request.Offset == 0
|
||||
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
|
||||
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
|
||||
}
|
||||
|
||||
public IActionResult RecentReportsForm()
|
||||
{
|
||||
var serverInfo = Manager.GetServers().Select(server =>
|
||||
new ServerInfo
|
||||
{
|
||||
Name = server.Hostname,
|
||||
Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24).ToList()
|
||||
});
|
||||
|
||||
return View("Partials/_Reports", serverInfo);
|
||||
}
|
||||
|
||||
public IActionResult FlagForm()
|
||||
{
|
||||
var info = new ActionInfo()
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_FLAG_NAME"],
|
||||
Name = "Flag",
|
||||
Inputs = new List<InputInfo>()
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "reason",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
|
||||
},
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "PresetReason",
|
||||
Type = "select",
|
||||
@ -349,7 +375,7 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
var server = Manager.GetServers().First();
|
||||
|
||||
return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = server.EndPoint,
|
||||
command = $"{_appConfig.CommandPrefix}{_flagCommandName} @{targetId} {presetReason ?? reason}"
|
||||
@ -358,13 +384,13 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public IActionResult UnflagForm()
|
||||
{
|
||||
var info = new ActionInfo()
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNFLAG_NAME"],
|
||||
Name = "Unflag",
|
||||
Inputs = new List<InputInfo>()
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "reason",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
|
||||
@ -381,7 +407,7 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
var server = Manager.GetServers().First();
|
||||
|
||||
return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = server.EndPoint,
|
||||
command = $"{_appConfig.CommandPrefix}{_unflagCommandName} @{targetId} {reason}"
|
||||
@ -390,25 +416,25 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public IActionResult KickForm(int id)
|
||||
{
|
||||
var info = new ActionInfo()
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_KICK_NAME"],
|
||||
Name = "Kick",
|
||||
Inputs = new List<InputInfo>()
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "reason",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_REASON"],
|
||||
},
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "PresetReason",
|
||||
Type = "select",
|
||||
Label = Localization["WEBFRONT_ACTION_LABEL_PRESET_REASON"],
|
||||
Values = GetPresetPenaltyReasons()
|
||||
},
|
||||
new InputInfo()
|
||||
new()
|
||||
{
|
||||
Name = "targetId",
|
||||
Type = "hidden",
|
||||
@ -424,14 +450,14 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public async Task<IActionResult> KickAsync(int targetId, string reason, string presetReason = null)
|
||||
{
|
||||
var client = Manager.GetActiveClients().FirstOrDefault(_client => _client.ClientId == targetId);
|
||||
var client = Manager.GetActiveClients().FirstOrDefault(client => client.ClientId == targetId);
|
||||
|
||||
if (client == null)
|
||||
{
|
||||
return BadRequest(Localization["WEBFRONT_ACTION_KICK_DISCONNECT"]);
|
||||
}
|
||||
|
||||
return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = client.CurrentServer.EndPoint,
|
||||
command = $"{_appConfig.CommandPrefix}{_kickCommandName} {client.ClientNumber} {presetReason ?? reason}"
|
||||
@ -440,9 +466,9 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
|
||||
.Concat(_appConfig.GlobalRules)
|
||||
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? new string[0]))
|
||||
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
||||
.Distinct()
|
||||
.Select((value, index) => new
|
||||
.Select((value, _) => new
|
||||
{
|
||||
Value = value
|
||||
})
|
||||
@ -453,4 +479,4 @@ namespace WebfrontCore.Controllers
|
||||
})
|
||||
.ToDictionary(item => item.Value, item => item.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,13 @@ using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using Stats.Config;
|
||||
using WebfrontCore.Permissions;
|
||||
using WebfrontCore.ViewComponents;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
@ -20,14 +22,22 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
private readonly IMetaServiceV2 _metaService;
|
||||
private readonly StatsConfiguration _config;
|
||||
private readonly IGeoLocationService _geoLocationService;
|
||||
|
||||
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config) : base(manager)
|
||||
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
|
||||
IGeoLocationService geoLocationService) : base(manager)
|
||||
{
|
||||
_metaService = metaService;
|
||||
_config = config;
|
||||
_geoLocationService = geoLocationService;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ProfileAsync(int id, MetaType? metaFilterType, CancellationToken token = default)
|
||||
[Obsolete]
|
||||
public IActionResult ProfileAsync(int id, MetaType? metaFilterType,
|
||||
CancellationToken token = default) => RedirectToAction("Profile", "Client", new
|
||||
{ id, metaFilterType });
|
||||
|
||||
public async Task<IActionResult> Profile(int id, MetaType? metaFilterType, CancellationToken token = default)
|
||||
{
|
||||
var client = await Manager.GetClientService().Get(id);
|
||||
|
||||
@ -41,7 +51,8 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
var persistentMetaTask = new[]
|
||||
{
|
||||
_metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId, token),
|
||||
_metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId,
|
||||
token),
|
||||
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token)
|
||||
};
|
||||
|
||||
@ -72,6 +83,7 @@ namespace WebfrontCore.Controllers
|
||||
}
|
||||
|
||||
displayLevel = string.IsNullOrEmpty(client.Tag) ? displayLevel : $"{displayLevel} ({client.Tag})";
|
||||
var ingameClient = Manager.GetActiveClients().FirstOrDefault(c => c.ClientId == client.ClientId);
|
||||
|
||||
var clientDto = new PlayerInfo
|
||||
{
|
||||
@ -79,29 +91,38 @@ namespace WebfrontCore.Controllers
|
||||
Level = displayLevel,
|
||||
LevelInt = displayLevelInt,
|
||||
ClientId = client.ClientId,
|
||||
IPAddress = client.IPAddressString,
|
||||
IPAddress = PermissionsSet.HasPermission(WebfrontEntity.ClientIPAddress, WebfrontPermission.Read)
|
||||
? client.IPAddressString
|
||||
: null,
|
||||
NetworkId = client.NetworkId,
|
||||
Meta = new List<InformationResponse>(),
|
||||
Aliases = client.AliasLink.Children
|
||||
.Select(_alias => _alias.Name)
|
||||
.GroupBy(_alias => _alias.StripColors())
|
||||
.Select(alias => (alias.Name, alias.DateAdded))
|
||||
.GroupBy(alias => alias.Name.StripColors())
|
||||
// we want the longest "duplicate" name
|
||||
.Select(_grp => _grp.OrderByDescending(_name => _name.Length).First())
|
||||
.Distinct()
|
||||
.OrderBy(a => a)
|
||||
.ToList(),
|
||||
IPs = client.AliasLink.Children
|
||||
.Where(i => i.IPAddress != null)
|
||||
.OrderByDescending(i => i.DateAdded)
|
||||
.Select(i => i.IPAddress.ConvertIPtoString())
|
||||
.Prepend(client.CurrentAlias.IPAddress.ConvertIPtoString())
|
||||
.Select(grp => grp.OrderByDescending(item => item.Name.Length).First())
|
||||
.Distinct()
|
||||
.ToList(),
|
||||
HasActivePenalty = activePenalties.Any(_penalty => _penalty.Type != EFPenalty.PenaltyType.Flag),
|
||||
Online = Manager.GetActiveClients().FirstOrDefault(c => c.ClientId == client.ClientId) != null,
|
||||
IPs = PermissionsSet.HasPermission(WebfrontEntity.ClientIPAddress, WebfrontPermission.Read)
|
||||
? client.AliasLink.Children
|
||||
.Select(alias => (alias.IPAddress.ConvertIPtoString(), alias.DateAdded))
|
||||
.GroupBy(alias => alias.Item1)
|
||||
.Select(grp => grp.OrderByDescending(item => item.DateAdded).First())
|
||||
.Distinct()
|
||||
.ToList()
|
||||
: new List<(string, DateTime)>(),
|
||||
HasActivePenalty = activePenalties.Any(penalty => penalty.Type != EFPenalty.PenaltyType.Flag),
|
||||
Online = ingameClient != null,
|
||||
TimeOnline = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
|
||||
LinkedAccounts = client.LinkedAccounts,
|
||||
MetaFilterType = metaFilterType
|
||||
MetaFilterType = metaFilterType,
|
||||
ConnectProtocolUrl = ingameClient?.CurrentServer.EventParser.URLProtocolFormat.FormatExt(
|
||||
ingameClient.CurrentServer.ResolvedIpEndPoint.Address.IsInternal()
|
||||
? Program.Manager.ExternalIPAddress
|
||||
: ingameClient.CurrentServer.IP,
|
||||
ingameClient.CurrentServer.Port),
|
||||
CurrentServerName = ingameClient?.CurrentServer?.Hostname,
|
||||
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString)
|
||||
};
|
||||
|
||||
var meta = await _metaService.GetRuntimeMeta<InformationResponse>(new ClientPaginationRequest
|
||||
@ -124,9 +145,9 @@ namespace WebfrontCore.Controllers
|
||||
clientDto.Meta.AddRange(Authorized ? meta : meta.Where(m => !m.IsSensitive));
|
||||
|
||||
var strippedName = clientDto.Name.StripColors();
|
||||
ViewBag.Title = strippedName.Substring(strippedName.Length - 1).ToLower()[0] == 's' ?
|
||||
strippedName + "'" :
|
||||
strippedName + "'s";
|
||||
ViewBag.Title = strippedName.Substring(strippedName.Length - 1).ToLower()[0] == 's'
|
||||
? strippedName + "'"
|
||||
: strippedName + "'s";
|
||||
ViewBag.Title += " " + Localization["WEBFRONT_CLIENT_PROFILE_TITLE"];
|
||||
ViewBag.Description = $"Client information for {strippedName}";
|
||||
ViewBag.Keywords = $"IW4MAdmin, client, profile, {strippedName}";
|
||||
@ -135,13 +156,13 @@ namespace WebfrontCore.Controllers
|
||||
return View("Profile/Index", clientDto);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> PrivilegedAsync()
|
||||
public async Task<IActionResult> Privileged()
|
||||
{
|
||||
if (Manager.GetApplicationSettings().Configuration().EnablePrivilegedUserPrivacy && !Authorized)
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
|
||||
var admins = (await Manager.GetClientService().GetPrivilegedClients())
|
||||
.OrderByDescending(_client => _client.Level)
|
||||
.ThenBy(_client => _client.Name);
|
||||
@ -155,10 +176,12 @@ namespace WebfrontCore.Controllers
|
||||
adminsDict.Add(admin.Level, new List<ClientInfo>());
|
||||
}
|
||||
|
||||
adminsDict[admin.Level].Add(new ClientInfo()
|
||||
adminsDict[admin.Level].Add(new ClientInfo
|
||||
{
|
||||
Name = admin.Name,
|
||||
ClientId = admin.ClientId
|
||||
ClientId = admin.ClientId,
|
||||
LastConnection = admin.LastConnection,
|
||||
IsMasked = admin.Masked
|
||||
});
|
||||
}
|
||||
|
||||
@ -169,7 +192,7 @@ namespace WebfrontCore.Controllers
|
||||
return View("Privileged/Index", adminsDict);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> FindAsync(string clientName)
|
||||
public async Task<IActionResult> Find(string clientName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clientName))
|
||||
{
|
||||
@ -187,11 +210,13 @@ namespace WebfrontCore.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
ViewBag.Title = $"{clientsDto.Count} {Localization["WEBFRONT_CLIENT_SEARCH_MATCHING"]} \"{clientName}\"";
|
||||
ViewBag.SearchTerm = clientName;
|
||||
ViewBag.ResultCount = clientsDto.Count;
|
||||
return View("Find/Index", clientsDto);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Meta(int id, int count, int offset, long? startAt, MetaType? metaFilterType, CancellationToken token)
|
||||
public IActionResult Meta(int id, int count, int offset, long? startAt, MetaType? metaFilterType,
|
||||
CancellationToken token)
|
||||
{
|
||||
var request = new ClientPaginationRequest
|
||||
{
|
||||
@ -201,14 +226,15 @@ namespace WebfrontCore.Controllers
|
||||
Before = DateTime.FromFileTimeUtc(startAt ?? DateTime.UtcNow.ToFileTimeUtc())
|
||||
};
|
||||
|
||||
var meta = await ProfileMetaListViewComponent.GetClientMeta(_metaService, metaFilterType, Client.Level, request, token);
|
||||
|
||||
if (!meta.Any())
|
||||
return ViewComponent(typeof(ProfileMetaListViewComponent), new
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return View("Components/ProfileMetaList/_List", meta);
|
||||
clientId = request.ClientId,
|
||||
count = request.Count,
|
||||
offset = request.Offset,
|
||||
startAt = request.Before,
|
||||
metaType = metaFilterType,
|
||||
token
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,15 +42,20 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult TopPlayersAsync()
|
||||
public IActionResult TopPlayers(string serverId = null)
|
||||
{
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
|
||||
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
|
||||
ViewBag.Servers = _manager.GetServers()
|
||||
.Select(_server => new ServerInfo() {Name = _server.Hostname, ID = _server.EndPoint});
|
||||
ViewBag.Localization = _translationLookup;
|
||||
ViewBag.SelectedServerId = serverId;
|
||||
|
||||
return View("~/Views/Client/Statistics/Index.cshtml");
|
||||
return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers()
|
||||
.Select(server => new ServerInfo
|
||||
{
|
||||
Name = server.Hostname,
|
||||
IPAddress = server.IP,
|
||||
Port = server.Port
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -169,7 +174,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
|
||||
var penalty = await context.Penalties
|
||||
.Select(_penalty => new
|
||||
{_penalty.OffenderId, _penalty.PenaltyId, _penalty.When, _penalty.AutomatedOffense})
|
||||
{ _penalty.OffenderId, _penalty.PenaltyId, _penalty.When, _penalty.AutomatedOffense })
|
||||
.FirstOrDefaultAsync(_penalty => _penalty.PenaltyId == penaltyId);
|
||||
|
||||
if (penalty == null)
|
||||
|
@ -39,7 +39,7 @@ namespace WebfrontCore.Controllers
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
return View("Index", Manager.GetApplicationSettings().Configuration());
|
||||
return RedirectToAction("Files");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Files()
|
||||
@ -256,4 +256,4 @@ namespace WebfrontCore.Controllers
|
||||
.Where(_attr => _attr.GetType() == typeof(ConfigurationIgnore))
|
||||
.FirstOrDefault() as ConfigurationIgnore) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,11 +34,11 @@ namespace WebfrontCore.Controllers
|
||||
return View(activeServers);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ExecuteAsync(long serverId, string command)
|
||||
public async Task<IActionResult> Execute(long serverId, string command)
|
||||
{
|
||||
var server = Manager.GetServers().First(s => s.EndPoint == serverId);
|
||||
|
||||
var client = new EFClient()
|
||||
var client = new EFClient
|
||||
{
|
||||
ClientId = Client.ClientId,
|
||||
Level = Client.Level,
|
||||
@ -50,7 +50,7 @@ namespace WebfrontCore.Controllers
|
||||
}
|
||||
};
|
||||
|
||||
var remoteEvent = new GameEvent()
|
||||
var remoteEvent = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.Command,
|
||||
Data = command.StartsWith(_appconfig.CommandPrefix) ||
|
||||
@ -97,15 +97,15 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
response = new[]
|
||||
{
|
||||
new CommandResponseInfo()
|
||||
new CommandResponseInfo
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMADS_RESTART_SUCCESS"]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return View("_Response", response);
|
||||
|
||||
return remoteEvent.Failed ? StatusCode(400, response) : Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,49 +30,15 @@ namespace WebfrontCore.Controllers
|
||||
return View(showOnly);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ListAsync(int offset = 0, EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool hideAutomatedPenalties = true)
|
||||
public async Task<IActionResult> ListAsync(int offset = 0, int count = 30, EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool hideAutomatedPenalties = true)
|
||||
{
|
||||
return await Task.FromResult(View("_List", new ViewModels.PenaltyFilterInfo()
|
||||
return await Task.FromResult(View("_List", new ViewModels.PenaltyFilterInfo
|
||||
{
|
||||
Offset = offset,
|
||||
Count = count,
|
||||
ShowOnly = showOnly,
|
||||
IgnoreAutomated = hideAutomatedPenalties
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// retrieves all permanent bans ordered by ban date
|
||||
/// if request is authorized, it will include the client's ip address.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IActionResult> PublicAsync()
|
||||
{
|
||||
IList<PenaltyInfo> penalties;
|
||||
|
||||
await using var ctx = _contextFactory.CreateContext(false);
|
||||
var iqPenalties = ctx.Penalties
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Type == EFPenalty.PenaltyType.Ban && p.Active)
|
||||
.OrderByDescending(_penalty => _penalty.When)
|
||||
.Select(p => new PenaltyInfo()
|
||||
{
|
||||
Id = p.PenaltyId,
|
||||
OffenderId = p.OffenderId,
|
||||
OffenderName = p.Offender.CurrentAlias.Name,
|
||||
OffenderNetworkId = (ulong)p.Offender.NetworkId,
|
||||
OffenderIPAddress = Authorized ? p.Offender.CurrentAlias.IPAddress.ConvertIPtoString() : null,
|
||||
Offense = p.Offense,
|
||||
PunisherId = p.PunisherId,
|
||||
PunisherNetworkId = (ulong)p.Punisher.NetworkId,
|
||||
PunisherName = p.Punisher.CurrentAlias.Name,
|
||||
PunisherIPAddress = Authorized ? p.Punisher.CurrentAlias.IPAddress.ConvertIPtoString() : null,
|
||||
TimePunished = p.When,
|
||||
AutomatedOffense = Authorized ? p.AutomatedOffense : null,
|
||||
});
|
||||
|
||||
penalties = await iqPenalties.ToListAsync();
|
||||
|
||||
return Json(penalties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ namespace WebfrontCore.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var serverInfo = new ServerInfo()
|
||||
var serverInfo = new ServerInfo
|
||||
{
|
||||
Name = s.Hostname,
|
||||
ID = s.EndPoint,
|
||||
@ -44,7 +44,7 @@ namespace WebfrontCore.Controllers
|
||||
ClientId = p.ClientId,
|
||||
Level = p.Level.ToLocalizedLevelName(),
|
||||
LevelInt = (int) p.Level,
|
||||
ZScore = p.GetAdditionalProperty<EFClientStatistics>(IW4MAdmin.Plugins.Stats.Helpers.StatManager
|
||||
ZScore = p.GetAdditionalProperty<EFClientStatistics>(StatManager
|
||||
.CLIENT_STATS_KEY)?.ZScore
|
||||
}).ToList(),
|
||||
ChatHistory = s.ChatHistory.ToList(),
|
||||
@ -55,36 +55,39 @@ namespace WebfrontCore.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult Scoreboard()
|
||||
public ActionResult Scoreboard(string serverId)
|
||||
{
|
||||
ViewBag.Title = Localization["WEBFRONT_TITLE_SCOREBOARD"];
|
||||
ViewBag.SelectedServerId = string.IsNullOrEmpty(serverId) ? Manager.GetServers().FirstOrDefault()?.ToString() : serverId;
|
||||
|
||||
return View(ProjectScoreboard(Manager.GetServers(), null, true));
|
||||
}
|
||||
|
||||
[HttpGet("[controller]/{id}/scoreboard")]
|
||||
public ActionResult Scoreboard(long id, [FromQuery]string order = null, [FromQuery] bool down = true)
|
||||
public ActionResult Scoreboard(string id, [FromQuery]string order = null, [FromQuery] bool down = true)
|
||||
{
|
||||
var server = Manager.GetServers().FirstOrDefault(srv => srv.EndPoint == id);
|
||||
|
||||
var server = Manager.GetServers().FirstOrDefault(srv => srv.ToString() == id);
|
||||
|
||||
if (server == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ViewBag.SelectedServerId = id;
|
||||
return View("_Scoreboard", ProjectScoreboard(new[] {server}, order, down).First());
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoreboardInfo> ProjectScoreboard(IEnumerable<Server> servers, string order,
|
||||
bool down)
|
||||
{
|
||||
return servers.Select(server => new ScoreboardInfo
|
||||
return servers.Select((server, index) => new ScoreboardInfo
|
||||
{
|
||||
OrderByKey = order,
|
||||
ShouldOrderDescending = down,
|
||||
MapName = server.CurrentMap.ToString(),
|
||||
ServerName = server.Hostname,
|
||||
ServerId = server.EndPoint,
|
||||
ServerId = server.ToString(),
|
||||
ClientInfo = server.GetClientsAsList().Select(client =>
|
||||
new
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models.Client;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using static SharedLibraryCore.GameEvent;
|
||||
|
||||
namespace WebfrontCore.Middleware
|
||||
@ -81,7 +82,7 @@ namespace WebfrontCore.Middleware
|
||||
// they've been removed
|
||||
if (!_privilegedClientIds.Contains(clientId) && clientId != 1)
|
||||
{
|
||||
await context.SignOutAsync();
|
||||
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
}
|
||||
}
|
||||
|
||||
|
26
WebfrontCore/Permissions/WebfrontEntity.cs
Normal file
26
WebfrontCore/Permissions/WebfrontEntity.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace WebfrontCore.Permissions;
|
||||
|
||||
public enum WebfrontEntity
|
||||
{
|
||||
ClientIPAddress,
|
||||
ClientGuid,
|
||||
ClientLevel,
|
||||
MetaAliasUpdate,
|
||||
Penalty,
|
||||
PrivilegedClientsPage,
|
||||
HelpPage,
|
||||
ConsolePage,
|
||||
ConfigurationPage,
|
||||
AuditPage,
|
||||
RecentPlayersPage,
|
||||
ProfilePage,
|
||||
AdminMenu
|
||||
}
|
||||
|
||||
public enum WebfrontPermission
|
||||
{
|
||||
Read,
|
||||
Create,
|
||||
Update,
|
||||
Delete
|
||||
}
|
96
WebfrontCore/QueryHelpers/BanInfoResourceQueryHelper.cs
Normal file
96
WebfrontCore/QueryHelpers/BanInfoResourceQueryHelper.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using WebfrontCore.QueryHelpers.Models;
|
||||
|
||||
namespace WebfrontCore.QueryHelpers;
|
||||
|
||||
public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, BanInfo>
|
||||
{
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
public BanInfoResourceQueryHelper(IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public async Task<ResourceQueryHelperResult<BanInfo>> QueryResource(BanInfoRequest query)
|
||||
{
|
||||
if (query.Count > 30)
|
||||
{
|
||||
query.Count = 30;
|
||||
}
|
||||
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
|
||||
var matchingClients = await context.Clients.Where(client =>
|
||||
EF.Functions.ILike(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name, $"%{query.ClientName.Trim()}%"))
|
||||
.Where(client => client.Level == EFClient.Permission.Banned)
|
||||
.OrderByDescending(client => client.LastConnection)
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Count)
|
||||
.Select(client => new
|
||||
{
|
||||
client.CurrentAlias.Name,
|
||||
client.NetworkId,
|
||||
client.AliasLinkId,
|
||||
client.ClientId
|
||||
}).ToListAsync();
|
||||
|
||||
var usedIps = await context.Aliases
|
||||
.Where(alias => matchingClients.Select(client => client.AliasLinkId).Contains(alias.LinkId))
|
||||
.Where(alias => alias.IPAddress != null)
|
||||
.Select(alias => new { alias.IPAddress, alias.LinkId })
|
||||
.ToListAsync();
|
||||
|
||||
var usedIpsGrouped = usedIps
|
||||
.GroupBy(alias => alias.LinkId)
|
||||
.ToDictionary(key => key.Key, value => value.Select(alias => alias.IPAddress).Distinct());
|
||||
|
||||
var searchingNetworkIds = matchingClients.Select(client => client.NetworkId);
|
||||
var searchingIps = usedIpsGrouped.SelectMany(group => group.Value);
|
||||
|
||||
var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier =>
|
||||
searchingNetworkIds.Contains(identifier.NetworkId) ||
|
||||
searchingIps.Contains(identifier.IPv4Address))
|
||||
.Select(penalty => new
|
||||
{
|
||||
penalty.CreatedDateTime,
|
||||
PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name,
|
||||
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense) ? penalty.Penalty.Offense : "Anticheat Detection",
|
||||
LinkId = penalty.Penalty.Offender.AliasLinkId,
|
||||
penalty.Penalty.PunisherId
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var groupedPenalties = matchedPenalties.GroupBy(penalty => penalty.LinkId)
|
||||
.ToDictionary(key => key.Key, value => value.FirstOrDefault());
|
||||
|
||||
var results = matchingClients.Select(client =>
|
||||
{
|
||||
var matchedPenalty =
|
||||
groupedPenalties.ContainsKey(client.AliasLinkId) ? groupedPenalties[client.AliasLinkId] : null;
|
||||
return new BanInfo
|
||||
{
|
||||
DateTime = matchedPenalty?.CreatedDateTime,
|
||||
OffenderName = client.Name.StripColors(),
|
||||
OffenderId = client.ClientId,
|
||||
PunisherName = matchedPenalty?.PunisherName.StripColors(),
|
||||
PunisherId = matchedPenalty?.PunisherId,
|
||||
Offense = matchedPenalty?.Offense
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new ResourceQueryHelperResult<BanInfo>
|
||||
{
|
||||
RetrievedResultCount = results.Count,
|
||||
TotalResultCount = results.Count,
|
||||
Results = results
|
||||
};
|
||||
}
|
||||
}
|
12
WebfrontCore/QueryHelpers/Models/BanInfo.cs
Normal file
12
WebfrontCore/QueryHelpers/Models/BanInfo.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
public class BanInfo
|
||||
{
|
||||
public string OffenderName { get; set; }
|
||||
public int OffenderId { get; set; }
|
||||
public string PunisherName { get; set; }
|
||||
public int? PunisherId { get; set; }
|
||||
public string Offense { get; set; }
|
||||
public DateTime? DateTime { get; set; }
|
||||
public long? TimeStamp => DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null;
|
||||
}
|
8
WebfrontCore/QueryHelpers/Models/BanInfoRequest.cs
Normal file
8
WebfrontCore/QueryHelpers/Models/BanInfoRequest.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
|
||||
namespace WebfrontCore.QueryHelpers.Models;
|
||||
|
||||
public class BanInfoRequest : PaginationRequest
|
||||
{
|
||||
public string ClientName { get; set; }
|
||||
}
|
@ -24,11 +24,12 @@ using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Helpers;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
using Stats.Client.Abstractions;
|
||||
using Stats.Config;
|
||||
using WebfrontCore.Controllers.API.Validation;
|
||||
using WebfrontCore.Middleware;
|
||||
using WebfrontCore.QueryHelpers;
|
||||
using WebfrontCore.QueryHelpers.Models;
|
||||
|
||||
namespace WebfrontCore
|
||||
{
|
||||
@ -127,10 +128,13 @@ namespace WebfrontCore
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IMetaServiceV2>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ApplicationConfiguration>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ClientService>());
|
||||
services.AddSingleton<IResourceQueryHelper<BanInfoRequest, BanInfo>, BanInfoResourceQueryHelper>();
|
||||
services.AddSingleton(
|
||||
Program.ApplicationServiceProvider.GetRequiredService<IServerDistributionCalculator>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider
|
||||
.GetRequiredService<IConfigurationHandler<DefaultSettings>>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider
|
||||
.GetRequiredService<IGeoLocationService>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider
|
||||
.GetRequiredService<StatsConfiguration>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IServerDataViewer>());
|
||||
|
42
WebfrontCore/TagHelpers/HasPermission.cs
Normal file
42
WebfrontCore/TagHelpers/HasPermission.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using WebfrontCore.Permissions;
|
||||
|
||||
namespace WebfrontCore.TagHelpers;
|
||||
|
||||
[HtmlTargetElement("has-permission")]
|
||||
public class HasPermission : TagHelper
|
||||
{
|
||||
[HtmlAttributeName("entity")] public WebfrontEntity Entity { get; set; }
|
||||
|
||||
[HtmlAttributeName("required-permission")]
|
||||
public WebfrontPermission Permission { get; set; }
|
||||
|
||||
private readonly IDictionary<string, List<string>> _permissionSets;
|
||||
private readonly IHttpContextAccessor _contextAccessor;
|
||||
|
||||
public HasPermission(ApplicationConfiguration appConfig, IHttpContextAccessor contextAccessor)
|
||||
{
|
||||
_permissionSets = appConfig.PermissionSets;
|
||||
_contextAccessor = contextAccessor;
|
||||
}
|
||||
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
output.TagName = null;
|
||||
var permissionLevel = _contextAccessor?.HttpContext?.User.Claims
|
||||
.FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value;
|
||||
|
||||
var hasPermission = permissionLevel != null && _permissionSets.ContainsKey(permissionLevel) &&
|
||||
_permissionSets[permissionLevel].HasPermission(Entity, Permission);
|
||||
if (!hasPermission)
|
||||
{
|
||||
output.SuppressOutput();
|
||||
}
|
||||
}
|
||||
}
|
@ -7,11 +7,9 @@ namespace WebfrontCore.ViewComponents
|
||||
{
|
||||
public class PenaltyListViewComponent : ViewComponent
|
||||
{
|
||||
private const int PENALTY_COUNT = 15;
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(int offset, EFPenalty.PenaltyType showOnly, bool ignoreAutomated)
|
||||
public async Task<IViewComponentResult> InvokeAsync(int offset, int count, EFPenalty.PenaltyType showOnly, bool ignoreAutomated)
|
||||
{
|
||||
var penalties = await Program.Manager.GetPenaltyService().GetRecentPenalties(PENALTY_COUNT, offset, showOnly, ignoreAutomated);
|
||||
var penalties = await Program.Manager.GetPenaltyService().GetRecentPenalties(count, offset, showOnly, ignoreAutomated);
|
||||
penalties = User.Identity.IsAuthenticated ? penalties : penalties.Where(p => !p.Sensitive).ToList();
|
||||
|
||||
return View("~/Views/Penalty/PenaltyInfoList.cshtml", penalties);
|
||||
|
@ -9,21 +9,29 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using WebfrontCore.Permissions;
|
||||
|
||||
namespace WebfrontCore.ViewComponents
|
||||
{
|
||||
public class ProfileMetaListViewComponent : ViewComponent
|
||||
{
|
||||
private readonly IMetaServiceV2 _metaService;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
|
||||
public ProfileMetaListViewComponent(IMetaServiceV2 metaService)
|
||||
public ProfileMetaListViewComponent(IMetaServiceV2 metaService, ApplicationConfiguration appConfig)
|
||||
{
|
||||
_metaService = metaService;
|
||||
_appConfig = appConfig;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(int clientId, int count, int offset, DateTime? startAt, MetaType? metaType, CancellationToken token)
|
||||
public async Task<IViewComponentResult> InvokeAsync(int clientId, int count, int offset, DateTime? startAt,
|
||||
MetaType? metaType, CancellationToken token)
|
||||
{
|
||||
var level = (EFClient.Permission)Enum.Parse(typeof(EFClient.Permission), UserClaimsPrincipal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value ?? "User");
|
||||
var level = (EFClient.Permission)Enum.Parse(typeof(EFClient.Permission),
|
||||
UserClaimsPrincipal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value ??
|
||||
EFClient.Permission.User.ToString());
|
||||
|
||||
var request = new ClientPaginationRequest
|
||||
{
|
||||
@ -34,16 +42,26 @@ namespace WebfrontCore.ViewComponents
|
||||
};
|
||||
|
||||
var meta = await GetClientMeta(_metaService, metaType, level, request, token);
|
||||
ViewBag.Localization = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
|
||||
if (!meta.Any())
|
||||
{
|
||||
return Content(string.Empty);
|
||||
}
|
||||
|
||||
return View("_List", meta);
|
||||
}
|
||||
|
||||
public static async Task<IEnumerable<IClientMeta>> GetClientMeta(IMetaServiceV2 metaService, MetaType? metaType,
|
||||
private async Task<IEnumerable<IClientMeta>> GetClientMeta(IMetaServiceV2 metaService, MetaType? metaType,
|
||||
EFClient.Permission level, ClientPaginationRequest request, CancellationToken token)
|
||||
{
|
||||
IEnumerable<IClientMeta> meta = null;
|
||||
|
||||
if (!_appConfig.PermissionSets.TryGetValue(level.ToString(), out var permissionSet))
|
||||
{
|
||||
permissionSet = new List<string>();
|
||||
}
|
||||
|
||||
if (metaType is null or MetaType.All)
|
||||
{
|
||||
meta = await metaService.GetRuntimeMeta(request, token);
|
||||
@ -51,35 +69,32 @@ namespace WebfrontCore.ViewComponents
|
||||
|
||||
else
|
||||
{
|
||||
switch (metaType)
|
||||
meta = metaType switch
|
||||
{
|
||||
case MetaType.Information:
|
||||
meta = await metaService.GetRuntimeMeta<InformationResponse>(request, metaType.Value, token);
|
||||
break;
|
||||
case MetaType.AliasUpdate:
|
||||
meta = await metaService.GetRuntimeMeta<UpdatedAliasResponse>(request, metaType.Value, token);
|
||||
break;
|
||||
case MetaType.ChatMessage:
|
||||
meta = await metaService.GetRuntimeMeta<MessageResponse>(request, metaType.Value, token);
|
||||
break;
|
||||
case MetaType.Penalized:
|
||||
meta = await metaService.GetRuntimeMeta<AdministeredPenaltyResponse>(request, metaType.Value, token);
|
||||
break;
|
||||
case MetaType.ReceivedPenalty:
|
||||
meta = await metaService.GetRuntimeMeta<ReceivedPenaltyResponse>(request, metaType.Value, token);
|
||||
break;
|
||||
case MetaType.ConnectionHistory:
|
||||
meta = await metaService.GetRuntimeMeta<ConnectionHistoryResponse>(request, metaType.Value, token);
|
||||
break;
|
||||
case MetaType.PermissionLevel:
|
||||
meta = await metaService.GetRuntimeMeta<PermissionLevelChangedResponse>(request, metaType.Value, token);
|
||||
break;
|
||||
}
|
||||
MetaType.Information => await metaService.GetRuntimeMeta<InformationResponse>(request,
|
||||
metaType.Value, token),
|
||||
MetaType.AliasUpdate => permissionSet.HasPermission(WebfrontEntity.MetaAliasUpdate,
|
||||
WebfrontPermission.Read)
|
||||
? await metaService.GetRuntimeMeta<UpdatedAliasResponse>(request,
|
||||
metaType.Value, token)
|
||||
: new List<IClientMeta>(),
|
||||
MetaType.ChatMessage => await metaService.GetRuntimeMeta<MessageResponse>(request, metaType.Value,
|
||||
token),
|
||||
MetaType.Penalized => await metaService.GetRuntimeMeta<AdministeredPenaltyResponse>(request,
|
||||
metaType.Value, token),
|
||||
MetaType.ReceivedPenalty => await metaService.GetRuntimeMeta<ReceivedPenaltyResponse>(request,
|
||||
metaType.Value, token),
|
||||
MetaType.ConnectionHistory => await metaService.GetRuntimeMeta<ConnectionHistoryResponse>(request,
|
||||
metaType.Value, token),
|
||||
MetaType.PermissionLevel => await metaService.GetRuntimeMeta<PermissionLevelChangedResponse>(
|
||||
request, metaType.Value, token),
|
||||
_ => meta
|
||||
};
|
||||
}
|
||||
|
||||
if (level < EFClient.Permission.Trusted)
|
||||
{
|
||||
meta = meta.Where(_meta => !_meta.IsSensitive);
|
||||
meta = meta?.Where(_meta => !_meta.IsSensitive);
|
||||
}
|
||||
|
||||
return meta;
|
||||
|
@ -33,7 +33,8 @@ namespace WebfrontCore.ViewComponents
|
||||
{
|
||||
if (game.HasValue)
|
||||
{
|
||||
ViewBag.Maps = _defaultSettings.Maps.FirstOrDefault(map => map.Game == game);
|
||||
ViewBag.Maps = _defaultSettings.Maps.FirstOrDefault(map => map.Game == game)?.Maps.ToList() ??
|
||||
new List<Map>();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -93,8 +94,8 @@ namespace WebfrontCore.ViewComponents
|
||||
}).ToList(),
|
||||
ChatHistory = server.ChatHistory.ToList(),
|
||||
Online = !server.Throttled,
|
||||
IPAddress =
|
||||
$"{(server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP)}:{server.Port}",
|
||||
IPAddress = server.IP,
|
||||
ExternalIPAddress = server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP,
|
||||
ConnectProtocolUrl = server.EventParser.URLProtocolFormat.FormatExt(
|
||||
server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP,
|
||||
server.Port)
|
||||
|
@ -16,22 +16,16 @@ namespace WebfrontCore.ViewComponents
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(int count, int offset, long? serverId = null)
|
||||
public async Task<IViewComponentResult> InvokeAsync(int count, int offset, string serverEndpoint = null)
|
||||
{
|
||||
if (serverId == 0)
|
||||
{
|
||||
serverId = null;
|
||||
}
|
||||
|
||||
var server = Plugin.ServerManager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
|
||||
|
||||
if (server != null)
|
||||
{
|
||||
serverId = StatManager.GetIdForServer(server);
|
||||
}
|
||||
var server = Plugin.ServerManager.GetServers()
|
||||
.FirstOrDefault(server => server.ToString() == serverEndpoint);
|
||||
|
||||
var serverId = server is null ? (long?)null : StatManager.GetIdForServer(server);
|
||||
|
||||
ViewBag.UseNewStats = _config?.EnableAdvancedMetrics ?? true;
|
||||
ViewBag.SelectedServerName = server?.Hostname;
|
||||
|
||||
return View("~/Views/Client/Statistics/Components/TopPlayers/_List.cshtml",
|
||||
ViewBag.UseNewStats
|
||||
? await Plugin.Manager.GetNewTopStats(offset, count, serverId)
|
||||
|
@ -14,5 +14,6 @@ namespace WebfrontCore.ViewModels
|
||||
public string Value { get; set; }
|
||||
public Dictionary<string, string> Values { get; set; }
|
||||
public bool Checked { get; set; }
|
||||
public bool Required { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ namespace WebfrontCore.ViewModels
|
||||
/// number of items offset from the start of the list
|
||||
/// </summary>
|
||||
public int Offset { get; set; }
|
||||
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// show only a certain type of penalty
|
||||
|
@ -3,17 +3,16 @@ using SharedLibraryCore.Database.Models;
|
||||
|
||||
namespace WebfrontCore.ViewModels
|
||||
{
|
||||
|
||||
public class ScoreboardInfo
|
||||
{
|
||||
public string ServerName { get; set; }
|
||||
public long ServerId { get; set; }
|
||||
public string MapName { get; set; }
|
||||
public string OrderByKey { get; set; }
|
||||
public bool ShouldOrderDescending { get; set; }
|
||||
public List<ClientScoreboardInfo> ClientInfo { get; set; }
|
||||
public string ServerName { get; set; }
|
||||
public string ServerId { get; set; }
|
||||
public string MapName { get; set; }
|
||||
public string OrderByKey { get; set; }
|
||||
public bool ShouldOrderDescending { get; set; }
|
||||
public List<ClientScoreboardInfo> ClientInfo { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class ClientScoreboardInfo
|
||||
{
|
||||
public string ClientName { get; set; }
|
||||
|
22
WebfrontCore/ViewModels/SideContextMenuItem.cs
Normal file
22
WebfrontCore/ViewModels/SideContextMenuItem.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WebfrontCore.ViewModels;
|
||||
|
||||
public class SideContextMenuItem
|
||||
{
|
||||
public bool IsLink { get; set; }
|
||||
public bool IsButton { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Reference { get; set; }
|
||||
public string Icon { get; set; }
|
||||
public string Tooltip { get; set; }
|
||||
public int? EntityId { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class SideContextMenuItems
|
||||
{
|
||||
public string MenuTitle { get; set; }
|
||||
public List<SideContextMenuItem> Items { get; set; } = new();
|
||||
}
|
54
WebfrontCore/ViewModels/TableInfo.cs
Normal file
54
WebfrontCore/ViewModels/TableInfo.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace WebfrontCore.ViewModels;
|
||||
|
||||
public class TableInfo
|
||||
{
|
||||
public string Header { get; set; }
|
||||
public List<ColumnDefinition> Columns { get; } = new();
|
||||
public List<RowDefinition> Rows { get; } = new();
|
||||
public int InitialRowCount { get; }
|
||||
|
||||
public TableInfo(int initialRowCount = 0)
|
||||
{
|
||||
InitialRowCount = initialRowCount;
|
||||
}
|
||||
}
|
||||
|
||||
public class RowDefinition
|
||||
{
|
||||
public List<string> Datum { get; } = new();
|
||||
}
|
||||
|
||||
public class ColumnDefinition
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string ColumnSpan { get; set; }
|
||||
}
|
||||
|
||||
public static class TableInfoExtensions
|
||||
{
|
||||
public static TableInfo WithColumns(this TableInfo info, IEnumerable<string> columns)
|
||||
{
|
||||
info.Columns.AddRange(columns.Select(column => new ColumnDefinition
|
||||
{
|
||||
Title = column
|
||||
}));
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public static TableInfo WithRows<T>(this TableInfo info, IEnumerable<T> source,
|
||||
Func<T, IEnumerable<string>> selector)
|
||||
{
|
||||
info.Rows.AddRange(source.Select(row =>
|
||||
{
|
||||
var rowDef = new RowDefinition();
|
||||
rowDef.Datum.AddRange(selector(row));
|
||||
return rowDef;
|
||||
}));
|
||||
return info;
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
@using SharedLibraryCore.Configuration
|
||||
@using System.Text.RegularExpressions
|
||||
@model WebfrontCore.ViewModels.CommunityInfo
|
||||
@{
|
||||
IEnumerable<KeyValuePair<(string, long), string[]>> allRules = new[] {new KeyValuePair<(string, long), string[]>((ViewBag.Localization["WEBFRONT_ABOUT_GLOBAL_RULES"], 0), Model.GlobalRules)};
|
||||
IEnumerable<KeyValuePair<(string, long), string[]>> allRules = new[] { new KeyValuePair<(string, long), string[]>((ViewBag.Localization["WEBFRONT_ABOUT_GLOBAL_RULES"], 0), Model.GlobalRules) };
|
||||
var serverRules = Model.ServerRules?.Where(server => server.Value != null && server.Value.Any()).ToList();
|
||||
if (serverRules?.Any() ?? false)
|
||||
{
|
||||
@ -9,26 +10,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
<div class="row text-break">
|
||||
<div class="content text-wrap mt-20">
|
||||
@if (Model.CommunityInformation.EnableBanner)
|
||||
{
|
||||
<img class="img-fluid mb-3" style="max-height: 250px" src="images/community/banner.png" alt="@Model.CommunityInformation.Name"/>
|
||||
<img class="img-fluid mb-20" style="max-height: 250px" src="images/community/banner.png" alt="@Model.CommunityInformation.Name"/>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CommunityInformation.Name))
|
||||
{
|
||||
<h2 class="mb-4 p-0 col-12 text-center text-md-left">
|
||||
<h2 class="content-title">
|
||||
<color-code value="@Model.CommunityInformation.Name"></color-code>
|
||||
</h2>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CommunityInformation.Description))
|
||||
{
|
||||
<div class="p-4 bg-dark border border-primary mb-4 text-white-50 col-12">
|
||||
<h4 class="text-primary">@ViewBag.Localization["WEBFRONT_ABOUT_TITLE"]</h4>
|
||||
<color-code value="@Model.CommunityInformation.Description"></color-code>
|
||||
<div class="mt-3">
|
||||
@foreach (var social in Model.CommunityInformation.SocialAccounts ?? new SocialAccountConfiguration[0])
|
||||
|
||||
<div class="card m-0 rounded">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CommunityInformation.Description))
|
||||
{
|
||||
<h5 class="text-primary mt-0">@ViewBag.Localization["WEBFRONT_ABOUT_TITLE"]</h5>
|
||||
<div class="text-md-justify">
|
||||
<color-code value="@Model.CommunityInformation.Description"></color-code>
|
||||
</div>
|
||||
<div class="mt-10">
|
||||
@foreach (var social in Model.CommunityInformation.SocialAccounts ?? Array.Empty<SocialAccountConfiguration>())
|
||||
{
|
||||
<div>
|
||||
<a href="@social.Url" target="_blank" title="@social.Title">
|
||||
@ -38,8 +42,8 @@
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(social.IconUrl))
|
||||
{
|
||||
var url = Uri.TryCreate(social.IconUrl, UriKind.Absolute, out var parsedUrl)
|
||||
? parsedUrl.AbsoluteUri
|
||||
var url = Uri.TryCreate(social.IconUrl, UriKind.Absolute, out var parsedUrl)
|
||||
? parsedUrl.AbsoluteUri
|
||||
: $"images/community/{social.IconUrl}";
|
||||
<img class="img-fluid" style="max-width: 1rem; fill: white" src="@url" alt="@social.Title"/>
|
||||
}
|
||||
@ -48,34 +52,40 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@if (allRules.Any(rule => rule.Value.Any()))
|
||||
{
|
||||
<h2 class="pb-3 p-0 col-12 text-center text-md-left">@ViewBag.Localization["WEBFRONT_ABOUT_COMMUNITY_GUIDELINES"]</h2>
|
||||
<h2 class="content-title mt-20">@ViewBag.Localization["WEBFRONT_ABOUT_COMMUNITY_GUIDELINES"]</h2>
|
||||
}
|
||||
|
||||
@foreach (var ((serverName, id), rules) in allRules)
|
||||
{
|
||||
if (!rules.Any())
|
||||
<div class="card m-0 rounded">
|
||||
@foreach (var ((serverName, id), rules) in allRules)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!rules.Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = 1;
|
||||
<div class="col-12 bg-dark p-4 border border-primary mb-4 col-12">
|
||||
<div class="text-primary h4">
|
||||
var start = 1;
|
||||
<h5 class="text-primary mt-0">
|
||||
<color-code value="@serverName"></color-code>
|
||||
</div>
|
||||
</h5>
|
||||
@foreach (var rule in rules)
|
||||
{
|
||||
<div class="text-white-50">
|
||||
<span class="text-white">@start.</span>
|
||||
<color-code value="@rule"></color-code>
|
||||
<div class="rule">
|
||||
@if (!rule.StartsWith("#") && !Regex.IsMatch(rule, @"^\d+(.|\))"))
|
||||
{
|
||||
<span>@start.</span>
|
||||
}
|
||||
<span class="text-muted">
|
||||
<color-code value="@rule"></color-code>
|
||||
</span>
|
||||
</div>
|
||||
start++;
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user