Console error
(06:57:06) | Web request callback raised an exception (FormatException: Index (zero based) must be greater than or equal to zero and less than the size of the argument list.)
  at System.Text.StringBuilder.AppendFormatHelper (System.IFormatProvider provider, System.String format, System.ParamsArray args) [0x000f9] in <8ce0bd04a7a04b4b9395538239d3fdd8>:0
at System.String.FormatHelper (System.IFormatProvider provider, System.String format, System.ParamsArray args) [0x00023] in <8ce0bd04a7a04b4b9395538239d3fdd8>:0
at System.String.Format (System.String format, System.Object arg0, System.Object arg1) [0x00009] in <8ce0bd04a7a04b4b9395538239d3fdd8>:0
at Oxide.Plugins.Ember+<>cDisplayClass39_0.<HttpRequest>b0 (System.Int32 code, System.String response) [0x00069] in <1008f8a73ee74758b7d9660194d9809f>:0
at Oxide.Core.Libraries.WebRequests+WebRequest.<OnComplete>b__46_0 () [0x00034] in <112d89ea5d3348c8b949af0ab1a866d2>:0

I can offer you a modified plugin that has optimization and additional features.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using UnityEngine;

namespace Oxide.Plugins {
	[Info ("Ember", "Mkekala", "2.0.1")]
	[Description ("Integrates Ember actions, ban management & role sync with Rust")]
	public class Ember : RustPlugin {
		#region Configuration
		private class PluginConfig {
			public string Host = "http://*.*.*.*";
			public string Token;
			public LogLevel LogLevel = LogLevel.Info;

			[JsonProperty (PropertyName = "_MapImageParamsComment")]
			public string MapImageParamsComment = "Параметры генерации карты (RustMapApi). Загружается в store при подключении сервера.";

			public float MapImageScale = 0.5f;
			public string MapImageName = "Icons";

			[JsonProperty (PropertyName = "_MapImageScale_desc")]
			public string MapImageScaleDesc = "Масштаб карты (0.0–1.0). Разрешение = MapImageScale × размер мира. 0.5 = половина размера карты в пикселях.";

			[JsonProperty (PropertyName = "_MapImageName_desc")]
			public string MapImageNameDesc = "Icons — карта с иконками монументов (порты, аэродром, военка и т.д.). Default — карта без иконок (только рельеф).";
		}

		private enum LogLevel { Trace, Debug, Info, Warning, Error };

		private PluginConfig config;

		private class IntegrationSettings {
			[JsonProperty ("ban_log")]
			public bool banLog;
			[JsonProperty ("role_sync")]
			public RoleSyncSettings roleSync = new ();
		}

		private class RoleSyncSettings {
			[JsonProperty ("create")]
			public bool Create;
			[JsonProperty ("send")]
			public bool Send;
			[JsonProperty ("receive")]
			public bool Receive;
		}

		private IntegrationSettings settings = new ();
		#endregion

		#region Constants
		private const string commandNamespace = "ember";

		private static readonly string[] permissions = {
			"ban",
			"unban",
		};

		private static readonly string[] userHooks = {
			"OnUserBanned",
			"OnUserUnbanned",
			"OnUserGroupAdded",
			"OnUserGroupRemoved",
		};

		private const float retryApiInitIntervalSeconds = 60f,
			maxPollTimerDelayUserConnectingSeconds = 5f;

		private const BindingFlags selfReflectionFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic;

		private const string mapImageBlobPath = "map.jpg";
		private const RustMapApiEncodingMode mapImageEncoding = RustMapApiEncodingMode.Jpg;
		private enum RustMapApiEncodingMode { Jpg = 1, Png = 2 };
		#endregion

		#region Variables
		private bool connected;
		private Dictionary<string, Action<JToken>> actionHandlers = new ();
		private Dictionary<string, string> connectingUsers = new ();
		private Timer retryApiInitTimer;
		private TimerManager.TimerProxy pollTimer;

		[PluginReference]
		private Plugin RustMapApi;

		private string _cmdBanName;
		private string _cmdUnbanName;
		#endregion

		#region Domain methods
		#region Polling/action flow
		private void AddConnectingUser (string steamid, string name = "") {
			if (connectingUsers.ContainsKey (steamid))
				return;

			connectingUsers.Add (steamid, name);

			if (connected) RunPollWithin (maxPollTimerDelayUserConnectingSeconds);
		}

