C#:
using ConVar;
using Facepunch;
using Facepunch.Extend;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Libraries;
using Rust;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Networking;
using WebSocketSharp;
using Application = UnityEngine.Application;
using Time = Oxide.Core.Libraries.Time;
#pragma warning disable 8600
#pragma warning disable 8601
namespace Oxide.Plugins {
[Info("Server Armour", "Pho3niX90", "2.87.1")]
[Description("Protect your server! Auto ban known hackers, scripters and griefer accounts, and notify server owners of threats.")]
class ServerArmour : CovalencePlugin {
#if CARBON
bool isCarbon = true;
#else
bool isCarbon = false;
#endif
#region Variables
string api_hostname = "https://serverarmour.com";
//
Dictionary<string, ISAPlayer> _playerData = new Dictionary<string, ISAPlayer>();
private Time _time = GetLibrary<Time>();
private double cacheLifetime = 1;
// minutes
private SAConfig config;
string specifier = "G";
CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");
//StringComparison defaultCompare = StringComparison.InvariantCultureIgnoreCase;
const string DATE_FORMAT = "yyyy/MM/dd HH:mm";
const string DATE_FORMAT2 = "yyyy-MM-dd HH:mm:ss";
const string DATE_FORMAT_BAN = "yyyy-MM-ddTHH:mm:ss.fffZ";
Regex logRegex = new Regex(@"(^assets.*prefab).*?position (.*) on");
Regex logRegexNull = new Regex(@"(.*) changed its network group to null");
bool debug = false;
Timer debugDisableTimer;
const int DefaultTempDebugMinutes = 15;
const int MaxTempDebugMinutes = 120;
bool apiConnected = false;
bool serverStarted = false;
int serverId;
string orgUuid;
private Dictionary<string, string> headers;
string adminIds = "";
Timer updateTimer;
Timer mapUploadTimer;
Timer nativeBanSyncTimer;
readonly Queue<string> combatUploadQueue = new Queue<string>();
Timer combatUploadFlushTimer;
bool combatUploadInFlight;
int combatDroppedEntries = 0;
int combatUploadRequests = 0;
int combatUploadFailures = 0;
readonly System.Random retryRandom = new System.Random();
private const int ApiMaxRetries = 8;
private const float ApiWatchdogSeconds = 35f;
private const int SteamVisibilityPrivate = 1;
private const int SteamVisibilityFriendsOnly = 2;
private const int SteamVisibilityPublic = 3;
private readonly Dictionary<string, Timer> playerCheckDebounceTimers = new Dictionary<string, Timer>();
private readonly Dictionary<string, Timer> playerPeriodicCheckTimers = new Dictionary<string, Timer>();
private readonly Dictionary<string, Timer> playerRecheckTimers = new Dictionary<string, Timer>();
private readonly Dictionary<string, bool> apiInFlight = new Dictionary<string, bool>();
private readonly Dictionary<string, int> playerRecheckAttempts = new Dictionary<string, int>();
private readonly HashSet<string> addBanInFlight = new HashSet<string>();
private readonly List<ISABan> pendingBans = new List<ISABan>();
private bool mapUploadInProgress = false;
private string lastUploadedMapFingerprint = string.Empty;
// related to auto updating
Dictionary<string, byte[]> fileBackups = new Dictionary<string, byte[]>();
List<string> ignoredPlugins = new List<string>();
#endregion
#region Libraries
private readonly Game.Rust.Libraries.Player Player = Interface.Oxide.GetLibrary<Game.Rust.Libraries.Player>();
#endregion
#region Permissions
const string PermissionToBan = "serverarmour.ban";
const string PermissionToUnBan = "serverarmour.unban";
const string PermissionAdminWebsite = "serverarmour.website.admin";
const string PermissionWhitelistRecentVacKick = "serverarmour.whitelist.recentvac";
const string PermissionWhitelistBadIPKick = "serverarmour.whitelist.badip";
const string PermissionWhitelistVacCeilingKick = "serverarmour.whitelist.vacceiling";
const string PermissionWhitelistServerCeilingKick = "serverarmour.whitelist.banceiling";
const string PermissionWhitelistGameBanCeilingKick = "serverarmour.whitelist.gamebanceiling";
const string PermissionWhitelistTotalBanCeiling = "serverarmour.whitelist.totalbanceiling";
const string PermissionWhitelistSteamProfile = "serverarmour.whitelist.steamprofile";
const string PermissionWhitelistFamilyShare = "serverarmour.whitelist.familyshare";
const string PermissionWhitelistTwitterBan = "serverarmour.whitelist.twitterban";
const string PermissionWhitelistAllowCountry = "serverarmour.whitelist.allowcountry";
const string PermissionWhitelistAllowHighPing = "serverarmour.whitelist.allowhighping";
const string PermissionWhitelistCombatGuidance = "serverarmour.whitelist.combatguidance";
const string PermissionWhitelistAccountTrust = "serverarmour.whitelist.accounttrust";
const string PermissionRadar = "serverarmour.radar";
const string DISCORD_INTRO_URL = "https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks";
/**
* Plugin Name, Download Url
*/
Dictionary<string, string> requiredPlugins = new Dictionary<string, string> {
{
"DiscordApi", "https://serverarmour.com/api/v1/shop/product/379a27d9-d245-43c9-ad8f-332efa8a25e6/download"
}
, {
"CombatLogInfo", "https://serverarmour.com/api/v1/shop/product/4195c37c-8eb5-47f6-8908-45aaa06df59c/download"
}
}
;
// int serverId = 0;
#endregion
#region Plugins
#pragma warning disable 0649
[PluginReference]
Plugin DiscordApi, DiscordMessages, BetterChat, Ember, Clans, AdminToggle, CombatLogInfo, ServerArmourUpdater;
#pragma warning restore 0649
void DiscordSend(string steamId, string name, EmbedFieldList report, int color = 39423, bool isBan = false) {
string webHook;
if (isBan) {
if (config.DiscordBanWebhookURL.IsNullOrEmpty() || config.DiscordBanWebhookURL.Equals(DISCORD_INTRO_URL)) {
Puts("Discord webhook not setup.");
return;
}
webHook = config.DiscordBanWebhookURL;
}
else {
if (config.DiscordWebhookURL.IsNullOrEmpty() || config.DiscordWebhookURL.Equals(DISCORD_INTRO_URL)) {
Puts("Discord webhook not setup.");
return;
}
webHook = config.DiscordWebhookURL;
}
List<EmbedFieldList> fields = new List<EmbedFieldList>();
if (config.DiscordQuickConnect) {
fields.Add(new EmbedFieldList() {
name = server.Name, value = $"[steam://connect/{config.ServerIp}:{server.Port}](steam://connect/{config.ServerIp}:{server.Port})", inline = true
}
);
}
fields.Add(new EmbedFieldList() {
name = "Steam Profile", value = $"[{name}\n{steamId}](https://steamcommunity.com/profiles/{steamId})", inline = !config.DiscordQuickConnect
}
);
fields.Add(new EmbedFieldList() {
name = "Server Armour Profile ", value = $"[{name}\n{steamId}](https://serverarmour.com/profile/{steamId})", inline = !config.DiscordQuickConnect
}
);
fields.Add(report);
var fieldsObject = fields.Cast<object>().ToArray();
string json = JsonConvert.SerializeObject(fieldsObject);
if (DiscordApi != null && DiscordApi.IsLoaded) {
DiscordApi?.Call("API_SendEmbeddedMessage", webHook, "Server Armour Report: ", color, json);
}
else if (DiscordMessages != null && DiscordMessages.IsLoaded) {
DiscordMessages?.Call("API_SendFancyMessage", webHook, "Server Armour Report: ", color, json);
}
else {
LogWarning("No discord API plugin loaded, will not publish to hook!");
}
}
void CheckPing(IPlayer player) {
// fix: https://discord.com/channels/751155344532570223/751155561776808158/1160293816104914974
if (player == null || !player.IsConnected) return;
try {
if (!HasPerm(player.Id, PermissionWhitelistAllowHighPing) && (config.AutoKickMaxPing > 0 && config.AutoKickMaxPing < player.Ping)) {
KickPlayer(player.Id, GetMsg("Your Ping is too High", new Dictionary<string, string> {
["ping"]
= player.Ping.ToString(), ["maxPing"] = config.AutoKickMaxPing.ToString()
}
), "C", true);
}
}
catch (Exception) {
}
}
#endregion
#region Hooks
void OnServerSave() {
if (config.AutoKickMaxPing > 0) foreach (var player in players.Connected) timer.Once(5f, () => CheckPing(player));
}
void OnServerInitialized(bool first) {
LoadData();
if (first) {
//quick fix to ignore bans on server restart. (rust rebanning.)
Unsubscribe(nameof(OnUserBanned));
timer.Once(60, () => {
Subscribe(nameof(OnUserBanned));
serverStarted = true;
FlushPendingBans();
CheckLocalBans();
}
);
}
else {
serverStarted = true;
FlushPendingBans();
CheckLocalBans();
}
// CheckOnlineUsers();
string ServerGPort = ConVar.Server.port.ToString();
string ServerQPort = ConVar.Server.queryport > 0 ? ConVar.Server.queryport.ToString() : ServerGPort;
string ServerRPort = RCon.Port.ToString();
Puts($"Server Ports are, Game Port: {ServerGPort} | Query Port:{ServerQPort} | RCON Port: {ServerRPort}");
RegPerm(PermissionToBan);
RegPerm(PermissionToUnBan);
RegPerm(PermissionAdminWebsite);
RegPerm(PermissionWhitelistBadIPKick);
RegPerm(PermissionWhitelistRecentVacKick);
RegPerm(PermissionWhitelistServerCeilingKick);
RegPerm(PermissionWhitelistVacCeilingKick);
RegPerm(PermissionWhitelistGameBanCeilingKick);
RegPerm(PermissionWhitelistTotalBanCeiling);
RegPerm(PermissionWhitelistSteamProfile);
RegPerm(PermissionWhitelistFamilyShare);
RegPerm(PermissionWhitelistTwitterBan);
RegPerm(PermissionWhitelistAllowCountry);
RegPerm(PermissionWhitelistAllowHighPing);
RegPerm(PermissionWhitelistCombatGuidance);
RegPerm(PermissionWhitelistAccountTrust);
RegPerm(PermissionRadar);
RegisterTag();
string framework = this.isCarbon ? "Carbon" : "Oxide";
headers = new Dictionary<string, string> {
{
"server-key", config.ServerApiKey
}
, {
"Accept", "application/json"
}
, {
"Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"
}
, {
"User-Agent", $"Server Armour/{this.Version} <{framework}>"
}
}
;
config.CombatLogBatchSize = Math.Max(1, config.CombatLogBatchSize);
config.CombatLogQueueMaxEntries = Math.Max(config.CombatLogBatchSize, config.CombatLogQueueMaxEntries);
config.CombatLogPayloadTargetKb = Math.Max(64, config.CombatLogPayloadTargetKb);
config.CombatLogRetryBaseSeconds = Math.Max(1, config.CombatLogRetryBaseSeconds);
config.CombatLogFlushSeconds = Math.Max(0.1f, config.CombatLogFlushSeconds);
InitChatModeration();
InitTeamingSignals();
string[] _admins = permission.GetUsersInGroup("admin");
if (config.OwnerSteamId != null && config.OwnerSteamId.Length > 0 && !_admins.Contains(config.OwnerSteamId)) {
var adminsList = new List<string>();
adminsList.AddRange(_admins);
adminsList.AddRange(config.OwnerSteamId.Split(','));
var extraAdmins = permission.GetPermissionUsers(PermissionAdminWebsite);
if (extraAdmins != null && extraAdmins.Length > 0) {
adminsList.AddRange(extraAdmins);
}
int e = 0;
foreach (string extraAdminsGroups in permission.GetPermissionGroups(PermissionAdminWebsite)) {
foreach (string extraAdmin in permission.GetUsersInGroup(extraAdminsGroups)) {
if (!adminsList.Contains(extraAdmin)) {
adminsList.Add(extraAdmin);
e++;
}
}
}
_admins = adminsList.Distinct().ToArray();
}
for (int i = 0, n = _admins.Length; i < n; i++) {
adminIds += $"{_admins[i].Substring(0, 17)}" + (i < _admins.Length - 1 ? "," : string.Empty);
}
CheckServerConnection();
Application.logMessageReceived += HandleLog;
if (ServerArmourUpdater != null && ServerArmourUpdater.IsLoaded) {
foreach (var item in plugins.GetAll()) {
if (item.Filename == null || item.Filename.Length == 0) continue;
if (requiredPlugins.ContainsKey(item.Name)) requiredPlugins.Remove(item.Name);
}
Puts($"Needed plugins = {requiredPlugins.Count}");
foreach (var plugin in requiredPlugins) {
ServerArmourUpdater?.Call("QueueDownload", plugin.Key, plugin.Value, "Server Armour");
}
}
}
void CheckServerConnection() {
string body = ServerGetString();
DoRequest("check_server", body, (code, response) => {
JObject obj = null;
try {
obj = JObject.Parse(response);
}
catch (Exception) {
timer.Once(15, CheckServerConnection);
return;
}
if (obj != null) {
var msg = obj["message"].ToString();
try {
this.serverId = int.Parse(obj["serverId"].ToString());
this.orgUuid = obj["orgUuid"].ToString();
}
catch (Exception) {
}
Puts(msg);
if (msg.Equals("connected")) {
apiConnected = true;
Puts($"Connected to SA API | Server ID = {this.serverId}" + (!this.orgUuid.IsNullOrEmpty() ? $" | orgUuid = ${this.orgUuid}" : ""));
ServerStatusUpdate();
updateTimer = timer.Every(60, ServerStatusUpdate);
nativeBanSyncTimer = timer.Every(180f, SyncPardonedNativeBans);
timer.Once(30f, SyncPardonedNativeBans);
QueueAutomaticMapUpload("api_connected", config.AutoUploadRconMapInitialDelaySeconds);
}
else {
Puts($"ServerApiKey = {config.ServerApiKey}");
var errMsg = "Server Armour has not initialized. Is your apikey correct? Get it from https://serverarmour.com/my-servers or join discord for support https://discord.gg/jxvRaPR";
LogError(errMsg);
timer.Once(500, () => LogError(errMsg));
return;
}
Puts("Server Armour has initialized.");
}
else {
timer.Once(5, CheckServerConnection);
return;
}
}
);
}
void Unload() {
_playerData?.Clear();
_playerData = null;
apiInFlight.Clear();
foreach (var debounceTimer in playerCheckDebounceTimers.Values) {
if (debounceTimer != null && !debounceTimer.Destroyed) debounceTimer.Destroy();
}
playerCheckDebounceTimers.Clear();
foreach (var periodicTimer in playerPeriodicCheckTimers.Values) {
if (periodicTimer != null && !periodicTimer.Destroyed) periodicTimer.Destroy();
}
playerPeriodicCheckTimers.Clear();
foreach (var recheckTimer in playerRecheckTimers.Values) {
if (recheckTimer != null && !recheckTimer.Destroyed) recheckTimer.Destroy();
}
playerRecheckTimers.Clear();
playerRecheckAttempts.Clear();
Application.logMessageReceived -= HandleLog;
if (updateTimer != null && !updateTimer.Destroyed) updateTimer.Destroy();
if (mapUploadTimer != null && !mapUploadTimer.Destroyed) mapUploadTimer.Destroy();
if (nativeBanSyncTimer != null && !nativeBanSyncTimer.Destroyed) nativeBanSyncTimer.Destroy();
if (debugDisableTimer != null && !debugDisableTimer.Destroyed) debugDisableTimer.Destroy();
UnloadChatModeration();
DestroyAllPlayerRadars();
}
void OnUserConnected(IPlayer iPlayer) {
this.DoConnectionChecks(iPlayer);
}
object CanUserLogin(string name, string id, string ipAddress) {
if (string.IsNullOrEmpty(id)) return null;
if (TryGetNativeBanReason(id, out string nativeReason)) return nativeReason;
ISABan cacheBan = IsBanned(id);
if (cacheBan != null) return cacheBan.reason;
return null;
}
void DoConnectionChecks(IPlayer player, string type = "C") {
if (config.AutoKick_KickWeirdSteam64 && !player.Id.IsSteamId()) {
KickPlayer(player.Id, GetMsg("Strange Steam64ID"), type);
return;
}
if (EnforceNativeAndCacheBans(player, type)) return;
if (!apiConnected && !config.ServerApiKey.IsNullOrEmpty()) {
LogError("User not checked. Server armour is not loaded.");
}
else if (apiConnected) {
CheckActiveBanFromApi(player.Id, player.Address, player.IsConnected);
WebCheckPlayer(player.Id, player.Address, player.IsConnected);
timer.Once(10f, () => CheckPing(player));
}
try {
var connectedSeconds = GetConnectedSeconds(player);
if (connectedSeconds < 600) SchedulePeriodicConnectionCheck(player);
if (config.ShowProtectedMsg && connectedSeconds < 60) SendReplyWithIcon(player, GetMsg("Protected MSG"));
}
catch (Exception) {
}
}
bool EnforceNativeAndCacheBans(IPlayer player, string type = "U") {
if (player == null || string.IsNullOrEmpty(player.Id)) return false;
if (TryKickIfNativelyBanned(player.Id, type)) return true;
KickIfBanned(GetPlayerCache(player.Id));
return false;
}
int GetConnectedSeconds(IPlayer player) {
return BasePlayer.FindByID(ulong.Parse(player.Id))?.secondsConnected ?? int.MaxValue;
}
int GetConnectedSeconds(ISAPlayer player) {
return BasePlayer.FindByID(ulong.Parse(player.steamid))?.secondsConnected ?? int.MaxValue;
}
void OnUserDisconnected(IPlayer player) {
if (apiConnected) {
try {
_playerData = _playerData?.Where(pair => MinutesAgo((uint)pair.Value.cacheTimestamp) < cacheLifetime) ?.ToDictionary(pair => pair.Key, pair => pair.Value);
}
catch (Exception) {
}
}
if (player == null || string.IsNullOrEmpty(player.Id)) return;
if (playerCheckDebounceTimers.TryGetValue(player.Id, out Timer debounceTimer) && debounceTimer != null && !debounceTimer.Destroyed) debounceTimer.Destroy();
playerCheckDebounceTimers.Remove(player.Id);
ClearPlayerRecheck(player.Id);
ClearPeriodicConnectionCheck(player.Id);
DestroyPlayerRadar(player.Object as BasePlayer);
}
void OnPluginLoaded(Plugin plugin) {
if (plugin.Title == "BetterChat") RegisterTag();
}
void OnNewSave(string filename) {
QueueAutomaticMapUpload("new_save", Mathf.Max(config.AutoUploadRconMapInitialDelaySeconds, 30f));
}
/**
* To detect publisher bans
*/
void OnPlayerKicked(BasePlayer player, string reason) {
if (reason.Contains("PublisherBanned")) {
DoRequest($"player/eacban", $"reason={FormEncode(reason)}&steamId={player.UserIDString}&username={FormEncode(player.displayName)}&serverId={this.serverId}", (code, response) => {
}
, RequestMethod.POST);
}
}
void OnUserUnbanned(string name, string id, string ipAddress) {
DoPardon(id);
}
void OnUserBanned(string name, string id, string ipAddress, string reason) {
//this is to make sure that if an app like battlemetrics for example, bans a player, we catch it.
if (config.IgnoreCheatDetected && reason.StartsWith("Cheat Detected")) {
return;
}
try {
if (AdminToggle != null && AdminToggle.Call<bool>("IsAdmin", id)) {
return;
}
}
catch (Exception) {
}
timer.Once(10f, () => {
//lets make sure first it wasn't us.
if (!IsPlayerCached(id) || !ContainsMyBan(id)) {
IPlayer bPlayer = players.FindPlayerById(id);
if (bPlayer != null) {
if ((bPlayer.IsAdmin && config.IgnoreAdmins)) return;
DateTime nowUtc = DateTime.UtcNow;
long expiry = 0;
if (ulong.TryParse(bPlayer.Id, out ulong banSteamId)) expiry = ServerUsers.Get(banSteamId)?.expiry ?? 0;
AddBan(new ISABan {
steamid = ulong.Parse(bPlayer.Id), serverName = server.Name, serverIp = config.ServerIp, reason = reason, created = nowUtc.ToString(DATE_FORMAT), banUntil = ResolveBanUntil(expiry)
}
, false);
}
}
}
);
}
void OnPlayerReported(BasePlayer reporter, string targetName, string targetId, string subject, string message, string type) {
if (!serverStarted) return;
string messageClean = FormEncode(message);
string subjectClean = FormEncode(subject);
DoRequest($"player/{reporter.UserIDString}/addf7", $"target={targetId}&subject={subjectClean}&message={messageClean}", (c, s) => {
}
);
}
#endregion
#region Checks
void GetPlayerRiskScore(ulong steamId) {
GetJson($"player/{steamId}/risk", (c, s) => {
if (c < 300) {
double riskscore = 0;
double.TryParse(s.GetValue("score").ToString(), out riskscore);
// TODO check the risk
}
}
);
}
#endregion
#region API_Hooks
#endregion
#region WebRequests
void GetPlayerBans(IPlayer player) {
if (player == null || player.Id == null) return;
if (EnforceNativeAndCacheBans(player)) return;
if (apiConnected) {
WebCheckPlayer(player.Id, player.Address, player.IsConnected);
timer.Once(10f, () => CheckPing(player));
}
}
void GetPlayerBans(string playerId, string playerName) {
if (TryKickIfNativelyBanned(playerId)) return;
KickIfBanned(GetPlayerCache(playerId));
if (apiConnected) WebCheckPlayer(playerId, "0.0.0.0", true);
}
void WebCheckPlayer(string id, string address, bool connected) {
if (!apiConnected || string.IsNullOrEmpty(id)) return;
string debounceKey = id;
if (playerCheckDebounceTimers.TryGetValue(debounceKey, out Timer existing) && existing != null && !existing.Destroyed) existing.Destroy();
playerCheckDebounceTimers[debounceKey] = timer.Once(0.35f, () => {
playerCheckDebounceTimers.Remove(debounceKey);
WebCheckPlayerNow(id, address, connected);
}
);
}
void WebCheckPlayerNow(string id, string address, bool connected) {
if (!apiConnected) return;
DoRequest($"player/{id}?bans=true&linked={config.AutoKick_ActiveBans.ToString().ToLowerInvariant()}", $"ipAddress={address}", (code, response) => {
// LogDebug("Getting player from API");
ISAPlayer isaPlayer = null;
try {
isaPlayer = JsonConvert.DeserializeObject<ISAPlayer>(response);
}
catch (Exception) {
SchedulePlayerRecheck(id, address, connected);
return;
}
if (isaPlayer == null || string.IsNullOrEmpty(isaPlayer.steamid)) {
SchedulePlayerRecheck(id, address, connected);
return;
}
ClearPlayerRecheck(id);
if (isaPlayer.bans == null) isaPlayer.bans = new List<ISABan>();
if (isaPlayer.eacBans == null) isaPlayer.eacBans = new List<EacBan>();
isaPlayer.cacheTimestamp = _time.GetUnixTimestamp();
isaPlayer.lastConnected = _time.GetUnixTimestamp();
// add cache for player
// LogDebug("Checking cache");
if (!IsPlayerCached(isaPlayer.steamid)) {
AddPlayerCached(isaPlayer);
}
else {
UpdatePlayerData(isaPlayer);
}
// lets check bans first
try {
EnsureNativeBanMatchesApi(isaPlayer);
KickIfBanned(isaPlayer);
}
catch (Exception ane) {
LogDebug($"KickIfBanned error for {id}: {ane.Message}");
}
try {
SyncNativeBanIfPardoned(id);
}
catch (Exception ane) {
LogDebug($"SyncNativeBanIfPardoned error for {id}: {ane.Message}");
}
//script vars
string pSteamId = isaPlayer.steamid ?? id;
//string lSteamId = GetFamilyShare(isaPlayer.steamid);
//
LogDebug("Check for a twitter/eac game ban");
if (config.AutoKick_KickTwitterGameBanned && !HasPerm(pSteamId, PermissionWhitelistTwitterBan) && isaPlayer.eacBans != null && isaPlayer.eacBans.Count > 0) {
KickPlayer(isaPlayer?.steamid, $"https://twitter.com/rusthackreport/status/{isaPlayer.eacBans[isaPlayer.eacBans.Count - 1].id}", "C");
}
LogDebug($"Check for a recent vac: DissallowVacBanDays = {config?.DissallowVacBanDays}, steamDaysSinceLastBan = {isaPlayer?.steamDaysSinceLastBan}, steamNumberOfVACBans = {isaPlayer?.steamNumberOfVACBans}, steamDaysSinceLastBan = {isaPlayer?.steamDaysSinceLastBan}, whitelisted = {HasPerm(pSteamId, PermissionWhitelistRecentVacKick)}");
bool pRecentVac = (isaPlayer.steamNumberOfVACBans > 0 || isaPlayer.steamDaysSinceLastBan > 0) && isaPlayer.steamDaysSinceLastBan < config.DissallowVacBanDays;
//check main player
//bool lRecentVac = (isaPlayer.lender?.steamNumberOfVACBans > 0 || isaPlayer.lender?.steamDaysSinceLastBan > 0)
//&& isaPlayer.lender?.steamDaysSinceLastBan < config.DissallowVacBanDays; //check the lender player
if (config.AutoKickOn && !HasPerm(pSteamId, PermissionWhitelistRecentVacKick) && pRecentVac) {
int vacLast = isaPlayer.steamDaysSinceLastBan;
int until = config.DissallowVacBanDays - vacLast;
Interface.CallHook("OnSARecentVacKick", vacLast, until);
string msg = GetMsg(pRecentVac ? "Reason: VAC Ban Too Fresh" : "Reason: VAC Ban Too Fresh - Lender", new Dictionary<string, string> {
["daysago"]
= vacLast.ToString(), ["daysto"] = until.ToString()
}
);
KickPlayer(isaPlayer?.steamid, msg, "C");
}
LogDebug("Check for too many vac bans");
if (!HasPerm(pSteamId, PermissionWhitelistVacCeilingKick) && HasReachedVacCeiling(isaPlayer)) {
Interface.CallHook("OnSATooManyVacKick", pSteamId, isaPlayer?.steamNumberOfVACBans);
KickPlayer(isaPlayer?.steamid, GetMsg("VAC Ceiling Kick"), "C");
}
LogDebug("Check for too many game bans");
if (!HasPerm(pSteamId, PermissionWhitelistGameBanCeilingKick) && HasReachedGameBanCeiling(isaPlayer)) {
Interface.CallHook("OnSATooManyGameBansKick", pSteamId, isaPlayer.steamNumberOfGameBans);
KickPlayer(isaPlayer?.steamid, GetMsg("Too Many Previous Game Bans"), "C");
}
// LogDebug("Check for players with too many server bans");
//if (!HasPerm(pSteamId, PermissionWhitelistServerCeilingKick) && HasReachedServerCeiling(isaPlayer))
//{
// Interface.CallHook("OnSATooManyServerBans", pSteamId, config.AutoKickCeiling, ServerBanCount(isaPlayer) + ServerBanCount(isaPlayer.lender));
// KickPlayer(isaPlayer?.steamid, GetMsg("Too Many Previous Bans"), "C");
//}
//if (!HasPerm(PermissionWhitelistTotalBanCeiling, pSteamId) && HasReachedTotalBanCeiling(isaPlayer))
//{
// Interface.CallHook("OnSATooManyBans", pSteamId, config.AutoKickCeiling, TotalBans(isaPlayer.lender));
// KickPlayer(isaPlayer?.steamid, GetMsg("Too Many Previous Bans"), "C");
//}
LogDebug("Check for players with a private profile");
int visibilityState = GetCommunityVisibilityState(isaPlayer);
if (!HasPerm(pSteamId, PermissionWhitelistSteamProfile) && config.AutoKick_KickPrivateProfile) {
if (ShouldKickForPrivateProfile(visibilityState)) {
Interface.CallHook("OnSAProfilePrivate", pSteamId, visibilityState);
KickPlayer(isaPlayer?.steamid, GetMsg("Profile Private"), "C");
}
else if (!IsKnownCommunityVisibility(visibilityState)) {
SchedulePlayerRecheck(id, address, connected);
}
}
LogDebug("Check for a hidden steam level");
if (!HasPerm(pSteamId, PermissionWhitelistSteamProfile) && isaPlayer.steamlevel == -1 && config.AutoKick_KickHiddenLevel) {
Interface.CallHook("OnSASteamLevelHidden", pSteamId);
KickPlayer(isaPlayer?.steamid, GetMsg("Steam Level Hidden"), "C");
}
LogDebug($"Player {isaPlayer.steamid} is at steam level {isaPlayer.steamlevel}");
if (!HasPerm(pSteamId, PermissionWhitelistSteamProfile) && isaPlayer.steamlevel < config.AutoKick_MinSteamProfileLevel && isaPlayer.steamlevel >= 0) {
Interface.CallHook("OnSAProfileLevelLow", pSteamId, config.AutoKick_MinSteamProfileLevel, isaPlayer.steamlevel);
KickPlayer(isaPlayer?.steamid, GetMsg("Profile Low Level", new Dictionary<string, string> {
["level"]
= config.AutoKick_MinSteamProfileLevel.ToString()
}
), "C");
}
LogDebug($"IP/CACHE| ID:{id} ADD:{address} COUNTRY: {isaPlayer.ipInfo.isocode} RATING:{isaPlayer?.ipInfo?.rating} AGE: {isaPlayer.ipInfo?.lastcheck}");
if (config.AutoKickOn && config.AutoKick_BadIp && !HasPerm(id, PermissionWhitelistBadIPKick) && config.AutoKick_BadIp) {
if (IsBadIp(isaPlayer)) {
if (isaPlayer.ipInfo?.proxy == "yes") KickPlayer(id, GetMsg("Reason: Proxy IP"), "C");
else KickPlayer(id, GetMsg("Reason: Bad IP"), "C");
Interface.CallHook("OnSAVPNKick", id, isaPlayer?.ipInfo?.rating);
}
}
if (!HasPerm(id, PermissionWhitelistAllowCountry) && !config.AutoKickLimitCountry.IsNullOrEmpty()) {
var playerIso = isaPlayer.ipInfo?.isocode?.Trim();
var countriesAllowed = config.AutoKickLimitCountry.Split(',');
if (playerIso != null && playerIso != "" && !countriesAllowed.Contains(playerIso)) KickPlayer(id, GetMsg("Country Not Allowed", new Dictionary<string, string> {
["country"]
= playerIso, ["country2"] = config.AutoKickLimitCountry.ToString()
}
), "C");
}
if (ShouldKickForCombatGuidance(isaPlayer) && !HasPerm(pSteamId, PermissionWhitelistCombatGuidance)) {
Interface.CallHook("OnSACombatAnomalyGuidanceKick", pSteamId, isaPlayer.combatAnomalyGuidanceAction, isaPlayer.combatAnomalyGuidanceRank);
KickPlayer(isaPlayer?.steamid, GetMsg("Combat Anomaly Guidance Kick", new Dictionary<string, string> {
["action"]
= isaPlayer.combatAnomalyGuidanceAction ?? "review"
}
), "C");
}
if (ShouldKickForAccountTrust(isaPlayer) && !HasPerm(pSteamId, PermissionWhitelistAccountTrust)) {
Interface.CallHook("OnSAAccountTrustKick", pSteamId, isaPlayer.accountTrustTier ?? "critical", isaPlayer.accountTrustTierRank);
KickPlayer(isaPlayer?.steamid, GetMsg("Account Trust Kick", new Dictionary<string, string> {
["tier"]
= isaPlayer.accountTrustTier ?? "critical", ["eas"] = isaPlayer.accountTrustEas.ToString("0.##"), ["scs"] = isaPlayer.accountTrustScs.ToString("0.##")
}
), "C");
}
GetPlayerReport(isaPlayer, connected);
}
, RequestMethod.POST, 5);
}
void KickIfBanned(ISAPlayer isaPlayer) {
if (isaPlayer == null) return;
IPlayer iPlayer = covalence.Players.FindPlayer(isaPlayer.steamid);
if (iPlayer != null && iPlayer.IsAdmin && config.IgnoreAdmins) return;
try {
if (AdminToggle != null && AdminToggle.Call<bool>("IsAdmin", iPlayer.Id) && config.IgnoreAdmins) return;
}
catch (Exception) {
}
ISABan ban = IsBanned(isaPlayer?.steamid);
if (ban != null) {
if (!ban.steamid.ToString().Equals(isaPlayer.steamid)) {
KickPlayer(isaPlayer?.steamid, $"Linked: {ban.steamid}, {ban.reason}", "U");
}
else {
KickPlayer(isaPlayer?.steamid, ban.reason, "U");
}
}
}
void AddBan(ISABan thisBan, bool doNative = true) {
if (thisBan == null) return;
if (!serverStarted) {
pendingBans.Add(thisBan);
return;
}
string steamIdKey = thisBan.steamid.ToString();
if (addBanInFlight.Contains(steamIdKey)) return;
addBanInFlight.Add(steamIdKey);
string reason = FormEncode(thisBan.reason);
try {
DoRequest($"player/{thisBan.steamid}/addban", $"reason={reason}&dateTime={FormEncode(thisBan.created)}&banUntil={FormEncode(thisBan.banUntil)}&createdBySteamId={thisBan.bannedBy}", (code, response) => {
addBanInFlight.Remove(steamIdKey);
if (IsPlayerCached(steamIdKey)) AddPlayerData(steamIdKey, thisBan);
else {
ISAPlayer newPlayer = new ISAPlayer((ulong)thisBan.steamid);
newPlayer.bans.Add(thisBan);
AddPlayerCached(newPlayer);
}
}
, RequestMethod.POST, 0);
}
catch (Exception) {
addBanInFlight.Remove(steamIdKey);
}
}
void FlushPendingBans() {
if (pendingBans.Count == 0) return;
foreach (ISABan pendingBan in pendingBans.ToList()) AddBan(pendingBan);
pendingBans.Clear();
}
#endregion
#region Commands
[Command("sa.apply_key")]
void ApplyKey(IPlayer player, string command, string[] args) {
var key = args[0].ToString();
if (!player.IsServer) {
return;
}
this.config.ServerApiKey = key;
Puts($"Applying key = {key}");
config.SetConfig(ref config.ServerApiKey, "io.serverarmour.com", "Server Key");
SaveConfig();
if (headers != null) headers["server-key"] = key;
InitChatModeration();
}
[Command("sa.clb", "getreport")]
void SCmdCheckLocalBans(IPlayer player, string command, string[] args) {
CheckLocalBans();
SyncPardonedNativeBans();
}
[Command("unban", "playerunban", "sa.unban"), Permission(PermissionToUnBan)]
void SCmdUnban(IPlayer player, string command, string[] args) {
if (args == null || (args.Length > 2 || args.Length < 1)) {
SendReplyWithIcon(player, GetMsg("UnBan Syntax"));
return;
}
var reason = args.Length > 1 ? args[1] : "";
SaUnban(args[0], player, reason);
}
void SilentBan(ulong steamId, TimeSpan timeSpan, string reason, IPlayer enforcer = null) {
Unsubscribe(nameof(OnUserBanned));
var username = covalence.Players.FindPlayerById(steamId.ToString())?.Name ?? "";
NativeBan(steamId, reason, Convert.ToInt64(timeSpan.TotalSeconds), username, enforcer);
Subscribe(nameof(OnUserBanned));
}
void SilentBan(ulong steamId, int timeSpan, string reason, IPlayer enforcer = null) {
Unsubscribe(nameof(OnUserBanned));
var username = covalence.Players.FindPlayerById(steamId.ToString())?.Name ?? "";
NativeBan(steamId, reason, -1, username, enforcer);
Subscribe(nameof(OnUserBanned));
}
bool SilentUnban(string playerId, IPlayer admin) {
Unsubscribe(nameof(OnUserUnbanned));
bool removed = NativeUnban(playerId, admin);
Subscribe(nameof(OnUserUnbanned));
return removed;
}
void NativeBan(ulong playerId, string reason, long duration = -1, string playerUsername = "unnamed", IPlayer player = null) {
if (!playerId.IsSteamId()) {
if (serverStarted) player?.Reply(string.Concat("This doesn't appear to be a 64bit steamid: ", playerId));
return;
}
ServerUsers.User user = ServerUsers.Get(playerId);
if (user != null && user.@group == ServerUsers.UserGroup.Banned && !user.IsExpired) {
long epochDuration = duration == -1 ? -1 : (long)(DateTime.UtcNow.AddSeconds(duration) - Epoch).TotalSeconds;
ServerUsers.Set(playerId, ServerUsers.UserGroup.Banned, playerUsername, reason, epochDuration);
if (serverStarted) player?.Reply(string.Concat("User ", playerId, " is already banned"));
return;
}
string str3 = "";
BasePlayer basePlayer = BasePlayer.FindByID(playerId);
string durationSuffix = (duration > (long)0 ? string.Concat(" for ", duration.FormatSecondsLong()) : "");
if (basePlayer != null && basePlayer.IsConnected) {
playerUsername = basePlayer.displayName;
if (basePlayer.IsConnected && basePlayer.net.connection.ownerid != 0 && basePlayer.net.connection.ownerid != basePlayer.net.connection.userid) {
str3 = string.Concat(str3, string.Format(" and also banned ownerid {0}", basePlayer.net.connection.ownerid));
ServerUsers.Set(basePlayer.net.connection.ownerid, ServerUsers.UserGroup.Banned, basePlayer.displayName, reason, duration);
}
Chat.Broadcast(string.Concat(new string[] {
"Kickbanning ", basePlayer.displayName, durationSuffix, " (", reason, ")"
}
), "SERVER", "#eee", (ulong)0);
//Net.sv.Kick(basePlayer.net.connection, string.Concat("Banned", str, ": ", str2), false);
}
ServerUsers.Set(playerId, ServerUsers.UserGroup.Banned, playerUsername, reason, duration == -1 ? -1 : (long)(DateTime.UtcNow.AddSeconds(duration) - Epoch).TotalSeconds);
if (serverStarted) player?.Reply(string.Format("Banned User{0}: {1} - \"{2}\" for \"{3}\"{4}", new object[] {
durationSuffix, playerId, playerUsername, reason, str3
}
));
}
bool NativeUnban(string playerId, IPlayer admin = null) {
ulong playerIdLong = 0;
if (!ulong.TryParse(playerId, out playerIdLong)) {
var msg = string.Format("This doesn't appear to be a 64bit steamid: {0}", playerId);
if (admin == null) {
Puts(msg);
}
else {
admin.Reply(msg);
}
return false;
}
ServerUsers.User user = ServerUsers.Get(playerIdLong);
if (user == null || user.@group != ServerUsers.UserGroup.Banned) {
admin?.Reply(string.Format("User {0} isn't banned", playerId));
return false;
}
ServerUsers.Remove(playerIdLong);
admin?.Reply($"Unbanned User: {playerId}");
return true;
}
void SaUnban(string playerId, IPlayer admin = null, string reason = "") {
try {
SaUnbanInternal(playerId, admin, reason);
}
catch (Exception ex) {
LogDebug($"SaUnban failed for {playerId}: {ex.Message}");
}
}
void SaUnbanInternal(string playerId, IPlayer admin = null, string reason = "") {
IPlayer iPlayer = players.FindPlayer(playerId);
SilentUnban(playerId, admin);
ulong playerIdLong = 0;
ulong.TryParse(playerId, out playerIdLong);
if (playerIdLong == 0 && iPlayer == null) {
admin?.Reply(GetMsg("Player Not Found", new Dictionary<string, string> {
["player"]
= playerId
}
));
return;
}
Puts($"Player {iPlayer?.Name ?? playerId} ({playerId}) was unbanned by {admin?.Name ?? "console"} ({admin?.Id ?? "console"})");
// Add ember support.
if (Ember != null) {
Ember?.Call("Unban", playerId, Player.FindById(admin?.Id));
}
//
if (serverStarted) {
DoPardon(playerId, iPlayer, admin);
}
string msgClean;
if (reason.IsNullOrEmpty()) {
msgClean = GetMsg("Player Now Unbanned Clean - NoReason", new Dictionary<string, string> {
["player"]
= iPlayer?.Name ?? playerId
}
);
}
else {
msgClean = GetMsg("Player Now Unbanned Clean - Reason", new Dictionary<string, string> {
["player"]
= iPlayer?.Name ?? playerId, ["reason"] = reason
}
);
}
if (config.DiscordBanReport) {
DiscordSend(playerId, iPlayer?.Name ?? playerId, new EmbedFieldList() {
name = "Player Unbanned", value = msgClean, inline = true
}
, 3066993, true);
}
}
void DoPardon(string playerId, IPlayer iPlayer = null, IPlayer admin = null) {
try {
RemoveBans(playerId);
}
catch (Exception ex) {
LogDebug($"RemoveBans failed for {playerId}: {ex.Message}");
}
string pardonedBy = admin?.Id ?? admin?.Name ?? "console";
DoRequest($"player/ban/pardon/{playerId}", $"pardonedBy={pardonedBy}", (code, response) => {
if (config.RconBroadcast) RCon.Broadcast(RCon.LogType.Chat, new Chat.ChatEntry {
Message = $"Player was unbanned by {admin?.Name ?? "console"} ({admin?.Id ?? "console"})", UserId = playerId, Username = iPlayer?.Name ?? "", Time = Facepunch.Math.Epoch.Current
}
);
if (admin != null && config.RconBroadcast) {
RCon.Broadcast(RCon.LogType.Chat, new Chat.ChatEntry {
Message = $"Player {iPlayer?.Name} ({playerId}) was unbanned", UserId = admin?.Id, Username = admin?.Name ?? "", Time = Facepunch.Math.Epoch.Current
}
// ... [Code preview truncated due to length. Full source in attached file] ...