Иконка ресурса

FREE UpdateChecker : 4.6.1 4.6.1

Нет прав для скачивания
C#:
#region

using System;
using System.IO;
using Oxide.Core;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using Newtonsoft.Json;
using Oxide.Core.Plugins;
using System.Collections;
using Oxide.Game.Rust.Cui;
using System.Globalization;
using JetBrains.Annotations;
using UnityEngine.Networking;
using System.Collections.Generic;
using System.Text.RegularExpressions;

#endregion

namespace Oxide.Plugins;

[Info("UpdateChecker", "tofurahie", "4.6.1")]
[Description("Update checker for all of your plugins")]
internal class UpdateChecker : RustPlugin
{
    #region GUI

    private class UI
    {
        private const string Layer = "UI_UpdateChecker";

        public static void MainParent(ref CuiElementContainer container, string name = null, string aMin = "0.5 0.5", string aMax = "0.5 0.5",
            bool overAll = true, bool keyboardEnabled = true, bool cursorEnabled = true) => container.Add(new CuiPanel
        {
            KeyboardEnabled = keyboardEnabled,
            CursorEnabled = cursorEnabled,
            RectTransform = { AnchorMin = aMin, AnchorMax = aMax },
            Image = { Color = "0 0 0 0" }
        }, overAll ? "Overlay" : "Hud", Layer + ".bg" + name, Layer + ".bg" + name);

        public static void Panel(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
            string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string bgColor = "0.33 0.33 0.33 1", string material = null,
            string sprite = null, int itemID = 0, ulong skinID = 0) => container.Add(new CuiElement
        {
            Parent = Layer + parent,
            Name = name != null && name.Contains(".other.") ? name.Replace(".other.", "") : Layer + name,
            DestroyUi = destroy == null ? null : Layer + destroy,
            Components =
            {
                new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
                new CuiImageComponent { Color = HexToRustFormat(bgColor), Material = material, Sprite = sprite, ItemId = itemID, SkinId = skinID },
            },
        });

        public static void Icon(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
            string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", int itemID = 0, ulong skinID = 0) => container.Add(new CuiElement
        {
            Parent = Layer + parent,
            Name = Layer + name,
            DestroyUi = destroy == null ? null : Layer + destroy,
            Components =
            {
                new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
                new CuiImageComponent { ItemId = itemID, SkinId = skinID },
            },
        });

        public static void Image(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
            string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string image = "", string color = "1 1 1 1") => container.Add(new CuiElement
        {
            Parent = Layer + parent,
            Name = Layer + name,
            DestroyUi = destroy == null ? null : Layer + destroy,
            Components =
            {
                new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
                new CuiRawImageComponent
                {
                    Png = !image.StartsWith("http") && !image.StartsWith("www") ? image : null,
                    Url = image.StartsWith("http") || image.StartsWith("www") ? image : null, Color = HexToRustFormat(color),
                    Sprite = "assets/content/textures/generic/fulltransparent.tga"
                },
            },
        });

        public static void Label(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
            string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string text = null, int fontSize = 16, string color = "1 1 1 1",
            TextAnchor align = TextAnchor.MiddleCenter, string outlineDistance = null, string outlineColor = "0 0 0 1",
            VerticalWrapMode wrapMode = VerticalWrapMode.Truncate, string font = "robotocondensed-regular.ttf") => container.Add(new CuiElement
        {
            Parent = Layer + parent,
            Name = Layer + name,
            DestroyUi = destroy == null ? null : Layer + destroy,
            Components =
            {
                new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
                new CuiTextComponent
                    { Text = text, FontSize = fontSize, Color = HexToRustFormat(color), Align = align, Font = font, VerticalOverflow = wrapMode },
                outlineDistance == null
                    ? new CuiOutlineComponent { Distance = "0 0", Color = "0 0 0 0" }
                    : new CuiOutlineComponent
                        { Distance = outlineDistance, Color = HexToRustFormat(outlineColor) },
            },
        });