		private void RunPoll () {
			var connectingUsersBatch = Interlocked.Exchange (ref connectingUsers, new ());
			int playerCount = connectingUsersBatch.Count + BasePlayer.activePlayerList.Count;

			LogMessage ($"Poll invoked with {playerCount} player(s)", LogLevel.Trace);

			if (playerCount == 0)
				return;

			List<string> names = new ();
			foreach (string name in connectingUsersBatch.Values)
				names.Add (name.Replace (",", ""));

			List<string> connectedUserIds = new ();

			foreach (BasePlayer ply in BasePlayer.activePlayerList)
				if (!connectingUsersBatch.ContainsKey (ply.UserIDString))
					connectedUserIds.Add (ply.UserIDString);
				else playerCount -= 1;

			JObject payload = JObject.FromObject (new {
				players = new {
					steam = new {
						connecting = new {
							ids = string.Join (",", connectingUsersBatch.Keys),
							names = string.Join (",", names),
						},
						connected = new {
							ids = string.Join (",", connectedUserIds)
						}
					}
				}
			});
			
			LogMessage (string.Format (lang.GetMessage ("ConsolePolling", this), playerCount));

			var failureHandler = (int code, string response) => {
				LogMessage (string.Format (lang.GetMessage ("ConsoleApiRequestFailed", this), code, response), LogLevel.Error);

				if (connectingUsersBatch.Count > 0)
					LogMessage ($"Queuing poll retry for {connectingUsersBatch.Count} connecting player(s)", LogLevel.Debug);
				foreach (var (steamid, name) in connectingUsersBatch.Select ((u) => (u.Key, u.Value)))
					AddConnectingUser (steamid, name);
			};

			var successHandler = (JToken response) => {
				foreach (JToken action in response as JArray)
					DispatchAction (action);
			};

			HttpRequest (RequestMethod.POST, "poll", payload, successHandler, failureHandler);
		}

		private void RunPollWithin (float seconds) {
			LogMessage (string.Format ("Timer interval: {0}s, time remaining: {1}s, run within: {2}s", pollTimer.DefaultInterval, pollTimer.Remaining (), seconds), LogLevel.Trace);
			pollTimer.InvokeWithin (seconds);
		}

		private void DispatchAction (JToken action) {
			string type = (string) action["type"];

			LogMessage (action.ToString (), LogLevel.Trace);

			if (!actionHandlers.ContainsKey (type)) {
				LogMessage (string.Format (lang.GetMessage ("ActionUnrecognized", this), type), LogLevel.Warning);
				return;
			}

			LogMessage (string.Format (lang.GetMessage ("ActionDispatching", this), type), LogLevel.Debug);

			var handler = actionHandlers[type];

			string error = string.Empty;

			try {
				handler (action["payload"]);
			} catch (Exception ex) {
				error = ex.Message;

				LogMessage (string.Format ("Action execution failed for type \"{0}\". Error: {1}", type, error), LogLevel.Warning);
			}

			var execuctionId = (float?) action["execution_id"];
			if (execuctionId != null)
				HttpRequest (RequestMethod.POST, $"actions/executions/{execuctionId}/finish", JObject.FromObject (new { error }));
		}
		#endregion

		#region Player endpoints
		public void Ban (string offenderSteamid, string durationMinutes, string reason, bool global, string adminSteamid, BasePlayer caller) {
			string scope = global ? "global" : null;
			
			var parameters = JObject.FromObject (new {
				scope,
				reason,
				duration_minutes = durationMinutes,
			});

			if (adminSteamid != null)
				parameters["admin_steam_account_id"] = adminSteamid;
			
			string url = $"players/steam/{offenderSteamid}/bans";

			var failureHandler = (int code, string response) => {
				if (caller != null)
					SendLogMessage (caller, string.Format (lang.GetMessage ("PlayerBanFailed", this, caller.UserIDString), offenderSteamid), LogLevel.Warning);
			};

			var successHandler = (JToken _) => {
				LogMessage (string.Format (lang.GetMessage ("PlayerBanned", this), offenderSteamid));

				if (caller != null)
					SendLogMessage (caller, string.Format (lang.GetMessage ("PlayerBanned", this, caller.UserIDString), offenderSteamid));
			};

			HttpRequest (RequestMethod.POST, url, parameters, successHandler, failureHandler, "Ban");
		}

		public void Unban (string offenderSteamid, BasePlayer caller) {
			string url = $"players/steam/{offenderSteamid}/bans";

			var failureHandler = (int code, string response) => {
				if (caller != null)
					SendLogMessage (caller, string.Format (lang.GetMessage ("PlayerUnbanFailed", this, caller.UserIDString), offenderSteamid), LogLevel.Warning);
			};

			var successHandler = (JToken _) => {
				string actor = caller?.UserIDString ?? "console";

				LogMessage (string.Format (lang.GetMessage ("PlayerUnbanned", this), offenderSteamid, actor));

				if (caller != null)
					SendLogMessage (caller, string.Format (lang.GetMessage ("PlayerUnbanned", this, caller.UserIDString), offenderSteamid, actor));
			};

			HttpRequest (RequestMethod.DELETE, url, null, successHandler, failureHandler, "Unban");
		}

		private void PostRole (string steamid, string role) {
			if (role == "default")
				return;

			string url = $"players/steam/{steamid}/roles";
			JObject parameters = new (new JProperty ("name", role));

			var successHandler = (JToken _) =>
				LogMessage (string.Format (lang.GetMessage ("ConsoleRoleAdded", this), role, steamid));

			HttpRequest (RequestMethod.POST, url, parameters, successHandler, null, "RoleAdd");
		}

