started update for readme
start update for version changes hopefully fixed pesky stat bug move vpn detection into script plugin
This commit is contained in:
parent
1343d4959e
commit
0538d9f479
@ -415,6 +415,34 @@ namespace IW4MAdmin.Application
|
||||
try
|
||||
{
|
||||
await newEvent.Owner.ExecuteEvent(newEvent);
|
||||
|
||||
// todo: this is a hacky mess
|
||||
if (newEvent.Origin?.DelayedEvents?.Count > 0 &&
|
||||
newEvent.Origin?.State == Player.ClientState.Connected)
|
||||
{
|
||||
var events = newEvent.Origin.DelayedEvents;
|
||||
|
||||
// add the delayed event to the queue
|
||||
while (events?.Count > 0)
|
||||
{
|
||||
var e = events.Dequeue();
|
||||
e.Origin = newEvent.Origin;
|
||||
// check if the target was assigned
|
||||
if (e.Target != null)
|
||||
{
|
||||
// update the target incase they left or have newer info
|
||||
e.Target = newEvent.Owner.GetPlayersAsList()
|
||||
.FirstOrDefault(p => p.NetworkId == e.Target.NetworkId);
|
||||
// we have to throw out the event because they left
|
||||
if (e.Target == null)
|
||||
{
|
||||
Logger.WriteWarning($"Delayed event for {e.Origin} was removed because the target has left");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.GetEventHandler().AddEvent(e);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
Logger.WriteDebug("Processed Event");
|
||||
#endif
|
||||
|
@ -1,42 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
public class VPNCheck
|
||||
{
|
||||
public static async Task<bool> UsingVPN(string ip, string apiKey)
|
||||
{
|
||||
#if DEBUG
|
||||
return await Task.FromResult(false);
|
||||
|
||||
#else
|
||||
try
|
||||
{
|
||||
using (var RequestClient = new System.Net.Http.HttpClient())
|
||||
{
|
||||
RequestClient.DefaultRequestHeaders.Add("X-Key", apiKey);
|
||||
string response = await RequestClient.GetStringAsync($"http://v2.api.iphub.info/ip/{ip}");
|
||||
var responseJson = JsonConvert.DeserializeObject<JObject>(response);
|
||||
int blockType = Convert.ToInt32(responseJson["block"]);
|
||||
/*if (responseJson.ContainsKey("isp"))
|
||||
{
|
||||
if (responseJson["isp"].ToString() == "TSF-IP-CORE")
|
||||
return true;
|
||||
}*/
|
||||
return blockType == 1;
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@ using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Localization;
|
||||
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using IW4MAdmin.Application.RconParsers;
|
||||
using IW4MAdmin.Application.EventParsers;
|
||||
using IW4MAdmin.Application.IO;
|
||||
@ -96,9 +95,6 @@ namespace IW4MAdmin
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// if they're authenticated but haven't been added yet
|
||||
// we want to set their delayed events
|
||||
var delayedEventQueue = Players[polledPlayer.ClientNumber].DelayedEvents;
|
||||
|
||||
#if !DEBUG
|
||||
if (polledPlayer.Name.Length < 3)
|
||||
@ -253,14 +249,6 @@ namespace IW4MAdmin
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Manager.GetApplicationSettings().Configuration().EnableClientVPNs &&
|
||||
Manager.GetApplicationSettings().Configuration().VpnExceptionIds?.FirstOrDefault(i => i == player.ClientId) != null &&
|
||||
await VPNCheck.UsingVPN(player.IPAddressString, Manager.GetApplicationSettings().Configuration().IPHubAPIKey))
|
||||
{
|
||||
await player.Kick(Utilities.CurrentLocalization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], new Player() { ClientId = 1 });
|
||||
return true;
|
||||
}
|
||||
|
||||
var e = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Connect,
|
||||
@ -271,28 +259,6 @@ namespace IW4MAdmin
|
||||
Manager.GetEventHandler().AddEvent(e);
|
||||
|
||||
player.State = Player.ClientState.Connected;
|
||||
|
||||
// add the delayed event to the queue
|
||||
while (delayedEventQueue?.Count > 0)
|
||||
{
|
||||
e = delayedEventQueue.Dequeue();
|
||||
e.Origin = player;
|
||||
// check if the target was assigned
|
||||
if (e.Target != null)
|
||||
{
|
||||
// update the target incase they left or have newer info
|
||||
e.Target = GetPlayersAsList()
|
||||
.FirstOrDefault(p => p.NetworkId == e.Target.NetworkId);
|
||||
// we have to throw out the event because they left
|
||||
if (e.Target == null)
|
||||
{
|
||||
Logger.WriteWarning($"Delayed event for {e.Origin} was removed because the target has left");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Manager.GetEventHandler().AddEvent(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -353,24 +319,54 @@ namespace IW4MAdmin
|
||||
}
|
||||
}
|
||||
|
||||
// this allows us to catch exceptions but still run it parallel
|
||||
async Task pluginHandlingAsync(Task onEvent, string pluginName)
|
||||
//// this allows us to catch exceptions but still run it parallel
|
||||
//async Task pluginHandlingAsync(Task onEvent, string pluginName)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// await onEvent;
|
||||
// }
|
||||
|
||||
// // this happens if a plugin (login) wants to stop commands from executing
|
||||
// catch (AuthorizationException e)
|
||||
// {
|
||||
// await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
||||
// canExecuteCommand = false;
|
||||
// }
|
||||
|
||||
// catch (Exception Except)
|
||||
// {
|
||||
// Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{pluginName}]");
|
||||
// Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
|
||||
// Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
|
||||
// while (Except.InnerException != null)
|
||||
// {
|
||||
// Except = Except.InnerException;
|
||||
// Logger.WriteDebug($"Inner exception: {Except.Message}");
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
//var pluginTasks = SharedLibraryCore.Plugins.PluginImporter.ActivePlugins.
|
||||
// Select(p => pluginHandlingAsync(p.OnEventAsync(E, this), p.Name));
|
||||
|
||||
//// execute all the plugin updates simultaneously
|
||||
//await Task.WhenAll(pluginTasks);
|
||||
|
||||
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
await onEvent;
|
||||
await plugin.OnEventAsync(E, this);
|
||||
}
|
||||
|
||||
// this happens if a plugin (login) wants to stop commands from executing
|
||||
catch (AuthorizationException e)
|
||||
{
|
||||
await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
||||
canExecuteCommand = false;
|
||||
}
|
||||
|
||||
catch (Exception Except)
|
||||
{
|
||||
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{pluginName}]");
|
||||
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]");
|
||||
Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
|
||||
Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
|
||||
while (Except.InnerException != null)
|
||||
@ -381,12 +377,6 @@ namespace IW4MAdmin
|
||||
}
|
||||
}
|
||||
|
||||
var pluginTasks = SharedLibraryCore.Plugins.PluginImporter.ActivePlugins.
|
||||
Select(p => pluginHandlingAsync(p.OnEventAsync(E, this), p.Name));
|
||||
|
||||
// execute all the plugin updates simultaneously
|
||||
await Task.WhenAll(pluginTasks);
|
||||
|
||||
// hack: this prevents commands from getting executing that 'shouldn't' be
|
||||
if (E.Type == GameEvent.EventType.Command &&
|
||||
E.Extra != null &&
|
||||
@ -406,19 +396,15 @@ namespace IW4MAdmin
|
||||
{
|
||||
if (E.Type == GameEvent.EventType.Connect)
|
||||
{
|
||||
// this may be a fix for a hard to reproduce null exception error
|
||||
lock (ChatHistory)
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
Name = E.Origin?.Name ?? "ERROR!",
|
||||
Message = "CONNECTED",
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
Name = E.Origin?.Name ?? "ERROR!",
|
||||
Message = "CONNECTED",
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
|
||||
if (E.Origin.Level > Player.Permission.Moderator)
|
||||
await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
|
||||
await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.Join)
|
||||
@ -592,6 +578,8 @@ namespace IW4MAdmin
|
||||
Owner = this
|
||||
};
|
||||
|
||||
client.State = Player.ClientState.Disconnecting;
|
||||
|
||||
Manager.GetEventHandler().AddEvent(e);
|
||||
// todo: needed?
|
||||
// wait until the disconnect event is complete
|
||||
@ -601,11 +589,10 @@ namespace IW4MAdmin
|
||||
|
||||
AuthQueue.AuthenticateClients(CurrentPlayers);
|
||||
|
||||
// all polled players should be authenticated
|
||||
var addPlayerTasks = AuthQueue.GetAuthenticatedClients()
|
||||
.Select(client => AddPlayer(client));
|
||||
|
||||
await Task.WhenAll(addPlayerTasks);
|
||||
foreach (var c in AuthQueue.GetAuthenticatedClients())
|
||||
{
|
||||
await AddPlayer(c);
|
||||
}
|
||||
|
||||
return CurrentPlayers.Count;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
idna==2.7
|
||||
pip==18.0
|
||||
requests==2.19.1
|
||||
setuptools==39.0.1
|
||||
urllib3==1.23
|
||||
certifi>=2018.4.16
|
||||
chardet>=3.0.4
|
||||
idna>=2.7
|
||||
pip>=18.0
|
||||
requests>=2.19.1
|
||||
setuptools>=39.0.1
|
||||
urllib3>=1.23
|
||||
|
@ -35,6 +35,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IW4ScriptCommands", "Plugin
|
||||
EndProject
|
||||
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DiscordWebhook", "DiscordWebhook\DiscordWebhook.pyproj", "{15A81D6E-7502-46CE-8530-0647A380B5F4}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -312,6 +317,7 @@ Global
|
||||
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}
|
||||
|
@ -108,7 +108,6 @@
|
||||
<Folder Include="master\templates\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="FolderProfile.pubxml" />
|
||||
<Content Include="master\config\master.json" />
|
||||
<Content Include="requirements.txt" />
|
||||
<Content Include="master\templates\index.html" />
|
||||
@ -125,7 +124,7 @@
|
||||
<Architecture>X64</Architecture>
|
||||
</Interpreter>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
|
||||
<!-- Specify pre- and post-build commands in the BeforeBuild and
|
||||
AfterBuild targets below. -->
|
||||
<Target Name="BeforeBuild">
|
||||
|
@ -15,9 +15,9 @@ class Base():
|
||||
self.scheduler.start()
|
||||
self.scheduler.add_job(
|
||||
func=self._remove_staleinstances,
|
||||
trigger=IntervalTrigger(seconds=120),
|
||||
trigger=IntervalTrigger(seconds=60),
|
||||
id='stale_instance_remover',
|
||||
name='Remove stale instances if no heartbeat in 120 seconds',
|
||||
name='Remove stale instances if no heartbeat in 60 seconds',
|
||||
replace_existing=True
|
||||
)
|
||||
self.scheduler.add_job(
|
||||
@ -41,7 +41,7 @@ class Base():
|
||||
|
||||
def _remove_staleinstances(self):
|
||||
for key, value in list(self.instance_list.items()):
|
||||
if int(time.time()) - value.last_heartbeat > 120:
|
||||
if int(time.time()) - value.last_heartbeat > 60:
|
||||
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
|
||||
del self.instance_list[key]
|
||||
del self.token_list[key]
|
||||
|
@ -11,7 +11,7 @@ class Authenticate(Resource):
|
||||
if ctx.get_token(instance_id) is not False:
|
||||
return { 'message' : 'that id already has a token'}, 401
|
||||
else:
|
||||
expires = datetime.timedelta(days=1)
|
||||
expires = datetime.timedelta(days=30)
|
||||
token = create_access_token(instance_id, expires_delta=expires)
|
||||
ctx.add_token(instance_id, token)
|
||||
return { 'access_token' : token }, 200
|
||||
|
63
Plugins/ScriptPlugins/VPNDetection.js
Normal file
63
Plugins/ScriptPlugins/VPNDetection.js
Normal file
@ -0,0 +1,63 @@
|
||||
const plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 1.0,
|
||||
name: 'VPN Kick Plugin',
|
||||
|
||||
manager: null,
|
||||
logger: null,
|
||||
vpnExceptionIds: [],
|
||||
|
||||
checkForVpn(origin) {
|
||||
let exempt = false;
|
||||
// prevent players that are exempt from being kicked
|
||||
this.vpnExceptionIds.forEach(function(id) {
|
||||
if (id === origin.ClientId) {
|
||||
exempt = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (exempt) {
|
||||
return;
|
||||
}
|
||||
|
||||
let usingVPN = false;
|
||||
|
||||
try {
|
||||
let httpRequest = System.Net.WebRequest.Create('https://api.xdefcon.com/proxy/check/?ip=' + origin.IPAddressString);
|
||||
let response = httpRequest.GetResponse();
|
||||
let data = response.GetResponseStream();
|
||||
let streamReader = new System.IO.StreamReader(data);
|
||||
let jsonResponse = streamReader.ReadToEnd();
|
||||
streamReader.Dispose();
|
||||
response.Close();
|
||||
let parsedJSON = JSON.parse(jsonResponse);
|
||||
usingVPN = parsedJSON['success'] && parsedJSON['proxy'];
|
||||
} catch (e) {
|
||||
this.logger.WriteError(e.message);
|
||||
}
|
||||
|
||||
if (usingVPN) {
|
||||
let library = importNamespace('SharedLibraryCore');
|
||||
let kickOrigin = new library.Objects.Player();
|
||||
kickOrigin.ClientId = 1;
|
||||
origin.Kick(_localization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], kickOrigin);
|
||||
}
|
||||
},
|
||||
|
||||
onEventAsync(gameEvent, server) {
|
||||
// connect event
|
||||
if (gameEvent.Type === 3) {
|
||||
this.checkForVpn(gameEvent.Origin)
|
||||
}
|
||||
},
|
||||
|
||||
onLoadAsync(manager) {
|
||||
this.manager = manager;
|
||||
this.logger = manager.GetLogger();
|
||||
},
|
||||
|
||||
onUnloadAsync() {},
|
||||
|
||||
onTickAsync(server) {}
|
||||
}
|
@ -435,6 +435,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
case Penalty.PenaltyType.Ban:
|
||||
if (attacker.Level == Player.Permission.Banned)
|
||||
break;
|
||||
await saveLog();
|
||||
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
|
||||
{
|
||||
ClientId = 1,
|
||||
@ -448,7 +449,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
}
|
||||
});
|
||||
await saveLog();
|
||||
break;
|
||||
case Penalty.PenaltyType.Flag:
|
||||
if (attacker.Level != Player.Permission.User)
|
||||
@ -937,7 +937,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
int serverId = sv.GetHashCode();
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
|
||||
// Log.WriteDebug("Syncing stats contexts");
|
||||
// Log.WriteDebug("Syncing stats contexts");
|
||||
await statsSvc.ServerStatsSvc.SaveChangesAsync();
|
||||
//await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
await statsSvc.KillStatsSvc.SaveChangesAsync();
|
||||
|
@ -66,7 +66,7 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers
|
||||
.Include(s => s.HitDestination)
|
||||
.Include(s => s.CurrentViewAngle)
|
||||
.Include(s => s.PredictedViewAngles)
|
||||
.OrderBy(s => s.When)
|
||||
.OrderBy(s => new { s.When, s.Hits })
|
||||
.ToListAsync();
|
||||
|
||||
if (penaltyInfo != null)
|
||||
|
194
README.md
194
README.md
@ -1,19 +1,16 @@
|
||||
|
||||
|
||||
# IW4MAdmin
|
||||
### Quick Start Guide
|
||||
### Version 2.1
|
||||
### Version 2.2
|
||||
_______
|
||||
### About
|
||||
**IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/33/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/5/plutonium-iw5), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
|
||||
|
||||
### Download
|
||||
Latest binary builds are always available at https://raidmax.org/IW4MAdmin
|
||||
### Setup
|
||||
**IW4MAdmin** requires minimal configuration to run. There is only one prerequisite.
|
||||
* [.NET Core 2.0.7 Runtime](https://www.microsoft.com/net/download/dotnet-core/runtime-2.0.7) *or newer*
|
||||
|
||||
* [.NET Core 2.1 Runtime](https://www.microsoft.com/net/download) *or newer*
|
||||
1. Extract `IW4MAdmin-<version>.zip`
|
||||
2. Open command prompt or terminal in the extracted folder
|
||||
3. Run `dotnet IW4MAdmin.dll`
|
||||
2. Run `StartIW4MAdmin.cmd`
|
||||
___
|
||||
|
||||
### Configuration
|
||||
@ -37,14 +34,33 @@ When **IW4MAdmin** is launched for the _first time_, you will be prompted to set
|
||||
* Allow clients to use a [VPN](https://en.wikipedia.org/wiki/Virtual_private_network)
|
||||
* _This feature requires an active api key on [iphub.info](https://iphub.info/)_
|
||||
|
||||
`Enable discord link`
|
||||
* Shows a link to your server's discord on the webfront
|
||||
* _This feature requires an invite link to your discord server_
|
||||
`Enable social link`
|
||||
* Shows a link to your community's social media/website on the webfront
|
||||
|
||||
`Use Custom Encoding Parser`
|
||||
* Allows alternative encodings to be used for parsing game information and events
|
||||
* **Russian users should use this and then specify** `windows-1251` **as the encoding string**
|
||||
|
||||
#### Server Configuration
|
||||
After initial configuration is finished, you will be prompted to configure your servers for **IW4MAdmin**.
|
||||
|
||||
`Enter server IP Address`
|
||||
* For almost all scenarios `127.0.0.1` is sufficient
|
||||
|
||||
`Enter server port`
|
||||
* The port that your server is listening on (can be obtained via `net_port`)
|
||||
|
||||
`Enter server RCon password`
|
||||
* The *\(R\)emote (Con)sole* password set in your server configuration (can be obtained via `rcon_password`)
|
||||
|
||||
`Use Pluto T6 parser`
|
||||
* Used if setting up a server for Plutonium T6 (BO2)
|
||||
|
||||
`Use Pluto IW5 parser`
|
||||
* Used if setting a server for Plutonium IW5 (MW3)
|
||||
|
||||
`Enter number of reserved slots`
|
||||
* The number of client slots reserver for privileged players (unavailable for regular users to occupy)
|
||||
#### Advanced Configuration
|
||||
If you wish to further customize your experience of **IW4MAdmin**, the following configuration file(s) will allow you to changes core options using any text-editor.
|
||||
|
||||
@ -54,6 +70,20 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
|
||||
`WebfrontBindUrl`
|
||||
* Specifies the address and port the webfront will listen on.
|
||||
* The value can be an [IP Address](https://en.wikipedia.org/wiki/IP_address):port or [Domain Name](https://en.wikipedia.org/wiki/Domain_name):port
|
||||
* Example http://gameserver.com:8080
|
||||
|
||||
`CustomLocale`
|
||||
* Specifies a [locale name](https://msdn.microsoft.com/en-us/library/39cwe7zf.aspx) to use instead of system default
|
||||
* Locale must be from the `Equivalent Locale Name` column
|
||||
|
||||
`ConnectionString`
|
||||
* Specifies the [connection string](https://www.connectionstrings.com/mysql/) to a MySQL server that is used instead of SQLite
|
||||
|
||||
`RConPollRate`
|
||||
* Specifies (in milliseconds) how often to poll each server for updates
|
||||
|
||||
`VpnExceptionIds`
|
||||
* Specifies the list of `Client IDs` exempt from the VPN check (if enabled)
|
||||
|
||||
`Servers`
|
||||
* Specifies the list of servers **IW4MAdmin** will monitor
|
||||
@ -67,12 +97,22 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
|
||||
* Specifies the list of messages that are broadcasted to the particular server
|
||||
* `Rules`
|
||||
* Specifies the list of rules that apply to the particular server
|
||||
* `ReservedSlotNumber`
|
||||
* Specifies the number of client slots to reserve for privileged users
|
||||
|
||||
`AutoMessagePeriod`
|
||||
* Specifies (in seconds) how often messages should be broadcasted to the server(s)
|
||||
* Specifies (in seconds) how often messages should be broadcasted to each server
|
||||
|
||||
`AutoMessages`
|
||||
* Specifies the list of messages that are broadcasted to **all** servers
|
||||
* Specially formatted **tokens** can be used to broadcast dynamic information
|
||||
* `{{TOTALPLAYERS}}` — displays how many players have connected
|
||||
* `{{TOPSTATS}}` — displays the top 5 players on the server based on performance
|
||||
* `{{MOSTPLAYED}}` — displays the top 5 players based on number of kills
|
||||
* `{{TOTALPLAYTIME}}` — displays the cumulative play time (in man-hours) on all monitored servers
|
||||
* `{{VERSION}}` — displays the version of **IW4MAdmin**
|
||||
* `{{ADMINS}}` — displays the currently connected and *unmasked* privileged users online
|
||||
* `{{NEXTMAP}} &dmash; displays the next map in rotation
|
||||
|
||||
`GlobalRules`
|
||||
* Specifies the list of rules that apply to **all** servers`
|
||||
@ -88,44 +128,48 @@ ___
|
||||
### Commands
|
||||
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|
||||
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|
||||
|prune|pa|demote any admins that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|
||||
|prune|pa|demote any privileged clients that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|
||||
|quit|q|quit IW4MAdmin|False|!q |Owner|
|
||||
|rcon|rcon|send rcon command to server|False|!rcon \<command\>|Owner|
|
||||
|ban|b|permanently ban a player from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|
||||
|unban|ub|unban player by database id|True|!ub \<databaseID\> \<reason\>|SeniorAdmin|
|
||||
|find|f|find player in database|False|!f \<player\>|Administrator|
|
||||
|rcon|rcon|send rcon command to server|False|!rcon \<commands\>|Owner|
|
||||
|ban|b|permanently ban a client from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|
||||
|unban|ub|unban client by client id|True|!ub \<client id\> \<reason\>|SeniorAdmin|
|
||||
|find|f|find client in database|False|!f \<player\>|Administrator|
|
||||
|killserver|kill|kill the game server|False|!kill |Administrator|
|
||||
|map|m|change to specified map|False|!m \<map\>|Administrator|
|
||||
|maprotate|mr|cycle to the next map in rotation|False|!mr |Administrator|
|
||||
|plugins|p|view all loaded plugins|False|!p |Administrator|
|
||||
|alias|known|get past aliases and ips of a player|True|!known \<player\>|Moderator|
|
||||
|baninfo|bi|get information about a ban for a player|True|!bi \<player\>|Moderator|
|
||||
|tempban|tb|temporarily ban a client for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Administrator|
|
||||
|alias|known|get past aliases and ips of a client|True|!known \<player\>|Moderator|
|
||||
|baninfo|bi|get information about a ban for a client|True|!bi \<player\>|Moderator|
|
||||
|fastrestart|fr|fast restart current map|False|!fr |Moderator|
|
||||
|flag|fp|flag a suspicious player and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|
||||
|flag|fp|flag a suspicious client and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|
||||
|kick|k|kick a client by name|True|!k \<player\> \<reason\>|Moderator|
|
||||
|list|l|list active clients|False|!l |Moderator|
|
||||
|mask|hide|hide your presence as an administrator|False|!hide |Moderator|
|
||||
|mask|hide|hide your presence as a privileged client|False|!hide |Moderator|
|
||||
|reports|reps|get or clear recent reports|False|!reps \<optional clear\>|Moderator|
|
||||
|say|s|broadcast message to all players|False|!s \<message\>|Moderator|
|
||||
|setlevel|sl|set player to specified administration level|True|!sl \<player\> \<level\>|Moderator|
|
||||
|say|s|broadcast message to all clients|False|!s \<message\>|Moderator|
|
||||
|setlevel|sl|set client to specified privilege level|True|!sl \<player\> \<level\>|Moderator|
|
||||
|setpassword|sp|set your authentication password|False|!sp \<password\>|Moderator|
|
||||
|tempban|tb|temporarily ban a player for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Moderator|
|
||||
|unflag|uf|Remove flag for client|True|!uf \<player\>|Moderator|
|
||||
|uptime|up|get current application running time|False|!up |Moderator|
|
||||
|usage|us|get current application memory usage|False|!us |Moderator|
|
||||
|kick|k|kick a player by name|True|!k \<player\> \<reason\>|Trusted|
|
||||
|login|l|login using password|False|!l \<password\>|Trusted|
|
||||
|warn|w|warn player for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|
||||
|warnclear|wc|remove all warning for a player|True|!wc \<player\>|Trusted|
|
||||
|admins|a|list currently connected admins|False|!a |User|
|
||||
|usage|us|get application memory usage|False|!us |Moderator|
|
||||
|balance|bal|balance teams|False|!bal |Trusted|
|
||||
|login|li|login using password|False|!li \<password\>|Trusted|
|
||||
|warn|w|warn client for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|
||||
|warnclear|wc|remove all warnings for a client|True|!wc \<player\>|Trusted|
|
||||
|admins|a|list currently connected privileged clients|False|!a |User|
|
||||
|getexternalip|ip|view your external IP address|False|!ip |User|
|
||||
|help|h|list all available commands|False|!h \<optional command\>|User|
|
||||
|ping|pi|get client's ping|False|!pi \<optional client\>|User|
|
||||
|privatemessage|pm|send message to other player|True|!pm \<player\> \<message\>|User|
|
||||
|report|rep|report a player for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|
||||
|help|h|list all available commands|False|!h \<optional commands\>|User|
|
||||
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
|
||||
|owner|iamgod|claim ownership of the server|False|!iamgod |User|
|
||||
|ping|pi|get client's latency|False|!pi \<optional player\>|User|
|
||||
|privatemessage|pm|send message to other client|True|!pm \<player\> \<message\>|User|
|
||||
|report|rep|report a client for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|
||||
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|
||||
|rules|r|list server rules|False|!r |User|
|
||||
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|
||||
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|
||||
|whoami|who|give information about yourself.|False|!who |User|
|
||||
|topstats|ts|view the top 5 players in this server|False|!ts |User|
|
||||
|whoami|who|give information about yourself|False|!who |User|
|
||||
|
||||
_These commands include all shipped plugin commands._
|
||||
|
||||
@ -192,6 +236,7 @@ ___
|
||||
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|
||||
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|
||||
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|
||||
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
|
||||
|
||||
- To qualify for top stats, a client must have played for at least `1 hour` and connected within the past `30 days`.
|
||||
|
||||
@ -208,6 +253,7 @@ ___
|
||||
#### Profanity Determent
|
||||
- This plugin warns and kicks players for using profanity
|
||||
- Profane words and warning message can be specified in `ProfanityDetermentSettings.json`
|
||||
- If a client's name contains a word listed in the settings, they will immediately be kicked
|
||||
___
|
||||
### Webfront
|
||||
`Home`
|
||||
@ -221,6 +267,7 @@ ___
|
||||
|
||||
`Login`
|
||||
* Allows privileged users to login using their `Client ID` and password set via `setpassword`
|
||||
* `ClientID` is a number that can be found by using `!find <client name>` or find the client on the webfront and copy the ID following `ProfileAsync/`
|
||||
|
||||
`Profile`
|
||||
* Shows a client's information and history
|
||||
@ -229,7 +276,78 @@ ___
|
||||
* Allows logged in privileged users to execute commands as if they are in-game
|
||||
|
||||
---
|
||||
### Extending Plugins
|
||||
#### Code
|
||||
IW4Madmin functionality can be extended by writing additional plugins in C#.
|
||||
#### JavaScript
|
||||
IW4MAdmin functionality can be extended using JavaScript.
|
||||
The JavaScript parser supports [some](https://github.com/sebastienros/jint/issues/343) of ECMAScript 6's new features.
|
||||
#### Plugin Object Template
|
||||
In order to be properly parsed by the JavaScript engine, every plugin must conform to the following template.
|
||||
```js
|
||||
const plugin = {
|
||||
author: 'YourHandle',
|
||||
version: 1.0,
|
||||
name: 'Sample JavaScript Plugin',
|
||||
|
||||
### Misc
|
||||
onEventAsync(gameEvent, server) {
|
||||
},
|
||||
|
||||
onLoadAsync(manager) {
|
||||
},
|
||||
|
||||
onUnloadAsync() {
|
||||
},
|
||||
|
||||
onTickAsync(server) {
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Required Properties
|
||||
- `author` — [string] Author of the plugin (usually your name or online name/alias)
|
||||
- `version` — [float] Version number of your plugin (useful if you release several different versions)
|
||||
- `name` — [string] Name of your plugin (be descriptive!)
|
||||
- `onEventAsync` — [function] Handler executed when an event occurs
|
||||
- `gameEvent` — [parameter object] Object containing event type, origin, target, and other info (see the GameEvent class declaration)
|
||||
- `server` — [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
|
||||
- `onLoadAsync` — [function] Handler executed when the plugin is loaded by code
|
||||
- `manager` — [parameter object] Object reference to the application manager (see the IManager interface definition)
|
||||
- `onUnloadAsync` — [function] Handler executed when the plugin is unloaded by code (see live reloading)
|
||||
- `onTickAsync` — [function] Handler executed approximately once per second by code *(unimplemented as of version 2.1)*
|
||||
- `server` — [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
|
||||
### Live Reloading
|
||||
Thanks to JavaScript's flexibility and parsability, the plugin importer scans the plugins folder and reloads the JavaScript plugins on demand as they're modified. This allows faster development/testing/debugging.
|
||||
|
||||
---
|
||||
### Discord Webhook
|
||||
If you'd like to receive notifications on your Discord guild, configure and start `DiscordWebhook.py`
|
||||
#### Requirements
|
||||
- [Python 3.6](https://www.python.org/downloads/) or newer
|
||||
- The following [PIP](https://pypi.org/project/pip/) packages (provided in `requirements.txt`)
|
||||
```certifi>=2018.4.16
|
||||
chardet>=3.0.4
|
||||
idna>=2.7
|
||||
pip>=18.0
|
||||
requests>=2.19.1
|
||||
setuptools>=39.0.1
|
||||
urllib3>=1.23
|
||||
```
|
||||
#### Configuration Options
|
||||
- `IW4MAdminUrl` — Base url corresponding to your IW4MAdmin `WebfrontBindUrl`.
|
||||
Example http://127.0.0.1
|
||||
- `DiscordWebhookNotificationUrl` — [required] Discord generated URL to send notifications/alerts to; this includes **Reports** and **Bans**
|
||||
Example https://discordapp.com/api/webhooks/id/token
|
||||
- `DiscordWebhookInformationUrl` — [optional] Discord generated URL to send information to; this includes information such as player messages
|
||||
- `NotifyRoleIds` — [optional] List of [discord role ids](https://discordhelp.net/role-id) to mention when notification hook is sent
|
||||
#### Launching
|
||||
With Python installed, open a terminal/command prompt window open in the `Webhook` folder and execute `python DiscordWebhook.py`
|
||||
|
||||
---
|
||||
## Misc
|
||||
#### Anti-cheat
|
||||
This is an [IW4x](https://iw4xcachep26muba.onion.link/) only feature (wider game support planned), that uses analytics to detect aimbots and aim-assist tools.
|
||||
To utilize anti-cheat, enable it during setup **and** copy `_customcallbacks.gsc` from `userraw` into your `IW4x Server\userraw\scripts` folder.
|
||||
The anti-cheat feature is a work in progress and as such will be constantly tweaked and may not be 100% accurate, however the goal is to deter as many cheaters as possible from IW4x.
|
||||
#### Database Storage
|
||||
All **IW4MAdmin** information is stored in `Database.db`. Should you need to reset your database, this file can simply be deleted. Additionally, this file should be preserved during updates to retain client information.
|
||||
By default, all **IW4MAdmin** information is stored in `Database.db`. Should you need to reset your database, this file can simply be deleted. Additionally, this file should be preserved during updates to retain client information.
|
||||
Setting the `ConnectionString` property in `IW4MAdminSettings.json` will cause **IW4MAdmin** to attempt to use a MySQL connection for database storage.
|
@ -22,13 +22,13 @@ namespace SharedLibraryCore.Configuration
|
||||
public string CustomLocale { get; set; }
|
||||
public string ConnectionString { get; set; }
|
||||
public int RConPollRate { get; set; } = 5000;
|
||||
public List<int> VpnExceptionIds { get; set; }
|
||||
public string Id { get; set; }
|
||||
public List<ServerConfiguration> Servers { get; set; }
|
||||
public int AutoMessagePeriod { get; set; }
|
||||
public List<string> AutoMessages { get; set; }
|
||||
public List<string> GlobalRules { get; set; }
|
||||
public List<MapConfiguration> Maps { get; set; }
|
||||
public List<int> VpnExceptionIds { get; set; }
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
|
@ -9,8 +9,8 @@ namespace SharedLibraryCore.Configuration
|
||||
public string IPAddress { get; set; }
|
||||
public ushort Port { get; set; }
|
||||
public string Password { get; set; }
|
||||
public List<string> Rules { get; set; }
|
||||
public List<string> AutoMessages { get; set; }
|
||||
public IList<string> Rules { get; set; }
|
||||
public IList<string> AutoMessages { get; set; }
|
||||
public bool UseT6MParser { get; set; }
|
||||
public bool UseIW5MParser { get; set; }
|
||||
public string ManualLogPath { get; set; }
|
||||
|
@ -87,6 +87,7 @@ namespace SharedLibraryCore
|
||||
{
|
||||
return queuedEvent.Origin != null &&
|
||||
!queuedEvent.Origin.IsAuthenticated &&
|
||||
queuedEvent.Origin.State != Player.ClientState.Connected &&
|
||||
// we want to allow join and quit events
|
||||
queuedEvent.Type != EventType.Join &&
|
||||
queuedEvent.Type != EventType.Quit &&
|
||||
@ -104,6 +105,7 @@ namespace SharedLibraryCore
|
||||
{
|
||||
return queuedEvent.Target != null &&
|
||||
!queuedEvent.Target.IsAuthenticated &&
|
||||
queuedEvent.Target.State != Player.ClientState.Connected &&
|
||||
queuedEvent.Target.NetworkId != 0;
|
||||
}
|
||||
}
|
||||
|
@ -13,37 +13,33 @@ namespace SharedLibraryCore.Plugins
|
||||
public static List<IPlugin> ActivePlugins = new List<IPlugin>();
|
||||
public static List<Assembly> PluginAssemblies = new List<Assembly>();
|
||||
|
||||
private static void LoadScriptPlugins(IManager mgr)
|
||||
{
|
||||
string[] scriptFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.js");
|
||||
|
||||
foreach(string fileName in scriptFileNames)
|
||||
{
|
||||
var plugin = new ScriptPlugin(fileName);
|
||||
plugin.Initialize(mgr).Wait();
|
||||
ActivePlugins.Add(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Load(IManager Manager)
|
||||
{
|
||||
string[] dllFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.dll");
|
||||
string[] scriptFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.js");
|
||||
|
||||
if (dllFileNames.Length == 0)
|
||||
if (dllFileNames.Length == 0 &&
|
||||
scriptFileNames.Length == 0)
|
||||
{
|
||||
Manager.GetLogger().WriteDebug(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_NOTFOUND"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// load up the script plugins
|
||||
foreach (string fileName in scriptFileNames)
|
||||
{
|
||||
var plugin = new ScriptPlugin(fileName);
|
||||
plugin.Initialize(Manager).Wait();
|
||||
Manager.GetLogger().WriteDebug($"Loaded script plugin \"{ plugin.Name }\" [{plugin.Version}]");
|
||||
ActivePlugins.Add(plugin);
|
||||
}
|
||||
|
||||
ICollection<Assembly> assemblies = new List<Assembly>(dllFileNames.Length);
|
||||
foreach (string dllFile in dllFileNames)
|
||||
{
|
||||
// byte[] rawDLL = File.ReadAllBytes(dllFile);
|
||||
//Assembly assembly = Assembly.Load(rawDLL);
|
||||
assemblies.Add(Assembly.LoadFrom(dllFile));
|
||||
}
|
||||
|
||||
int LoadedPlugins = 0;
|
||||
int LoadedCommands = 0;
|
||||
foreach (Assembly Plugin in assemblies)
|
||||
{
|
||||
@ -74,19 +70,17 @@ namespace SharedLibraryCore.Plugins
|
||||
ActivePlugins.Add(newNotify);
|
||||
PluginAssemblies.Add(Plugin);
|
||||
Manager.GetLogger().WriteDebug($"Loaded plugin \"{ newNotify.Name }\" [{newNotify.Version}]");
|
||||
LoadedPlugins++;
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception E)
|
||||
{
|
||||
Manager.GetLogger().WriteWarning($"Could not load plugin {Plugin.Location} - {E.Message}");
|
||||
Manager.GetLogger().WriteWarning($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"]} {Plugin.Location} - {E.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LoadScriptPlugins(Manager);
|
||||
Manager.GetLogger().WriteInfo($"Loaded {LoadedPlugins} plugins and registered {LoadedCommands} commands.");
|
||||
Manager.GetLogger().WriteInfo($"Loaded {ActivePlugins.Count} plugins and registered {LoadedCommands} commands.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ namespace SharedLibraryCore
|
||||
|
||||
public float Version { get; set; }
|
||||
|
||||
public string Author {get;set;}
|
||||
public string Author { get; set; }
|
||||
|
||||
private Jint.Engine ScriptEngine;
|
||||
private readonly string FileName;
|
||||
@ -49,15 +49,22 @@ namespace SharedLibraryCore
|
||||
|
||||
public async Task Initialize(IManager mgr)
|
||||
{
|
||||
bool firstRun = ScriptEngine == null;
|
||||
// it's been loaded before so we need to call the unload event
|
||||
if (ScriptEngine != null)
|
||||
if (!firstRun)
|
||||
{
|
||||
await OnUnloadAsync();
|
||||
}
|
||||
|
||||
Manager = mgr;
|
||||
string script = File.ReadAllText(FileName);
|
||||
ScriptEngine = new Jint.Engine();
|
||||
ScriptEngine = new Jint.Engine(cfg =>
|
||||
cfg.AllowClr(new[]
|
||||
{
|
||||
typeof(System.Net.WebRequest).Assembly,
|
||||
typeof(Objects.Player).Assembly,
|
||||
})
|
||||
.CatchClrExceptions());
|
||||
|
||||
ScriptEngine.Execute(script);
|
||||
ScriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
|
||||
@ -67,7 +74,7 @@ namespace SharedLibraryCore
|
||||
this.Name = pluginObject.name;
|
||||
this.Version = (float)pluginObject.version;
|
||||
|
||||
if (ScriptEngine != null)
|
||||
if (!firstRun)
|
||||
{
|
||||
await OnLoadAsync(mgr);
|
||||
}
|
||||
|
@ -20,6 +20,16 @@ namespace SharedLibraryCore
|
||||
public static Encoding EncodingType;
|
||||
public static Localization.Layout CurrentLocalization;
|
||||
|
||||
public static string HttpRequest(string location, string header, string headerValue)
|
||||
{
|
||||
using (var RequestClient = new System.Net.Http.HttpClient())
|
||||
{
|
||||
RequestClient.DefaultRequestHeaders.Add(header, headerValue);
|
||||
string response = RequestClient.GetStringAsync(location).Result;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
//Get string with specified number of spaces -- really only for visual output
|
||||
public static String GetSpaces(int Num)
|
||||
{
|
||||
|
@ -7,7 +7,7 @@
|
||||
}
|
||||
<div id="profile_wrapper" class="row d-flex d-sm-inline-flex justify-content-center justify-content-left pb-3">
|
||||
<div class="mr-auto ml-auto ml-sm-0 mr-sm-0">
|
||||
<div id="profile_avatar" class="mb-4 mb-md-0 text-center level-bgcolor-@Model.LevelInt" style="background-image:url('@string.Format("https://gravatar.com/avatar/{0}?size=168&default=blank&rating=pg", gravatarUrl)">
|
||||
<div id="profile_avatar" class="mb-4 mb-md-0 text-center level-bgcolor-@Model.LevelInt" 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>
|
||||
|
@ -29,7 +29,7 @@
|
||||
<tr class="d-table-row d-md-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<td>
|
||||
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId }) })
|
||||
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId })
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span> @Model.TimeRemaining @loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]</span>
|
||||
<span> @Model.TimeRemaining</span>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
@ -60,7 +60,7 @@
|
||||
@Model.Offense
|
||||
</td>
|
||||
<td>
|
||||
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId }) })
|
||||
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId })
|
||||
</td>
|
||||
<td class="text-right text-light">
|
||||
@{
|
||||
@ -70,7 +70,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span> @Model.TimeRemaining <!-- @loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"] --></span>
|
||||
<span> @Model.TimeRemaining </span>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
|
@ -47,7 +47,7 @@
|
||||
color: rgba(235, 211, 101, 0.75);
|
||||
}
|
||||
|
||||
.level-bgcolor-moderator, .level-bgcolor-3 {
|
||||
.level-bgcolor-moderator, .level-bgcolor-3 {
|
||||
background-color: #f0de8b;
|
||||
background-color: rgba(235, 211, 101, 0.75);
|
||||
}
|
||||
@ -57,7 +57,7 @@
|
||||
color: rgba(236, 130, 222, 0.69);
|
||||
}
|
||||
|
||||
.level-bgcolor-administrator, .level.bgcolor-4 {
|
||||
.level-bgcolor-administrator, .level-bgcolor-4 {
|
||||
background-color: #f1a8e8;
|
||||
background-color: rgba(236, 130, 222, 0.69);
|
||||
}
|
||||
@ -80,6 +80,14 @@
|
||||
background-color: rgb(0, 122, 204);
|
||||
}
|
||||
|
||||
.level-color-8 {
|
||||
color: #de4423;
|
||||
}
|
||||
|
||||
.level-bgcolor-8 {
|
||||
background-color: #de4423;
|
||||
}
|
||||
|
||||
.profile-meta-title {
|
||||
color: white;
|
||||
}
|
||||
|
16
version.txt
16
version.txt
@ -1,5 +1,21 @@
|
||||
Version 2.2:
|
||||
-upgraded projects to .NET 2.1
|
||||
-added top player stats page
|
||||
-added JavaScript plugin support
|
||||
-added webhook script to send notifications to discord
|
||||
-added abillity to exempt specific clients from VPN check
|
||||
-added reserved slots for privileged users
|
||||
-added support for localized permission levels
|
||||
-added linux support!
|
||||
-added {{NEXTMAP}}, {{ADMINS}}, and {{MOSTPLAYED}} automessage tokens
|
||||
-updated event api
|
||||
-updated webfront tweak
|
||||
-update client search by IP
|
||||
-updated event management and client authentication
|
||||
-fixed some namespace discrepancies
|
||||
-fixed parsing of certain chat messages
|
||||
-fixed various bugs
|
||||
-introduced new bugs to fix in the next version
|
||||
|
||||
Version 2.1:
|
||||
CHANGELOG:
|
||||
|
Loading…
Reference in New Issue
Block a user