using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using static ConsoleSystem; namespace Oxide.Plugins { [Info("Update Notice", "Psystec", "3.2.2", ResourceId = 2837)] [Description("Notifies you when new Rust updates are released.")] internal sealed class UpdateNotice : RustPlugin { //DO NOT EDIT. Please ask for permission or notify me should you require changes. //Check out our API host sponser YaMang's plugins at codefling.com: https://codefling.com/yamang-w #region Fields private Configuration _configuration; private UpdateInfo _updateInfo; private const string AdminPermission = "updatenotice.admin"; private const string ApiUrl = "http://api.yamang.xyz:2095/rustapi"; private bool _initLoad = true; private int _checkingInterval = 180; private readonly Dictionary _properNames = new Dictionary { {"carbon", "Carbon"}, {"uMod", "Oxide"}, {"rustClient", "Client"}, {"rustClientStaging", "ClientStaging"}, {"rustServer", "Server"}, {"newsgid", "DevBlog"}, }; private readonly Dictionary _properCommands = new Dictionary { {"carbon", "carbon"}, {"uMod", "umod"}, {"rustClient", "client"}, {"rustClientStaging", "clientstaging"}, {"rustServer", "server"}, {"newsgid", "devblog"}, }; #endregion Fields #region Classes private sealed class UpdateInfo { public string carbon { get; set; } public string uMod { get; set; } public string rustClient { get; set; } public string rustClientStaging { get; set; } public string rustServer { get; set; } public string newsgid { get; set; } public string newsurl { get; set; } public string newsdate { get; set; } } #region Discord Message private sealed class DiscordMessageEmbeds { /// /// if used, it overrides the default username of the webhook /// public string username { get; set; } /// /// if used, it overrides the default avatar of the webhook /// public string avatar_url { get; set; } /// /// simple message, the message contains (up to 2000 characters) /// public string content { get; set; } /// /// array of embed objects. That means, you can use more than one in the same body /// public Embed[] embeds { get; set; } } private sealed class Embed { /// /// embed author object /// public Author author { get; set; } /// /// title of embed /// public string title { get; set; } /// /// url of embed. If title was used, it becomes hyperlink /// public string url { get; set; } /// /// description text /// public string description { get; set; } /// /// color code of the embed. You have to use Decimal numeral system, not Hexadecimal. Use color picker and converter: https://htmlcolorcodes.com/color-picker/ and https://www.binaryhexconverter.com/hex-to-decimal-converter /// public int color { get; set; } /// /// rray of embed field objects /// public Field[] fields { get; set; } /// /// embed thumbnail object /// public Thumbnail thumbnail { get; set; } /// /// embed image object /// public Image image { get; set; } /// /// embed footer object /// public Footer footer { get; set; } } private sealed class Author { /// /// name of author /// public string name { get; set; } /// /// url of author. If name was used, it becomes a hyperlink /// public string url { get; set; } /// /// url of author icon /// public string icon_url { get; set; } } private sealed class Thumbnail { /// /// url of thumbnail /// public string url { get; set; } } private sealed class Image { /// /// url of image /// public string url { get; set; } } private sealed class Footer { /// /// footer text, doesn't support Markdown /// public string text { get; set; } /// /// url of footer icon /// public string icon_url { get; set; } } private sealed class Field { /// /// name of the field /// public string name { get; set; } /// /// alue of the field /// public string value { get; set; } /// /// if true, fields will be displayed in same line, but there can only be 3 max in same line or 2 max if you used thumbnail /// public bool inline { get; set; } } #endregion Discord Message #endregion Classes #region Configuration private sealed class Configuration { [JsonProperty("Only Notify Admin")] public bool OnlyNotifyAdmins { get; set; } = false; [JsonProperty("Enable Discord Notifications")] public bool EnableDiscordNotify { get; set; } = false; [JsonProperty("Discord role id to mention (0 = no mention)")] public ulong DiscordRoleId { get; set; } = 0; [JsonProperty("Discord Webhook URL")] public string DiscordWebhookURL { get; set; } = "https://support.discordapp.com/hc/en-us/articles/228383668"; [JsonProperty("Enable GUI Notifications (Needs GUIAnnouncements)")] public bool EnableGuiNotifications { get; set; } = true; [JsonProperty("Enable Chat Notifications")] public bool EnableChatNotifications { get; set; } = true; [JsonProperty("GUI Notifications Tint Color")] public string GUINotificationsTintColor { get; set; } = "Purple"; [JsonProperty("GUI Notifications Text Color")] public string GUINotificationsTextColor { get; set; } = "Yellow"; [JsonProperty("Enable Server Version Notifications")] public bool EnableServer { get; set; } = true; [JsonProperty("Enable DevBlog Notifications")] public bool EnableDevBlog { get; set; } = true; [JsonProperty("Enable Client Version Notifications")] public bool EnableClient { get; set; } = true; [JsonProperty("Enable Staging Version Notifications")] public bool EnableStaging { get; set; } = false; [JsonProperty("Enable UMod Version Notifications")] public bool EnableUMod { get; set; } = false; [JsonProperty("Enable Carbon Version Notifications")] public bool EnableCarbon { get; set; } = false; } protected override void SaveConfig() => Config.WriteObject(_configuration); private void LoadNewConfig() => _configuration = Config.ReadObject(); protected override void LoadDefaultConfig() => _configuration = new Configuration(); protected override void LoadConfig() { base.LoadConfig(); _configuration = Config.ReadObject(); } #endregion Configuration #region Localization protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["ServerUpdated"] = "Server Update Released!", ["DevBlogUpdated"] = "DevBlog Update Released!", ["ClientUpdated"] = "Client Update Released!", ["ClientStagingUpdated"] = "Client Staging Update Released!", ["UModUpdated"] = "UMod Update Released!", ["CarbonUpdated"] = "Carbon Update Released!", ["FailedToCheckUpdates"] = "Failed to check for RUST updates, if this keeps happening please contact the developer.", ["PluginNotFoundGuiAnnouncements"] = "GUIAnnouncements plugin was not found. GUI Announcements disabled.", ["NoPermission"] = "You do not have permission to use this command.", ["DiscordWebhookURLNotConfigured"] = "Discord Webhook URL is not configured.", ["IntervalCheck"] = "Checking interval must be 180 seconds or greater! Setting this lower may get your server banned. Auto adjusted to 300.", ["Help.Command"] = "COMMAND", ["Help.Description"] = "DESCRIPTION", ["Help.Gui"] = "Tests GUI notification (Needs GUIAnnouncements Plugin)", ["Help.Discord"] = "Tests Discord notification", ["Help.Current"] = "Display's all current versions", ["Help.Server"] = "Simulate Server update release", ["Help.DevBlog"] = "Simulate DevBlog update release", ["Help.Client"] = "Simulate Client update release", ["Help.Staging"] = "Simulate Staging update release", ["Help.Oxide"] = "Simulate Oxide update release", ["Help.Carbon"] = "Simulate Carbon update release", ["Help.All"] = "Simulate all updates released (depends on config)", ["Help.ForceCheck"] = "Forces a version check", ["Help.LoadConfig"] = "Reload the config file", ["Chat.Prefix"] = "Update Notice", }, this); lang.RegisterMessages(new Dictionary { ["ServerUpdated"] = "Mise à jour du Serveur disponible !", ["DevBlogUpdated"] = "Mise à jour du \"DevBlog\" disponible !", ["ClientUpdated"] = "Mise à jour du Client disponible !", ["ClientStagingUpdated"] = "Mise à jour de la branche \"Staging\" du Client disponible !", ["UModUpdated"] = "Mise à jour de UMod disponible !", ["CarbonUpdated"] = "Mise à jour de Carbon disponible !", ["FailedToCheckUpdates"] = "Récupération des mises à jour de RUST impossible, si cela se reproduit veuillez contacter le développeur.", ["PluginNotFoundGuiAnnouncements"] = "Le plugin GUIAnnouncements n'a pas été trouvé. GUI Announcements désactivé.", ["NoPermission"] = "Vous n'avez pas la permission d'utiliser cette commande", ["DiscordWebhookURLNotConfigured"] = "L'URL du Discord Webhook n'est pas configuré.", ["IntervalCheck"] = "L'interval de vérification doit être de 180 secondes ou plus ! Configurer une valeur inférieure pourrait voir votre serveur banni. Ajustement automatique à 300.", ["Help.Command"] = "COMMANDE", ["Help.Description"] = "DESCRIPTION", ["Help.Gui"] = "Simule la notification par GUI (Requiert le plugin GUIAnnouncements)", ["Help.Discord"] = "Simule la notification Discord", ["Help.Current"] = "Affiche toutes les versions actuelles", ["Help.Server"] = "Simule la notification de la mise à jour du Server", ["Help.DevBlog"] = "Simule la notification de la mise à jour \"DevBlog\"", ["Help.Client"] = "Simule la notification de la mise à jour du Client", ["Help.Staging"] = "Simule la notification de la mise à jour de la branche \"Staging\"", ["Help.Oxide"] = "Simule la notification de la mise à jour d'Oxide", ["Help.Carbon"] = "Simule la notification de la mise à jour de Carbon", ["Help.All"] = "Simule toutes les notifications de mise à jour (dépend de la config)", ["Help.ForceCheck"] = "Force l'actualisation des données", ["Help.LoadConfig"] = "Recharge la configuration", ["Chat.Prefix"] = "Avis de mise à jour", }, this, "fr"); } #endregion Localization #region Hooks private void Init() { permission.RegisterPermission(AdminPermission, this); } private void Loaded() { if (GUIAnnouncements == null) { PrintWarning(Lang("PluginNotFoundGuiAnnouncements")); _configuration.EnableGuiNotifications = false; } if (_checkingInterval < 180) { PrintWarning(Lang("IntervalCheck")); _checkingInterval = 300; } timer.Every(_checkingInterval, CompareVersions); } #endregion Hooks #region Testing [ConsoleCommand("updatenotice")] private void AdvertCommands(Arg arg) { if (arg.IsClientside && !HasPermission(arg.Player(), AdminPermission)) { SendReply(arg, Lang("NoPermission")); return; } if (arg.Args.IsNullOrEmpty()) { SendHelp(arg); return; } if (_updateInfo == null) { CompareVersions(); } var command = arg.Args[0].ToLower(); switch (command) { case "gui": TestNotification("GUI", GUIAnnouncements, "Test message from Update Notice by Psystec", SendtoGui); break; case "discord": TestNotification("Discord", !string.IsNullOrEmpty(_configuration.DiscordWebhookURL), "Test message from Update Notice by Psystec", SendToDiscord); break; case "current": DisplayCurrentVersions(arg); break; case "server": case "devblog": case "client": case "clientstaging": case "umod": case "carbon": TestUpdate(command, true); break; case "all": TestAllUpdates(); break; case "forcecheck": Puts("Forcing Version Check"); CompareVersions(); break; case "loadconfig": Puts("Loading Configuration File"); LoadNewConfig(); break; default: SendHelp(arg); break; } } private void TestNotification(string type, bool condition, string message, Action notificationMethod) { if (!condition) { PrintWarning(Lang($"PluginNotFound{type}Announcements")); return; } Puts($"Testing {type} Messages: {message}"); notificationMethod.Invoke(message); } private void TestUpdate(string type, bool isCmd) { Puts($"Testing {type} Update"); Interface.CallHook($"On{type}Update", "TestUpdate"); // Yeah that's reflexion but fuck infernal switch case if (isCmd) { typeof(UpdateInfo).GetProperty(_properCommands.FirstOrDefault(x => x.Value == type).Key)?.SetValue(_updateInfo, $"Simulating {type} Update"); return; } typeof(UpdateInfo).GetProperty(_properNames.FirstOrDefault(x => x.Value == type).Key)?.SetValue(_updateInfo, $"Simulating {type} Update"); } private void TestAllUpdates() { Puts("Testing All Updates"); foreach (var keyValuePair in _properNames) { TestUpdate(keyValuePair.Value, false); } } private void DisplayCurrentVersions(Arg arg) { SendReply(arg, $"Update Notice by Psystec\n" + $"Server: {_updateInfo.rustServer}\n" + $"DevBlog: {_updateInfo.newsgid}\n" + $"Client: {_updateInfo.rustClient}\n" + $"Staging: {_updateInfo.rustClientStaging}\n" + $"UMod: {_updateInfo.uMod}\n" + $"Carbon: {_updateInfo.carbon}"); } private void SendHelp(Arg arg) { var width = 30; SendReply(arg, (Lang("Help.Command")).PadRight(width) + Lang("Help.Description")); SendReply(arg, ("updatenotice gui").PadRight(width) + Lang("Help.Gui")); SendReply(arg, ("updatenotice discord").PadRight(width) + Lang("Help.Discord")); SendReply(arg, ("updatenotice current").PadRight(width) + Lang("Help.Current")); SendReply(arg, ("updatenotice server").PadRight(width) + Lang("Help.Server")); SendReply(arg, ("updatenotice devblog").PadRight(width) + Lang("Help.DevBlog")); SendReply(arg, ("updatenotice client").PadRight(width) + Lang("Help.Client")); SendReply(arg, ("updatenotice staging").PadRight(width) + Lang("Help.Staging")); SendReply(arg, ("updatenotice oxide").PadRight(width) + Lang("Help.Oxide")); SendReply(arg, ("updatenotice carbon").PadRight(width) + Lang("Help.Carbon")); SendReply(arg, ("updatenotice all").PadRight(width) + Lang("Help.All")); SendReply(arg, ("updatenotice forcecheck").PadRight(width) + Lang("Help.ForceCheck")); SendReply(arg, ("updatenotice loadconfig").PadRight(width) + Lang("Help.LoadConfig")); } #endregion Testing #region Version Comparison private void CompareVersions() { webrequest.Enqueue(ApiUrl, null, (code, response) => { if (code != 200 || response == null) { PrintWarning($"API: {Lang("FailedToCheckUpdates")}\nError Code: {code} | Message: {response}"); return; } var nVData = JsonConvert.DeserializeObject(response); if (_initLoad) { _updateInfo = nVData; _initLoad = false; } UpdateCheck("Carbon", nVData.carbon, _updateInfo.carbon, _configuration.EnableCarbon); UpdateCheck("Client", nVData.rustClient, _updateInfo.rustClient, _configuration.EnableClient); UpdateCheck("ClientStaging", nVData.rustClientStaging, _updateInfo.rustClientStaging, _configuration.EnableStaging); UpdateCheck("DevBlog", nVData.newsgid, _updateInfo.newsgid, _configuration.EnableDevBlog, _updateInfo.newsurl); UpdateCheck("Oxide", nVData.uMod, _updateInfo.uMod, _configuration.EnableUMod); UpdateCheck("Server", nVData.rustServer, _updateInfo.rustServer, _configuration.EnableServer); _updateInfo = nVData; }, this); } private void UpdateCheck(string type, string newData, string oldData, bool enableFeature, string additionalInfo = null) { if (newData == oldData) return; Interface.CallHook($"On{type}Update", newData); if (!enableFeature) return; Puts(Lang($"{type}Updated")); if (_configuration.EnableChatNotifications) SendToChat(Lang($"{type}Updated")); if (_configuration.EnableGuiNotifications) SendtoGui(Lang($"{type}Updated")); if (_configuration.EnableDiscordNotify) SendToDiscord(Lang($"{type}Updated") + (additionalInfo != null ? $": {additionalInfo}" : "")); } #endregion Version Comparison #region Helpers private string Lang(string key, string id = null, params object[] args) => string.Format(lang.GetMessage(key, this, id), args); private bool HasPermission(BasePlayer player, string perm) { if (permission.UserHasPermission(player.userID.ToString(), perm)) return true; PrintWarning("UserID: " + player.UserIDString + " | UserName: " + player.displayName + " | " + Lang("NoPermission")); return false; } private void SendtoGui(string message) { if (GUIAnnouncements == null) { PrintWarning(Lang("PluginNotFoundGuiAnnouncements")); return; } foreach (var player in BasePlayer.activePlayerList) { if (_configuration.OnlyNotifyAdmins) { if (!_configuration.OnlyNotifyAdmins || !HasPermission(player, AdminPermission)) continue; GUIAnnouncements?.Call("CreateAnnouncement", message, _configuration.GUINotificationsTintColor, _configuration.GUINotificationsTextColor, player); Puts($"Announcement created for: {player.displayName}: {message}"); } else { GUIAnnouncements?.Call("CreateAnnouncement", message, _configuration.GUINotificationsTintColor, _configuration.GUINotificationsTextColor, player); Puts($"Announcement created for: {player.displayName}: {message}"); } } } private void SendToChat(string message) { rust.BroadcastChat(null, $"{Lang("Chat.Prefix")}\n{message}"); } private void SendToDiscord(string message) { var headers = new Dictionary { { "Content-Type", "application/json" } }; var dc = new DiscordMessageEmbeds { content = _configuration.DiscordRoleId == 0 ? message : $"<@&{_configuration.DiscordRoleId}> {message}", embeds = new[] { new Embed { title = "Update Notice", description = $"Alert Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\nVisit [Umod](https://umod.org/plugins/update-notice) for more information or [Discord](https://discord.gg/EyRgFdA) for any assistance.", url = "https://discord.gg/EyRgFdA", color = 16711686, thumbnail = new Thumbnail { url = "https://assets.umod.org/images/icons/plugin/5ea987f1379b2.png" }, footer = new Footer { icon_url = "https://assets.umod.org/user/7O3gGkDgaP/14G1myUYST6LEi2.png", text = "Created by Psystec" } } } }; var payload = JsonConvert.SerializeObject(dc); webrequest.Enqueue(_configuration.DiscordWebhookURL, payload, (code, response) => { if (code == 200 || code == 204) return; if (response == null) { PrintWarning($"Discord didn't respond. Error Code: {code}"); } else { Puts($"Discord respond with: {response} Payload: {payload}"); } }, this, Core.Libraries.RequestMethod.POST, headers); } #endregion Helpers #region Internal API public string GetServerVersion() => _updateInfo?.rustServer; public string GetDevBlogVersion() => _updateInfo?.newsgid; public string GetClientVersion() => _updateInfo?.rustClient; public string GetStagingVersion() => _updateInfo?.rustClientStaging; public string GetUModVersion() => _updateInfo?.uMod; public string GetCarbonVersion() => _updateInfo?.carbon; public string GetDevBlogLink() => _updateInfo?.newsurl; #endregion Internal API #region External API [PluginReference] Plugin GUIAnnouncements; #endregion External API } }