		private void DeleteRole (string steamid, string role) {
			string url = $"players/steam/{steamid}/roles/{role}";

			var successHandler = (JToken _) =>
				LogMessage (string.Format (lang.GetMessage ("ConsoleRoleRevoked", this), role, steamid));

			HttpRequest (RequestMethod.DELETE, url, null, successHandler, null, "RoleRevoke");
		}
		#endregion
		#endregion

		#region Utilities
		private void CallUntilReturns<T> (Func<T> callback, T value, float intervalSeconds = 5f, int maxRetries = 10)
		{
			if (maxRetries <= 0) {
				LogMessage ("CallUntilReturns: достигнут лимит попыток, останавливаем", LogLevel.Warning);
				return;
			}
			if (callback ().Equals (value))
				return;
			
			timer.In (intervalSeconds, () => CallUntilReturns (callback, value, intervalSeconds, maxRetries - 1));
		}

		private static List<BasePlayer> GetPlayersByName (string name) {
			List<BasePlayer> matches = new List<BasePlayer> ();
			string nameLower = name.ToLower ();

			foreach (BasePlayer ply in BasePlayer.activePlayerList)
				if (ply.displayName.ToLower ().Contains (nameLower))
					matches.Add (ply);

			return matches;
		}

		private static BasePlayer GetPlayerBySteamID (string steamid) {
			foreach (BasePlayer ply in BasePlayer.activePlayerList)
				if (ply.UserIDString == steamid)
					return ply;

			return null;
		}

		#region Reflection
		private static string GetMethodCommandName (string methodName) {
			return typeof (Ember).GetMethod (methodName, selfReflectionFlags)
				.GetCustomAttribute<ConsoleCommandAttribute> ().Command;
		}
		#endregion
		
		#region Timers
		protected class TimerManager {
			protected static readonly Oxide.Core.Libraries.Timer CoreTimer =
				Oxide.Core.Interface.Oxide.GetLibrary<Oxide.Core.Libraries.Timer> ("Timer");

			protected readonly Plugin Plugin;

			public TimerManager (Plugin plugin) {
				Plugin = plugin;
			}

			public TimerProxy Repeat (float interval, int repetitions, Action callback) {
				return new TimerProxy (Plugin, CoreTimer, interval, repetitions, callback);
			}

			public class TimerProxy {
				protected readonly Oxide.Core.Libraries.Timer.TimerInstance Instance;
				protected float StartedAt;

				public float DefaultInterval;
				public bool Destroyed => Instance.Destroyed;

				public TimerProxy (Plugin plugin, Oxide.Core.Libraries.Timer coreTimer, float interval, int repetitions, Action callback) {
					Action callbackProxy = () => {
						RestoreInterval ();
						callback ();
					};

					Instance = coreTimer.Repeat (interval, repetitions, callbackProxy, plugin);
					StartedAt = Oxide.Core.Interface.Oxide.Now;
					DefaultInterval = Instance.Delay;
				}

				protected void RestoreInterval () {
					if (Instance.Delay == DefaultInterval)
						return;

					Reset (DefaultInterval, Instance.Repetitions);
				}

				protected void Reset (float interval = -1, int repetitions = 1) {
					Instance.Reset (interval, repetitions);
					StartedAt = Oxide.Core.Interface.Oxide.Now;
				}

				public float Remaining () {
					if (Destroyed)
						return -1f;

					return Instance.Delay - (Oxide.Core.Interface.Oxide.Now - StartedAt) % Instance.Delay;
				}

				public void InvokeWithin (float seconds) {
					if (seconds <= 0f || Remaining () <= seconds)
						return;

					Reset (seconds, Instance.Repetitions);
				}

				public void Destroy () {
					Instance.DestroyToPool ();
				}
			}
		}
		#endregion

		#region Logging
		private void LogMessage (string message, LogLevel level = LogLevel.Info, [CallerMemberName] string callerName = "", [CallerLineNumber] int callerLine = -1) {
			if (level < config.LogLevel)
				return;

			if (level == LogLevel.Trace)
				message = string.Format ("[{0}:{1}] {2}", callerLine, callerName, message);

			message = string.Format ("[{0}] {1}", level.ToString (), message);

			if (level < LogLevel.Warning)
				Puts (message);
			else if (level < LogLevel.Error)
				PrintWarning (message);
			else
				PrintError (message);
		}
		
		private void SendLogMessage (BasePlayer player, string messageKey, LogLevel level = LogLevel.Info) {
			string message = string.Format (
				"{1}<color={0}>{2}</color>",
				GetLogLevelColor (level),
				lang.GetMessage ("DebugChatPrefix", this),
				lang.GetMessage (messageKey, this, player.UserIDString)
			);

			PrintToConsole (player, message);
			PrintToChat (player, message);
		}

		private void SendCommandReplies (ConsoleSystem.Arg arg, string messageKey, LogLevel level = LogLevel.Info) {
			BasePlayer player = arg.Player ();

			string format = player ? "<color={0}>{1}</color>" : "{1}",
				message = string.Format (
					format,
					GetLogLevelColor (level),
					lang.GetMessage (messageKey, this, player?.UserIDString)
				);

			SendReply (arg, message); // Caller console
			if (player) SendReply (player, message); // Caller chat
		}

		private static string GetLogLevelColor (LogLevel level = LogLevel.Info) {
			if (level >= LogLevel.Error)
				return "red";
			if (level >= LogLevel.Warning)
				return "orange";

			return "white";
		}
		#endregion

		#region Web API
		private void HttpRequest (RequestMethod method, string path, JObject parameters, Action<JToken> onSuccess = null, Action<int, string> onFailure = null, string failureMessageKeySuffix = "") {
			var headers = new Dictionary<string, string> { { "Authorization", "Bearer " + config.Token }, { "Accept", "application/json" } };
			string url = config.Host + "/api/game/" + path;
			string body = null;

			if (parameters != null && method != RequestMethod.GET) {
				body = parameters.ToString ();
				headers.Add ("Content-Type", "application/json");
			}

			LogMessage (string.Format ("HTTP Request: {0} {1} {2}", method, url, body?.Truncate (5000)), LogLevel.Trace);

			webrequest.Enqueue (
				url,
				body,
				(code, response) => {
					if (code < 200 || code > 299 || response == null ||
						(!TryParseJson (response, out JToken json) && response != string.Empty)
					) {
						if (failureMessageKeySuffix != string.Empty)
							LogMessage (string.Format (lang.GetMessage ("ConsoleApiRequestFailed" + failureMessageKeySuffix, this), code, response), LogLevel.Warning);

						onFailure?.Invoke (code, response);

						return;
					}

					LogMessage (string.Format ("HTTP Response: {0} {1} {2} {3}", method, url, code, json?.ToString ()), LogLevel.Trace);

					onSuccess?.Invoke (json ?? new JObject ());
				},
				this,
				method,
				headers
			);
		}
		
		private static string NormalizeHostUri (string uri) { 
			return uri.TrimEnd (new Char[] { '/' });
		}

		private static bool TryParseJson<T> (string payload, out T result) where T : JToken {
			result = null;

			try {
				result = (T) JToken.Parse (payload);
				return true;
			} catch (JsonReaderException) {
				return false;
			}
		}
		#endregion
		#endregion

		#region Init methods
		protected override void LoadConfig () {
			base.LoadConfig ();

			config = Config.ReadObject<PluginConfig> ();

			string normalizedHost = NormalizeHostUri (config.Host);

			if (config.Host != normalizedHost) {
				config.Host = normalizedHost;
				Config.WriteObject (config);
			}
		}

		protected override void LoadDefaultConfig () {
			Config.WriteObject (new PluginConfig (), true);
		}

		public void RegisterActionHandler (string type, Action<JToken> handler) {
			LogMessage ($"Registering action \"{type}\"", LogLevel.Debug);

			actionHandlers[type] = handler;
		}

		private void RegisterActionHandlers () {
			RegisterActionHandler ("command", HandleCommandAction);
			RegisterActionHandler ("roles", HandleRolesAction);
			RegisterActionHandler ("settings", HandleSettingsAction);
		}

		private void RegisterPermissions () {
			foreach (string suffix in permissions) {
				string name = string.Format ("{0}.{1}", Title.ToLower (), suffix);
				permission.RegisterPermission (name, this);
			}
		}

		private void InitPollTimer (float interval = 60f) {
			if (pollTimer?.Destroyed == false)
				return;

			pollTimer = new TimerManager (this).Repeat (interval, -1, RunPoll);
		}

		private void SetPollInterval (float interval) {
			if (interval < 0f || (pollTimer?.Destroyed == false && pollTimer.DefaultInterval == interval))
				return;

			LogMessage (string.Format (lang.GetMessage ("PollIntervalSetting", this), interval), LogLevel.Info);

			if (pollTimer?.Destroyed != false) {
				InitPollTimer (interval);
			} else {
				pollTimer.DefaultInterval = interval;
				pollTimer.InvokeWithin (interval);
			}
		}

		private void DeinitApi () {
			pollTimer?.Destroy ();

			foreach (string hook in userHooks)
				Unsubscribe (hook);

			connected = false;
		}