        public static void Button(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
            string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string text = null, int fontSize = 16, string color = "1 1 1 1",
            string command = null, string bgColor = "0 0 0 0", VerticalWrapMode wrapMode = VerticalWrapMode.Truncate,
            TextAnchor align = TextAnchor.MiddleCenter, string font = "robotocondensed-regular.ttf", string material = null, string sprite = null) =>
            container.Add(new CuiButton
            {
                RectTransform = { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
                Text = { Text = text, FontSize = fontSize, Color = HexToRustFormat(color), Align = align, Font = font, VerticalOverflow = wrapMode },
                Button =
                {
                    Command = command, Close = command == null ? Layer + name : null, Color = HexToRustFormat(bgColor), Material = material, Sprite = sprite
                }
            }, Layer + parent, command == null ? null : Layer + name, destroy == null ? null : Layer + destroy);

        public static void Input(ref CuiElementContainer container, string parent, string name = null, string destroy = null, string aMin = "0 0",
            string aMax = "1 1", string oMin = "0 0", string oMax = "0 0", string text = null, int limit = 40, int fontSize = 16, string color = "1 1 1 1",
            string command = null, TextAnchor align = TextAnchor.MiddleCenter, bool autoFocus = false, bool hudMenuInput = false, bool readOnly = false,
            bool isPassword = false, bool needsKeyboard = false, bool singleLine = true, string font = "robotocondensed-regular.ttf") => container.Add(
            new CuiElement
            {
                Parent = Layer + parent,
                Name = Layer + name,
                DestroyUi = destroy == null ? null : Layer + destroy,
                Components =
                {
                    new CuiRectTransformComponent { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
                    new CuiInputFieldComponent
                    {
                        Text = text, Command = command, CharsLimit = limit, FontSize = fontSize, Color = HexToRustFormat(color), Align = align, Font = font,
                        Autofocus = autoFocus, IsPassword = isPassword, ReadOnly = readOnly, HudMenuInput = hudMenuInput, NeedsKeyboard = needsKeyboard,
                        LineType = singleLine ? InputField.LineType.SingleLine : InputField.LineType.MultiLineNewline
                    },
                }
            });

        public static void Outline(ref CuiElementContainer container, string layer, string size = "1 1 1 1", string color = "0 0 0 1",
            bool external = false)
        {
            var borders = size.Split(' ');

            if (borders[0] != "0")
                Panel(ref container, layer, aMin: "0 1", aMax: "1 1", oMin: $"-{borders[0]} {(external ? "0" : "-" + borders[0])}",
                    oMax: $"{borders[0]} {(external ? borders[0] : "0")}", bgColor: color);

            if (borders[1] != "0")
                Panel(ref container, layer, aMin: "1 0", aMax: "1 1", oMin: $"{(external ? "0" : "-" + borders[1])} -{borders[1]}",
                    oMax: $"{(external ? borders[1] : "0")} {borders[1]}", bgColor: color);

            if (borders[2] != "0")
                Panel(ref container, layer, aMin: "0 0", aMax: "1 0", oMin: $"-{borders[2]} {(external ? "-" + borders[2] : "0")}",
                    oMax: $"{borders[2]} {(external ? "0" : borders[2])}", bgColor: color);

            if (borders[3] != "0")
                Panel(ref container, layer, aMin: "0 0", aMax: "0 1", oMin: $"{(external ? "-" + borders[3] : "0")} -{borders[3]}",
                    oMax: $"{(external ? "0" : borders[3])} {borders[3]}", bgColor: color);
        }

        public static string HexToRustFormat(string hex)
        {
            if (string.IsNullOrEmpty(hex))
                return hex;

            Color color;

            if (hex.Contains(":"))
                return ColorUtility.TryParseHtmlString(hex.Substring(0, hex.IndexOf(":", StringComparison.Ordinal)), out color)
                    ? $"{color.r:F2} {color.g:F2} {color.b:F2} {hex.Substring(hex.IndexOf(":", StringComparison.Ordinal) + 1, hex.Length - hex.IndexOf(":", StringComparison.Ordinal) - 1)}"
                    : hex;

            return ColorUtility.TryParseHtmlString(hex, out color) ? $"{color.r:F2} {color.g:F2} {color.b:F2} {color.a:F2}" : hex;
        }

        public static void Create(BasePlayer player, CuiElementContainer container)
        {
            CuiHelper.AddUi(player, container);
        }

        public static void CreateToAll(CuiElementContainer container, string layer)
        {
            foreach (var player in BasePlayer.activePlayerList)
                CuiHelper.AddUi(player, container);
        }

        public static void Destroy(BasePlayer player, string layer) => CuiHelper.DestroyUi(player, Layer + layer);

        public static void DestroyToAll(string layer)
        {
            foreach (var player in BasePlayer.activePlayerList)
                Destroy(player, layer);
        }
    }

    #endregion

    #region Static

    private const int PLUGIN_SCAN_BATCH_SIZE = 20;
    private const string PERM = "updatechecker.setup";
    private static readonly Regex PluginInfoRegex = new(@"Info\(\""(.*?)\"",\""(.*?)\"",\""(.*?)\""", RegexOptions.Compiled);
    private static readonly Regex CamelBoundaryRegex = new(@"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", RegexOptions.Compiled);
    private static readonly Regex LeadingFileIdRegex = new(@"^\d+-", RegexOptions.Compiled);
    private IEnumerator _checkNewVersionCoroutine, _sendDiscordMessageCoroutine, _updatePluginListCoroutine;
    private bool _isChecking;

    private readonly HashSet<string> _configIgnoreList = new()
    {
        "RustEdit"
    };

    #region Classes

    [Serializable]
    private class Configuration
    {
        [JsonProperty("Command to open UI")]
        public string Command = "ucsetup";

        [JsonProperty("Command to send the test message to your discord")]
        public string CommandDiscord = "uctest";

        [JsonProperty("Console command to check orphaned config files")]
        public string CommandCheckOrphanedConfigFiles = "uc_check_config";

        [JsonProperty("Console command to move orphaned config files")]
        public string CommandDeleteOrphanedConfigFiles = "uc_clean_config";

        [JsonProperty("Discord WebHook")]
        public string DiscordWebHook = "";

        [JsonProperty("Discord message ID")]
        public string MessageID = "";

        [JsonProperty("Check updates on load [disable it if you have problem with the config]")]
        public bool CheckUpdateOnLoad = true;

        [JsonProperty("Embed side line color [hex]")]
        public string EmbedLineColor = "#ffffff";

        [JsonProperty("Difference between UTC and your time [in minutes]")]
        public int UTC = 60;

        [JsonProperty("Use 24 time format")]
        public bool TimeFormat = true;

        [JsonProperty("Check Interval(In minutes)")]
        public int CheckMinutes = 60;

        [JsonProperty("Ignore 'All plugins have the latest version' discord message")]
        public bool IgnoreAllPluginsUpToDateMessage = false;

        [JsonProperty("Ignore not found plugins")]
        public bool IgnoreNotFound = false;

        [JsonProperty("Ignore not loaded plugins")]
        public bool IgnoreNotLoaded = true;

        [JsonProperty("Add a link to the plugin to be updated")]
        public bool UseURL = true;

        [JsonProperty("Remove missing plugins from config automatically")]
        public bool AutoRemoveMissingPlugins = true;

        [JsonProperty("List of plugins", ObjectCreationHandling = ObjectCreationHandling.Replace)]
        public List<PluginInfo> ListOfPlugins = new();

        [JsonProperty("Enable auto search")]
        public Dictionary<string, bool> AutoSearch = new()
        {
            ["uMod"] = true,
            ["Codefling"] = true,
            ["Lone.Design"] = true,
            ["Chaos"] = true,
            ["RustWorkshop"] = true,
            ["Github"] = true,
            ["ModPulse"] = true,
            ["RustPlugins"] = true,
            ["ServerArmour"] = true,
            ["ImperialPlugins"] = true,
            ["MyVector"] = true,
            ["SkyPlugins"] = true,
            ["Game4Freak"] = true,
        };

        [JsonProperty("Links to version lists [json: plugin name -> Major, Minor, Patch, Author, Url]", ObjectCreationHandling = ObjectCreationHandling.Replace)]
        public List<string> VersionLists = new()
        {
            "https://www.robjmaps.com/downloads/Plugin_Versions.json"
        };
    }

    [Serializable]
    private class PluginInfo
    {
        [JsonIgnore]
        public bool IsFounded;

        [JsonIgnore]
        public bool IsLoaded;

        [JsonIgnore]
        public string SearchUrl;

        [JsonIgnore]
        public string Title;

        [JsonProperty("Name")]
        public string Name;

        [JsonProperty("Author")]
        public string Author;

        [JsonProperty("Plugin version")]
        public string Version;

        [JsonProperty("Link to plugin")]
        public string Url;

        [JsonProperty("Marketplace")]
        public string Marketplace;

        [JsonProperty("Ignore")]
        public bool Ignore;

        public PluginInfo(string author, string name, string title, string version, string url = "", string marketplace = "", bool ignore = false, bool isLoaded = false)
        {
            Name = name;
            Author = author;
            Title = title;
            Version = version;
            Url = url;
            Marketplace = marketplace;
            Ignore = ignore;
            IsLoaded = isLoaded;
        }
    }

    [Serializable]
    private class RequestData
    {
        [JsonProperty("status")]
        public int Status { get; set; }

        [JsonProperty("data")]
        public PluginData[] Data { get; set; }
    }

    [Serializable]
    private class PluginData
    {
        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("manualName")]
        public string ManualName { get; set; }

        [JsonProperty("author")]
        public string Author { get; set; }

        [JsonProperty("latestVersion")]
        public string LatestVersion { get; set; }

        [JsonProperty("url")]
        public string Url { get; set; }

        [JsonProperty("slug")]
        public string Slug { get; set; }

        [JsonProperty("marketplace")]
        public string Marketplace { get; set; }

        [JsonProperty("tags")]
        public string Tags { get; set; }
    }

    private class MarketplaceResponse
    {
        public PluginData[] Data;
        public bool RateLimited;
        public bool Failed;
    }

    [Serializable]
    private class ListedVersion
    {
        public int Major { get; set; }

        public int Minor { get; set; }

        public int Patch { get; set; }

        public string Author { get; set; }

        public string Url { get; set; }

        public string Marketplace { get; set; }

        public string Version => $"{Major}.{Minor}.{Patch}";

        public bool IsWrittenBy(string author)
        {
            return string.IsNullOrWhiteSpace(Author) || string.Equals(Author.Trim(), author?.Trim(), StringComparison.OrdinalIgnoreCase);
        }
    }

    [Serializable]
    private class AboutPlugin
    {
        public string Name;
        public string Title;
        public string Author;
        public string Version;
        public bool IsLoaded;

        public AboutPlugin(string name, string title, string author, string version, bool isLoaded)
        {
            Name = name;
            Title = title;
            Author = author;
            Version = version;
            IsLoaded = isLoaded;
        }
    }

    #endregion

    #endregion

    #region OxideHooks

    [UsedImplicitly]
    [HookMethod(nameof(OnServerInitialized))]
    private void OnServerInitialized()
    {
        cmd.AddChatCommand(_config.Command, this, nameof(ChatSetupCommand));
        cmd.AddChatCommand(_config.CommandDiscord, this, nameof(DiscordTestCommand));
        cmd.AddConsoleCommand(_config.CommandCheckOrphanedConfigFiles, this, nameof(console_uc_check_config));
        cmd.AddConsoleCommand(_config.CommandDeleteOrphanedConfigFiles, this, nameof(console_uc_clean_config));

        permission.RegisterPermission(PERM, this);

        if (_config.CheckUpdateOnLoad)
            CheckPlugins();

        timer.Every(60 * Math.Max(_config.CheckMinutes, 1), CheckPlugins);
    }

    [UsedImplicitly]
    [HookMethod(nameof(Unload))]
    private void Unload()
    {
        UI.DestroyToAll(".bg");

        if (_updatePluginListCoroutine != null)
            ServerMgr.Instance.StopCoroutine(_updatePluginListCoroutine);

        if (_checkNewVersionCoroutine != null)
            ServerMgr.Instance.StopCoroutine(_checkNewVersionCoroutine);

        if (_sendDiscordMessageCoroutine != null)
            ServerMgr.Instance.StopCoroutine(_sendDiscordMessageCoroutine);
    }

    #endregion

    #region Commands

    [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
    private void console_uc_check_config(ConsoleSystem.Arg arg)
    {
        if (arg.HasArgs() || arg.Player() != null)
            return;

        UpdatePluginList();

        var configsNames = "";
        foreach (var check in Directory.GetFiles(Interface.Oxide.ConfigDirectory, "*.json").Select(Path.GetFileNameWithoutExtension))
            if (_config.ListOfPlugins.All(x => !string.Equals(x.Name, check, StringComparison.CurrentCultureIgnoreCase)) && !_configIgnoreList.Contains(check))
                configsNames += check + ", ";

        if (configsNames == string.Empty)
        {
            PrintWarning($"All config files are in use");
            return;
        }

        PrintWarning(configsNames.TrimEnd().TrimEnd(',') +
                     $"\nYou can move orphaned config files to UpdateCheckerConfigsBackup via command \"{_config.CommandDeleteOrphanedConfigFiles}\"");
    }

    [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
    private void console_uc_clean_config(ConsoleSystem.Arg arg)
    {
        if (arg.HasArgs() || arg.Player() != null)
            return;

        UpdatePluginList();
        Directory.CreateDirectory(Interface.Oxide.ConfigDirectory + "/UpdateCheckerConfigsBackup");

        var count = 0;
        foreach (var check in Directory.GetFiles(Interface.Oxide.ConfigDirectory, "*.json"))
            if (_config.ListOfPlugins.All(x => !string.Equals(x.Name, Path.GetFileNameWithoutExtension(check), StringComparison.CurrentCultureIgnoreCase)) &&
                !_configIgnoreList.Contains(Path.GetFileNameWithoutExtension(check)))
            {
                File.Move(check, Interface.Oxide.ConfigDirectory + $"/UpdateCheckerConfigsBackup/{Path.GetFileNameWithoutExtension(check)}.json");
                count++;
            }


        if (count == 0)
        {
            PrintWarning("All config files are in use");
            return;
        }

        PrintError($"{count} config files ${(count > 1 ? "were" : "was")} moved to {Interface.Oxide.ConfigDirectory + $"/UpdateCheckerConfigsBackup"}");
    }

    [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
    private void ChatSetupCommand(BasePlayer player, string command, string[] args)
    {
        if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PERM))
        {
            SendReply(player, "You don't have permissions to use this command");
            return;
        }

        ShowUIBG(player);
    }

    [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
    private void DiscordTestCommand(BasePlayer player, string command, string[] args)
    {
        if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PERM))
        {
            SendReply(player, "You don't have permissions to use this command");
            return;
        }

        var updateList = _config.AutoSearch.ToDictionary(static check => check.Key,
            static _ => (new List<string>(), new List<string> { "Your discord webhook is working" }));

        _sendDiscordMessageCoroutine = SendMessageDiscord(updateList);
        ServerMgr.Instance.StartCoroutine(_sendDiscordMessageCoroutine);
    }

    [ConsoleCommand("checkupdates")]
    [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
    private void ConsolecheckupdatesCommand(ConsoleSystem.Arg arg)
    {
        if (arg == null || arg.Player() != null)
            return;

        if (_isChecking)
        {
            PrintWarning("The plugin already scans other plugins for updates");
            return;
        }

        CheckPlugins();
    }

    [ConsoleCommand("UI_UC")]
    [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
    private void UI_UC(ConsoleSystem.Arg arg)
    {
        if (!arg.HasArgs())
            return;

        var player = arg.Player();
        if (player == null || (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PERM)))
            return;

        switch (arg.GetString(0))
        {
            case "SEARCH":
                ShowUIPlugins(player, string.Join(" ", arg.Args.Skip(1)).Replace(" ", ""));
                break;
            case "PAGE":
                ShowUIPlugins(player, string.Join(" ", arg.Args.Skip(2)), arg.GetInt(1));
                break;
            case "CHECK":
                ShowUICurrentPluginInfo(player,
                    _config.ListOfPlugins.FirstOrDefault(x =>
                        string.Equals(x.Name, string.Join(" ", arg.Args.Skip(1)), StringComparison.CurrentCultureIgnoreCase)));

                break;
            case "CHANGEURL":
                ChangePluginUrl(arg.GetString(1), arg.GetString(2));
                break;
            case "CHANGEIGNORE":
            {
                var pluginInfo = _config.ListOfPlugins.FirstOrDefault(x => string.Equals(x.Name, arg.GetString(1)));
                if (pluginInfo == null)
                    break;

                pluginInfo.Ignore = !pluginInfo.Ignore;
                ShowUICurrentPluginInfo(player, pluginInfo);
                SaveConfigKeepingManualEdits(pluginInfo.Name);
                break;
            }
        }
    }

    #endregion

    #region Functions

    private void ChangePluginUrl(string name, string url)
    {
        var pluginInfo = _config.ListOfPlugins.FirstOrDefault(x => string.Equals(x.Name, name));
        if (pluginInfo == null || string.Equals(pluginInfo.Url, url, StringComparison.Ordinal))
            return;

        pluginInfo.Url = url;
        SaveConfigKeepingManualEdits(pluginInfo.Name);
    }

    private IEnumerator SendMessageDiscord(Dictionary<string, (List<string>, List<string>)> infoList)
    {
        if (string.IsNullOrEmpty(_config.DiscordWebHook))
        {
            _isChecking = false;
            yield break;
        }

        var updateListBuilder = new StringBuilder();
        foreach (var checks in infoList)
        foreach (var check in checks.Value.Item2)
            updateListBuilder.Append(check);

        var updateList = updateListBuilder.ToString();
        var isAllUpToDate = updateList.Contains("All plugins are up to date");
        var message = new
        {
            content = "",
            embeds = new[]
            {
                new
                {
                    title = "",
                    description = updateList,
                    color = isAllUpToDate
                        ? 65280
                        : int.Parse(_config.EmbedLineColor.Substring(1), NumberStyles.HexNumber),
                }
            }
        };

        var spaceBuilder = new StringBuilder();
        if (updateList.Length < 4000)
        {
            using var request = string.IsNullOrEmpty(_config.MessageID)
                ? new UnityWebRequest(_config.DiscordWebHook, "POST")
                : new UnityWebRequest($"{_config.DiscordWebHook}/messages/{_config.MessageID}", "PATCH");

            request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)));
            request.SetRequestHeader("Content-Type", "application/json");

            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
                PrintError(request.responseCode == 404
                    ? "Failed to find the message with the specified ID in the config, please check it and try again."
                    : "The discord hook is borken, please check it and try again.");

            _isChecking = false;
            yield break;
        }

        foreach (var check in infoList)
        foreach (var url in check.Value.Item2)
        {
            spaceBuilder.Append(url);
            if (spaceBuilder.Length + url.Length + "\nThe list of updates has reached its limit, please update your plugins.".Length <= 4000)
                continue;

            message = new
            {
                content = "",
                embeds = new[]
                {
                    new
                    {
                        title = "",
                        description = spaceBuilder.ToString(),
                        color = int.Parse(_config.EmbedLineColor.Substring(1), NumberStyles.HexNumber),
                    },
                    new
                    {
                        title = "",
                        description = "The list of updates has reached its limit, please update your plugins.",
                        color = 16711680,
                    }
                }
            };

            using var request = string.IsNullOrEmpty(_config.MessageID)
                ? new UnityWebRequest(_config.DiscordWebHook, "POST")
                : new UnityWebRequest($"{_config.DiscordWebHook}/messages/{_config.MessageID}", "PATCH");

            request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)));
            request.SetRequestHeader("Content-Type", "application/json");

            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
                PrintError(request.responseCode == 404
                    ? "Failed to find the message with the specified ID in the config, please check it and try again."
                    : "The discord hook is borken, please check it and try again.");

            yield break;
        }

        _isChecking = false;
    }

    private void CheckPlugins()
    {
        _isChecking = true;

        ReloadConfigFromDisk();
        _updatePluginListCoroutine = CheckPluginsRoutine();
        ServerMgr.Instance.StartCoroutine(_updatePluginListCoroutine);
    }

    private IEnumerator CheckPluginsRoutine()
    {
        var allPlugins = new List<AboutPlugin>();
        var scanRoutine = GetAllPluginsRoutine(allPlugins);
        while (scanRoutine.MoveNext())
            yield return scanRoutine.Current;

        UpdatePluginList(allPlugins);
        SaveConfigKeepingManualEdits();

        _checkNewVersionCoroutine = CheckNewVersion();
        ServerMgr.Instance.StartCoroutine(_checkNewVersionCoroutine);
    }

    private List<AboutPlugin> GetAllPlugins()
    {
        var allFoundedPlugins = new List<AboutPlugin>();
        var scanRoutine = GetAllPluginsRoutine(allFoundedPlugins);
        while (scanRoutine.MoveNext())
        {
        }

        return allFoundedPlugins;
    }

    private IEnumerator GetAllPluginsRoutine(List<AboutPlugin> allFoundedPlugins)
    {
        foreach (var check in plugins.GetAll().Where(static x => !x.IsCorePlugin))
            allFoundedPlugins.Add(new AboutPlugin(check.Name, check.Title, check.Author, check.Version.ToString(), check.IsLoaded));

        var foundedNames = new HashSet<string>(allFoundedPlugins.Select(static x => x.Name), StringComparer.OrdinalIgnoreCase);

        var filesInBatch = 0;
        foreach (var fileSystemInfo in new DirectoryInfo(Interface.Oxide.PluginDirectory).GetFiles("*" + "cs")
                     .Where(static f => (f.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden))
        {
            if (!foundedNames.Contains(Path.GetFileNameWithoutExtension(fileSystemInfo.Name)) && TryParseAboutPlugin(fileSystemInfo, out var aboutPlugin))
                allFoundedPlugins.Add(aboutPlugin);

            filesInBatch++;
            if (filesInBatch < PLUGIN_SCAN_BATCH_SIZE)
                continue;

            filesInBatch = 0;
            yield return null;
        }
    }

    private static bool TryParseAboutPlugin(FileInfo fileSystemInfo, out AboutPlugin aboutPlugin)
    {
        aboutPlugin = null;

        var regexInfo = PluginInfoRegex.Match(File.ReadAllText(fileSystemInfo.FullName).Replace(" ", ""));
        if (!regexInfo.Success)
            return false;

        aboutPlugin = new AboutPlugin(Path.GetFileNameWithoutExtension(fileSystemInfo.Name), regexInfo.Groups[1].Value, regexInfo.Groups[2].Value,
            regexInfo.Groups[3].Value, false);

        return true;
    }

    private void UpdatePluginList()
    {
        UpdatePluginList(GetAllPlugins());
    }

    private void UpdatePluginList(List<AboutPlugin> allPlugins)
    {
        var listChanged = false;

        if (_config.AutoRemoveMissingPlugins)
        {
            var foundedNames = new HashSet<string>(allPlugins.Select(static x => x.Name), StringComparer.OrdinalIgnoreCase);
            var removedNames = new List<string>();

            for (var index = _config.ListOfPlugins.Count - 1; index >= 0; index--)
            {
                var pluginInfo = _config.ListOfPlugins[index];
                if (foundedNames.Contains(pluginInfo.Name) || File.Exists(Path.Combine(Interface.Oxide.PluginDirectory, pluginInfo.Name + ".cs")))
                    continue;

                removedNames.Add(pluginInfo.Name);
                _config.ListOfPlugins.RemoveAt(index);
            }

            if (removedNames.Count != 0)
            {
                PrintWarning($"Removed from the list, no such plugin on the server: {string.Join(", ", removedNames)}");
                listChanged = true;
            }
        }

        var pluginsByName = new Dictionary<string, PluginInfo>(StringComparer.OrdinalIgnoreCase);
        foreach (var pluginInfo in _config.ListOfPlugins)
            pluginsByName.TryAdd(pluginInfo.Name, pluginInfo);

        foreach (var entry in allPlugins)
        {
            if (pluginsByName.TryGetValue(entry.Name, out PluginInfo pluginInfo))
            {
                pluginInfo.Author = entry.Author;
                pluginInfo.Title = entry.Title;
                pluginInfo.Version = entry.Version;
                pluginInfo.IsLoaded = entry.IsLoaded;
            }
            else
            {
                var newPluginInfo = new PluginInfo(entry.Author, entry.Name, entry.Title, entry.Version, "", "", false, entry.IsLoaded);
                _config.ListOfPlugins.Add(newPluginInfo);
                pluginsByName[newPluginInfo.Name] = newPluginInfo;
                listChanged = true;
            }
        }

        if (listChanged)
            _config.ListOfPlugins = _config.ListOfPlugins.OrderBy(static x => x.Name).ToList();
    }

    private IEnumerator CheckNewVersion()
    {
        _isChecking = true;

        var listedVersions = new Dictionary<string, ListedVersion>(StringComparer.OrdinalIgnoreCase);
        var listsRoutine = LoadVersionLists(listedVersions);
        while (listsRoutine.MoveNext())
            yield return listsRoutine.Current;

        var updateList = _config.AutoSearch.ToDictionary(static check => check.Key, static _ => (new List<string>(), new List<string>()));
        updateList["uMod"] = (
            new List<string> { $"{DateTime.UtcNow.AddMinutes(_config.UTC).ToString(_config.TimeFormat ? "d MMM yyyy HH:mm" : "d MMM yyyy hh:mm tt")}" },
            new List<string>
                { $"### {DateTime.UtcNow.AddMinutes(_config.UTC).ToString(_config.TimeFormat ? "d MMM yyyy HH:mm" : "d MMM yyyy hh:mm tt")}" });

        updateList.Add("NotFound", (new List<string> { "\n[NotFound]" }, new List<string> { "\n[NotFound]" }));
        updateList.Add("NotLoaded", (new List<string> { "\n[NotLoaded]" }, new List<string> { "\n[NotLoaded]" }));

        var isUpToDate = true;
        var searchedPlugins = new List<PluginData>();
        var linkedPlugins = new List<string>();

        for (var index = 0; index < _config.ListOfPlugins.Count; index++)
        {
            if (!IsLoaded)
                yield break;

            var check = _config.ListOfPlugins[index];
            if (check.Ignore)
                continue;

            if (!check.IsLoaded)
            {
                if (_config.IgnoreNotLoaded)
                    continue;

                updateList["NotLoaded"].Item1[0] += $" {check.Name},";
                updateList["NotLoaded"].Item2[0] += $" {check.Name},";
            }

            if (string.IsNullOrEmpty(check.Url) && listedVersions.TryGetValue(check.Name, out var listed) && listed.IsWrittenBy(check.Author))
            {
                ApplyLatestVersion(check, listed.Marketplace, listed.Version, listed.Url, updateList, ref isUpToDate);
                continue;
            }

            check.SearchUrl = string.IsNullOrEmpty(check.Url) ? null : NormalizeUrl(check.Url);

            var response = new MarketplaceResponse();
            var searchRoutine = RequestMarketplace(string.IsNullOrEmpty(check.SearchUrl) ? check.Name : check.SearchUrl, response);
            while (searchRoutine.MoveNext())
                yield return searchRoutine.Current;

            if (response.RateLimited)
            {
                PrintError("Waiting 30 seconds for rate limit");
                index--;
                yield return new WaitForSeconds(30f);

                continue;
            }

            if (response.Failed)
                continue;

            check.IsFounded = false;

            var foundedPlugins = response.Data;

            var hasExactUrl = !string.IsNullOrEmpty(check.SearchUrl) && foundedPlugins != null &&
                              foundedPlugins.Any(x => IsSameUrl(check.SearchUrl, x.Url, false));

            if (!string.IsNullOrEmpty(check.SearchUrl) && !hasExactUrl)
            {
                var nameResponse = new MarketplaceResponse();
                var nameRoutine = RequestMarketplace(check.Name, nameResponse);
                while (nameRoutine.MoveNext())
                    yield return nameRoutine.Current;

                if (nameResponse.RateLimited)
                {
                    PrintError("Waiting 30 seconds for rate limit");
                    index--;
                    yield return new WaitForSeconds(30f);

                    continue;
                }

                if (nameResponse.Data != null)
                {
                    foundedPlugins = foundedPlugins == null ? nameResponse.Data : foundedPlugins.Concat(nameResponse.Data).ToArray();
                    hasExactUrl = foundedPlugins.Any(x => IsSameUrl(check.SearchUrl, x.Url, false));
                }
            }

            var hadUrl = !string.IsNullOrEmpty(check.Url);

            searchedPlugins.Clear();
            if (foundedPlugins != null)
                searchedPlugins.AddRange(foundedPlugins);

            TryResolvePlugin(check, foundedPlugins, hasExactUrl, allowWeakMatch: false, updateList, ref isUpToDate);

            if (!check.IsFounded)
            {
                var rescueRateLimited = false;
                foreach (var rescueQuery in GetRescueQueries(check))
                {
                    var rescueResponse = new MarketplaceResponse();
                    var rescueRoutine = RequestMarketplace(rescueQuery, rescueResponse);
                    while (rescueRoutine.MoveNext())
                        yield return rescueRoutine.Current;

                    if (rescueResponse.RateLimited)
                    {
                        rescueRateLimited = true;
                        break;
                    }

                    if (rescueResponse.Failed || rescueResponse.Data == null)
                        continue;

                    searchedPlugins.AddRange(rescueResponse.Data);
                    if (TryResolvePlugin(check, rescueResponse.Data, hasExactUrl: false, allowWeakMatch: false, updateList, ref isUpToDate))
                        break;
                }

                if (rescueRateLimited)
                {
                    PrintError("Waiting 30 seconds for rate limit");
                    index--;
                    yield return new WaitForSeconds(30f);

                    continue;
                }

                if (!check.IsFounded)
                    TryResolvePlugin(check, searchedPlugins.ToArray(), hasExactUrl, allowWeakMatch: true, updateList, ref isUpToDate);
            }

            if (!hadUrl && !string.IsNullOrEmpty(check.Url))
                linkedPlugins.Add($"{check.Name} -> {check.Url}");

            if (check.IsFounded)
                continue;

            updateList["NotFound"].Item1[0] += $" {check.Name},";
            updateList["NotFound"].Item2[0] += $" {check.Name},";
        }

        if (linkedPlugins.Count != 0)
            PrintWarning($"The link was empty and has been filled in from the search:\n{string.Join("\n", linkedPlugins)}");

        updateList["NotFound"].Item1[0] = updateList["NotFound"].Item1[0].TrimEnd(',');
        updateList["NotFound"].Item2[0] = updateList["NotFound"].Item2[0].TrimEnd(',');

        updateList["NotLoaded"].Item1[0] = updateList["NotLoaded"].Item1[0].TrimEnd(',');
        updateList["NotLoaded"].Item2[0] = updateList["NotLoaded"].Item2[0].TrimEnd(',');

        if (_config.IgnoreNotFound || !updateList["NotFound"].Item1[0].Contains(" "))
            updateList.Remove("NotFound");

        if (!updateList["NotLoaded"].Item1[0].Contains(" "))
            updateList.Remove("NotLoaded");

        if (isUpToDate)
        {
            updateList["uMod"].Item1.Add("\nAll plugins are up to date");
            updateList["uMod"].Item2.Add("\n**All plugins are up to date**");
        }

        if (updateList.Any(static x => x.Value.Item2.Count != 0) || (!_config.IgnoreNotFound && updateList["NotFound"].Item2[0].Split(' ').Length != 1))
        {
            var printWarningBuilder = new StringBuilder();
            foreach (var check in updateList)
            foreach (var item in check.Value.Item1)
                printWarningBuilder.Append(item);

            PrintWarning(printWarningBuilder.ToString());

            if (!isUpToDate || (isUpToDate && !_config.IgnoreAllPluginsUpToDateMessage))
            {
                _sendDiscordMessageCoroutine = SendMessageDiscord(updateList);
                ServerMgr.Instance.StartCoroutine(_sendDiscordMessageCoroutine);
            }
        }

        SaveConfigKeepingManualEdits();
    }

    private IEnumerator LoadVersionLists(Dictionary<string, ListedVersion> listedVersions)
    {
        foreach (var listLink in _config.VersionLists)
        {
            if (!Uri.TryCreate(NormalizeUrl(listLink), UriKind.Absolute, out var listUri))
            {
                PrintError($"The version list link is not a valid URL: {listLink}");
                continue;
            }

            var marketplace = listUri.Host.StartsWith("www.", StringComparison.OrdinalIgnoreCase) ? listUri.Host.Substring("www.".Length) : listUri.Host;
            _config.AutoSearch.TryAdd(marketplace, true);
            if (!_config.AutoSearch[marketplace])
                continue;

            using var request = UnityWebRequest.Get(listUri);
            request.SetRequestHeader("User-Agent", $"Update Checker/{Version}");
            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                PrintError($"Failed to load the version list {listLink}: {request.error}");
                continue;
            }

            Dictionary<string, ListedVersion> versions;
            try
            {
                versions = JsonConvert.DeserializeObject<Dictionary<string, ListedVersion>>(request.downloadHandler.text);
            }
            catch (Exception exception)
            {
                PrintError($"The version list {listLink} is not readable: {exception.Message}");
                continue;
            }

            if (versions == null)
                continue;

            foreach (var version in versions)
            {
                if (version.Value == null)
                    continue;

                version.Value.Marketplace = marketplace;
                listedVersions.TryAdd(version.Key, version.Value);
            }
        }
    }

    private bool TryResolvePlugin(PluginInfo check, PluginData[] foundedPlugins, bool hasExactUrl, bool allowWeakMatch,
        Dictionary<string, (List<string>, List<string>)> updateList, ref bool isUpToDate)
    {
        if (foundedPlugins == null || foundedPlugins.Length == 0)
            return false;

        var validPlugins = foundedPlugins.Where(static x => !string.IsNullOrEmpty(x.Name) && !string.IsNullOrEmpty(x.Marketplace)).ToList();
        if (validPlugins.Count == 0)
            return false;

        foreach (var pluginData in validPlugins)
            RegisterMarketplace(pluginData.Marketplace, updateList);

        var marketplaceOrder = new Dictionary<string, int>(StringComparer.Ordinal);
        foreach (var marketplace in _config.AutoSearch.Keys)
            marketplaceOrder[marketplace] = marketplaceOrder.Count;

        var orderedFoundedPlugins = validPlugins.OrderBy(x => marketplaceOrder.TryGetValue(x.Marketplace, out var order) ? order : -1).ToList();

        List<PluginData> candidates;
        if (!string.IsNullOrEmpty(check.SearchUrl))
        {
            candidates = orderedFoundedPlugins.Where(x => IsSameUrl(check.SearchUrl, x.Url, !hasExactUrl)).ToList();
        }
        else
        {
            var enabledPlugins = orderedFoundedPlugins
                .Where(x => !_config.AutoSearch.TryGetValue(x.Marketplace, out var isEnabled) || isEnabled).ToList();

            var checkNameLower = check.Name.ToLower();
            var checkTitleLower = string.IsNullOrEmpty(check.Title) ? null : check.Title.Replace(" ", "").ToLower();
            var checkAuthorLower = check.Author.ToLower();

            candidates = enabledPlugins.Where(x => x.Author != null && x.Author.ToLower().Contains(checkAuthorLower) &&
                                                   (IsNameMatch(x, checkNameLower) || checkTitleLower != null && IsNameMatch(x, checkTitleLower))).ToList();

            if (candidates.Count == 0)
                candidates = enabledPlugins.Where(x => IsExactNameMatch(x, checkNameLower) ||
                                                       checkTitleLower != null && IsExactNameMatch(x, checkTitleLower)).ToList();

            if (candidates.Count == 0 && allowWeakMatch)
                candidates = enabledPlugins.Where(x => IsNameMatch(x, checkNameLower) ||
                                                       checkTitleLower != null && IsNameMatch(x, checkTitleLower)).ToList();

            if (candidates.Count == 0 && allowWeakMatch && enabledPlugins.Count != 0)
            {
                var firstNameLower = enabledPlugins[0].Name.ToLower();
                candidates = enabledPlugins.Where(x => x.Tags != null && x.Tags.Contains(firstNameLower)).ToList();
            }
        }

        if (candidates.Count == 0)
            return false;

        var newPluginData = candidates[0];
        foreach (var candidate in candidates)
        {
            if (string.IsNullOrEmpty(candidate.LatestVersion))
                continue;

            if (string.IsNullOrEmpty(newPluginData.LatestVersion) || ParseVersion(candidate.LatestVersion) > ParseVersion(newPluginData.LatestVersion))
                newPluginData = candidate;
        }

        if (string.IsNullOrEmpty(check.Url))
            check.Url = newPluginData.Url;

        ApplyLatestVersion(check, newPluginData.Marketplace, newPluginData.LatestVersion, check.Url, updateList, ref isUpToDate);

        return true;
    }

    private void ApplyLatestVersion(PluginInfo check, string marketplace, string latestVersion, string url,
        Dictionary<string, (List<string>, List<string>)> updateList, ref bool isUpToDate)
    {
        check.IsFounded = true;
        check.Marketplace = marketplace;
        RegisterMarketplace(marketplace, updateList);

        if (string.IsNullOrEmpty(latestVersion) || ParseVersion(latestVersion) <= ParseVersion(check.Version))
            return;

        isUpToDate = false;
        Interface.Oxide.CallHook("OnUpdateCheckerUpdateFound", check.Name, check.Version, latestVersion, url, marketplace);

        var link = _config.UseURL && !string.IsNullOrEmpty(url) ? url : null;
        var (consoleLines, discordLines) = updateList[marketplace];
        consoleLin
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху