Compare commits
56 Commits
2022.03.29
...
2022.06.02
Author | SHA1 | Date | |
---|---|---|---|
0f9f4f597b | |||
880f9333d9 | |||
31da5d352e | |||
83a469cae3 | |||
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 |
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/*
|
||||
|
||||
|
@ -25,6 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jint" Version="3.0.0-beta-2037" />
|
||||
<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,
|
||||
@ -907,9 +907,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 +922,16 @@ 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 */))
|
||||
foreach (var disconnectingClient in polledClients[1]
|
||||
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
|
||||
{
|
||||
disconnectingClient.CurrentServer = this;
|
||||
var e = new GameEvent()
|
||||
@ -946,23 +947,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 +971,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,54 +991,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 gameEvent = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.ConnectionLost,
|
||||
Owner = this,
|
||||
Origin = Utilities.IW4MAdminClient(this),
|
||||
Target = Utilities.IW4MAdminClient(this),
|
||||
Extra = e,
|
||||
Data = ConnectionErrors.ToString()
|
||||
};
|
||||
|
||||
Manager.AddEvent(gameEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
RunServerCollection();
|
||||
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;
|
||||
|
||||
RunServerCollection();
|
||||
|
||||
// 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;
|
||||
}
|
||||
@ -1052,21 +1048,23 @@ 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;
|
||||
}
|
||||
@ -1076,7 +1074,7 @@ namespace IW4MAdmin
|
||||
{
|
||||
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
|
||||
|
||||
if (lastCount - playerCountStart < appConfig?.ServerDataCollectionInterval)
|
||||
if (DateTime.Now - _lastPlayerCount < appConfig?.ServerDataCollectionInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -1097,7 +1095,7 @@ namespace IW4MAdmin
|
||||
Map = CurrentMap.Name
|
||||
});
|
||||
|
||||
playerCountStart = DateTime.Now;
|
||||
_lastPlayerCount = DateTime.Now;
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
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;
|
||||
|
@ -456,7 +456,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
private void GetDvarAsync(Server server, string dvarName, Delegate onCompleted)
|
||||
{
|
||||
Task.Run<Task>(async () =>
|
||||
Task.Run(() =>
|
||||
{
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
@ -464,14 +464,14 @@ namespace IW4MAdmin.Application.Misc
|
||||
var success = true;
|
||||
try
|
||||
{
|
||||
result = (await server.GetDvarAsync<string>(dvarName, token: tokenSource.Token)).Value;
|
||||
result = server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
success = false;
|
||||
}
|
||||
|
||||
await _onProcessing.WaitAsync();
|
||||
_onProcessing.Wait();
|
||||
try
|
||||
{
|
||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
||||
@ -495,7 +495,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
private void SetDvarAsync(Server server, string dvarName, string dvarValue, Delegate onCompleted)
|
||||
{
|
||||
Task.Run<Task>(async () =>
|
||||
Task.Run(() =>
|
||||
{
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
@ -503,14 +503,14 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
try
|
||||
{
|
||||
await server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token);
|
||||
server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
success = false;
|
||||
}
|
||||
|
||||
await _onProcessing.WaitAsync();
|
||||
_onProcessing.Wait();
|
||||
try
|
||||
{
|
||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
||||
|
@ -80,6 +80,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();
|
||||
}
|
||||
|
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";
|
||||
|
||||
@ -23,6 +23,8 @@ init()
|
||||
level.eventTypes.setClientDataCompleted = "SetClientDataCompleted";
|
||||
level.eventTypes.executeCommandRequested = "ExecuteCommandRequested";
|
||||
|
||||
level.iw4adminIntegrationDebug = false;
|
||||
|
||||
SetDvarIfUninitialized( level.eventBus.inVar, "" );
|
||||
SetDvarIfUninitialized( level.eventBus.outVar, "" );
|
||||
SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
|
||||
@ -34,17 +36,25 @@ init()
|
||||
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 call [[level.overrideMethods["god"]]]( false );
|
||||
self Show();
|
||||
|
||||
self.isHidden = false;
|
||||
|
||||
self Show();
|
||||
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,8 @@ 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
|
||||
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,6 +67,11 @@ 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();
|
||||
@ -73,14 +79,19 @@ namespace Integrations.Cod
|
||||
}
|
||||
}
|
||||
|
||||
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}",
|
||||
@ -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;
|
||||
@ -438,7 +467,7 @@ namespace Integrations.Cod
|
||||
|
||||
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);
|
||||
@ -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,7 +552,8 @@ 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)
|
||||
@ -638,7 +670,8 @@ 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,28 +24,32 @@ 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)
|
||||
{
|
||||
@ -57,7 +62,7 @@ namespace LiveRadar.Web.Controllers
|
||||
_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)
|
||||
{
|
||||
@ -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);
|
||||
}
|
||||
var server = serverId == null
|
||||
? _manager.GetServers().FirstOrDefault()
|
||||
: _manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
|
||||
|
||||
[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)
|
||||
if (server == null)
|
||||
{
|
||||
radarUpdate.Name = client.Name.StripColors();
|
||||
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||
}*/
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
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,7 +566,7 @@ const pollForEvents = server => {
|
||||
if (!state.waitingOnOutput) {
|
||||
if (state.queuedMessages.length === 0) {
|
||||
logger.WriteDebug('No messages in queue');
|
||||
return;``
|
||||
return;
|
||||
}
|
||||
|
||||
state.waitingOnOutput = true;
|
||||
|
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,12 +23,12 @@ 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) {
|
||||
vpnExceptionIds.forEach(function (id) {
|
||||
if (id === origin.ClientId) {
|
||||
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 () {
|
||||
|
@ -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,12 +130,17 @@ 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)
|
||||
&& !communityName.Contains("IW4MAdmin")
|
||||
@ -156,6 +161,14 @@ 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")]
|
||||
@ -122,13 +122,13 @@ namespace SharedLibraryCore.Configuration
|
||||
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)
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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,7 +156,7 @@ namespace WebfrontCore.Controllers
|
||||
return View("Profile/Index", clientDto);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> PrivilegedAsync()
|
||||
public async Task<IActionResult> Privileged()
|
||||
{
|
||||
if (Manager.GetApplicationSettings().Configuration().EnablePrivilegedUserPrivacy && !Authorized)
|
||||
{
|
||||
@ -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()
|
||||
|
@ -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,7 +97,7 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
response = new[]
|
||||
{
|
||||
new CommandResponseInfo()
|
||||
new CommandResponseInfo
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMADS_RESTART_SUCCESS"]
|
||||
@ -105,7 +105,7 @@ namespace WebfrontCore.Controllers
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -131,6 +131,8 @@ namespace WebfrontCore
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ namespace WebfrontCore.ViewModels
|
||||
/// </summary>
|
||||
public int Offset { get; set; }
|
||||
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// show only a certain type of penalty
|
||||
/// </summary>
|
||||
|
@ -3,15 +3,14 @@ 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
|
||||
|
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">
|
||||
@ -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>
|
||||
|
@ -1,16 +1,22 @@
|
||||
@model WebfrontCore.ViewModels.ActionInfo
|
||||
@using Humanizer
|
||||
@model WebfrontCore.ViewModels.ActionInfo
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
<h5 class="modal-title mb-10">@Model.Name.Titleize()</h5>
|
||||
@if (Model.Inputs.Any())
|
||||
{
|
||||
<hr class="mb-10"/>
|
||||
}
|
||||
<form class="action-form @(Model.ShouldRefresh ? "refreshable" : "")" action="/Action/@Model.Action">
|
||||
@foreach (var input in Model.Inputs)
|
||||
{
|
||||
string inputType = input.Type ?? "text";
|
||||
string value = input.Value ?? "";
|
||||
var inputType = input.Type ?? "text";
|
||||
var value = input.Value ?? "";
|
||||
|
||||
if (input.Type != "hidden")
|
||||
{
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group mb-10">
|
||||
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" id="basic-addon-@input.Name">@input.Label</span>
|
||||
@ -21,7 +27,9 @@
|
||||
<select name="@input.Name" class="form-control" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
|
||||
@foreach (var item in input.Values)
|
||||
{
|
||||
<option value="@item.Key">@item.Value</option>
|
||||
<option value="@item.Key">
|
||||
<color-code value="@item.Value"></color-code>
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
@ -37,7 +45,7 @@
|
||||
|
||||
else
|
||||
{
|
||||
<input type="@inputType" name="@input.Name" value="@value" class="form-control" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
|
||||
<input type="@inputType" name="@input.Name" value="@value" class="form-control @(input.Required ? "required" : "")" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
|
||||
}
|
||||
|
||||
</div>
|
||||
@ -47,5 +55,12 @@
|
||||
<input type="@inputType" name="@input.Name" value="@value" hidden="hidden">
|
||||
}
|
||||
}
|
||||
<button type="submit" class="btn btn-block btn-primary">@Model.ActionButtonLabel</button>
|
||||
@if (Model.Inputs.Any())
|
||||
{
|
||||
<hr class="mb-10"/>
|
||||
}
|
||||
<div class="ml-auto">
|
||||
<button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button>
|
||||
<a href="#" class="btn mr-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a>
|
||||
</div>
|
||||
</form>
|
@ -1,31 +1,28 @@
|
||||
@{
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
}
|
||||
<h4 class="pb-3 text-center">@ViewBag.Title</h4>
|
||||
<div class="content mt-0">
|
||||
<h2 class="content-title mt-20">@ViewBag.Title</h2>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead class="d-none d-lg-table-header-group">
|
||||
<tr class="bg-primary pt-2 pb-2">
|
||||
<table class="table">
|
||||
<thead class="d-none d-lg-table-header-group">
|
||||
<tr class="bg-primary text-light pt-2 pb-2">
|
||||
<th scope="col">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</th>
|
||||
<th scope="col">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<th scope="col">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</th>
|
||||
<th scope="col">@loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"]</th>
|
||||
<!--<th scope="col">@loc["WEBFRONT_ADMIN_AUDIT_LOG_PREVIOUS"]</th>-->
|
||||
<th scope="col">@loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"]</th>
|
||||
<th scope="col" class="text-right">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="audit_log_table_body" class="border-bottom bg-dark">
|
||||
<partial name="_ListAuditLog" />
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
<span id="load_audit_log_button" class="loader-load-more oi oi-chevron-bottom text-center text-primary w-100 h3 pb-0 mb-0 d-none d-lg-block"></span>
|
||||
</thead>
|
||||
<tbody id="audit_log_table_body" class="border-bottom bg-dark">
|
||||
<partial name="_ListAuditLog"/>
|
||||
</tbody>
|
||||
</table>
|
||||
<i id="loaderLoad" class="loader-load-more oi oi-chevron-bottom text-center text-primary d-none d-lg-block mt-10"></i>
|
||||
</div>
|
||||
|
||||
@section scripts {
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
||||
</environment>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
initLoader('/Admin/ListAuditLog', '#audit_log_table_body', @ViewBag.IntialOffset);
|
||||
|
@ -1,99 +1,71 @@
|
||||
@using SharedLibraryCore.Dtos
|
||||
@model IEnumerable<AuditInfo>
|
||||
@{
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
}
|
||||
|
||||
@foreach (var info in Model)
|
||||
{
|
||||
<!-- mobile -->
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</th>
|
||||
<td class="text-light">
|
||||
@info.Action
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<td>
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@info.OriginId" class="link-inverse">
|
||||
<color-code value="@info.OriginName"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</th>
|
||||
<td>
|
||||
@if (info.TargetId != null)
|
||||
{
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@info.TargetId" class="link-inverse">
|
||||
<color-code value="@info.TargetName"></color-code>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>--</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"]</th>
|
||||
<td class="text-light">
|
||||
@info.Data
|
||||
</td>
|
||||
</tr>
|
||||
@*<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_ADMIN_AUDIT_LOG_PREVIOUS"]</th>
|
||||
<td class="text-light">
|
||||
@info.OldValue
|
||||
</td>
|
||||
</tr>*@
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"]</th>
|
||||
<td class="text-light">
|
||||
@info.NewValue
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="w-25 bg-primary" style="border-bottom: 1px solid #222">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
<td class="text-light mb-2 border-bottom">
|
||||
@info.When.ToString()
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- desktop -->
|
||||
<tr class="d-none d-lg-table-row">
|
||||
<td class="text-light font-weight-bold">
|
||||
<tr class="d-none d-lg-table-row bg-dark-dm bg-light-lm">
|
||||
<td class="font-weight-bold">
|
||||
@info.Action
|
||||
</td>
|
||||
<td>
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@info.OriginId" class="link-inverse">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId">
|
||||
<color-code value="@info.OriginName"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@if (info.TargetId != null)
|
||||
{
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@info.TargetId" class="link-inverse">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId">
|
||||
<color-code value="@info.TargetName"></color-code>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>--</span>
|
||||
<span>–</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-light">
|
||||
@info.Data
|
||||
|
||||
@*<td class="text-light">
|
||||
@info.OldValue
|
||||
</td>*@
|
||||
<td class="text-light">
|
||||
<td>
|
||||
@info.Data
|
||||
<td >
|
||||
@info.NewValue
|
||||
</td>
|
||||
<td class="text-light text-right">
|
||||
<td class="text-right">
|
||||
@info.When.ToString()
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- mobile -->
|
||||
<tr class="d-table-row d-lg-none d-flex bg-dark-dm bg-light-lm">
|
||||
<td class="bg-primary text-light text-right flex-grow-0">
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="mt-5 mb-5">@info.Action</div>
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
|
||||
<color-code value="@info.OriginName"></color-code>
|
||||
</a>
|
||||
@if (info.TargetId != null)
|
||||
{
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId" class="mt-5 mb-5">
|
||||
<color-code value="@info.TargetName"></color-code>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mt-5 mb-5">–</div>
|
||||
}
|
||||
<div class="mt-5 mb-5"> @info.Data</div>
|
||||
<div class="mt-5 mb-5">@info.NewValue</div>
|
||||
<div class="mt-5 mb-5">@info.When.ToString()</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -3,60 +3,72 @@
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
}
|
||||
|
||||
<div class="row d-none d-lg-block ">
|
||||
<h4 class="pb-2 text-center col-12">@ViewBag.Title</h4>
|
||||
<div class="mr-auto ml-auto col-12 col-lg-8 border-bottom">
|
||||
<div class="row pt-2 pb-2 bg-primary">
|
||||
<div class="col-5 ">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
|
||||
<div class="col-4">@loc["WEBFRONT_PROFILE_LEVEL"]</div>
|
||||
<div class="col-3 text-right">@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]</div>
|
||||
</div>
|
||||
<!-- desktop -->
|
||||
<div class="content mt-0">
|
||||
<h2 class="content-title mt-20 mb-0">Search Results</h2>
|
||||
<div class="text-muted mb-15"><span class="badge">@ViewBag.SearchTerm</span> returned <span class="text-primary">@ViewBag.ResultCount</span> matche(s)</div>
|
||||
|
||||
<table class="table d-none d-md-table">
|
||||
<thead>
|
||||
<tr class="bg-primary text-light">
|
||||
<td>@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</td>
|
||||
<td>@loc["WEBFRONT_PROFILE_LEVEL"]</td>
|
||||
<td class="text-right">@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var client in Model)
|
||||
{
|
||||
<div class="row pt-2 pb-2 bg-dark">
|
||||
<div class="col-5">
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@client.ClientId">
|
||||
<tr class="bg-dark-dm bg-light-lm">
|
||||
<td class="col-5">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId">
|
||||
<color-code value="@client.Name"></color-code>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
@if (!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy)
|
||||
{
|
||||
<div class="col-4 level-color-0">@loc["GLOBAL_PERMISSION_USER"]</div>
|
||||
<td class="col-3 level-color-0">@loc["GLOBAL_PERMISSION_USER"]</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-4 level-color-@client.LevelInt">@client.Level</div>
|
||||
<td class="col-3 level-color-@client.LevelInt">@client.Level</td>
|
||||
}
|
||||
<div class="col-3 text-right">@client.LastConnectionText</div>
|
||||
</div>
|
||||
<td class="col-4 text-right">@client.LastConnection.HumanizeForCurrentCulture()</td>
|
||||
</tr>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row d-lg-none">
|
||||
<div class="w-100 bg-primary text-center h3 mb-0 p-3" style="border-bottom: 1px solid #222">@ViewBag.Title</div>
|
||||
@foreach (var client in Model)
|
||||
{
|
||||
<div class="col-5 bg-primary font-weight-bold" style="border-bottom: 1px solid #222">
|
||||
<div class="p-2">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
|
||||
<div class="p-2">@loc["WEBFRONT_PROFILE_LEVEL"]</div>
|
||||
<div class="p-2">@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]</div>
|
||||
</div>
|
||||
<div class="col-7 bg-dark border-bottom">
|
||||
<div class="p-2">
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@client.ClientId" class="link-inverse">
|
||||
<color-code value="@client.Name"></color-code>
|
||||
</a>
|
||||
</div>
|
||||
@if (!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy)
|
||||
{
|
||||
<div class="p-2 level-color-0">@loc["GLOBAL_PERMISSION_USER"]</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="p-2 level-color-@client.LevelInt">@client.Level</div>
|
||||
}
|
||||
<div class="p-2 text-white-50">@client.LastConnectionText</div>
|
||||
</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<!--- mobile -->
|
||||
<table class="table bg-dark-dm bg-light-lm d-md-none">
|
||||
<tbody>
|
||||
@foreach (var client in Model)
|
||||
{
|
||||
<tr class="d-flex">
|
||||
<td class="bg-primary text-light">
|
||||
<div>@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
|
||||
<div>@loc["WEBFRONT_PROFILE_LEVEL"]</div>
|
||||
<div>@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]</div>
|
||||
</td>
|
||||
<td class="flex-grow">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="link-inverse">
|
||||
<color-code value="@client.Name"></color-code>
|
||||
</a>
|
||||
@if (!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy)
|
||||
{
|
||||
<div class="p-2 level-color-0">@loc["GLOBAL_PERMISSION_USER"]</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="p-2 level-color-@client.LevelInt">@client.Level</div>
|
||||
}
|
||||
<div>@client.LastConnection.HumanizeForCurrentCulture()</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
|
@ -1,35 +1,36 @@
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<MessageResponse>
|
||||
|
||||
<div class="content mt-0">
|
||||
@if (ViewBag.Error != null)
|
||||
{
|
||||
<h4 class="text-red">@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_INVALID_QUERY"], ViewBag.Error.Message)</h4>
|
||||
<h2 class="content-title text-red mt-20">@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_INVALID_QUERY"], ViewBag.Error.Message)</h2>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<h4 class="pb-3 text-center">@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], Model.TotalResultCount.ToString("N0"))</h4>
|
||||
<h2 class="content-title mt-20 mb-0">Search Results</h2>
|
||||
<div class="text-muted mb-15">@Html.Raw(Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], $"<span class=\"badge\">{Model.TotalResultCount.ToString("N0")}</span>"))</div>
|
||||
|
||||
<table class="table table-striped table-hover">
|
||||
<table class="table bg-dark-dm bg-light-lm rounded" style="table-layout: fixed">
|
||||
<thead class="d-none d-lg-table-header-group">
|
||||
<tr class="bg-primary pt-2 pb-2">
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
|
||||
<th scope="col" class="text-right">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
<tr class="bg-primary text-light">
|
||||
<th colspan="20%">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<th colspan="45%">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<th colspan="20%">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
|
||||
<th colspan="15%" class="text-right">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="message_table_body" class="border-bottom bg-dark">
|
||||
<tbody id="message_table_body">
|
||||
<partial name="~/Views/Client/Message/_Item.cshtml" model="@Model.Results" />
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<span id="load_more_messages_button" class="loader-load-more oi oi-chevron-bottom text-center text-primary w-100 h3 pb-0 mb-0 d-none d-lg-block"></span>
|
||||
<div id="loaderLoad" class="mt-10 m-auto text-center d-none d-lg-block">
|
||||
<i class="loader-load-more oi oi-chevron-bottom "></i>
|
||||
</div>
|
||||
|
||||
@section scripts {
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
||||
</environment>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
initLoader('/Message/FindNext?query=@ViewBag.Query', '#message_table_body', @Model.RetrievedResultCount, @ViewBag.QueryLimit);
|
||||
@ -37,3 +38,4 @@ else
|
||||
</script>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
@ -5,64 +5,56 @@
|
||||
{
|
||||
<!-- desktop -->
|
||||
<tr class="d-none d-lg-table-row">
|
||||
<td>
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@message.ClientId" class="link-inverse">
|
||||
<td colspan="20%" class="text-break">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@message.ClientId" class="link-inverse">
|
||||
<color-code value="@message.ClientName"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-light w-50 text-break">
|
||||
<td colspan="45%" class="text-break">
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)"></color-code>
|
||||
<color-code value="@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message"></color-code>
|
||||
}
|
||||
</td>
|
||||
<td class="text-light">
|
||||
<td colspan="20%" class="text-break">
|
||||
<color-code value="@(message.ServerName ?? "--")"></color-code>
|
||||
</td>
|
||||
<td class="text-right text-light">
|
||||
<td colspan="15%" class="text-right text-break">
|
||||
@message.When
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- mobile -->
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<td class="text-light">
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@message.ClientId" class="link-inverse">
|
||||
<tr class="d-flex d-lg-none">
|
||||
<td class="bg-primary text-light">
|
||||
<div>@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
|
||||
<div>@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</div>
|
||||
<div>@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</div>
|
||||
<div>@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</div>
|
||||
</td>
|
||||
<td class="flex-fill">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@message.ClientId">
|
||||
<color-code value="@message.ClientName"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<div>
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message"></color-code>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<color-code value="@(message.ServerName ?? "--")"></color-code>
|
||||
</div>
|
||||
<div> @message.When</div>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<td class="text-light">
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message"></color-code>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
|
||||
<td class="text-light">
|
||||
<color-code value="@(message.ServerName ?? "--")"></color-code>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary" style="border-bottom: 1px solid #222">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
<td class="text-light mb-2 border-bottom">
|
||||
@message.When
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -1,24 +1,46 @@
|
||||
@model Dictionary<SharedLibraryCore.Database.Models.EFClient.Permission, IList<SharedLibraryCore.Dtos.ClientInfo>>
|
||||
@model Dictionary<Data.Models.Client.EFClient.Permission, IList<SharedLibraryCore.Dtos.ClientInfo>>
|
||||
<div class="content mt-0">
|
||||
<h4 class="content-title mt-20">@ViewBag.Title</h4>
|
||||
|
||||
<h4 class="pb-3 text-center ">@ViewBag.Title</h4>
|
||||
|
||||
<div class="row border-bottom">
|
||||
@{
|
||||
foreach (var key in Model.Keys)
|
||||
{
|
||||
<div class="col-12 bg-primary pt-2 pb-2">
|
||||
@Utilities.ToLocalizedLevelName(key)
|
||||
</div>
|
||||
|
||||
<div class="col-12 bg-dark pt-2 pb-2">
|
||||
@foreach (var client in Model[key])
|
||||
@foreach (var key in Model.Keys)
|
||||
{
|
||||
<table class="table mb-20">
|
||||
<thead>
|
||||
<tr class="level-bgcolor-@((int)key)">
|
||||
<th class="text-light">@key.ToLocalizedLevelName()</th>
|
||||
<th class="text-right font-weight-bold">Last Connected</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<has-permission entity="ClientLevel" required-permission="Read">
|
||||
@foreach (var client in Model[key].OrderByDescending(client => client.LastConnection))
|
||||
{
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@client.ClientId">
|
||||
<color-code value="@client.Name"></color-code>
|
||||
</a>
|
||||
<br />
|
||||
if (!ViewBag.Authorized && client.IsMasked)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
<tr class="bg-dark-dm bg-light-lm">
|
||||
<td>
|
||||
@if (client.IsMasked)
|
||||
{
|
||||
<span data-toggle="tooltip" data-title="Client is masked">
|
||||
<span class="oi oi-shield mr-5 font-size-12"></span>
|
||||
</span>
|
||||
}
|
||||
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId">
|
||||
<color-code value="@client.Name"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
@client.LastConnection.HumanizeForCurrentCulture()
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</has-permission>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
@ -1,6 +1,8 @@
|
||||
@using SharedLibraryCore.Interfaces
|
||||
@using Data.Models
|
||||
@using Data.Models.Client
|
||||
@using WebfrontCore.Permissions
|
||||
@using WebfrontCore.ViewModels
|
||||
@model SharedLibraryCore.Dtos.PlayerInfo
|
||||
@{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(Model.Name.ToUpper(), "[A-Z]").Value;
|
||||
@ -11,194 +13,331 @@
|
||||
var isTempBanned = Model.ActivePenalty?.Type == EFPenalty.PenaltyType.TempBan;
|
||||
var translationKey = $"WEBFRONT_PROFILE_{Model.ActivePenalty?.Type.ToString().ToUpper()}_INFO";
|
||||
var ignoredMetaTypes = new[] { MetaType.Information, MetaType.Other, MetaType.QuickMessage };
|
||||
|
||||
string ClassForPenaltyType(EFPenalty.PenaltyType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
EFPenalty.PenaltyType.Ban => "alert-danger",
|
||||
EFPenalty.PenaltyType.Flag => "alert-secondary",
|
||||
EFPenalty.PenaltyType.TempBan => "alert-secondary",
|
||||
_ => "alert"
|
||||
};
|
||||
}
|
||||
|
||||
string ClassForProfileBackground()
|
||||
{
|
||||
return (ViewBag.PermissionsSet as IEnumerable<string>).HasPermission(WebfrontEntity.ClientLevel, WebfrontPermission.Read) ? $"level-bgcolor-{Model.LevelInt}" : "level-bgcolor-0";
|
||||
}
|
||||
}
|
||||
|
||||
<div id="profile_wrapper" class="pb-3 row d-flex flex-column flex-lg-row">
|
||||
<!-- Initial/Avatar Column -->
|
||||
<div id="profile_avatar" class="d-block d-lg-inline-flex flex-column mr-auto ml-auto mr-lg-0 ml-lg-0 justify-content-center text-center level-bgcolor-@(!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy ? "0" : Model.LevelInt.ToString()) @(isTempBanned ? "penalties-bgcolor-tempban" : "")" style="background-image:url('@string.Format("https://gravatar.com/avatar/{0}?size=168&default=blank&rating=pg", gravatarUrl)')">
|
||||
@if (string.IsNullOrEmpty(gravatarUrl))
|
||||
{
|
||||
<span class="profile-shortcode">@shortCode</span>
|
||||
}
|
||||
</div>
|
||||
<!-- Name/Level Column -->
|
||||
<div class="w-50 d-block d-lg-inline-flex flex-column flex-fill text-center text-lg-left pb-3 pb-lg-0 pt-3 pt-lg-0 pl-3 pr-3 ml-auto mr-auto" style="overflow-wrap: anywhere">
|
||||
<div class="mt-n2 d-block d-lg-inline-flex @(ViewBag.Authorized ? "" : "flex-fill")">
|
||||
<div id="profile_name" class="client-name h1 mb-0">
|
||||
<color-code value="@Model.Name"></color-code>
|
||||
</div>
|
||||
@if (ViewBag.Authorized)
|
||||
<div class="content row mt-20">
|
||||
<div class="col-12 col-lg-9 col-xl-10">
|
||||
@if (Model.ActivePenalty != null)
|
||||
{
|
||||
<has-permission entity="ClientLevel" required-permission="Read">
|
||||
<div class="alert @ClassForPenaltyType(Model.ActivePenalty.Type) mt-10 mb-10" role="alert">
|
||||
@foreach (var result in Utilities.SplitTranslationTokens(translationKey))
|
||||
{
|
||||
<div id="profile_aliases_btn" class="oi oi-caret-bottom h3 ml-0 ml-lg-2 mb-0 pt-lg-2 mt-lg-1"></div>
|
||||
switch (result.MatchValue)
|
||||
{
|
||||
case "reason":
|
||||
<span class="text-light-dm font-weight-lighter">@(ViewBag.Authorized ? !string.IsNullOrEmpty(Model.ActivePenalty.AutomatedOffense) && Model.ActivePenalty.Type != EFPenalty.PenaltyType.Warning ? Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.ActivePenalty.AutomatedOffense) : Model.ActivePenalty.Offense.StripColors() : Model.ActivePenalty.Offense.StripColors())</span>
|
||||
break;
|
||||
case "time":
|
||||
<span class="text-light-dm font-weight-lighter">
|
||||
@((Model.ActivePenalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture())
|
||||
</span>
|
||||
break;
|
||||
default:
|
||||
<span>@result.MatchValue</span>
|
||||
break;
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</has-permission>
|
||||
}
|
||||
|
||||
@if (ViewBag.Authorized)
|
||||
{
|
||||
<div class="d-flex flex-row justify-content-start flex-fill flex-column flex-lg-row mr-lg-2 mb-2 mb-md-0">
|
||||
<div class="ip-lookup-profile align-self-center mr-0 mr-lg-2 ml-lg-n1" data-ip="@Model.IPAddress"></div>
|
||||
<div id="ip_lookup_country" class="h4 mb-2 mb-lg-0 align-self-center text-muted"></div>
|
||||
</div>
|
||||
<h2 class="content-title mb-10">Player Profile</h2>
|
||||
|
||||
<div id="profile_aliases" class="text-muted pt-0 pt-lg-2 pb-2">
|
||||
@foreach (var linked in Model.LinkedAccounts)
|
||||
{
|
||||
<div>
|
||||
@Html.ActionLink(linked.Value.ToString("X"), "ProfileAsync", "Client", new { id = linked.Key }, new { @class = "link-inverse" })
|
||||
</div>
|
||||
}
|
||||
@foreach (var alias in Model.Aliases)
|
||||
{
|
||||
<div>
|
||||
<color-code value="@alias"></color-code>
|
||||
</div>
|
||||
}
|
||||
<div id="profile_wrapper" class="mb-10 mt-10">
|
||||
|
||||
@foreach (var ip in Model.IPs)
|
||||
<!-- online status indicator -->
|
||||
@if (Model.Online)
|
||||
{
|
||||
<div class="bg-success rounded-circle position-absolute status-indicator z-20 mt-10 ml-10" data-toggle="tooltip" data-placement="bottom" data-title="Client is online"></div>
|
||||
<div class="bg-success rounded-circle position-absolute status-indicator with-ripple z-10 mt-10 ml-10"></div>
|
||||
}
|
||||
|
||||
<!-- main profile row -->
|
||||
<div class="card p-15 ml-0 mr-0 mt-0 mb-10 d-flex flex-fill flex-wrap flex-column flex-md-row justify-content-center">
|
||||
<div class="pl-5 pr-5 d-flex flex-column flex-md-row">
|
||||
<div id="profile_avatar" class="w-150 w-md-100 h-150 h-md-100 mt-5 mb-5 d-flex justify-content-center align-self-center rounded @ClassForProfileBackground() @(isTempBanned ? "penalties-bgcolor-tempban" : "")" style="background-image:url('@($"https://gravatar.com/avatar/{gravatarUrl}?size=168&default=blank&rating=pg")">
|
||||
@if (string.IsNullOrEmpty(gravatarUrl))
|
||||
{
|
||||
<div>
|
||||
<a class="ip-locate-link" href="#" data-ip="@ip">@ip</a>
|
||||
<a href="/Client/FindAsync?clientName=@ip" class="oi oi-magnifying-glass text-muted"></a>
|
||||
</div>
|
||||
<div class="profile-shortcode align-self-center text-dark-lm">@shortCode</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (Model.ActivePenalty != null && (Model.ActivePenalty.Type != EFPenalty.PenaltyType.Flag || ViewBag.Authorized))
|
||||
{
|
||||
<div class="font-weight-bold h4 mb-0 penalties-color-@Model.ActivePenalty.Type.ToString().ToLower()">
|
||||
@foreach (var result in Utilities.SplitTranslationTokens(translationKey))
|
||||
{
|
||||
switch (result.MatchValue)
|
||||
<div class="d-flex flex-column align-self-center ml-20 mr-20 mt-10 mb-10 mt-md-0 mb-md-0 text-center text-md-left">
|
||||
<!-- name -->
|
||||
<div id="profile_name">
|
||||
<span class="font-size-20 font-weight-medium">
|
||||
<color-code value="@Model.Name"></color-code>
|
||||
</span>
|
||||
<has-permission entity="MetaAliasUpdate" required-permission="Read">
|
||||
<div class="dropdown with-arrow">
|
||||
<div data-toggle="dropdown" id="profileAliasHistory" aria-haspopup="true" aria-expanded="false">
|
||||
@if (Model.Aliases.Any())
|
||||
{
|
||||
<i class="oi oi-caret-bottom font-size-12" aria-hidden="true"></i>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-center @(Model.Aliases.Where(alias => !alias.Item1.Contains(" ")).Max(alias => (int?)alias.Item1.Length) >= 15 ? "w-250" : "")" aria-labelledby="profileAliasHistory">
|
||||
@foreach (var (alias, dateAdded) in Model.Aliases.OrderByDescending(alias => alias.Item2).Take(15))
|
||||
{
|
||||
<a asp-controller="Client" asp-action="Find" asp-route-clientName="@alias.StripColors()" class="dropdown-item" data-toggle="tooltip" data-title="@dateAdded.HumanizeForCurrentCulture()">
|
||||
<i class="oi oi-magnifying-glass text-muted mr-5"></i>
|
||||
<color-code value="@alias"></color-code>
|
||||
</a>
|
||||
}
|
||||
@if (Model.Aliases.Count > 15)
|
||||
{
|
||||
<div class="dropdown-divider"></div>
|
||||
<span class="dropdown-item bg-dark-dm bg-light-lm">...and @(Model.Aliases.Count - 15) more</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</has-permission>
|
||||
</div>
|
||||
<!-- permission level -->
|
||||
<has-permission entity="ClientLevel" required-permission="Read">
|
||||
<div class="align-self-center align-self-md-start font-weight-bold font-size-16 level-color-@Model.LevelInt">
|
||||
<div class="d-flex flex-row">
|
||||
<span>@Model.Level</span>
|
||||
</div>
|
||||
</div>
|
||||
</has-permission>
|
||||
|
||||
<!-- guid -->
|
||||
<has-permission entity="ClientGuid" required-permission="Read">
|
||||
<div class="dropdown dropup with-arrow">
|
||||
<div class="text-muted" data-toggle="dropdown" id="altGuidFormatsDropdown" aria-haspopup="true" aria-expanded="false">@Model.NetworkId.ToString("X")</div>
|
||||
<div class="dropdown-menu" aria-labelledby="altGuidFormatsDropdown">
|
||||
<div class="p-10 font-size-12">
|
||||
<div class="">Alternative Formats</div>
|
||||
<div class="dropdown-divider mt-5 mb-5"></div>
|
||||
<div class="text-muted font-weight-lighter">@((ulong)Model.NetworkId)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</has-permission>
|
||||
|
||||
<!-- ip address -->
|
||||
<div class="align-self-center align-self-md-start d-flex flex-row">
|
||||
<span class="text-muted mr-5">@Model.IPAddress</span>
|
||||
<has-permission entity="MetaAliasUpdate" required-permission="Read">
|
||||
<div class="dropdown with-arrow">
|
||||
<div data-toggle="dropdown" id="profileIPAddressHistory" aria-haspopup="true" aria-expanded="false">
|
||||
@if (Model.IPs.Any(ip => !string.IsNullOrEmpty(ip.Item1)))
|
||||
{
|
||||
<i class="oi oi-caret-bottom font-size-12 text-muted" aria-hidden="true"></i>
|
||||
}
|
||||
</div>
|
||||
<div class="dropdown-menu dropdown-menu-center" aria-labelledby="profileAliasHistory">
|
||||
@foreach (var (ip, dateAdded) in Model.IPs.OrderByDescending(ip => ip.Item2).Take(15))
|
||||
{
|
||||
if (string.IsNullOrEmpty(ip))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
<div class="d-flex dropdown-item" data-toggle="tooltip" data-title="@dateAdded.HumanizeForCurrentCulture()">
|
||||
<a asp-controller="Client" asp-action="Find" asp-route-clientName="@ip">
|
||||
<i class="oi oi-magnifying-glass text-muted mr-5"></i>
|
||||
</a>
|
||||
<a href="#contextModal" class="profile-ip-lookup dropdown-item p-0 m-0" data-ip="@ip">
|
||||
<color-code value="@ip"></color-code>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
@if (Model.IPs.Count > 15)
|
||||
{
|
||||
<div class="dropdown-divider"></div>
|
||||
<span class="dropdown-item bg-dark-dm bg-light-lm">...and @(Model.IPs.Count - 15) more</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</has-permission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill d-flex justify-content-center justify-content-md-end mt-10 mt-md-0">
|
||||
<!-- country flag -->
|
||||
<div id="ipGeoDropdown" class="dropdown with-arrow align-self-center">
|
||||
<a href="#" data-toggle="dropdown" id="avatar-popover-toggle" aria-haspopup="true" aria-expanded="false">
|
||||
@if (!string.IsNullOrEmpty(Model.GeoLocationInfo.CountryCode))
|
||||
{
|
||||
case "reason":
|
||||
<span class="text-white font-weight-lighter">@(ViewBag.Authorized ? !string.IsNullOrEmpty(Model.ActivePenalty.AutomatedOffense) && Model.ActivePenalty.Type != EFPenalty.PenaltyType.Warning ? Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.ActivePenalty.AutomatedOffense) : Model.ActivePenalty.Offense.StripColors() : Model.ActivePenalty.Offense.StripColors())</span>
|
||||
break;
|
||||
case "time":
|
||||
<span class="text-white font-weight-lighter">
|
||||
@((Model.ActivePenalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture())
|
||||
</span>
|
||||
break;
|
||||
default:
|
||||
<span>@result.MatchValue</span>
|
||||
break;
|
||||
<div class="profile-country-flag w-100 rounded align-self-center" style="background-image: url('https://flagcdn.com/w160/@(Model.GeoLocationInfo.CountryCode.ToLower()).png')" data-ip="@Model.IPAddress"></div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy)
|
||||
{
|
||||
<div id="profile_level" class="font-weight-bold h4 mb-0 level-color-0">
|
||||
@ViewBag.Localization["GLOBAL_PERMISSION_USER"]
|
||||
</div>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<div id="profile_level" class="font-weight-bold h4 mb-0 level-color-@Model.LevelInt">
|
||||
@Model.Level
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pr-lg-0 text-center text-lg-right">
|
||||
@if (ViewBag.Authorized)
|
||||
{
|
||||
@if (!isPermBanned)
|
||||
{
|
||||
<div class="profile-action oi oi-flag h3 ml-2 @(isFlagged ? "text-secondary" : "text-success")" data-action="@(isFlagged ? "unflag" : "flag")" aria-hidden="true"></div>
|
||||
}
|
||||
|
||||
@if (Model.LevelInt < (int)ViewBag.User.Level && !Model.HasActivePenalty)
|
||||
{
|
||||
<div id="profile_action_ban_btn" class="profile-action oi oi-lock-unlocked text-success h3 ml-2" title="Ban Client" data-action="ban" aria-hidden="true"></div>
|
||||
}
|
||||
|
||||
@if (Model.LevelInt < (int)ViewBag.User.Level && Model.HasActivePenalty)
|
||||
{
|
||||
@if (isTempBanned)
|
||||
{
|
||||
<div id="profile_action_ban_btn" class="profile-action oi oi-lock-unlocked text-success h3 ml-2" title="Ban Client" data-action="ban" aria-hidden="true"></div>
|
||||
<div id="profile_action_unban_btn" class="profile-action oi oi-lock-locked penalties-color-tempban h3 ml-2" title="Unban Client" data-action="unban" aria-hidden="true"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="profile_action_unban_btn" class="profile-action oi oi-lock-locked text-danger h3 ml-2" title="Unban Client" data-action="unban" aria-hidden="true"></div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@if (Model.LevelInt != -1)
|
||||
{
|
||||
<div id="profile_action_edit_btn" class="profile-action oi oi-cog text-muted h3 ml-2" title="Client Options" data-action="edit" aria-hidden="true"></div>
|
||||
}
|
||||
}
|
||||
@if (ViewBag.UseNewStats)
|
||||
{
|
||||
<a asp-controller="ClientStatistics" asp-action="Advanced" asp-route-id="@Model.ClientId" class="oi oi-graph text-primary h3 ml-2" title="Stats" aria-hidden="true"></a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="profile_info" class="row d-block d-lg-flex flex-row border-bottom border-top pt-2 pb-2">
|
||||
<partial name="Meta/_Information.cshtml" model="@Model.Meta"/>
|
||||
</div>
|
||||
|
||||
<div class="row border-bottom">
|
||||
<div class="d-md-flex flex-fill">
|
||||
<div class="bg-dark p-2 pl-3 pr-3 text-center text-muted border-0 align-self-stretch align-middle" id="filter_meta_container_button">
|
||||
<span class="text-primary" id="meta_filter_dropdown_icon">▼</span>
|
||||
<a>@ViewBag.Localization["WEBFRONT_CLIENT_META_FILTER"]</a>
|
||||
</div>
|
||||
<div id="filter_meta_container" class="d-none d-md-flex flex-md-fill flex-md-wrap">
|
||||
@{
|
||||
const int defaultTabCount = 5;
|
||||
var metaTypes = Enum.GetValues(typeof(MetaType))
|
||||
.Cast<MetaType>()
|
||||
.Where(type => !ignoredMetaTypes.Contains(type))
|
||||
.OrderByDescending(type => type == MetaType.All)
|
||||
.ToList();
|
||||
var selectedMeta = metaTypes.FirstOrDefault(meta => metaTypes.IndexOf(Model.MetaFilterType ?? MetaType.All) >= defaultTabCount && meta != MetaType.All && meta == Model.MetaFilterType);
|
||||
}
|
||||
@foreach (var type in metaTypes.Take(defaultTabCount - 1).Append(selectedMeta == MetaType.Other ? metaTypes[defaultTabCount - 1] : selectedMeta))
|
||||
{
|
||||
<a asp-action="ProfileAsync" asp-controller="Client"
|
||||
class="meta-filter nav-link p-2 pl-3 pr-3 text-center @(Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-white" : "text-muted")"
|
||||
asp-route-id="@Model.ClientId"
|
||||
asp-route-metaFilterType="@type"
|
||||
data-meta-type="@type">
|
||||
@type.ToTranslatedName()
|
||||
</a>
|
||||
}
|
||||
<div class="d-md-none" id="additional_meta_filter">
|
||||
@foreach (var type in (selectedMeta == MetaType.Other ? metaTypes.Skip(defaultTabCount) : metaTypes.Skip(defaultTabCount).Append(metaTypes[defaultTabCount - 1])).Where(meta => selectedMeta == MetaType.Other || meta != selectedMeta))
|
||||
{
|
||||
<a asp-action="ProfileAsync" asp-controller="Client"
|
||||
class="meta-filter nav-link p-2 pl-3 pr-3 text-center @(Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-white" : "text-muted")"
|
||||
asp-route-id="@Model.ClientId"
|
||||
asp-route-metaFilterType="@type"
|
||||
data-meta-type="@type">
|
||||
@type.ToTranslatedName()
|
||||
</a>
|
||||
}
|
||||
<div class="dropdown-menu dropdown-menu-center z-30" aria-labelledby="avatar-popover-toggle">
|
||||
<has-permission entity="ClientIPAddress" required-permission="Read">
|
||||
<h6 class="dropdown-header font-weight-bold">@Model.IPAddress</h6>
|
||||
<div class="dropdown-divider"></div>
|
||||
</has-permission>
|
||||
|
||||
<div class="dropdown-item geo-country">@Model.GeoLocationInfo.Country</div>
|
||||
@if (!string.IsNullOrEmpty(Model.GeoLocationInfo.Region))
|
||||
{
|
||||
<div class="dropdown-item text-muted geo-region">@Model.GeoLocationInfo.Region</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.GeoLocationInfo.Organization))
|
||||
{
|
||||
<div class="dropdown-item geo-organization">@Model.GeoLocationInfo.Organization</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<hr class="mr-5 ml-5"/>
|
||||
<!-- meta info block -->
|
||||
<div class="d-flex flex-column flex-md-row text-center text-md-left flex-wrap">
|
||||
<partial name="Meta/_Information.cshtml" model="@Model.Meta"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ((!ViewBag.Authorized && !ViewBag.EnablePrivilegedUserPrivacy) || ViewBag.Authorized)
|
||||
|
||||
<hr class="mt-10 mb-10"/>
|
||||
|
||||
<!-- meta filter list -->
|
||||
<div class="mb-10 mt-15">
|
||||
@foreach (var type in Enum.GetValues(typeof(MetaType)).Cast<MetaType>().Where(meta => !ignoredMetaTypes.Contains(meta)).OrderByDescending(meta => meta == MetaType.All))
|
||||
{
|
||||
var buttonClass = !Model.MetaFilterType.HasValue && type == MetaType.All || Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-light" : "text-muted";
|
||||
<a asp-action="Profile" asp-controller="Client"
|
||||
class="meta-filter no-decoration"
|
||||
asp-route-id="@Model.ClientId"
|
||||
asp-route-metaFilterType="@type"
|
||||
data-meta-type="@type">
|
||||
<button class="btn btn-sm d-none d-md-inline mt-5 mb-5 @buttonClass">@type.ToTranslatedName()</button>
|
||||
<button class="btn btn-block d-block d-md-none mt-10 mb-10 @buttonClass">@type.ToTranslatedName()</button>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!ViewBag.Authorized && !ViewBag.EnablePrivilegedUserPrivacy || ViewBag.Authorized)
|
||||
{
|
||||
<div class="row d-md-flex pt-2">
|
||||
<div id="profile_events" class="text-muted text-left pl-4 pr-4 pl-md-0 pr-md-0">
|
||||
<div id="profile_events" class="text-muted text-left pl-md-0 pr-md-0">
|
||||
@await Component.InvokeAsync("ProfileMetaList", new { clientId = Model.ClientId, count = 30, offset = 0, startAt = DateTime.UtcNow, metaType = Model.MetaFilterType })
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="oi oi-chevron-bottom text-center mt-2 btn btn-primary btn-block loader-load-more" title="Load more meta" data-action="unban" aria-hidden="true"></div>
|
||||
<hr class="mt-10 mb-10"/>
|
||||
|
||||
<div class="text-center">
|
||||
<i id="loaderLoad" class="oi oi-chevron-bottom loader-load-more text-primary" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- actions desktop -->
|
||||
@{
|
||||
var menuItems = new SideContextMenuItems
|
||||
{
|
||||
MenuTitle = "Actions",
|
||||
};
|
||||
|
||||
if (Model.Online)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Join Game",
|
||||
IsLink = true,
|
||||
IsButton = true,
|
||||
Reference = Model.ConnectProtocolUrl,
|
||||
Tooltip = $"Playing on {Model.CurrentServerName.StripColors()}",
|
||||
Icon = "oi-play-circle"
|
||||
});
|
||||
}
|
||||
|
||||
if (Model.LevelInt != -1 && ViewBag.Authorized)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Change Level",
|
||||
IsButton = true,
|
||||
Reference = "edit",
|
||||
Icon = "oi-cog",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "View Stats",
|
||||
IsButton = true,
|
||||
IsLink = true,
|
||||
Reference = Url.Action("Advanced", "ClientStatistics", new { id = Model.ClientId }),
|
||||
Icon = "oi-graph",
|
||||
});
|
||||
|
||||
if (!isPermBanned && ViewBag.Authorized)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = isFlagged ? "Unflag" : "Flag",
|
||||
IsButton = true,
|
||||
Reference = isFlagged ? "unflag" : "flag",
|
||||
Icon = "oi-flag",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
if (Model.LevelInt < (int)ViewBag.User.Level && Model.Online)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Kick",
|
||||
IsButton = true,
|
||||
Reference = "kick",
|
||||
Icon = "oi-circle-x",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
if ((Model.LevelInt < (int)ViewBag.User.Level && !Model.HasActivePenalty || isTempBanned) && ViewBag.Authorized)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Ban",
|
||||
IsButton = true,
|
||||
Reference = "ban",
|
||||
Icon = "oi-lock-unlocked",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
if ((Model.LevelInt < (int)ViewBag.User.Level && Model.HasActivePenalty || isTempBanned) && ViewBag.Authorized)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Unban",
|
||||
IsButton = true,
|
||||
Reference = "unban",
|
||||
Icon = "oi-lock-locked",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
}
|
||||
<partial name="_SideContextMenu" for="@menuItems"></partial>
|
||||
|
||||
</div>
|
||||
|
||||
@section targetid {
|
||||
@ -207,7 +346,6 @@
|
||||
|
||||
@section scripts {
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
||||
<script type="text/javascript" src="~/js/profile.js"></script>
|
||||
</environment>
|
||||
<script>initLoader('/Client/Meta/@Model.ClientId', '#profile_events', 30, 30, [{ 'name': 'metaFilterType', 'value': '@Model.MetaFilterType' }]);</script>
|
||||
|
@ -1,53 +1,60 @@
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model AdministeredPenaltyResponse
|
||||
@{
|
||||
string localizationKey = $"WEBFRONT_CLIENT_META_PENALIZED_{Model.PenaltyType.ToString().ToUpper()}_V2";
|
||||
var localizationKey = $"WEBFRONT_CLIENT_META_PENALIZED_{Model.PenaltyType.ToString().ToUpper()}_V2";
|
||||
}
|
||||
|
||||
<div class="d-inline">
|
||||
@foreach (var match in Utilities.SplitTranslationTokens(localizationKey))
|
||||
<has-permission entity="Penalty" required-permission="Read">
|
||||
@if (TempData["ShowMetaHeader"] as bool? ?? false)
|
||||
{
|
||||
if (match.IsInterpolation)
|
||||
{
|
||||
if (match.MatchValue == "action")
|
||||
{
|
||||
<span class="penalties-color-@Model.PenaltyType.ToString().ToLower()">@match.TranslationValue</span>
|
||||
}
|
||||
|
||||
else if (match.MatchValue == "offender")
|
||||
{
|
||||
<span class="text-highlight">
|
||||
<a class="link-inverse" href="@Model.OffenderClientId">
|
||||
<color-code value="@Model.OffenderName"></color-code>
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
|
||||
else if (match.MatchValue == "reason")
|
||||
{
|
||||
<span class="text-white">
|
||||
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning)
|
||||
{
|
||||
<span>@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense)</span>
|
||||
<span class="oi oi-list-rich align-top text-primary automated-penalty-info-detailed" data-penalty-id="@Model.PenaltyId" style="margin-top: 0.125rem;" title="@ViewBag.Localization["WEBFRONT_CLIENT_META_AC_METRIC"]"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@Model.Offense"></color-code>
|
||||
}
|
||||
|
||||
</span>
|
||||
}
|
||||
|
||||
else if (match.MatchValue == "time")
|
||||
{
|
||||
<span class="text-white">@Model.LengthText</span>
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<span>@match.MatchValue</span>
|
||||
}
|
||||
<partial name="./_MetaHeader.cshtml" for="@Model.When"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-inline">
|
||||
@foreach (var match in Utilities.SplitTranslationTokens(localizationKey))
|
||||
{
|
||||
if (match.IsInterpolation)
|
||||
{
|
||||
if (match.MatchValue == "action")
|
||||
{
|
||||
<span class="penalties-color-@Model.PenaltyType.ToString().ToLower()">@match.TranslationValue</span>
|
||||
}
|
||||
|
||||
else if (match.MatchValue == "offender")
|
||||
{
|
||||
<span class="text-highlight">
|
||||
<a class="link-inverse" href="@Model.OffenderClientId">
|
||||
<color-code value="@Model.OffenderName"></color-code>
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
|
||||
else if (match.MatchValue == "reason")
|
||||
{
|
||||
<span class="text-light-dm text-dark-lm">
|
||||
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning)
|
||||
{
|
||||
<span>@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense)</span>
|
||||
<span class="oi oi-list-rich align-top text-primary automated-penalty-info-detailed" data-penalty-id="@Model.PenaltyId" style="margin-top: 0.125rem;" title="@ViewBag.Localization["WEBFRONT_CLIENT_META_AC_METRIC"]"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@Model.Offense"></color-code>
|
||||
}
|
||||
|
||||
</span>
|
||||
}
|
||||
|
||||
else if (match.MatchValue == "time")
|
||||
{
|
||||
<span class="text-light-dm text-dark-lm">@Model.LengthText</span>
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<span>@match.MatchValue</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</has-permission>
|
||||
|
@ -4,6 +4,11 @@
|
||||
var localizationKey = $"WEBFRONT_CLIENT_META_CONNECTION_{Model.ConnectionType.ToString().ToUpper()}";
|
||||
}
|
||||
|
||||
@if (TempData["ShowMetaHeader"] as bool? ?? false)
|
||||
{
|
||||
<partial name="./_MetaHeader.cshtml" for="@Model.When"/>
|
||||
}
|
||||
|
||||
@foreach (var token in Utilities.SplitTranslationTokens(localizationKey))
|
||||
{
|
||||
if (token.IsInterpolation)
|
||||
@ -11,10 +16,10 @@
|
||||
switch (token.MatchValue)
|
||||
{
|
||||
case "action":
|
||||
<span class="@(Model.ConnectionType == Reference.ConnectionType.Connect ? "text-light-green" : "text-warning")">@token.TranslationValue</span>
|
||||
<span class="@(Model.ConnectionType == Reference.ConnectionType.Connect ? "text-light-green" : "text-secondary")">@token.TranslationValue</span>
|
||||
break;
|
||||
case "server":
|
||||
<span class="text-white">
|
||||
<span class="text-light-dm text-dark-lm">
|
||||
<color-code value="@Model.ServerName"></color-code>
|
||||
</span>
|
||||
break;
|
||||
|
@ -1,20 +1,22 @@
|
||||
@model IEnumerable<SharedLibraryCore.Dtos.Meta.Responses.InformationResponse>
|
||||
@using Humanizer
|
||||
@model IEnumerable<SharedLibraryCore.Dtos.Meta.Responses.InformationResponse>
|
||||
@{
|
||||
var informationMeta = Model
|
||||
.Where(_meta => _meta.Type == SharedLibraryCore.Interfaces.MetaType.Information)
|
||||
.OrderBy(_meta => _meta.Order)
|
||||
.GroupBy(_meta => _meta.Column)
|
||||
.OrderBy(_grouping => _grouping.Key);
|
||||
.Where(meta => meta.Type == SharedLibraryCore.Interfaces.MetaType.Information)
|
||||
.OrderBy(meta => meta.Order)
|
||||
.Select((meta, i) => new { index = i, meta })
|
||||
.GroupBy(meta => meta.index / 5);
|
||||
}
|
||||
|
||||
<div class="d-flex flex-wrap">
|
||||
@foreach (var metaColumn in informationMeta)
|
||||
{
|
||||
<div class="text-center text-lg-left mr-0 mr-lg-4">
|
||||
<!-- <div class="mr-20"> -->
|
||||
@foreach (var meta in metaColumn)
|
||||
{
|
||||
<div class="profile-meta-entry" title="@meta.ToolTipText">
|
||||
<div class="m-md-5 p-15 w-half rounded bg-very-dark-dm bg-light-ex-lm profile-meta-entry font-size-12 w-md-100 w-lg-150" data-toggle="@(!string.IsNullOrEmpty(meta.meta.ToolTipText) ? "tooltip" : "")" data-title="@meta.meta.ToolTipText" data-placement="bottom">
|
||||
|
||||
@{var results = Utilities.SplitTranslationTokens(meta.Key);}
|
||||
@{var results = Utilities.SplitTranslationTokens(meta.meta.Key);}
|
||||
|
||||
@if (results.Any(_result => _result.IsInterpolation))
|
||||
{
|
||||
@ -22,22 +24,23 @@
|
||||
{
|
||||
if (result.IsInterpolation)
|
||||
{
|
||||
<span class="profile-meta-value text-primary"><color-code value="@meta.Value"></color-code></span>
|
||||
<div class="profile-meta-value text-primary font-size-14"><color-code value="@meta.meta.Value"></color-code></div>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<span class="profile-meta-title text-muted">@result.MatchValue</span>
|
||||
<span class="profile-meta-title text-muted font-size-12">@result.MatchValue.Titleize()</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<span class="profile-meta-value text-primary"><color-code value="@meta.Value"></color-code></span>
|
||||
<span class="profile-meta-title text-muted"> @meta.Key</span>
|
||||
<div class="profile-meta-value text-primary font-size-14"><color-code value="@meta.meta.Value"></color-code></div>
|
||||
<div class="profile-meta-title text-muted font-size-12">@meta.meta.Key.Titleize()</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</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