		private void TryInitApi () {
			retryApiInitTimer?.Destroy ();

			if (string.IsNullOrEmpty (config.Token)) {
				LogMessage ("Token unconfigured; not attempting web API init", LogLevel.Debug);
				return;
			}
			
			LogMessage (lang.GetMessage ("ConsoleApiConnectionChecking", this));

			JObject payload = JObject.FromObject (new {
				actions = new[] { "settings" },
				metadata = GetServerMetadata (),
			});

			var failureHandler = (int code, string response) => {
				LogMessage (string.Format (lang.GetMessage ("ConsoleApiRequestFailed", this), code, response), LogLevel.Error);

				DeinitApi ();

				foreach (BasePlayer player in BasePlayer.activePlayerList)
					if (player.net.connection.authLevel >= 1)
						SendLogMessage (player, "NoApiConnection", LogLevel.Error);

				if (code < 400 || code > 499)
					retryApiInitTimer = timer.Once (retryApiInitIntervalSeconds, () => TryInitApi ());
			};

			var successHandler = (JToken json) => {
				if (json.Count () == 0) {
					failureHandler (-1, string.Empty);
					return;
				}

				foreach (JToken action in json["actions"])
					DispatchAction (action);

				if (pollTimer?.Destroyed != false)
					InitPollTimer ();

				foreach (string hook in userHooks)
					Subscribe (hook);

				connected = true;

				LogMessage (lang.GetMessage ("ConsoleApiConnectionSuccess", this));

				foreach (BasePlayer player in BasePlayer.activePlayerList)
					if (player.net.connection.authLevel >= 2)
						SendLogMessage (player, "ConsoleApiConnectionSuccess");
			};

			HttpRequest (RequestMethod.POST, "meta", payload, successHandler, failureHandler);
		}

		private object GetServerMetadata () {
			bool customMap = World.CanLoadFromUrl () && !World.Url.Contains ("facepunch.com");
			uint? seed = World.Seed;
			uint size = World.Size;
			string worldName = World.Name.ToString ();

			DateTime? nextWipeUtc = WipeTimer.serverinstance?.GetWipeTime (DateTime.UtcNow).UtcDateTime;
			DateTime wipedAtUtc = SaveRestore.SaveCreatedTime.ToUniversalTime ();

			return new {
				integration_version = Version.ToString (),
				map = new {
					seed = !customMap ? seed : null,
					size,
					name = worldName,
					file_url = World.CanLoadFromUrl () ? World.Url : null,
					image_blob_path = RustMapApi != null ? mapImageBlobPath : null,
				},
				game = new
				{
					mode = BaseGameMode.GetActiveGameMode (true)?.shortname ?? "vanilla",
					ConVar.Server.maxplayers,
					protocol = Rust.Protocol.printable,
					wipe = new
					{
						id = SaveRestore.WipeId,
						began_at = wipedAtUtc.ToString ("O"),
						ends_at = nextWipeUtc?.ToString ("O"),
					},
				},
			};
		}
		#endregion

		#region Hooks
		void Init () {
			_cmdBanName = GetMethodCommandName (nameof (CmdBan));
			_cmdUnbanName = GetMethodCommandName (nameof (CmdUnban));
			RegisterPermissions ();
			RegisterActionHandlers ();
		}

		void OnServerInitialized (bool initial)
		{
			TryInitApi ();

			if (RustMapApi && initial) {
				CallUntilReturns (() => {
					if (!connected || !IsRustMapApiReady ())
						return false;

					TryUploadMapImage ();
					return true;
				}, true);
			}
		}

		void OnUserBanned (string name, string id, string ipAddress, string reason) {
			if (settings.banLog)
				Ban (id, "0", reason, false, null, null);
		}

		void OnUserUnbanned (string name, string id, string ipAddress) {
			if (settings.banLog)
				Unban (id, null);
		}

		void OnUserGroupAdded (string id, string groupName) {
			if (!settings.roleSync.Send)
				return;

			LogMessage ($"Syncing group addition for SteamID {id}, group \"{groupName}\"", LogLevel.Trace);
			PostRole (id, groupName);
		}

		void OnUserGroupRemoved (string id, string groupName) {
			if (!settings.roleSync.Send)
				return;

			LogMessage ($"Syncing group removal for SteamID {id}, group \"{groupName}\"", LogLevel.Trace);
			DeleteRole (id, groupName);
		}

		void OnPlayerConnected (BasePlayer player) {
			AddConnectingUser (player.UserIDString, player.displayName);

			if (!connected)
				if (player.net.connection.authLevel >= 1)
					SendLogMessage (player, "NoApiConnection", LogLevel.Error);
		}
		#endregion

		#region Action handlers
		private void HandleCommandAction (JToken payload) {
			var command = (string) payload;
			LogMessage (string.Format (lang.GetMessage ("ActionCommandRunning", this), command));

			rust.RunServerCommand (command);
		}

		private void HandleRolesAction (JToken payload) {
			if (!settings.roleSync.Receive && !settings.roleSync.Send)
				return;

			string steamid = (string) payload["player_steam_account_id"];
			if (string.IsNullOrEmpty (steamid)) {
				LogMessage ("HandleRolesAction: player_steam_account_id is null or empty", LogLevel.Warning);
				return;
			}

			BasePlayer player = GetPlayerBySteamID (steamid);
			string name = player?.displayName ?? "Unknown";

			LogMessage (string.Format (lang.GetMessage ("ConsoleSyncingRoles", this), name, steamid));

			if (settings.roleSync.Receive == true) {
				JToken grantToken = payload["grant"];
				if (grantToken != null && grantToken.Type == JTokenType.Array) {
					foreach (JToken roleToken in grantToken) {
						string role = roleToken?.ToString ();
						if (string.IsNullOrEmpty (role))
							continue;

						if (!permission.GroupExists (role)) {
							if (settings.roleSync.Create) {
								LogMessage (string.Format (lang.GetMessage ("ConsoleCreatingGroup", this), role));
								permission.CreateGroup (role, role, 0);
							} else {
								continue;
							}
						} else if (permission.UserHasGroup (steamid, role))
							continue;

						LogMessage (string.Format (lang.GetMessage ("ConsoleGrantingGroup", this), role, name, steamid));
						permission.AddUserGroup (steamid, role);

						if (player != null)
							SendReply (player, string.Format (lang.GetMessage ("GroupGranted", this, player.UserIDString), role));
					}
				}

				JToken revokeToken = payload["revoke"];
				if (revokeToken != null && revokeToken.Type == JTokenType.Array) {
					foreach (JToken roleToken in revokeToken) {
						string role = roleToken?.ToString ();
						if (string.IsNullOrEmpty (role))
							continue;

						if (!permission.UserHasGroup (steamid, role))
							continue;

						LogMessage (string.Format (lang.GetMessage ("ConsoleRevokingGroup", this), role, name, steamid));
						permission.RemoveUserGroup (steamid, role);

						if (player != null)
							SendReply (player, string.Format (lang.GetMessage ("GroupRevoked", this, player.UserIDString), role));
					}
				}
			}

			if (settings.roleSync.Send == true) {
				JToken exhaustiveToken = payload["exhaustive"];
				bool exhaustive = exhaustiveToken != null && exhaustiveToken.Type == JTokenType.Boolean && (bool) exhaustiveToken;

				if (exhaustive) {
					JToken grantToken = payload["grant"];
					if (grantToken != null && grantToken.Type == JTokenType.Array) {
						string[] roles = grantToken.ToObject<string[]> ();

						foreach (string group in permission.GetGroups ())
							if (permission.UserHasGroup (steamid, group))
								if (Array.IndexOf (roles, group) == -1)
									PostRole (steamid, group);
					}
				}
			}
		}

		private void HandleSettingsAction (JToken payload) {
			settings = payload?.ToObject<IntegrationSettings> () ?? new ();

			var intervalSec = (float?) payload["poll_interval_seconds"];
			LogMessage (string.Format ("Poll interval: {0}s", intervalSec ?? -1f), LogLevel.Trace);
			if (intervalSec != null) SetPollInterval ((float) intervalSec);
		}
		#endregion

		#region Console commands
		[ConsoleCommand ($"{commandNamespace}.ban")]
		private void CmdBan (ConsoleSystem.Arg arg) {
			string name = "ban", usage = lang.GetMessage ("CommandUsageBan", this, arg.Player ()?.UserIDString), permissionSuffix = "ban";
			const int minArgs = 3, minAuthLevel = 2;

			var handler = (ConsoleSystem.Arg arg) => {
				BasePlayer admin = arg.Player ();

				string offenderSteamid = "";
				if (arg.Args[0].IsSteamId ()) {
					offenderSteamid = arg.Args[0];
				} else {
					List<BasePlayer> offenderMatches = GetPlayersByName (arg.Args[0]);
					if (offenderMatches.Count > 0) {
						if (offenderMatches.Count == 1) {
							offenderSteamid = offenderMatches.First ().UserIDString;
						} else {
							SendCommandReplies (arg, lang.GetMessage ("MultiplePlayersFound", this, admin?.UserIDString));
							return;
						}
					} else {
						SendCommandReplies (arg, string.Format (lang.GetMessage ("NoPlayersFoundByName", this, admin?.UserIDString), arg.Args[0]));
						return;
					}
				}

				if (!int.TryParse (arg.Args[1], out int durationMinutes)) {
					SendCommandReplies (arg, lang.GetMessage ("InvalidTime", this, admin?.UserIDString));
					return;
				}

				BasePlayer offender = GetPlayerBySteamID (offenderSteamid);

				if (offender != null)
					offender.Kick (lang.GetMessage ("Banned", this, offender.UserIDString));

				bool global = arg.Args.Length == 4 && arg.Args[3] == "true";
				Ban (offenderSteamid, arg.Args[1], arg.Args[2], global, admin?.UserIDString, admin);
			};

			HandleConsoleCommand (arg, name, usage, handler, minArgs, minAuthLevel, permissionSuffix);
		}
		
		[ConsoleCommand ($"{commandNamespace}.unban")]
		private void CmdUnban (ConsoleSystem.Arg arg) {
			const string name = "unban", usage = "<SteamID64>", permissionSuffix = "unban";
			const int minArgs = 1, minAuthLevel = 2;

			var handler = (ConsoleSystem.Arg arg) => {
				BasePlayer admin = arg.Player ();
				string offenderSteamid = arg.Args[0];

				if (!offenderSteamid.IsSteamId ()) {
					SendCommandReplies (arg, string.Format (lang.GetMessage ("InvalidArgValue", this, admin.UserIDString), "SteamID64", offenderSteamid), LogLevel.Warning);
					return;
				}

				Unban (offenderSteamid, admin);
			};

			HandleConsoleCommand (arg, name, usage, handler, minArgs, minAuthLevel, permissionSuffix);
		}

		[ConsoleCommand ($"{commandNamespace}.connect")]
		private void CmdConnect (ConsoleSystem.Arg arg) {
			const string name = "connect", usage = "<\"URL\"> <\"token\">";
			const int minArgs = 2, minAuthLevel = 2;

			var handler = (ConsoleSystem.Arg arg) => {
				if (!Uri.TryCreate (arg.Args[0], UriKind.Absolute, out Uri uri)) {
					SendReply (arg, string.Format (lang.GetMessage ("InvalidArgValue", this), "url", arg.Args[0]));
					return;
				}

				LoadConfig ();
				config.Host = NormalizeHostUri (uri.ToString ());
				config.Token = arg.Args[1];
				Config.WriteObject (config);

				if (arg.Connection != null)
					SendReply (arg, lang.GetMessage ("ConsoleApiConnectionChecking", this));

				TryInitApi ();
			};

			HandleConsoleCommand (arg, name, usage, handler, minArgs, minAuthLevel);
		}

		[ConsoleCommand ($"{commandNamespace}.loglevel")]
		private void CmdLogLevel (ConsoleSystem.Arg arg) {
			const string name = "loglevel", usage = "[\"level\"]";
			const int minArgs = 0, minAuthLevel = 2;

			var handler = (ConsoleSystem.Arg arg) => {
				if (!arg.HasArgs ()) {
					SendReply (arg, string.Format (lang.GetMessage ("ConfigOptionSet", this), "LogLevel", config.LogLevel));
					return;
				}

				if (!Enum.TryParse (arg.Args[0], true, out LogLevel level)) {
					SendReply (arg, string.Format (lang.GetMessage ("InvalidArgValue", this), "level", arg.Args[0]));
					return;
				}

				LoadConfig ();
				config.LogLevel = level;
				Config.WriteObject (config);

				SendReply (arg, string.Format (lang.GetMessage ("ConfigOptionSet", this), "LogLevel", level));
			};

			HandleConsoleCommand (arg, name, usage, handler, minArgs, minAuthLevel);
		}

		[ConsoleCommand ($"{commandNamespace}.pollwithin")]
		private void CmdPollWithin (ConsoleSystem.Arg arg) {
			const string name = "pollwithin", usage = "<\"seconds\">";
			const int minArgs = 1, minAuthLevel = 2;

			var handler = (ConsoleSystem.Arg arg) => {
				if (!connected) {
					SendReply (arg, lang.GetMessage ("NoApiConnection", this));
					return;
				}

				RunPollWithin (float.Parse (arg.Args[0]));
			};

			HandleConsoleCommand (arg, name, usage, handler, minArgs, minAuthLevel);
		}

		private void HandleConsoleCommand (ConsoleSystem.Arg arg, string name, string usage, Action<ConsoleSystem.Arg> handler, int minArgs = 0, int minAuthLevel = 2, string permissionSuffix = null) {
			string namespacedCommand = $"{commandNamespace}.{name}";

			if (arg.Connection != null) {
				BasePlayer player = arg.Player ();
				string namespacedPermission = permissionSuffix != null ?
					$"{Title.ToLower ()}.{permissionSuffix}" : null;

				if (player.net.connection.authLevel < minAuthLevel && !permission.UserHasPermission (player.UserIDString, namespacedPermission)) {
					SendCommandReplies (arg, string.Format (lang.GetMessage ("CommandNoPermission", this, player.UserIDString), namespacedCommand), LogLevel.Warning);
					return;
				}
			}

			if (minArgs > 0 && (!arg.HasArgs () || arg.Args.Length < minArgs)) {
				string format = $"{lang.GetMessage ("InvalidArgs", this)}. {lang.GetMessage ("CommandUsage", this)}",
					fullUsageSyntax = $"{namespacedCommand} {usage}";

				SendCommandReplies (arg, string.Format (format, fullUsageSyntax), LogLevel.Warning);
				return;
			}

			handler (arg);
		}
		#endregion

		#region Chat commands
		[ChatCommand ("ban")]
		private void ChatCmdBan (BasePlayer player, string _command, string[] args) {
			player.SendConsoleCommand (_cmdBanName, args);
		}

		[ChatCommand ("unban")]
		private void ChatCmdUnban (BasePlayer player, string _command, string[] args) {
			player.SendConsoleCommand (_cmdUnbanName, args);
		}
		#endregion

		#region Plugin interop
		#region Rust Map Api
		private bool IsRustMapApiReady () {
			bool isReady = RustMapApi?.Call<bool> ("IsReady") ?? false;
			if (!isReady) LogMessage ($"Rust Map Api is not ready", LogLevel.Trace);
			return isReady;
		}

		private bool TryUploadMapImage () {
			if (!IsRustMapApiReady ())
				return false;

			string mapName = !string.IsNullOrEmpty (config.MapImageName) ? config.MapImageName : "Icons";
			int resolution = (int) (config.MapImageScale * World.Size);

			object response = RustMapApi.Call ("CreatePluginImage", this, mapName, resolution, (int) mapImageEncoding);
			if (response is string) {
				LogMessage ($"Rust Map Api call failed: {response}", LogLevel.Warning);
				return false;
			}

			var map = response as Hash<string, object>;
			if (map?["image"] is byte[] mapBytes) {
				LogMessage ($"Map image size: {mapBytes.Length} bytes", LogLevel.Trace);
			} else {
				LogMessage ($"Image missing from Rust Map Api response", LogLevel.Warning);
				return false;
			}

			// Кодирование и отправка в фоновом потоке — не блокирует игровой сервер
			Task.Run (() => {
				try {
					string base64 = Convert.ToBase64String (mapBytes);
					string body = "{\"visibility\":\"public\",\"encoding\":\"base64\",\"content\":\"" + base64 + "\"}"; 
					NextTick (() => {
						var headers = new Dictionary<string, string> {
							{ "Authorization", "Bearer " + config.Token },
							{ "Accept", "application/json" },
							{ "Content-Type", "application/json" }
						};
						string url = config.Host + "/api/game/blobs/" + mapImageBlobPath;
						webrequest.Enqueue (url, body, (code, response) => {
							if (code < 200 || code > 299)
								LogMessage ($"Map upload failed: {code} {response}", LogLevel.Warning);
							else
								LogMessage ("Map image uploaded successfully", LogLevel.Debug);
						}, this, RequestMethod.PUT, headers);
					});
				} catch (Exception ex) {
					NextTick (() => LogMessage ($"Map image encoding failed: {ex.Message}", LogLevel.Warning));
				}
			});
			return true;
		}
		#endregion
		#endregion

		#region Localization
		protected override void LoadDefaultMessages () {
			lang.RegisterMessages (new Dictionary<string, string> {
				{ "ActionCommandRunning", "Running action command: {0}"},
				{ "ActionDispatching", "Handling action \"{0}\""},
				{ "ActionUnrecognized", "Unrecognized action \"{0}\""},
				{ "Banned", "You've been banned from the server" },
				{ "CommandUsage", "Usage: {0}" },
				{ "CommandUsageBan", "<name/SteamID64> <duration:minutes, 0=permanent> \"<reason>\" [global:false/true]" },
				{ "CommandNoPermission", "Insufficient permissions for running command \"{0}\"" },
				{ "ConfigOptionSet", "Configuration option {0} set to {1}" },
				{ "ConsoleApiConnectionChecking", "Checking connection to web API" },
				{ "ConsoleApiConnectionSuccess", "Connection established and token validated successfully" },
				{ "ConsoleApiRequestFailed", "Web API request failed. Code: {0}. Response: {1}" },
				{ "ConsoleApiRequestFailedBan", "Failed to ban user. Code: {0}. Response: {1}" },
				{ "ConsoleApiRequestFailedRoleAdd", "Failed to add role \"{0}\". Code: {1}. Response: {2}" },
				{ "ConsoleApiRequestFailedRoleRevoke", "Failed to revoke role \"{0}\". Code: {1}. Response: {2}" },
				{ "ConsoleApiRequestFailedUnban", "Failed to unban user. Code: {0}. Response: {1}" },
				{ "ConsoleCreatingGroup", "Creating group \"{0}\"" },
				{ "ConsoleGrantingGroup", "Granting the \"{0}\" group to {1} ({2})" },
				{ "ConsolePolling", "Polling with {0} player(s)" },
				{ "ConsoleRevokingGroup", "Revoking the \"{0}\" group from {1} ({2})" },
				{ "ConsoleRoleAdded", "Role \"{0}\" added for {1}" },
				{ "ConsoleRoleRevoked", "Role \"{0}\" revoked from {1}" },
				{ "ConsoleSyncingRoles", "Syncing roles for {0} ({1})" },
				{ "DebugChatPrefix", "[Ember] " },
				{ "GroupGranted", "You've been granted the {0} group" },
				{ "GroupRevoked", "Your {0} group has been revoked" },
				{ "InvalidArgs", "Invalid arguments" },
				{ "InvalidArgValue", "Invalid value for argument {0}: \"{1}\"" },
				{ "InvalidTime", "Time must be a number, 0 for permanent" },
				{ "MultiplePlayersFound", "Multiple players found, please be more specific" },
				{ "NoApiConnection", "The plugin is not connected to the web API. Check the server console for details" },
				{ "NoPlayersFoundByName", "Player not found by name \"{0}\"" },
				{ "PlayerBanFailed", "Failed to ban player {0}" },
				{ "PlayerBanned", "Player {0} banned" },
				{ "PlayerUnbanFailed", "Failed to unban player {0}" },
				{ "PlayerUnbanned", "Player {0} unbanned by {1}" },
				{ "PollIntervalSetting", "Setting the polling interval to {0} second(s)" },
			}, this, "en");

			LogMessage ("Default messages created", LogLevel.Debug);
		}
		#endregion
	}
}​

Thank you!

dnJQqqTH7og9RDJ.png aykd520

Thank you!

Is everything working? Are there no more errors?

It's working very well at the moment!