From 6f671a9d57ea8e6ca9b079a8256501d1b81a6000 Mon Sep 17 00:00:00 2001 From: Brett Hewitson Date: Sat, 11 Jul 2020 13:09:27 +1000 Subject: [PATCH] Discord Plugin Source Added discord plugin source to github --- .gitignore | 11 + .../Plugin.Common/Enums/AlertTypeEnum.cs | 15 + .../Plugin.Common/Extensions/IconExtension.cs | 47 +++ .../Plugin.Common/Interfaces/IAlertPlugin.cs | 13 + .../source/Plugin.Common/Interfaces/IBeta.cs | 11 + .../Plugin.Common/Interfaces/IPlugin.cs | 56 ++++ .../source/Plugin.Common/Plugin.Common.csproj | 24 ++ .../source/Plugin.Common/PluginException.cs | 30 ++ .../source/Plugin.Common/PluginHelper.cs | 274 ++++++++++++++++ .../source/Plugin.Common/PluginItem.cs | 27 ++ .../Plugin.Common/Properties/AssemblyInfo.cs | 39 +++ .../source/Plugin.Common/Utils/JsonUtils.cs | 64 ++++ .../Plugin.Common/Utils/ResourceUtils.cs | 25 ++ .../Discord/source/Plugin.Discord/Art/Add.ico | Bin 0 -> 102134 bytes .../source/Plugin.Discord/Art/ChangeNotes.ico | Bin 0 -> 102134 bytes .../source/Plugin.Discord/Art/Delete.ico | Bin 0 -> 102134 bytes .../source/Plugin.Discord/Art/Download.ico | Bin 0 -> 102134 bytes .../source/Plugin.Discord/Art/Edit.ico | Bin 0 -> 102134 bytes .../source/Plugin.Discord/Art/favicon.ico | Bin 0 -> 15086 bytes .../source/Plugin.Discord/Config.Designer.cs | 158 ++++++++++ .../source/Plugin.Discord/Config.settings | 48 +++ .../source/Plugin.Discord/DiscordPlugin.cs | 260 ++++++++++++++++ .../Globalization/en-US/en-US.xaml | 119 +++++++ .../Plugin.Discord/Interfaces/IBindable.cs | 15 + .../Plugin.Discord/Lib/BrowserBehavior.cs | 32 ++ .../Plugin.Discord/Models/AlertTypeValue.cs | 55 ++++ .../Models/AlertTypeValueList.cs | 69 +++++ .../source/Plugin.Discord/Models/Bindable.cs | 85 +++++ .../Plugin.Discord/Models/ConfigProfile.cs | 188 +++++++++++ .../Models/DiscordPluginConfig.cs | 35 +++ .../Plugin.Discord/Models/ObservableList.cs | 131 ++++++++ .../Plugin.Discord/Models/ProfileNameValue.cs | 54 ++++ .../Models/ProfileNameValueList.cs | 67 ++++ .../Plugin.Discord/Models/VersionFeed.cs | 16 + .../Plugin.Discord/Models/VersionFeedEntry.cs | 17 + .../Plugin.Discord/Plugin.Discord.csproj | 73 +++++ .../Plugin.Discord/Properties/AssemblyInfo.cs | 34 ++ .../Plugin.Discord/Utils/NetworkUtils.cs | 90 ++++++ .../source/Plugin.Discord/Utils/TaskUtils.cs | 15 + .../Plugin.Discord/Utils/VersionFeedUtils.cs | 53 ++++ .../source/Plugin.Discord/VersionFeed.xml | 292 ++++++++++++++++++ .../source/Plugin.Discord/VersionFeedBeta.xml | 34 ++ .../Windows/ConfigProfileWindow.xaml | 228 ++++++++++++++ .../Windows/ConfigProfileWindow.xaml.cs | 223 +++++++++++++ .../Plugin.Discord/Windows/ConfigWindow.xaml | 137 ++++++++ .../Windows/ConfigWindow.xaml.cs | 271 ++++++++++++++++ .../Windows/VersionFeedWindow.xaml | 60 ++++ .../Windows/VersionFeedWindow.xaml.cs | 81 +++++ .../Discord/source/Plugin.Discord/app.config | 54 ++++ .../source/ServerManagerTool.Plugins.sln | 43 +++ 50 files changed, 3673 insertions(+) create mode 100644 .gitignore create mode 100644 Plugins/Discord/source/Plugin.Common/Enums/AlertTypeEnum.cs create mode 100644 Plugins/Discord/source/Plugin.Common/Extensions/IconExtension.cs create mode 100644 Plugins/Discord/source/Plugin.Common/Interfaces/IAlertPlugin.cs create mode 100644 Plugins/Discord/source/Plugin.Common/Interfaces/IBeta.cs create mode 100644 Plugins/Discord/source/Plugin.Common/Interfaces/IPlugin.cs create mode 100644 Plugins/Discord/source/Plugin.Common/Plugin.Common.csproj create mode 100644 Plugins/Discord/source/Plugin.Common/PluginException.cs create mode 100644 Plugins/Discord/source/Plugin.Common/PluginHelper.cs create mode 100644 Plugins/Discord/source/Plugin.Common/PluginItem.cs create mode 100644 Plugins/Discord/source/Plugin.Common/Properties/AssemblyInfo.cs create mode 100644 Plugins/Discord/source/Plugin.Common/Utils/JsonUtils.cs create mode 100644 Plugins/Discord/source/Plugin.Common/Utils/ResourceUtils.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Art/Add.ico create mode 100644 Plugins/Discord/source/Plugin.Discord/Art/ChangeNotes.ico create mode 100644 Plugins/Discord/source/Plugin.Discord/Art/Delete.ico create mode 100644 Plugins/Discord/source/Plugin.Discord/Art/Download.ico create mode 100644 Plugins/Discord/source/Plugin.Discord/Art/Edit.ico create mode 100644 Plugins/Discord/source/Plugin.Discord/Art/favicon.ico create mode 100644 Plugins/Discord/source/Plugin.Discord/Config.Designer.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Config.settings create mode 100644 Plugins/Discord/source/Plugin.Discord/DiscordPlugin.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Globalization/en-US/en-US.xaml create mode 100644 Plugins/Discord/source/Plugin.Discord/Interfaces/IBindable.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Lib/BrowserBehavior.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/AlertTypeValue.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/AlertTypeValueList.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/Bindable.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/ConfigProfile.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/DiscordPluginConfig.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/ObservableList.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/ProfileNameValue.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/ProfileNameValueList.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/VersionFeed.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Models/VersionFeedEntry.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Plugin.Discord.csproj create mode 100644 Plugins/Discord/source/Plugin.Discord/Properties/AssemblyInfo.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Utils/NetworkUtils.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Utils/TaskUtils.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Utils/VersionFeedUtils.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/VersionFeed.xml create mode 100644 Plugins/Discord/source/Plugin.Discord/VersionFeedBeta.xml create mode 100644 Plugins/Discord/source/Plugin.Discord/Windows/ConfigProfileWindow.xaml create mode 100644 Plugins/Discord/source/Plugin.Discord/Windows/ConfigProfileWindow.xaml.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Windows/ConfigWindow.xaml create mode 100644 Plugins/Discord/source/Plugin.Discord/Windows/ConfigWindow.xaml.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/Windows/VersionFeedWindow.xaml create mode 100644 Plugins/Discord/source/Plugin.Discord/Windows/VersionFeedWindow.xaml.cs create mode 100644 Plugins/Discord/source/Plugin.Discord/app.config create mode 100644 Plugins/Discord/source/ServerManagerTool.Plugins.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0c828fec --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/Plugins/Discord/source/.vs +/Plugins/Discord/source/Plugin.Common/bin +/Plugins/Discord/source/Plugin.Common/obj +/Plugins/Discord/source/Plugin.Common/Publish +/Plugins/Discord/source/Plugin.Discord/bin +/Plugins/Discord/source/Plugin.Discord/obj +/Plugins/Discord/source/Plugin.Discord/Publish diff --git a/Plugins/Discord/source/Plugin.Common/Enums/AlertTypeEnum.cs b/Plugins/Discord/source/Plugin.Common/Enums/AlertTypeEnum.cs new file mode 100644 index 00000000..ff88e7af --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Enums/AlertTypeEnum.cs @@ -0,0 +1,15 @@ +namespace ServerManagerTool.Plugin.Common +{ + public enum AlertType + { + Error, + Shutdown, + ShutdownMessage, + ShutdownReason, + Startup, + Backup, + UpdateResults, + ServerStatusChange, + ModUpdateDetected, + } +} diff --git a/Plugins/Discord/source/Plugin.Common/Extensions/IconExtension.cs b/Plugins/Discord/source/Plugin.Common/Extensions/IconExtension.cs new file mode 100644 index 00000000..b838d14a --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Extensions/IconExtension.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Windows.Markup; +using System.Windows.Media.Imaging; + +namespace ServerManagerTool.Plugin.Common +{ + /// + /// Simple extension for icon, to let you choose icon with specific size. + /// Usage sample: + /// Image Stretch="None" Source="{common:Icon /Controls;component/icons/custom.ico, 16}" + /// Or: + /// Image Source="{common:Icon Source={Binding IconResource}, Size=16}" + /// + public class IconExtension : MarkupExtension + { + private string _path; + + public string Path + { + get + { + return _path; + } + set + { + // Have to make full pack URI from short form, so System.Uri recognizes it. + _path = $"pack://application:,,,{value}"; + } + } + + public int Size { get; set; } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + var decoder = BitmapDecoder.Create(new Uri(Path), BitmapCreateOptions.DelayCreation, BitmapCacheOption.OnDemand); + + var result = decoder.Frames.SingleOrDefault(f => f.Width == Size); + if (result == default(BitmapFrame)) + { + result = decoder.Frames.OrderBy(f => f.Width).First(); + } + + return result; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Common/Interfaces/IAlertPlugin.cs b/Plugins/Discord/source/Plugin.Common/Interfaces/IAlertPlugin.cs new file mode 100644 index 00000000..1fde0a56 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Interfaces/IAlertPlugin.cs @@ -0,0 +1,13 @@ +namespace ServerManagerTool.Plugin.Common +{ + public interface IAlertPlugin : IPlugin + { + /// + /// Handles the alert message passed for the profile. + /// + /// The type of alert message. + /// The name of the profile the alert message is associated with. + /// The message of the alert. + void HandleAlert(AlertType alertType, string profileName, string alertMessage); + } +} diff --git a/Plugins/Discord/source/Plugin.Common/Interfaces/IBeta.cs b/Plugins/Discord/source/Plugin.Common/Interfaces/IBeta.cs new file mode 100644 index 00000000..964c32f8 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Interfaces/IBeta.cs @@ -0,0 +1,11 @@ +namespace ServerManagerTool.Plugin.Common +{ + public interface IBeta + { + bool BetaEnabled + { + get; + set; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Common/Interfaces/IPlugin.cs b/Plugins/Discord/source/Plugin.Common/Interfaces/IPlugin.cs new file mode 100644 index 00000000..9585bc65 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Interfaces/IPlugin.cs @@ -0,0 +1,56 @@ +using System; +using System.Windows; + +namespace ServerManagerTool.Plugin.Common +{ + public interface IPlugin + { + /// + /// Gets a values indicating if the plugin can be used + /// + bool Enabled + { + get; + } + /// + /// Gets a value indicating the code of the plugin + /// + string PluginCode + { + get; + } + /// + /// Gets a value indicating the name of the plugin + /// + string PluginName + { + get; + } + /// + /// Gets a value indicating the version of the plugin + /// + Version PluginVersion + { + get; + } + + /// + /// Gets a value that indicates if the plugin has a configuration form. + /// + bool HasConfigForm + { + get; + } + + /// + /// Performs any initialization for the plugin. + /// + void Initialize(); + + /// + /// Opens the configuration form. + /// + /// The owner window. + void OpenConfigForm(Window owner); + } +} diff --git a/Plugins/Discord/source/Plugin.Common/Plugin.Common.csproj b/Plugins/Discord/source/Plugin.Common/Plugin.Common.csproj new file mode 100644 index 00000000..70274438 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Plugin.Common.csproj @@ -0,0 +1,24 @@ + + + %24/Development/ServerManagers/Main/Plugin.Common + {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C} + https://dev.azure.com/bretthewitson + . + + + net462 + false + ServerManagerTool.Plugin.Common + ServerManager.Plugin.Common + + + none + false + + + + + + + + \ No newline at end of file diff --git a/Plugins/Discord/source/Plugin.Common/PluginException.cs b/Plugins/Discord/source/Plugin.Common/PluginException.cs new file mode 100644 index 00000000..15c6fa78 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/PluginException.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.Serialization; +using System.Security; + +namespace ServerManagerTool.Plugin.Common +{ + public class PluginException : Exception + { + public PluginException() + : base() + { + } + + public PluginException(string message) + : base(message) + { + } + + public PluginException(string message, Exception innerException) + : base(message, innerException) + { + } + + [SecuritySafeCritical] + protected PluginException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/Plugins/Discord/source/Plugin.Common/PluginHelper.cs b/Plugins/Discord/source/Plugin.Common/PluginHelper.cs new file mode 100644 index 00000000..254b134d --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/PluginHelper.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows; + +namespace ServerManagerTool.Plugin.Common +{ + public sealed class PluginHelper : IDisposable + { + private const string PLUGINFILE_FOLDER = "Plugins"; + private const string PLUGINFILE_EXTENSION = "dll"; + + private static volatile PluginHelper _instance; + private static readonly object _syncLock = new object(); + + private readonly Object _syncLockProcessAlert = new Object(); + private bool _disposed; + + private PluginHelper() + { + BetaEnabled = false; + Plugins = new ObservableCollection(); + } + + public static PluginHelper Instance + { + get + { + if (_instance != null) + return _instance; + + lock(_syncLock) + { + if (_instance == null) + _instance = new PluginHelper(); + } + return _instance; + } + } + + public static string PluginFolder + { + get + { + var folder = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location ?? Environment.CurrentDirectory); + return Path.Combine(folder, PLUGINFILE_FOLDER); + } + } + + internal bool BetaEnabled + { + get; + set; + } + + public ObservableCollection Plugins + { + get; + private set; + } + + internal void AddPlugin(string folder, string pluginFile) + { + if (!CheckPluginFile(pluginFile)) + throw new PluginException("The selected file does not contain server manager plugins or is for a previous version of server manager."); + + var pluginFolder = Path.Combine(folder, PLUGINFILE_FOLDER); + if (!Directory.Exists(pluginFolder)) + Directory.CreateDirectory(pluginFolder); + + var newPluginFile = Path.Combine(pluginFolder, $"{Path.GetFileName(pluginFile)}"); + if (File.Exists(newPluginFile)) + throw new PluginException("A file with the same name already exists, delete the existing file and try again."); + + File.Copy(pluginFile, newPluginFile, true); + + LoadPlugin(newPluginFile); + } + + internal bool CheckPluginFile(string pluginFile) + { + if (string.IsNullOrWhiteSpace(pluginFile)) + return false; + if (!File.Exists(pluginFile)) + return false; + + Assembly assembly = Assembly.Load(File.ReadAllBytes(pluginFile)); + if (assembly == null) + return false; + + Type[] types; + + try + { + types = assembly.GetTypes(); + } + catch + { + return false; + } + + if (types.Length == 0) + return false; + + // check if the file contains a plugin + foreach (Type type in types) + { + if (type.GetInterface(typeof(IPlugin).Name) != null) + return true; + } + + return false; + } + + internal void DeleteAllPlugins() + { + for (int index = Plugins.Count - 1; index >= 0; index--) + { + var pluginFile = Plugins[index].PluginFile; + + Plugins.RemoveAt(index); + + if (File.Exists(pluginFile)) + File.Delete(pluginFile); + } + } + + internal void DeletePlugin(string pluginFile) + { + if (string.IsNullOrWhiteSpace(pluginFile)) + return; + + for (int index = Plugins.Count - 1; index >= 0; index--) + { + if (Plugins[index].PluginFile.Equals(pluginFile, StringComparison.OrdinalIgnoreCase)) + Plugins.RemoveAt(index); + } + + if (File.Exists(pluginFile)) + File.Delete(pluginFile); + } + + internal void LoadPlugin(string pluginFile) + { + if (string.IsNullOrWhiteSpace(pluginFile)) + return; + if (!File.Exists(pluginFile)) + return; + + Assembly assembly = Assembly.Load(File.ReadAllBytes(pluginFile)); + if (assembly == null) + return; + + Type[] types; + + try + { + types = assembly.GetTypes(); + } + catch + { + return; + } + + if (types.Length == 0) + return; + + // check if the file contains one or more plugins + foreach (Type type in types) + { + try + { + if (type.GetInterface(typeof(IAlertPlugin).Name) != null) + { + var plugin = assembly.CreateInstance(type.FullName) as IAlertPlugin; + if (plugin != null && plugin.Enabled) + { + if (type.GetInterface(typeof(IBeta).Name) != null) + ((IBeta)plugin).BetaEnabled = BetaEnabled; + plugin.Initialize(); + + Plugins.Add(new PluginItem { Plugin = plugin, PluginFile = pluginFile, PluginType = nameof(IAlertPlugin) }); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(LoadPlugin)} - {type.FullName}\r\n{ex.Message}"); + } + } + } + + internal void LoadPlugins(string folder, bool ClearExisting) + { + if (ClearExisting) + Plugins.Clear(); + + var pluginFolder = Path.Combine(folder, PLUGINFILE_FOLDER); + if (string.IsNullOrWhiteSpace(pluginFolder)) + return; + if (!Directory.Exists(pluginFolder)) + return; + + var pluginFiles = Directory.GetFiles(pluginFolder, $"*.{PLUGINFILE_EXTENSION}"); + foreach (var pluginFile in pluginFiles) + { + LoadPlugin(pluginFile); + } + } + + internal void OpenConfigForm(string pluginCode, Window owner) + { + if (Plugins == null) + return; + + var pluginItem = Plugins.FirstOrDefault(p => p.Plugin.PluginCode.Equals(pluginCode, StringComparison.OrdinalIgnoreCase)); + OpenConfigForm(pluginItem.Plugin, owner); + } + + internal void OpenConfigForm(IPlugin plugin, Window owner) + { + if (plugin == null || !plugin.Enabled || !plugin.HasConfigForm) + return; + + plugin.OpenConfigForm(owner); + } + + internal bool ProcessAlert(AlertType alertType, string profileName, string alertMessage) + { + if (Plugins == null || Plugins.Count == 0 || string.IsNullOrWhiteSpace(alertMessage)) + return false; + + var plugins = Plugins.Where(p => (p.PluginType is nameof(IAlertPlugin)) && (p.Plugin?.Enabled ?? false)); + if (plugins.Count() == 0) + return false; + + lock (_syncLockProcessAlert) + { + var message = alertMessage.Replace("\\r\\n", "\\n"); + message = message.Replace("\\n", "\n"); + + foreach (var pluginItem in plugins) + { + ((IAlertPlugin)pluginItem.Plugin).HandleAlert(alertType, profileName, message.ToString()); + } + } + + return true; + } + + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _instance = null; + } + + _disposed = true; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Common/PluginItem.cs b/Plugins/Discord/source/Plugin.Common/PluginItem.cs new file mode 100644 index 00000000..5c2bdcec --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/PluginItem.cs @@ -0,0 +1,27 @@ +namespace ServerManagerTool.Plugin.Common +{ + public sealed class PluginItem + { + internal PluginItem() + { + } + + public IPlugin Plugin + { + get; + set; + } + + public string PluginFile + { + get; + set; + } + + public string PluginType + { + get; + set; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Common/Properties/AssemblyInfo.cs b/Plugins/Discord/source/Plugin.Common/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..8fe9a50c --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ServerManager Common Plugin Library")] +[assembly: AssemblyDescription("The library is used to provide common plugin functionality to the server managers.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Bletch1971")] +[assembly: AssemblyProduct("Server Managers")] +[assembly: AssemblyCopyright("Copyright © 2015-2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("679fe859-9a82-4ffb-a758-c1e8df915f58")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.1.0")] +[assembly: AssemblyFileVersion("1.0.1.0")] + +[assembly: InternalsVisibleTo("ARK Server Manager")] +[assembly: InternalsVisibleTo("ConanServerManager")] +[assembly: InternalsVisibleTo("ServerManager")] diff --git a/Plugins/Discord/source/Plugin.Common/Utils/JsonUtils.cs b/Plugins/Discord/source/Plugin.Common/Utils/JsonUtils.cs new file mode 100644 index 00000000..f969488b --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Utils/JsonUtils.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Runtime.Serialization.Json; + +namespace ServerManagerTool.Plugin.Common +{ + public static class JsonUtils + { + public static T DeserializeFromFile(string file) + { + if (string.IsNullOrEmpty(file) || !File.Exists(file)) + return default(T); + + StreamReader streamReader = null; + + try + { + streamReader = File.OpenText(file); + + Data​Contract​Json​Serializer serializer = new DataContractJsonSerializer(typeof(T)); + return (T)serializer.ReadObject(streamReader.BaseStream); + } + catch + { + return default(T); + } + finally + { + if (streamReader != null) + streamReader.Close(); + } + } + + public static bool SerializeToFile(T value, string file) + { + if (value == null) + return false; + + StreamWriter streamWriter = null; + + try + { + var folder = Path.GetDirectoryName(file); + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder); + + streamWriter = File.CreateText(file); + + Data​Contract​Json​Serializer serializer = new DataContractJsonSerializer(typeof(T)); + serializer.WriteObject(streamWriter.BaseStream, value); + + return true; + } + catch + { + return false; + } + finally + { + if (streamWriter != null) + streamWriter.Close(); + } + } + } +} diff --git a/Plugins/Discord/source/Plugin.Common/Utils/ResourceUtils.cs b/Plugins/Discord/source/Plugin.Common/Utils/ResourceUtils.cs new file mode 100644 index 00000000..18b9ae62 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Common/Utils/ResourceUtils.cs @@ -0,0 +1,25 @@ +using System; +using System.Windows; + +namespace ServerManagerTool.Plugin.Common +{ + public static class ResourceUtils + { + public static string GetResourceString(ResourceDictionary resources, string inKey) + { + if (resources == null) + throw new ArgumentNullException(nameof(resources), "parameter cannot be null."); + if (string.IsNullOrWhiteSpace(inKey)) + throw new ArgumentNullException(nameof(inKey), "parameter cannot be null."); + + if (resources.Contains(inKey) && resources[inKey] is string) + { + var resourceString = resources[inKey].ToString(); + resourceString = resourceString.Replace("\\r", "\r"); + resourceString = resourceString.Replace("\\n", "\n"); + return resourceString; + } + return null; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Art/Add.ico b/Plugins/Discord/source/Plugin.Discord/Art/Add.ico new file mode 100644 index 0000000000000000000000000000000000000000..f5b8bc3e20dbad3c30f828e813f16c7f65bd4bcf GIT binary patch literal 102134 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 U?@jBE&!qL&x6}IbvHt%50etm6&;S4c literal 0 HcmV?d00001 diff --git a/Plugins/Discord/source/Plugin.Discord/Art/ChangeNotes.ico b/Plugins/Discord/source/Plugin.Discord/Art/ChangeNotes.ico new file mode 100644 index 0000000000000000000000000000000000000000..8124758222c4eae979b208614d514cf87ff9d383 GIT binary patch literal 102134 zcmeHQYiu0XbsnNcn*K@ir)Ut15>1H@y(Qak;8-$M)GtZ2xJ$dOk|uC!TVe`OvMkjSZHbh`<>@)I zdzN>vW_IV!+}W8~o&^rhy|Z)gJ?Hz*y>n;waOgNK&WD_je$+wT=6vbb9p{UV<9zI6 zrF!pgI?n5OtgWq7e;UvG^hX?L)24F0!*LdtJI=OkrF!>6j&t~7$JxHUT>o&3^ZHK5 z>Bc^>DUMJ&j>_4F{if~Hzq<#b%B@?sAdK+1YTF$z{9WL_)bJ>L$SqEuT>zt(LBfuO0%!1?e zpzw#vw`vN&Eo^rSXq~~O%faQzUC*e8&*2&-A9Dn1#YvR!*b?f)YkjzR^CpnY&CR(o zJ39*`H*VZ;<@)vOK=Q!{A5`T1_umH+>a$+|G-lW_>h=FO$G=kXDa;st{oj{c1^49I z1Fh%!8n0sI*FW>{?c;x2v7e8b_))Ka?o-!6$Eer8ihr0f{Q8fAzZ$!K{j;rJ`15zH zYhhKdf9|86_8X&K|F!Vf$MhP*um33c(>T1A`1Q}W1;L-sWv%*AuYYc1s(B2(f2xNS z-ZSW0T|dM2U8^1UFFH8{aI@r;1-3gakp=sz`p`bC4P_e&-B;3QJKX@vM-UJM?m(c{ zdKt%ex&btX-!+M^J?eFo_vGr{hW6*YH&^$Zv`^>#`OM5r)gGPgs5L&>Wzz;4i^pYM zYy7Vt|FVmXEgOQ2|Ao=dz~@Il4SzZM=deC^7ySM4zqV#Wknz7X`WNt5qmRHBM?VX+ z_xk4XuT|nhzrP4lSK}eb_-ZUZcKps~U{Sa*9bdIYEr+$ibbO9Kb!;w&u_4I#e(}fS zs@l|sAmiJPKYn&VI>`8{e_rvYe$VBgIGB#l{Rh!OY%m?aR{VL{RL_PWJ{`9$`n@&RdEdd>_PF)> z<8eg`0!boJ_0T}6yyQ5)W?Pi*D@A%vLBAD!4yE#(0)Nw%k02lj2m*qDAkb_G)Y}`T z<$FKe$-W@>BI=%>%6t#TT78DeW%W6M{ybBChQ+xD{9IGje$P1BpP=^Z_brX|fox=Z zb!}pQQa;$UKM5bA+JF3wqj$`aM$U(*_IKre8=g7)RAN4av46uz8>~690%bX@KDr#% zK7JSc=uubz6vlql#;v(E zaP;gm73|gTxE!YsQSE>6jeo0Yd)z)mwV&+^qT}Eps{LN;XDwfB`w-QBzxA_GK7_G9 zsrA!q&lkpill3!h9>UnK+7x8{RL|GTQRhQc`-85Zy4`9$iVsokSM7*W4(mfy`%Tx+ zT0VyHA*%gh)=!&0gt0%V_0#KqI*k3c)=wKegt1>8H|qMS_NAAD%!jD<+gLw!yVZI< zAEMf?jultAmJeC5pK$T>AuIO#`B2Y(`aVhd;SZhy60*UstUq_-dXjeY*p$b>xeR_~ zwpZ8ON7}V+x6}mzK|l}?1eP8GrGz1e{$>h4tm$vg=@VR^C+L3T7Q~}(miyK9T=}@J zXUoTR-BUj9I3+v%bZ|dPIK5s{26&Ykl>V-N8$!1oh4Zu=eAkG41OY)n5D)|e0YN|z z5CjAPL0}Pqv_E5uYTu%#-X-ea#W6Efl`tS8TJ*^m!Z|tX^4TFo@ItEMO3~|7ph{F8|L?zh1Rw^8DAez7}#_ zUkkZzdrgeX|9ghp;Oz8&q#lE~{O{iXJFsPV75r4gAWUv@ls5IQjW+z>xPJw#8(Hqk z+L2aRJF*N`k1m6*=UU-`;V#7BKT{hAHvHc_vJ$!{+dXC7;T7=5-`!f}f11`r8~)$F zzZ)p;=-!b|ln%HDWz#6;zdzRj4-RjFpH2T)#s4U=9HuSr4_WiRkhKl}iBDY!lTQtA zf&ZR9d&hU`x%%~h>wF#1HD3pG9rn5qm;Zk>ycK>v{nMJhGY;&tu65tJuKUjQFc`$; z|1YM07RdLcF^J25_RB=;{Bxao*ERF5Ym+#L%YS|U7ZeBfS=YMnT-SZ)dQc4F^51L! zmqrZY@;@#6zj|?C!++ELUz*~;hW}yqe`&$MhW}~a|M7ZA+G{S@@ZXEep!+|swqD2d z>jBsKI-qOSZ?&vr5SRZZ`#&##y^gWZy4HQ?y6U%DRxyanf7AV+m%m=eblZb zTP-sNary6MyRBpFv#xdDxvu)HmI;Hn{13bT3yP8MJJ;D~U8{bp<+%Khvj1x&266eH zmi=G7II!V=T>HO9#DNX}naA#>_6en zKI@t>Xm6FkoF>ErIu2qFo>&<6xbja)(-=JF5SON(g@pxYU#`!Y%ulowi^Z1t z`FU4UIWD0wM51GG`}S=YN8*k+BrZ)q`*Qs>|35UaXXnOT->xlX`2^~Q$RLg*5*mXo zh}{_s^7aAov-a^uXI4-0?@IChnCW8MPsYo%9pLq2=aftf?WBzH6 zhq2R&^M8uwJn@d&_dQ&3e1~H~8U*9e^S!f;JdS+2|064irdUDSia7tLh(p>_n+8Bq z{HK9$vZ0aO`i&KYYp-*%%)h#hNGvR_J6=5d9cvc;(-sSU^R&^&{dni&aq&;@pHn$W zqAY8|;+jBl$DFG=PX75Fgh_C(k14UR7z>T&KTUB!aUrp=7z>S<|IELt_4ygJK07z- zURRhy<}+w}{XFg`N!{o@fjVAN{D+MN;@=gz@0z_)HJ5d6P3lSVua2#9(};2!_&13K z=Kqb0KZlD~FD%ADIW9~%_d8eRBgt|W`A074x&MO7X_&GvH)Wm=<@dtQSH1!7T)P0* zZ(J{}0~TX|$4`=fb?mre!S?*O#lIRe%mrzl|GE4wSpRH0>>U3lT(~+7*JtC40UlrF zBdN0OI7kEkjIYWs^Q>#)nPgz12ii}595$R-1w9x87q2bG0QaNz>mF<1Zv?h^imN}o z7qt5PKg|4BdD8jT>&*XPeka_0>MmHBZ-otGt8g9oRwV}1{^BktwGQaxhK-Lj@qcCJ zGK{=-5cU=NU|)U@3>8$_jrHBIH{a{ZBgg&}+E3pF?RkuWiB{M+)(N}DAH^7;J>hlF zXOD#0wQJYl>eZ_am-pU#4{Qtmc3b)FNsR}SSg^`}VPY!m{HJ+7b@?1TbnF4>I^71H zr&d7cX_Dnt(t&kX##>=!p)9xtbmW)A=CLm5r5GsR7wB`Jo_n1?e;(d?>#bDCn{U1e zQRJO>-tpw0VoU9>*Hs~m< zSd{z<=$dGQd&gG8p7C$veZfVyPdx_II^&eI4#HEtbL#ov*FH}EgT_FG|I6p$;paaI z-Gz4O%HIuL7z2SN-{!ViGqDQ(;Ky6w-+%dhnJSHfE4{u5*Cp?~}_ z-}yf?Gvn?7Xdj?U68i$LF@x?C{I36ovEc6iF>yC~{!jV(kG#6;KlARTRqm-w{QocV z|0l;Dgbk;=Vg0EtSa+JG6U)`IcAR3QEcp&tm+yoJ#x^4NJKXF4EIu37=Q>^gr>3Uh z?YG|!Bx%_vkR4w8gnHNiAaQ_eK_dKfOsH}2fA3#`CtrI44i<*WvKRGU7%t>0av(nl zB%eR_Sy+$pu|D4g8z#D8>)2-GzQ=tYsPF&PG5pGUtRztn!k6uHlIEY{K;?wi0Y9NV z;*B}{?GoiX`5bIIwFWll*TL_N-3x=`yWBP3i+kH#+U6ike|!xO(^hShCH^TcnE!+M zA-M1426*tq{g4~)_2piTEz`2?xCz^z%9rhO8u;gHfuB}+=$Dn^o(I+DDjjSnh1MTZ;U1To7->Kk29PBXHp40K7YM3Gef6SmnB2 zeAwnROndvTaVo_GcOkS>#_`2e`~JlobE!xM8`o-7ZKhWRZWyTA%+M0~`~4UXt22r(yc5 z6$e@6|Cg730heYj-f{g`V}Q#^@~@5^hCADB^jOFm|2*g2{J)dmz!)gq`>8lH?_OFx zFR8L^PNVdfMgA!U-2A_vv}4W(<^5pJhjtI>#l3zkDgM=Qqi|=V?IQmZ?(aYJx7?xs z{@dX9AC-$LJdj)B*H8HQALV~>{vYLlQXa5h458H{|`U^yH@i5UihD}${w;2oO_MYXfDHyhkHnRR~J72Pj>%bn*R~=|MIKg zemDPbhwuEib^SNlKg6{+pnNvwoZ^qmCOS?0N6!B{QOf_L-0!(1cKz4apP(_27XHb8 zzx=;a4qzz{@H>S;Ao+rt|Ch)7zZ08bV0>qJ?^m4<5ahluj&pQQlB(B=g*5R`_|SEr zmhkhRVf_2=O8(!8QvTm1_isM#$|opZ&zK z26*)ul&d(;qd1`c)4;zMMt;Y5ZP4@ovckRYSDYLN={t-3lV7#+|8$JAP!GaY9Oq<> zf5yNs|1S$%*Rzp*Y=q7#|AY-)3sj8i;hM$oRsZ5BXOVy6k+I-%7P&RGnSHjYv&cXD zzZB-bSB$X#Hgs0`H_89=!mwG6MU4Tx52SbXCHnl|gbSYYVe|h?#>(2cVPYYR{8J3r z$p6b4zb5tujfFJuui8r7))IdH+vxm1)y6cI{bHk@t=Z*2p8P*Q9Gl^|S1hE7|19PI znf(4A*SVC>#+;hQC$*zCY2lyNe98X{_Ff{+H@tlLa%lcJUN80bY74QRIf zKb~W0)@hml#6L-`XFv2EKmTdA{6F?TtPUGvI9@9D|Lk{A{MP%_9G_Ry14wUmV;9=`(Bm6Za&|oc|YuH(Tdqm4Au>x)yLekhX=%QtBs* z{1ZRSJC~OdXEuG!8vizZm%iM2l=%Ih@DS`dc^o*&{7(-1IQTT}Q}`GDF$Rj03I6_X z8nbe~y_l8xZx72X#-$DZ=jZ2Jh6eYv7Vv+_(OOc@DXZM0;V~(v6a&uxBkqVp;?nf} zufl(J6V)2!iUR*#8BI(EAm6u(vio{*6zqggRG@bwb zyL(ztT2ZD@iYN;xAd)-=1ABJDzFZ&V`ggm0;2ID6hX#O-=LYayx`TTbuuPm0cf?^B z{vB+$4CNfkZIpQw>Qf|P3~){tzxTxB#e)ZiTrQqI{ABU?bI%qr28!SRr|%Y@J$ht* z@4#;2jJP8XO{c%p-`gYnC(Luux%R~42a8Yt^Y@F$dGXNjeu|Cx!M$;zu!6M zyM6mf({w{Of~t>m@9lHG^L@YHcOLiLmu0!E2dxi%$U>THeQ|+hJ!e_g;>CLZ`wv;x zX&lSV)%*X9eE;rY%UZY2>@T;hiN`E!<3_!|qSmr@)mv6;tJ(j6%R2oxmbC&pQ4}@k z<4CNH&>Q@hnfXE2iWT2~wzTxbXlZHpg~uNIT)t&xx`OlZ*pelm$S*DJy1=|ESN`zX z%*-s;%9SrZTT$`$`xO5Qz|P@_vPeJ2kppoJ@(kA@_5XdhubV$U48XJb@jE0nwskrUro)m3GP=_ zo$brb{X%|D&ZqOMs?PN3JSrdZTvvHmcInb@07<}`z&iy#O)`Z_--EZ^*QprUsbjD*#!%-T~$@PpWU%zY;?ztyBF53eG&XD z2x-5xqoCk>`OL$-)z$l+ec*vdT$ye}5+wM~jq05e{JWH6KZCBzLieQlGv@mg`mjiU z#l`!oOG?^%N=mx^P*QT}H$_FSXGO{ze0;-(f6ZbUUADcaxOjiHGfzp$!A)glXWlC> zzwFb}Gf`UF;VCZOv&fk*+;&mXzb#_k^70;2_VNUEp}tMbUs`&oy1e}CdleN|(0@6F z`8qu%C3_?KQB?HXEauUDq_gZ5^p|JfD=mGqx}xGtPi5s*9|ZlHN&~jb%a3|WOZSD; zQC$4nEdCa)EOn#5`~&(xUsaXrAAOtzBja5m>Zx>Zm9QB|~nEXyX)P4Oo zrVp0BI3#5sTv^!-jQ3}imhR4~tnBjWep&V1A^Algbyrng&Z?>D^_aexeYEt`0o%v} zACJN2ZV$I($WI#-uGQ677H!<<^=#VYML(|uV?x+%{w}{s>O3afu;IFmC*!E5=4uwk z0go9cI99|f#3CdfSO>`N-g=z9PrT4U&cjFVXy!#rjj3m(&CUs8^D@cBkmyPdUES04gS7d;IP z0~4`Gef>pX-Vu}jnwkTP>grB=8XIq69KB7!n5xp?eaG=z!2dMra7V>w8n0rRt-En5aLesi3>!!&r?xgRFaAh z81KkxYI^54Tel8lZZU#+#0b*}+r!X%C#$*nRu=MmV#x2)x(EM&`2pr1oIi~E!fj)I z0)0bvY_zo8Uc_UO^2_n9Tb1sfZQJfbcO)70V2%^O7uyQTW?!5=`8KHg;=3X`?qBA(% zw(ZU$=5>~(F6wmV#j$Eg59AM!-(bEK(QaosZa@9>a2Cs$veaSEPuhLovs!(amVE~q zD^y%ATcP71&o4jaV%f5?v*V(Vx>?7QPyW;T4}MVFK)=_o|6l9N%8m}nIcsI*WgoUF zu;cA<%(Gy_S7Nk*b${B$GAwI!V(zB(ai4V+7XBJGEOo70_m2f-WnHT7%E~J^PrZWq ziWcmn9OsAcz=r)X*dTT9Z(|vjr4G@@RO_P+UBF>iL3w%CVIIS8Jm*Hmy%_+mDN(bzF7hHz3 zLD$XSm`Cfo8bu%HyR_F$`|9(kcYNc<>sUv)j`?Y1G4=tgi-@a(ZA+K_MMiG!XEV6XWB6V95lx5rjroxO z>hsH%{nw0*8?UzEIBd}C9cEnvb5^y!5s>=JUqXDn`7))Z=8|oHxH8UTdj8G2uC}dM z@q-LvxP1BdT$rP_A>X)T9?dy(1d(-@9$?mEuhT@%Z|h-R)P1dO<;uSWe-D^GL)(Bk zENt+On>jewXQGG~zd0pZUcJ^s-K=Bf%D+P!lDeha%Cz$G{@PVr+uMeE$1!h}xwmtU z9_O|?r+{C*ZL3y=tJ`lwe*Ry(>gsyiaBeUjVcp5O1{KRT{eVA{)g5RKRFT}h!c3~NorH;J3uSFQM!r7qf zuC3K|n{`T@-vq8-PPT7sg$?XaVzq(mrq7}MdDqY)MFKglUFpfnnBlf`t_Gz@?YZ(m)Fvn z)x)~m*02AEjMb}ukil(~AJ=Q*(aNf|a{300Z;m%KbU(j!YwrN+Q)}%wC&YPfRAD~U z!@7S0{2s)0iUro{)&JeaWBd)}aZVdmhxiA)dIzu$|3{X=Iiqh9pUmGFEXdPdp`3;$^_L+w%OThT+Pir1;~3C@<+(PGtZ1YUs(7PjGqreHL_3PkL_aZ+7~mZL+esH4{zOi zy+Hh?I`__r%+{JexvZS`Ei3z;Wi9mT3qY<`Oa8Fsa^t8MzamFAa_0OAOV<;YwUfea z9^-G!!@R9dgQ+VMM;Cf^9jJ?SvTm+Ztb=?CVy{yy$4=D04C|3cDy+i7f5|Q>dE*60 zZ%N4>tQEX51bGE(4!JowUjSfXE^tTH8_SOw)<-}3?_I^kyK_<3FLj-&ZrJd~3q?h* zW&hcqH9}FO^$M4=y{PD47sKXrxWUVHHehKNjNKe9#{e$qK@bSkvUdOZc63Gv) z4}1peBCi*+4%VgXWZmyzZKNA@?>Sdo{QnkzNjaecySmZ(wc3 zvA&|#ZLt3?)+-Ky3;MvZHW5DPbA0NgZq^aL?zf5Gd0eXs@*xAZS18-Dmhv`i$GS?7 ztj}Ou->)EPdHF^7@a8Z$O@I$#`x~(R&0*+{QaADBniGB4ZTs-ZBb8$Nd93Y(*eiC+ z`VRKt8`rzu94;-Ljt{i`vslm4wo|9n6+Pve)dAzf7OSY}|7K(VoL-l@jJ1QyT$`Fi zln>+qv9zaf>C$gZF_yILEK41*o%PDPSd_YH8|zj+pgru)E-yd$0@t-*v#f1lElAeQ zoGI6p{QJybS=objeHc7U#RtKCPa(@v$K-YU>osd^sQUuq=@4vxt5>giF;C$AHO_t9 z0~7Xl?Zc7bva-D~@StsH-m&l6j!))lc{rhEc#mx0BR zA=;$twdM&-xTftMuBg}t z9=?fr-2&ck$BHp!__s}F_x#ig0;?E*fcnq{aD|eOsDys?b?k#WnW=l z-hXy|>|@WlxXoiVHMHHYCw86wGNwFir_I-Lmo58_3qBNLe+X-+T&ty+d(R@JTvHDB zn_6pz?yK;j9XuS^P*Jge1GjlhuLVac8%|%yF=ZcZrOm5WwW+y#e*O=z&U>{GHVqrQ zV(=kEcaTqP+nx~$bmKR@jve0b$#kjT#U|KzwXmSz7p{y9xq;~c3v1R~t5!bf^<1v$ zs(U|p-^Ub1>br(IDO|7O8vIoC*!gJ-^}zOPg{xQp0=Uz544d}f0_6kNp$n0BD7Y@J zeFq<#?*0^Y!lvu6{T0~$fgsz(3C#oZLH98ALuB1P-uq{!Q%>q0rcGdt zz+G&Y^Z{$tsvlw9*josl^Z~k+pk8&~)wEJx{|M4!^!LGb?=)=>^a0mP3Q^yX|GJC1 zj>2m|(-2+@2`mqPhFI6ywZEDM?qa*758%GsAGtPfRz3{tYcMyl*2?(g)9I+IyJ$(_jA4Y%LJ>1aH$LntD z`WJ18Uv5cRaM_1(t{d}%!-XqW{1pNxQEOzphGW}4)X>mRyT{G-GhV+-h)^eW!w1Y0 zjuft3Ngt-WzNBrhtv!MnIF*P;)T#S(4*66OnTjJ3yUJN29E zuDlob4z3YWcnw&#rhK z3Cu6V2OUcsSC=pU60VcC7dAAU7{WpO&>gk zPdxE$HBLYN_-8Puq3tJe{p~idpGOd0-;VTKz;RpySNp(XXalw>!w@;{&=a#xyNveX zB<8^$^h@7>?VjAm#*>4%|C;*EwRUyQp4aWCDlAVuL(o@PRP<}yhx)RM+dRhKj7(Lp zonPZHh&jN?!OfdHbDNq@{F1g$rkB^nktY@z#x?)K;^KdD;h9y;`yRwIu`j`gJB28Z z-;@r8(-d?D*9(jWF-Dw(?bn9%HG6e`0q+HfN96tlWBadRyZ>C9^T~DV+ITNRA@mPJ zB!4`0*=4ltgVZJWCdl>ws40Eqy$MEGmO2n0L%==IcA3)peHfw-Lo8!N^h_zG&D*vO z^8SNfW5<-rJIg>9Fdf3R&+xVf`@p;?KkU>I-?p;-1(cl>3*IvkR0qrO7;GPg?YjbE zMr;mF$;0~fFY|s7rw^hlxQ`Z${`M7w_SV* z@`3lL#8dY<)SaD^^9c*jd@P2J^U>_*{U7+9B0dh|UJ_hCj170OT~hk+iQ_Wdn#-;6Wmy)VZ0S7VA9u{k)^ zKKxkuK%EfQBlp%g>y+dC?L2T_oS*+qWxLjyf%h1E!uA38rtsbv`~Dbg^S&E(4CQez z&M0u7p6%j`_&}Z1E%lf><(?m@8!t_0v))wP2y9D#soA+*wQAJ2Lyf@%M`2H$nqNK#* zD=j?s1I1|`6==R27ZX95K5;AE6ZJ|xHP1|@9HhN&=9@_Y7Ny(eM zAu;AZ=Et*myGu&kxDV!CUs;*`P6hRD1@4!XT|ga&QSaVeNM{@a*qCYRhK&~@*!V7N zbkj!oawH}U@a&`V9nT%&c{h0vMWnO`c(*GIP;SOBz`aq*ch-%2>g0VF5mWkt`~P;A zmPW^bzT-Ja(Rf&zkQlFONMAa38DU zon@&b-ufpihv(_3e+2JmLLJEw+Fwx-I|k+D?o8svGTQg#*hPP}@7G>SmhXPOehexq zJQ4c?++%C|uDMH|@5VOzUW@l^;r`cN+y|UIVSRWP8OE=J;5HP4ii-A3{o7HrN4|3* zdA1nctVf;0;=b3P;@FH}P+1v@f%wkfX3BT^EGLB6&V>KF%ssgBKDMN!s0(9H7y9&r zyKw(QUY+GwFL|X0t5$ zV=TwSyZA0C${GxK4`LtuNGmbE--ye1CkBWC#6enNz-J>EyVLSrFrXgzzso!)5a&CM z0^VJkOdX6>@|6(Z{TTFR!hc}Uk9#QlVSh>rpW{fl?}7nz1B3ou=Gg+i4>pyAyf8)+ z%Deb3De5p7+{7~$H{pLui1EEYslGcg0QZQ4l)!+`x-ceZ#CO4f^}+vL<~fm|cl8Iq zTP$h&jKLZA-H*Y|O!yBBcrWk(Fql!{GdhgH8RcDkmlW@+0|x!O%(FC8dgmL;#G4e7D~ZFYkq)v6OYwhMB~G zzT^Ff%6D>&=YM8ZXag~zO|`Wrk{Snfbxz-B*Ka58{4RlDP*;~648-^P`cr(i44e<9 znB2x(T*pD8F{rOUjyb;v7pzZNu;P?FupEXPizn%PO49)<9PQ>oi z;z0Y(a%cEVA$+&Lagt`+cMu1HL2UCxVx=+DzT;UrybCvdh5W^ef%g3<&SlPm_Zjh7 zp1n*e#`J|g;XTHP>F{wt-|>!Ntg}m zW=c~1!#F=v7^wRn8XHg7H8!4AzDIwL_GItLj_LOh-(}53Fpzg^C({^UMqeL%`tFYt7A^}(f#-qscf^vB(r5aksqcXpj3yleN%oDx_tD*WUp?M^pKyuWcfo+T5d-}`{e;`cHoqIPH3|CheD>YAco*L# zB?i=$3>y*i>iIb6ewFX?hXlX-Kc0Q%J9JJt=EdjWl)50h5^SS-{(*Ht_k)n#3DBQ# z-vx^VVgMV**yio$Uo-||55oWHV8Hl6oQ@@ockx|PV!%44!4qwS?_&YJi+=p3Fu->o z@m#$9jY|1eWwccJj`B(K-C?7`AWj>}Z&=?Q`X7Yu-BZv{tjI~yeRtTH$vUTKcZZD%gBWcjpHX=iy^bam=X;;uhspN&?pX`ncq;T=R;19OAnZh(A{;i1*=B0LFi!lUpiJPYr9PedzCfd%$^B0fNKru&y+ zEnd7G-~9OvzJv0*h3~RZ@Z9@*iMO z#&nHJ!}9$8PkD_k8e}muL+Jk5BmEc_g2O)00Gr-6Zr28RzD2NLs^zmnqhad`v zj^IFXkwhE=ZH5g?AeYFemP2^v`~>6zo{Q#t zOF{+THN)@6VE+O7z;gJW)SgZVza5uE+XeSjNJDVMx40Ar!~&QcP#6)ba2WEKekTs! z2XshqB$ni2Npf%uv6@%Jfe;+YJ$(lbdx7I|;1WBI#GKfpZxtNLi6aIQfg^4HOM9H6eu&}zFl_d> zcYMPyfO9ARvK<%)@4)thVtYUgtX`c6+h>#O;JQO_bZ~zVI0m+N!7Fm=?_bFQ-k~En z2E@ShaMU*AdxPNr4a>FN7zs=y?aK9I^#eRzK=bF5&hjQ*851hZze;orH5B91U&~?s* z7zn`;+|x(msJVAyF`aEaH{ko)6ppGNh!4j=oALd{C6FFS2>1HEO?-D#d?Nlz4Cp%c zb|GaOLENQt420mA4ZrAH2>0>gC*}M&a(?K6e%7Hl<9Iy<97AkQ8uy{L>wdu4p5`$S zf@9LS55+Ow7@*BxSFysF0S^J(%lL%v@y6ZW#n#YN#)fDy(3U3W1tB;Fa9>ezQO%Q+ zi(|AH2#2HCtj78P?#s(Pc(+pzI8O%e!Q%$wfODB6(uXP(=XSw$hv4Yo z{s?fqG=n&Djva0+2*%NA^IpUKK8*MKFwf(ADgC_TTh@5DPx3_i1%C|eLmccgF|e2O z&=ih=5FEihZKn^yy^i-}Vo2S79Ql6Z4q`~*91in=KpYn@-l1^iapXatABlUut1n;{t_fiyPp}G zCBFQ&r$Y?zn}wV=aqLt$&N}y@I0nQ3Iet{PCAKHZJ{OMd4(49>Q!~rG*e?B8=vd(I z2iB~4d6wsdv2mXoI0nQ3xZY3B(dX}{e7wAT-*`nu2i{$H*^KeC#d~nQ9M3qHaid=f z91Ad>S+f@dntR$1-0o>BZJu4;!{NyJHNStbZyfJ${IlPq`d~+%$$xbg!b)~=0rjVQxf zw(RSwFJU~*CGP$FEAG3|AEYJ!Iu2w^<8>-M7MM0__XAUp0nL3y#a{3q$UW<*uGW3N z?(fs9-Amgc=V(9V{>Aj>pZM!}3wa?wm6aTu{bK=nT)XziF|HMp`){DV?}hler%vHs z>hoinmNCFKPwtD6j&Iy}9p4h+cY4xOG#Bg(xLzC>1DeNuF~opyPu|0Ef5pUFdSc|@ zmfVj+iXbjXce?XW-{6N59RnV;*8`L;v|Y~I6sM8<0VntSEtb(`sl$W5lH6`vXz9 zPmbNm!Xu1c_VeuT$vL^FFb;l>ymKN3;EQRiyjRKD4}^;W&3$EMDEBN|Q={8Gb%wz* z-Hx$+llz}RRNKe+k8k8L26`WYbV8UX?fU`E7vn(3Kvk8D1v&=E_qugIvGVf1iT1va z{k`4pnOC?MJ-{U0xDK{gcqPXVk#kCJ?$?moF_4Ylx;Aa<#k=Q~;2UwhX)BsD#sJrg zJ>b+I1BP#M9)bI-z)!dNbjHBguJ|OkkZjHUHOz;1;Re=Bq`yY{zh=gQ-fWbaV==&Q z&N2o%5CaD(U5JHjh-C!ZJjUNT(C3qT-QMR4_vDn^Bc>JC6&0tg1q(i;ZeU)pARF(Y z_^llS$ZKN2i?~q|%eN>~Hji)_8UqK(dlc?t`UYpp`fa#l+h=9KoBL?7)Y16#UZTAD+;Gcr?yAF(xy4^D`{7!d~wrjZWRoqrooUyX9 zo^Wulx&XD${va?0yyyq!Rt)Gk2#5ijd(Hb?U_bAWS+acPt6qd8|&{r1$`y>Kt7b1aY;xPg9PJ_b@Kb{yQ_ zNFevI#Xw!%4SbXB2EJ1`FSCF_lDQ9zfubVL2?P59!~c8?%zXS4bMnve*lzc@r!&I& zy>JcGxKy7U==K_!$*|4FjuC5QZC_z8aH!sP@z~HU9*Ie{sE&FVexR;ob75 z$5I$qSD#DrJYTqtH4PU7{fL2n*gYQuakhQKy}~esxtAD;9s`><_v8E2{rG14yu^S( znsFZ(1NfE+#(*0f2Ozp1F#ONQz~u2yjL1LxPL87r%bJ>0>+^;4;8gbm&KQ6{*^n+B z12^H{P4olv5(Wdsy}>FCxtCaQ#(?SrEPo6r{^w&LH2#SR`DZ)f+)4GHH8tlk*DC<9 za<8Ufoa(8>z+M>#5Cb>S58Q-*^DzJnoPD6-USTr_xR=;xHzy#z`&{NI_n|Sc zd2=7f0lObC{NK+pK;Owf`$W!>fK4Cn)je-5T9l4`zHlC#`uhQi0r;H_>AH_&Kyhy{ zn8VylEJTZe0brp7W5K}vAjV%6{|@foo@4D^xR=x!18N+wbU!eFe&D`|0f}|?eVh-g zF>&L@xz7D$F(6~Xz(ix?fNw5}j&U{Ty-&G!#=s-|?f_!{hMM#6oEPn(n;6YS?&*`^ zzni}5dC(2qPx;Oq@AHN8z|`f+#CPAn1^zuH4>-AZ8}8fD=eOgY#^Y%B+0HZf6H;q_4*y-{QbbY(?>nd5d&gDOia5_F+j}HI|lsRqi>7K{gy4aefOitxDYM| zG-l}<1A*KlwsC&n-WA&J?@!*Hv98BEjtBmJz+lDmVzirdi-8QR>pidRrv0JZ-?Ybg zCl2@Xw#<1N8*kcUL3+kOY}{+E2hr!xiys3$H;5Jk#l@*SC)C{6)v0rPyWQ8?8iHR*+1{`DjJnvKI-HDm* zH;5^*CC0iR@Ni89*t@Nnje$_^VN+P{TUze;=4JXA$XF3B26X$IsTc_49=zi|wt4Pn zBF4kTfWe;gZPO3@B8le(!aeJ1$NhRv?oXo6zh(OT>Bsv_;F*qf=y8N|f?F61PO7mW zFb3K&79=AEoZNd1_Z{f-J22i0_w&5ZR7^1r{4vmhI3O2l4yM{>Vq+kf`wsN^e(sx_ zZ#l;JA#Be}@-fhDjo9mcKw_YS^Y8@5KWS{;8*ieQ7Qeu%FUx zME29#FX@v)V}R{AS`3i;Z>tz#KJbobG3Pw@UEn_32)E~K3>YpQG4M;roG=65K>rJl z^GZUwA2fY_+P8VOZQ)qBw{VQYIG95*z;T)DFoX735Eug;c=rAk%jL?jKJt+o)9yP> zyFZFP|0u@$QyA~by&mI%Pg?hlus?u%;A1&uZpOf{iGfp?4;-bCTh8fJThGt`Pk7#F zFSg13(T5X10!85Yb334khbiM0nK?AxbJph`&eV+arF7O5$n3mr#bKBOSn_q z!!OM9hODM0-R=Y5x8jch=FxdavY}%R#Q^(pjt5*H;Io(H{1~`DUd*=N($dd%lGbtV zh*_I^9OK+R3it9NmCzVq*|`)0#D^S{YjRHRo0^Wj*xdXM@iN?xB28EhF64*4u-}fc-3#}UNND7PP?y#@Istqr zX^j(nK|gRC{2%MyvgPa;_U8isiW~56xF1pDcvI6AHO~(m?}h6~sWS#Rc8r)980|9r zr*#aFe{xRlTUxG=`?U7Q!3nq@Wm^Sb!`7B9@o+COp<|$?=1rwb>m1ESU2`P{u)cQ% zbH1DD&cAB+hL;3!A2|k4&zy(>wt3C{@b69gNyjlQiQLcm7(lyM+@B)<+qd8KrI~>H zyX2q!ae~{ua4%`-Sim}U-FJyy+TjP=i5%UF(l`;kW9!$NIJytEOI}mVt_c3b8>G%J`PFsJ|G3SpA#`a9LY5~C-)Q=WcT*% zWB5%8=7M9XB*re)&(3h4FPsObq4NP?XvTt+#}QmB&M|g!{8TaUC^)~gW5?Kp5g*c2 z5M>W+GvobTWpi`$wIt5;? zlR~yjEQ0$nwk>6AYH%+x;g10aj@iI4m100}>;aBT1V?^`s*!5Sxi( zigF)02557N#DL()c5QR-Y(x5-CZ%FPY)%92Q#u9&$B5i(>qGl6#tH=G?m>qV%mwbv zgow>)#C@zWFw;1)J~f_)ZTDieKL#Gb{lIRS3s^b^?)5zL%)JRCKBUPMRS)Vl+>a?+ zT3W7SuD2FuCSzSM5QXXQu`Pw*sK$bs$MK$N_p##Ww|&QsyW}5oZwc(4i5QR=1NZlc z7wW!ir5E=SJz;Uo298}b5CeiE`)>B(Q^YaUcG#Tk7!aF@PnvTdIR@w(B$+WFII_LQ z!F^QQlNAGE^Bm$nDvrsD0l{&a+()%N(J>%4&q3~^;+W_d5F8W4eW309j)Tkx*snl( zo_+Rr6GnVU;}KQf^oMqzzjbRm&GiJQ$x=P02H~h;Ab~graGy9F{kA{z%$NnfC`(}X zrD@ns?#CgW&_x`ea}3Ys%th`cHp1eVO$-bc#AK@NNZQ~fa*{k8Lv4qxk3x1kY>&l$ zv6tNc4DwL&Y!=*8DGiI`L%`wZ)4-7&JeqtQ!`kjb-5-Zs464Kg5}R9E zQsG+8OvOn=9Er&>2r&xAEqI@Kh&i!$;F#?G(`7yQ;D@cUvR|ul0CihNGLh2$mmsGg zzlVs;W7sC=FG7kSj2Tsq{8v@&RyxyTZ6{GN;=;QZGOaahzOTkT!NXuvjFearGh!z= z5>sMJjEOZdw{fhk^~AMC=JT~K^qD`e3mX#_|Es@NYp2imC(AOsKFg}{`QEp9lz$VJ zmE-f>Q%AXvoY_8~R~_X(azeFR9pyf9W}-TEl>5kusw{Ps`^afgsTk0T&7Dvx25_qe zN{s=`Y2`qvF+kQv5!ZtlY@RXwU|<-YRR%Pm(c|HCX@y;)#X2&iB9EB^Sq z-qQbfD)0SAYLxf>{Tx%h9DxDKP}zq8UfE^}1KiGi7|`n6hXDb*)=uTU|5%Ok9{a8O zSdQ{u`~w1}dY4=OzR;`FOiA5(f1%`S)%)2-kKO~aQja-M6J*EEAUlKnfM0G=#T0&f zo%_Hg04LxUU;uCpFaWp*7_ie92o!dmKq_9{{t6ZE4g*wNI1Es6<1iqH`Wyz-aNl9T zPVPGpP`iEuRP_!7RP_!7RP_!7RP_!7afDC`&RrM~ zP`&hz{ya4S)x!W&z3%5=fLG_ubn8_2kLVG-dSCaCj{c%mAI~v*Pc;>l&_w##> zdFEM7E75vu0|scsW!juxns%n9X}j)frLW&k)7~JjtjtP3M!jRdrfCNsY^V3uw6La(wxo zm$~zW7mN_Wj))PAMUw%T735#on?YsXplUc=C_^YN5_X< zx8Z>vZN_rVkN}*`(_&wr=d1&d??K0I@LopzmMSCN@9xtFzWLrweK&1>KreY?sNK7=2Yme_261?{icvORVGOq;>Z`iVsC zQNVme`7PqNY4cZ}?*JxVBK#lq%V=8~)S|6k)0+Eh+S&!0*4Pt2Qle=O zV6pe1jdHYmFdDyWh?SPSUys(brao31X)At4h{8RY{!DPq!iQ!08!}{wR#Owxpb=k| zD3MTJUf!#^x@O0knwp`M4Xv(@?KpPq*j`q-PMtXQz)`-9Mpeds*@ zlJI*8)O(9Sf6@OyT4D*%Yn2oK5*!KP1K4jv0do&>o+_^P7a@fos_) zWjbX?j61aFB{TQ!bK=C|i6s{d`+rN$8@6&L&zH>Dr_ZG1H+xE*1a(zAV4Vn@-xAl7 zhkoMQ@O&lD??U$wp0WK3(MQQ~6GxZKJ$bJ_XHVaE>eE*Ze4}}3&z5yddv5&6+#P>? z?ooSB`Ofis_da&wDB-^ZzBHj182F(Cwi~}p`e0R6%^=eE5uOIe4)`T4SzWHpoHSB< zzF|Pg2^05A{_os@Z+!Mpk1sZ_&^Alhu&l?Xr>@xXS2HGz(4VEwiH8r@610)F0s~m| zkHp>5&k2H0YYQ?vAd4BzUJwM<4u+~c0Y=>6KTmJ2x ze>SmZpM&S0QdXi@j4`hd1so+ag2u8K5qQ}y-%LBkH|-KC-5H;_~?A7mwd)m z9;7{b{w~_#mHUsl=ESnAZ$5qZr7JHU{D*b7_x$n`!VkYa_-)CXe^S}Jx~c>AeT2Gt z)xoxW;v%1)5s**yj)xvvz70IhCH*e_cURHx^a+P*CrtjPcEQnmX*_>z%D9SsZl1lz zLm%DLyG6o{vvyxPrLKH$DVuZTp4#-f1GH)54>kJ{eq$)G#5Y}L`Q2)4tH!iX@El8^ zUX6Ci@x!$Vl?Q0|oH?||^a*2jzGZgVy&vDOP0PpEZ_~2ql+wGWkN@UQcbze`M{U)C z+INo|uHnB$w<4dJzfKE$+Z?4ZRb?S9Wk^LlTTaP!QvyCx^g2R?M#5R(q6J9cdK_SnkV0t0xHM9$!J z`nMh6kXA>IT0Unie&%EWOd^#-0k>A;@Qzr*Fz_qy)+3kLk( zbLS8E&t*sLIseFUl{=g^@xY$YI*58g_wS%*iHslEP0BwDZ0S?L*U-JfA$*7*Tu$5L zswyjo9#uQ)kPD9-dG>`x0HU&fDbeshfY3lpZmR(}E? z-J5*y%vcpWg0`Y_v4wZyvG_Ci%@q>lneun+vd4LzLfF9?Z)!|^E4V5ad10?64)_rt zS0eobxlW+1^pE=qEA4PUv|I_DreDUV;fJm9UFHbzt-x*()1OUQ0h4;x{AARqG22K# z$G;=n_%L}EzpKint^}Fexc}O;dZR@vHJY{lMw8aZXw-U|p+PG#sB84o4mNseWyaUE zi_B+9%X1&C$>^^&8$Go~W1coI)lOgtEP*MoWgb>Wh!bR8DSr*j$g^4RnT?98lc`7k#3I(c8V*VA8t4rb@M{n1By0rwij#$)ZF&6Cp&Rw;L+km8|K;@6ECctqz1$=vicNS-+0X;t~xRi|x4dl(1Jm^T<*HiRKte20h zfAyMyUl@;oJ0QF~e_%_Za^z*sw$5kpF1BTTlFHA7pFXtL6({WRzOfA4A;QJS@9_?E zu-z4WXY7SPjQkI;K4|%hiw3WI?At>>U4GGSpRT{NSDV3Jq=e=>dbK_Jt=&E~%Pt)J zNnQ0J%bYl!&+z_`@K0YzZ5oBOKk2uYpU^LP^upafHSPsJVc7+{t*7ieQs&jSo%odx zpzb~@U&b8lay@iyLwC3qgYaOkJTUEn7yzhJ1~*A+a!7})*5A#;R(JNuv1r~P9h z7w9|Muf57Q@m~P`JHSs^eoi?2*x3xhfxfrU4|#P`1|2UdJbtl>|2**D3Vy=!Iic{& z`0|>?V{Oa>w)p4YNICE)c$BBIO#E}ezZm?4_lqA#(Q zeWJAe{}gf_O#Eko|0eJgmY)%fU*<6I{t>X5%jsjBu-wn+=q8~-^x$WUCjK+QzYzR{ zApA0(0PAH7X7U<0K6iTb=+Ug7l20odyySrIPX6fZA^%nQSDZO`9X=}Ih6Vl1VJzIh zdy;f7cO9=Z`^noY8j#ysw>=0=r%c%Itp{iC`pL4>hJG?@+=$;n6T9Z^f3%qljKA4@ zJQZeKZEvkgwuh5{bH+2@@vpMv(n^bKa~UL%mhf#%h3;PdQbdD6iiK&+QFUg*=nHCqC-O_RJZ^ZR`9N8R3#zvlji(NJQr@nE#}X^OR! zwv;y4xAkw-rF>XvYp=4H;4pCsPX30&ki09JMM&*aFf;4w`xLO# z{pTYA`VjBer9UUnC!SCEz8&UEp7dYRY42lwKy_F)G#!AySx9)>?RWgr_$%H)C2tVl z&|!q8Ba%7IB)sX#X?v+AkMaVcJ0Rf#6!xj!$1LbKWp*-E~C1=+&IB z+weFqRe0ZBT|It?_*VxHPe5DN{Q+99Xoq2)1H#YAp6SKxe_mRMAh2Z(Av}OPr3d)= zj$6J#Y3d)O{0#ADtO-U)eWBUGlj;u<$EN?0HLKXtMjM~_rBJ?A?1s9j_ZrZAhT@fc z{CcV{L`=7Wt6${6cV0U_y^;Kkb_9*=$){kgcKR7vk5PU)VNQX<>Ef#u{nq|%`x?z2 zr&Y06EbFP1zma!aUX?mB4|>e~ACW1x{8|uh>W*rceexI7&8ROwc-3PMYZzZwy>WbP z^~UPTv2*?Eg~(6s9|>(ib3Wz=wm=e%{xaTJ8IzrNuiuQFI8T9th?MV9jiR_ zh9|FtcKZx=*m&jTLpD^8J@gtsxbA%DV-1wDkNF~rHGY$lITmyBP?OpK-~mb)VeT+c1{GLk{8JclI&H)mHu6ElaO-2>WsN-wVB9xbqG>YgX?w^ahnRC)H*9wr`crd9 zul}2>|71?r2Y)sbm?uk!#cJ;sI*t3#i6dM(W4}+qu|(BNCdbXh279~d^h?w3KGi?r zrOsWC@%HQO<;h>W%fqIHz6PQH9_V+3xid!oSNIGI$6XJa-)g6C%kfL|@WV}h+FYA;Z&g5D&mvR) zLcjeES;e{1FMDK4ro^%)3H)Q;FW&~KdE6^h&N#L>0h;um0R6Whe@DoLerpe>eeXZ-LuZlUY4{QB|b>n5^uofQfD&!=vMhD z`t5h{>Ro)c^wSsj1OMkX4XqjMUZGFL@KCRKhllsWo>w&KKMwiZ@9SJEa?x(q#hZWeo4uR zG-sFe)3cc`NnP1r0!G^VKVY>2>xx|MJq&Ze7vJi9A7hSnHebJ{oOPUjA@m)KolUet z?Xl!tBxT|UpxZoqu4G`v`fBPP6o!U;>sa<$dz4L!Jyt7!k!@_g|2wjfG7sRl1er1C zGv}@5+&f*S-n^iz_vx?fRqCUF?db;sa2Bjc_VCaN`(Uz{WS&71Tav)qTEEyX%j%wj zf)U#dI1&k%hlR|;L-Hhth?@FXu{;x6G_6$PW)3Y$+*sPuQ{smDHu<8;;CNF5aT~3g z>NTyE<44UgVxf+>rA*U$mGT|?EKTdpNj?=f2|ChEMr(|Zue7yBOR2FyVh%77zbtfc zYW-!Sv8NStkjhMhQ{pyFtCtu=F=txk8+JW`N4bKV7K67*%7w1_=KgJtxFy!yzZJS$ zOPl+*z(;HU=KjK0Tc76s!lzNv++X-M8d|<#%0bCR$w|pg=#hFUxeDWwwqiM_6(=+h z)HyAU6UW`>v=B)5U$+E0Py&p9+s5N{m5g<#F+ShKSpGQU`!mF^O0b_F=Xs%3Cs847 zI*`Q3QNaiIOC-KScn-cc60)6-ZDd}++CaaU^}x``2`mgi*%O;W_^CZ_&X#A-`T`4C zJT3Di$S#_BokN7e_cV0uSf6LDZ%^Lcc7qIlgH6dDJVhY~Y;$kRPW|~sfATHIa{B&v zk<*dTe-QnONh)FuAd&AT%#_CVV`96IReN7>W80Nyq^>2f@?Zqe)I zF0UMQRf23sAhUVEOzoXG;ETP8k2ohs_&Li9y86T0N*67r{h~*{wJ_h)Mrog>UqRO9 z_fT8n)qfE6zJ{N0#23S#;~bx5_t5qibdhl%+7E8Xe6cxs-`wPu0W`6n=l%XFBMpadWMV<&u`|6V9NbMqG_ zjcqapZy|%nXq&eWhWBqdc^8{Uz6S|i?3?e7KjQ2_Vrgdtbblwje?w>yJAlr#zF?1y zQk>HtJnaj=Y2!P3Pko1tZ0>c+rgNU#&(t9U@dwU%LN{@q=k=QtG76-nr^Jbx_b}>7P#it+6qEoI*yj zUI_gsd;aj|_V{-ihh z)h*I|=85TZWn>U~Zi+3t`~A2m`IPsb*zFB^tfnaukFVkRTKZP%`+z8LiJx}%N!t%( z1dUCVI@_W9cWJbRjb#MnQvrB`xd-!4M@*>P^uP0l8c#LsW=t7hwMmb~-t&78?E@-P zw)pSVx)FQWEbqWGHG;m&UHo63sZH2&#ruq@;JXP9JcZAMMmln>&*B zn)CQIPs>l^eJQ+$`2NFLCn_002Yaec{{7F{QuhgGU3>2`jju3?6z}M;`2l#(@*5IW zf87K5+X42@zH%D>kNfdJy|j5xnDNpL3;(i{jPEmq_xs`fQEWbsu z3wp|&{3DNNRGB+wj_|s(uFTWibuy)a@yxN4$9~a#FT6ht?-7J`_x3i9omj#AAo1f& z_}%5q0|z;I;`vo~9hJ_Pf0?@ot~YPoF|TiTyuSPS)%63#hqZa#FQw@BinHM#_<7mC z>_Z9vAKtcosC)Oq+|8%ztoeRl(}#;ad-3U)myP}B>;J6tckvIdJAFj!<_9>y^C0#g zL1+cmyi-TE!pnQ`nble(8~?}nf1Bv9J!C)r2|mKQiM7?6W=#AjK@E_pHdOxc__xQ(};3hu+?)eYt3;gN^<%^#N?;LPl z=?wUn7LkGT`zz=3)CrZ`m7L%lb9y^qorUfP1mW~60~YUG({vg%|8dQK)9+pN!x1)L z_?kIo%tmz&r8_=u@?qT7mi?LhApG#2Hvg4#D1P;kaX;#RspN&dec)F&m;4m}cjFg4 zLIL=feIR-FuKIMw`#Q1tRgU^{w`64R*i-zwzJXKU!W}m$TK&uX)yWI`d@hsT=xbM` zw@LcmO`8dpyFA6e$M=uUZy}GH7>)nK zhx4sL%ID+z_1~hGtokr*j(5VK|GY2Xln3cIQ<=&CB>KO753xIrf`2)O0(he`v zGSTKyj{lIm_uAzM{Wo~j3!RnX|7QBXM~H%dXkO%`8C#G&->h`67y{NvU^(}LGu}0= z+Iqz){$1aos&7&A|m3uNxf(Zn@7L@juR%&>w@k|3WA&B-}Mcv`j#>u z{-IOuR^vR9lmFb-FHRn8U-*;@uKeCcnX_J)=R)x>b5!X2yOTz2VI?%>WcO}ftnU)j z#{>G+-;jA$>(N5+FM4_GkB1C+Pn5hN%-BfVlg1}wUo>CK-l$6kpIp=1sMF(L zfa5qn{3;(^YzwTLUAmAp-WtmGQuPWE(-*!D?zFj)(19NK_A+>mB=^ejn`y7<@mTBB z+R7GZNG9T~iJC+s^!Ey)8yqsPO}*DvG9X`Uz94#c(_{OCG@q#GAbtTI%6h@m_XvOZ zjw9$d?za9LZZRieC8m06HH;?-x*)-)tQ> zT;46o`xOcDZ0%L(hf~fs=82`WU8CCj#4k{9-w0TU0))(!kB6Qu{DKedX?o1Aj+{%= zQaaki`|){3`hI^btOMD%+4Y zF9G9n)5+JGO zw*(D z=0ywCGx7Fu;J4_ANq#GLI+(H0E9p{*CT=O^{tDven2NdAf^<{8(Np3^kqdEy$cY#s zHP$!v5xF&$HuVuXayK>pY@Xrd$DRy z);DG!^Z)Gq+FN#*&yiGN~ik6{ld z%eOJbBayA25|KB3`yAx&ItLl*U6XG=5375RdU7|myw8~v=Dz%`UqjJzDDMWj_kBDv zUP<3;_0T~f^qLFIFYyUK#3$8umG_y^Yb!d4%)dAflsuEZ{>LKLxu2c9hrON_=KNH2 z65Bc;iQbXjh4}e@bV0ql?7_TG?!0b0$ZVVTIg?rF{KKxV_w@M)>{b4|nJYjUTUw-mm5xpJSStk;pJ$8XGpmpQtVvxxT+pDJe#1J0agEo6bk8CSv*l0cXb&Yjc;SzM6jgZFD2w!gS=GObK}9%=~w--CyQT zf08@m5;a*HWEXEB=jG8od$2k6dHSASkK9U&MGW-g-SG|ca%Zt0c&<34H=fWRtWDjYu8|K!Owtg$c{XJW- z-jR!(A+GLfa)9Q9rPGZ3rTPKB;3MXSGrtLWjE>Uqx&kcO??gVK z_PJ$k*%f$FG(zuX;dHMzvX3%Iwfm~Ya$bOb@=6$4b(Q%5bj}FF4|N{uekaDKT-H18 z6(MBSCVLCT+()qG>M(0OnHRuESDn{Ht^#XLki0#F4e+~&@xvk~AOe8$9u3{6{a%?D zKlq1hyLIm=aG5jR=p{2}9x~?M)8g7EwD$9pzi`s&{j4wQ*1f0hRVMpIq5Qy0#k4^s0v0h| z{?0mREXx&dcRNc0_zKRF1^GJt<3joX)*9yo;ip`FlcW2ua1xL)^Q9mi-~)Vr2QMG8 z#HWLIi~H%Fv|yhV`QYeon^^+rH{q1I$MbuISPRg%UBA7Q?uXF*i(zp8=d;VdRc;%ZN))%~Q@Wx5mUy8l&AQL&4l-f%@W^$FWV9tKVb!Y8o zOrKnxk|n&zo^?^5zqfo?;CGW>MtK(v^0>%P_g21sMU?gyH7w{qQPE$U)Qv|kEi=A+ ztcPJd&W+-PFIMz09{S$yMpD;PcEEUcLez9b2@iXor+D&?^*K@D!`}pb~)XHpbOjHej{UkIw~XN3^lOB}mz;hYh?G1s$SW z57H7fRyT}v-Rs6HH|*@Idu0QwuHVI|>moLQUVagvn>K7D@4M;nGiL_iknfbv zcgwvP(z)oqtJwf!lzd0x?2Bp7=;q{nb%7lD{T_cDUS}8b#hGvWe01NHY=E`OK0&&{ zJ|4?QmNKUY{yIM#jFIo>1HTY$qtJa9vH{r-^u3D){rnRdhkC!@r3>uq06*PZ`T8AR z?K&$jO5JxA8-VBggLEW5B3FF?n|E^lz8}~#bA=ssyF%!`E7$;I{OlkdNqMfaMyB@! z!AAdkSDM`IIzsowYy6J_I4ga_Z<*cEhw%%BF6ZKy>(>S1Y<=1Y~?QZPzAvR=at#QjH-TT>R#Db zG1&m}m>;Ao=4n~K)8w~sjtzpz-a?0*H+9?07P?reY!>Vg`G30!SEjRfFsw@f_WP%|w3epw&mwPv|`lAi!BmMPHd57Fp3=NMfI-YCT z)i54L_eC6@xoU{gzq}iNI4hy&SXug_l96|u{d<~X&_8?)fS)ggr@6BZG>nJPeHRFq zo^-Hj1Nf~?@X#+uwT5n12>lm>|HlRs%AS8dZ*Z5;z1Zi^u5$W-!haJX?_rdjyyNWs ze=*Ge*n>Dq@$=>lI~v9^`hOP)zqx*3Y7D@Jr-bgvIaUNm+j!BL!wutp^q)mo zc*Y2aE`#<#wk*SEEs?RSph9r~M>c`)e`Ou*s2=uF&VK~lIitT47Eo06_zmzRvi%?E z`VXNcQ&4td>i<6UpG9!$KNB7)2iL#pGf&_*<5BS&77loSrFiv-KM4!RT@P9Ne)k2> z&D8G@|K_GEY}(d~k&pg?u^}wJ&Zw8^{1-Z0l`CD?Rp9wg`35al*d5U>P5<}O|FZ}S zPq+MkvFl%c>(-;sr^YVye_O7yW?VTt2sY(2a)sRy?Na(bgYo|!^q)oOD*9*be`=79 z=)*bR2SeKfgJ7fo+dD#!x#CFE|J~?6i?HytuF?O|!M(vcO6;4f{A6rFhRy!)^>=fH z-4X4+0{SmOH){g)^DpF_*BlMlPx`}88k{XH+TZI->Hl>6|6TO|EJ9c4|HwYm9qNzg z+s_yS&I-~$<>%&$qfqVB^uGlCXA!!B{#k2&J4iQ_pOUX^rGE$EK>v;T;wV)6H2vR+ z{<8=RXBYqYkL*`x&`l&~bifm|ztDz_3@wxn^MSiH>z{dlX^`$Ie>or7IooFPGt5CR zbJj1G=al|WWBk8^{+~rC_Wn;7}1(t~}zs4W2BpNjGRw>16Vj{dU<3r{Ux z|8MX41>O758pYi|PW!>$eiER6&N?P0I_nq1b4vfS>HoK(|13f=>mOZA3(^JUE2EY_ zFf*Mocs^>J`NG>;^)Gw+LHegJM*eM<_#EJV5QG;S8I!NhqHLe0|6A$*S%hNk|H!yH zNC)WXkA)hG!F^r^-13`0Me~=Y|L>#!EW*NB#X0`d_kSL&gW59+r2ys>eUa&Zg7A07 z`)@yfQ~E!Z@&6X|pG7D}{fl1@l0CNZ59aFI`pF}Te0;zqK{P<)8-?;e+0wr?=^wrJ zLhf$|$sXNYn5~SmtuMa;BX*X7j>zsF&XzuB{WSeArvGOVimm^PZi8i>{IJk{U&$6) z0`G-G?(2(TPcWtbS@{1&=s%0F@RZ^i|78yzUV`po;=G4GOX;W3vGra!=yw#7vLEmi zbmaB!F<(B?^nWw@&mt6y{$(u1-##1E_u=#PjvjN-Z%q(QR+)ZQzB)sFe%%B8)M-J(+%{}-bFEJA1NpLO&gx%=5Z zC&o8WTYeAc%!9vq9c2SuO#k$I{W_+zwTeG0J;{l<>4 zfi9$fzO~FehQiOLc)YICOZLT*hi+#D>)!GYG9M_k59 z=PJoRMJy+o=bfi)(;)+eFD;gNLDX`In4chU*g)duOgy%i>pzKdZmZ^0_*vtni+2fa z1224Qa7qW*@by6qW>Yq9+_%yRf*S zbrU|PsP9$CRVLU#0y$*dBXY`0{AIrHH`!1Bn)D~|%bxn1K>>U(QtuM>H1oQ*7dRE@ zJ@CD~lTUd@_x}WUb*}R53hnHD!i;;xZrLE0=aK2ejy~s*jZB3$`uk+dhBA%qTa^C- zo9mD}^|Rrr5Ow8!fts6bspl=^;vA3V`Oo;r%c7ho=F6k#4g0);{`;34_z|8K%05uO zbab`$^cCJ6x6jYAl%2f)#GmWuicjfi-qCp(c6dH^yb8LZC8OQC?O4(u%KV_CX)h*x z$Z-I247|f8oNV=Xfpa@yhJ7ztlEcC$q0CB>wR&ZLDg zvj&-jo+oFLKRS|iu)MjR+(F1oo#`Xn|H@cRYfYyub? z?EcMN==vOd&jH1}2i+BQS3sIe=%2fyv$67fM(FVzba)juaud(|zODSmseZRSOP-WT z9jP0O)s~Z%`xB$wM=Btz-JrWA&@F*(33N-KTLRq@=$1gY1iB^AErD(c1WUl^m_Lo% z7aV_$n#Rw#rvBQ5#`;v!kYAS5`nRVWB+YOC+J9zsM}rnA*0fSp-ee}VJ}TX4rBfJ9 zSPdzX07p}R)LRW&EL05&V6z3ONm10K#iU}ZLeOaO)}~l$uqn0UHdU|5LeQ$=GnKAyM2F2iZFM1ENjI598|I%pH#zhy zX{n?BnqjT6R8q}mZc8d{L6LNWS#Gi30)q`Psc*@?HI=rMEooD_kW^2tHUwZkKyu8P z#u!Siv{V7TWsYrHy_Ig#G)R@kR=Ux)XO(WS%~hpEf>wPY0lfUx^UzRlrcH^(Bpox; zra_msr&ImGE^m_dLV%ral(d+Eoo|8SohmQo%uAlcY_4 zYV_@luCbWPiv%Go#~iVtylH!M>A7$TTL63Mizb(l+gfED{c9U z28goKmOqiMW~Hr8C%INyYD!;GY0c`iU@^;WepUS2q}&wlM|?fsllF2?-=5q4xv5&=Z6ZZ^T0{Fm65S%=g`@T{?Ai zZ{1r`6{sX>t*w2|xpnV7`+R%v^FG(-^ZOR~zV7wI_YA+MSlJ zQg@}PojZ3a^Evr>`D$cjL=6uQtJmM!sPfJXsqWi%)$f1*d-c^luH(t3z$g&R0z5{VWgj{^Ft{>wd~d zq8xmNnGco+%hbI8WJyWH{Z7wlyf$RvV%}eR@}zbDE3dqwo`3#%D-U+h_-`oFE&Gm6 zcz=0WnRWl`ufMMN4Bkt5g+4h_98n(6{U!|>IsR8!Q86L^e1=fCJjzGay8L>D_w=n; zu`m3L>Z;19_lqo7`|qfaKKe*~@x>|i{qNuIlN{7kS6lZ>eswN+Ag7Mz9w|_7zkRpw zjW>p#l>6&yYofltv9VEQXJ=bPU6=cW*Pp6$y}!4&S9NuDslvj-aK7dKhWdKze&)}P z`2KUax1_kZ54s*MH@Uy5u|e?}%m-y*d9X~03$dksl$DnDK~E#@Et+>u70oMMQZ&!! zn@Clv$bVZE`Mv_(3(`Drp@;4%`Wd(`A0JA8#=j@w!ye>c-qAc@oEzcKa&mIi{_LPn z%goGD`!e>az4U8)_Nw&VdsJH5Zk3vvrgrU2g>FssZ@N7Bd3hE_^mRJXwsq3|9>b!C ze24x#Gkh$6+!x-xdsqGR)m7@_qARNXQokA+8nTE!QTL1e{r!r>cTf)VnR@*AvEx?$ zX_H0pTYE~+s#o`2RR5ZBS*Z&kTz%zJ2?)ItIUf1@*w{szLRQl2P>+ z1$WhtGHb(rMYmJ>cs`$an9tPXCkhL#{8LuupYPbva7lf$Vl@1YUzXX+!RHfm{ds1% z=tQtC5WL7)R-arMc6|q*PrWjqsmF_pizD)X{P=N84lnPp>JR9y{yZ~$GW7huzCOig zGylR_-f!0RM93j{P`CJe+K2f}JuY>Pl)vO($}5^2d_LP1`t!^%{8YGnlf0zxLW|R(&dUvaatE zo`A=`+NjU3svNItH*emo_zub;`8K(o9Dl#7Zo8RpiC0%uh4XJd%PFqEQvhqwQ}W3_1=41(FW+4@Q&J=U|lZdYsa15!Mb^OL^Ypd@+YqcjwrshzecigdXa@~V$bWrZ^tOba zcX|G;TesA;YuD7-vu9OdL4no2*s$SlM7~9ahWb-6=bz8+!n5HU`%r&Gv?1iV!W$c+ zw`KJ9&DhwORX%JNl@yos!M8_!Hs8^Vwh-SzIapT2gO~@~JXMPJK^vr<)0c$XKE=hZ z&lVZt&6QHV^S(z+Qb=>5hj4u!$Pe;`_z_=_)g zD%Y24Kge@vPqJ;v`phJ@F-+2Zf?fYyd)Q}!~`;OPB8@C51vXJuYJ}x(-tpNuORDjwug(KW$TgzFXL?z0bT% zCJ%hS-S|+nJm_ObdigUPYaZw;*td=LKJyyJ6fFEj|D=ye9f-9Y_ ztjTibfqk0~>&~d^#%8r`+cxx3-&3D`mZqM1>YV!NPy222;5POgOMdu%wkI(zu$}$b zrlti22O?gvrf8Yrb~XL?j>BvSTZ0Ao_m$7LXbQ9t_dWu82s97m2l+zu zb@0Cc@%bqr^%cf0F0>ggJcnvoiJu=Mlr>LFs$cZjK3Ij!YjH;+!AHWp-k2amS7? zVrn1Tl?&%M>96(qM>EX6NHk(!)rq3jKa{~PPqC!Q94gyPGUg=w&Ye3-Ncy6}IX0q` ztP2q?^Pn;w#W5)%wi75h|PI=*O^ ze;IS+^O)!B*RNZ$kTXsU>Msnc&80Wh^303s>Cexp?|spw-me^=*Pe_n@qMw5Puk_b z0Q1T$e?E`pE;>Zcxad+=Q=j^<_=fsUexG{0Vpu(qbzZG0y%?cSvFMVOf0y+KcKJ2a zWQ~H9KilxkyOck}lp!mxL~UyxQcu*3s>iE9AsL?ER}+&iiA~ervVJi06Kh-_v%PTP zLWKVJ^z?-BpbW=)ht=Aizyuj4g9G!Qjyf*Z{F=Yl*KiO6opfCi9P)3D#62zo-9Up2-=P{Kp#y=0Dc?0Xx}b&Vc!&zt{6? z#^Wgi+p~7-XYKMBEluVen73%K+cnpa(tI*$2|b$-N74ve>i{g3JY8Q1a9!&9|s z=AV5ayFA5`rq8+P`STKO!cV<*#OWw7#(zNnWxM*GcF^G}&3BLnl# z{&cjwdAX*~S+%vbsh_>_s`}x-Kc)U1REIS|Qs%m?dJQ|-G}}weKgR^TJsfi z@*gXY1&67~P*v%^-y_z%%kRC)@ZA1-wfjKsb}!{^{zjiOmG{PzyUFlU)peDhpO3ZX zdpXA#vkWzpSmPV3e0DM%zdfdQog1*`tGI6b-FM$be|R_7o5du9zJ}Pz_F_%9lh2fa zW2|i7R-yd34~cC@&My;(&6{gH%MhwQKK)4E4a^YjWL*%bdO2J7c}h{F&?3WnN!6$L{rf$s9Xm zs0R+NdFQ>SVp!uIiwu%~j<<15{EZtotT8>#kzcxW$yy(P^_CO%-*K%x$Wz{#f4BX9 zvE*6*t>oV+?}Y`0eHgpv6zY`zCcEZA&#PTn@^4q}lUW<#B&6(@w9CU((j@;J2Ns<< zi!~(}Z?fHY>6AaW^vSOIaFb2)AI>}0W^m7_mwgp(dDgGXni04B#Cl!wPg~`h7L3zO zdR>X0Uo$NE=lkMGvL=Rk#~4Q+#`&gft7hIAZ*B^%nHgU<6WR}h{lE0BvVMm3bZ`v~ z*3qD@46dW8RO~;rdnS5n3Ns z10>8Vu)B)s_d@)G5Kjs5WyW>+-DLTfq34J-@Qz?SC6xa~A-*ug9|`e? zLL7VYt>^ede07Kyg?LYh4@JuFSJ~Ek=BW)9e@Lyc_#>h6S*Q+L@kQj;-$c%JT8lxe zL9$NEhqFfJ{^m+xt_0>vU@|3OuDRoRL)LyvuDjFL-tqjQoQU;zIJYCT{?2X) z*_+98J+MxJ#Pt@t!|U;ado@$B28rva^mTb(?AWPxd_MKHc}`{HWzC+xhL3X%Zp8Uq zFXXiy&N6Vy|5VdOevX64`XHUlnm?V(dNbiNkE(N7H>Y!1Z=-Wr!=-bs2^3-(#ba%w zQ#t8t9;ag&Sn}JP?d2p-tm)>O$!KN3^^@CnA5a&12duTAg1>Mbe+ldOOIXKW!aDvE zmbIbrSX=2-F2^ul&%P$*)=9Sk38!WrQmgi#Q!nr7Q5n_OBlck@stkyKV&xxSv#HB( zrw4t5cho24H`N;lE~}q>(XC$1=uzbzw<6XJvK$0^oeTCl7wmN|*bArptQVZ{n`*kO zS>=07`R&S}svGC1l-y9i*ndg=ct^Lj26X-5i>m!%Uqm_RvP)Q(UBbHT5*FFXE%{TU zpB*)oGSJt&dM$(W`abnuQLnWImg{)A4)zCF2m7JRI#`y2$gOjcTjx>+LHT3byB|w= z=xb*AF6ObecKj?XhM8CK0i0*pmtTf;%(vC1g6rx(_Fho`l6_tM{h?lkxmOi!y_(Y; zgy-Dc$Wd& zk?~lY>{L$rnq^ad#!dNkJt)30thQWL>IbLC5>^J3KOSqFo#Z#yJd6H||IziIvYWR0 z1=g)bD}%}U1M!9TTYlExECZ2~VcM(czs|)LxDGs88BE0=P=2?w0iCe*lJ2%#|EYQDu;5`SmsX zPV(BNi`+U-L>W;2c&zPr!e3wWPdV-UAJe1^#4oz>sn!e9Mxy-j*c;#^zrJUHGTPOD zOcVJ{uH!G^c})65yrk=UcI=Je>gTFo2t3J)KzIzpyGD!5Expmm%)RUpXDRi z>s<7Jd-FK9{*%^GmDh64+T+GE@Yk$aqdx!qbC$!D${-%+yt-lR<+?7rgjoh$dzIFF zUR8H=DW0Ljvv7es&T>mr85tQ?IqcfCYtm()?^$F%z540N{LZZ$L~fl^{v$XKucoGE zdwF@eb#^ZIH*(#=+O=zOZru)?t(}XrTrW+k4DRQiM>qL-U6-9Dz<{SG$@r{~w*8WhQ zBfM$T4Ygv$pnBm2OMaHYd%ApX;rqR3)N2_^8Bl(GZzkn||KHx&*p6=k4Or(GuUt8# zmqE6h%=+~@tZm>sBwv%UYp?gnJG^qye^Y+ufAi*>+p#wi{=hm%8Q)&A%Ao%F=iT?I zM#vvJXWDC?;_*9{x7&VJ+1E;L?roL)bC0W52K6Wd&wEAX8Hw8S8m*i}ezvvQw$}HR z${t6amw55wMRlh0jKUhF343ED|B(MZP&V`+X4(0S$)58aO+L44BEPHbp>xBrMl)f1 z-V(KEI9eHq{84d->{y%9hk8C?vJ?MA?K$=$zwAezp!1X+XSPG{6Ds?ImY;Il$&NJ{ zaoyKUIi3C*L*KZKhF83=iT<0i2lsTxe_ynd>`YJ8p6_U7Ch~JXxcGVQO}6%gPx8F} zXfj5-*4PmI&Oo&M#`~Jcugf0XQy%#p2Br3?>sO+ z_mlMf_?Um|!y1f)`+28w;GX^QZ(RhxcR`=XdXdET0ozDyC-R$?>@NiO?`wPbL*K#( z@8b`B6JvZYfAG7P=6-&}`*7aR1C!mUOy<((N?@)8lD7mx2M9Q`^OWr&rDi3c7m^#_ zzp>=UaTYcDFyC6R{eELn6k{iTFcA=KkhNV6Ft_N?@)8 z=1O3$1m;R$t_0>vV6Ft_N+3=pfHQD?SfAq~FDonaBkaeT6@Nu}g+&ZoR0eKQnV-Ka zD-BX9IQCnQ)5%gl#>d8~1YuB#MJ4`{;&CcQ7~k5ps0chrMd0If;zW@j^GZP~JmD`C zDnPiP&|e^Q9O2^y{^JpJ9Ptnx0|#Ml&JS5F%FjFIC*@g`4-Ps?%JUzcKu3`V$_-L3 zIM(!7gztO%EjoO}fB0~&AA7(2J9g~Enupvtp_I7CR905{ux85#ynWce?nl^OBkV=;dm-$T@_Ql7xBI;i^X`Q(f9>}|I77kjg)krN7rJ@#rWLNQZ}6w?O7o!{ z5(iwIo{Y1ke6VXD_#)WtD%kca*meqdBry#HTMyDnaL`GMPNu-d!vq^oA;G>=VBZ$O zwo_o&6X^ufK(O;56`n{r5hB=nN@2l?6xeu>ju)iBzBM|IaEOk9gO2Hxe=G&Io{~?> z3leNR1vU=KOF5bwq+D>=vqrfHYjh+x1@;}J!{8v;Ht2{&hYlT1vFPC8l!FHkugb{S z_cFds`!K?BLMd^NA@==!{XSs33g1i{fo+WR-R>I$pA^An#vppFm7^Blx^AF9hE|=7r$<$Gi~oKIVnI>UD$;n>ww*s|7{{Z!&zP`Q(_&*9cr~Lpv>^}m3+C4D`pFnoHV5T|g zf|+jWfK%Mm0jIc|UdVMXdcof~>4TZB>%}K)rx2SoYu0(9nbP|B?j%su2E_Vf^iEUr)2c z+)UFkH}Q;i&HSyzO{|^bZrIE9cH@a=C)E9RX{OEValxNujBx;kF+A(LVFLpu zw%+ll?wD9R#oe%%>+#e99sk{F=}Cb<);|XQ|0M8_<_83St{3OGDPMf?MYRcg^2_Qw zRPQac_dWIv#{I>(-Z)`yrVGYyJYG6L{80~#H8wQ%BoF@87XD}lTJ^u!KlicO*}Pqv zZub%v{D}$1-_%PlzoK4RyHzbuYgHegxU3qxZ>yVqv}e#Hbiu^hDei{7T#ttigz;|# z{=1XK{ssT43HZlD2L=a+)Rik&)hBy$)tmb})rzbO>iM*8wLIgz+Ffx?oxOtodO!RB zu^XSN4kRW1)zR^fhYrxbPhYsLHkVviZymX;Ufp*=E#1|vp8evS zO4)xw%HOT8G|vBrU7(*qN0r??yTa(yZ~Fphr{?0-*k)c+d(0~r6){Xel6vh(|P zY2yEN&UXH-{z06Daa;Ym@VZ)k=(2hxxs{=az=}Ce=&e90h|5a5f1N{Sz_?vz}$JZ@v_5Yv)=dTZ_FRE{j-d;oQTv<&sawxY|b`8>u_`hsJ^c7ES3&BWR%?uNZwpNtM9C;l~l?0fU$ z``=OVx3hIKEt(!s2l~+#sOaibAC=uu8}qNKH*zki7t?!`e2;|RB>6GEO~SUp-l}Wr zT#Ri4C(O-s!Pt$*q67SG1pevC;s0x@gZQH#n1XV$>|cDo$pvp>$$jAG&ULG{wl>w; z+NQeBc3JzPX`6O>FoZb;-gouZkjiYlt$th5tKQDLs$R!8d0t36Z`nWb7kUPMVRcTA z%5UmbS8jyb1|I4J6Kkiq8}@SDs}4-We{WLM|2U(_vVS-Hi9KiIDCK^^Eq zyi5H!txK&s-lIyoZlDb@UMD!=YNiX0Zrm%*`uvZ1kJx`xV^hzb^qGf0YfwL;Fv)9rA_7m*d0D79K2TpM}y^!lB)^_m-{P!dq{?$?aKe0P^ z?i~8^SHido?!v|1sRQ%_c69-9=;}JB%A32;7QCT8F2AAPMW5i!!@+*xv*;H-iSN{X zC*z9x$AdT2x3hb!?^As{y zqRu|`8R`MH2RETlkaGBndNK2o`sX9J)i+DOQjb>*tH)2?RezInF{A?>>N{EO>Ok{F zb>%who!%Q(JA`c!FT}dajhJUQ;xp{*pZFtvjQHdGKS@#l<19U^{zpGB!v2ZBt^=1Z zUr|`wHo@mh8&UK?aAtepEZT$;=XD8_Qx7iO7*cy1ZsWUrx73zW^a=27zNe1^`|{rh zu_ykP4tyD*1K*vdb-_6P&A>lnM&WR^+ZKMtT0v&TbO#BDXAFS!_ zS37EMtKUHf-YvVO{&)G8>hVh8ZV|rIR|)c{0~|AQs^`sgsn?@%I~$Ld7J>iXnT0>k zy`k*H+75e37abt}?QQMWZ;894iw+Qf(zV+|D(7^c+SWXz-tYKIeYas){Z&;6cVaK( zRR^pw?xz41PfG2bf`92cm^_ z9f%gzbs!S|eM#Z}aW+yA|C;FdQ%=?aI*z6e2)49qwxii5(|^w}%be{Io~3l~3tet}*4pE^SP!N<_{?ZF<98HGQwqfLtsVBGZi;@dA> zx{NWRGnRih@#k+)PF5d){Q!Qag>>cipvuR$X+OR)rhYRCKX5<$1uO&I?!`Zh%1q3u2lNRX7o;B8;cnIwx?YI=lMj)S*bjv5 z|1|7BN$|%RnL+m7g513Ru`D`EMa#7#a^bO3(OYX4#EyC-u7;ct!w3Z9eUCiW7a z2|5soe^ye||EKCsx!S+r@5a6AgJ5p*WY>Xl{9AhP{o|y?NLT zIzaga|CZA&J^226a`=D2zpl>K|C`tmACBwt8%LbqYX!Bmv|8&uxE7Fe=dtR6U~ckc z(t!#1XU#nPi4VX3!Lfbf%&{QBjA5LAqp%-~=Lqd+eBKcIiyy$ep^m$Ri9f<)Xb<&dXYWr6|KCtwZ;k)p`@B*8Km9q^ zyb6Zo`nUkY`Zd-ST%Rjx@>_|!aq%T4pU!o`OvaycJoNoKZi1>Rf`2RQe}9tMKiYv&?VrA%epts)a2Kv$Ci8Tz`qsv?@uoO?}k77{G8(@hQuWrmb}I`-W99`k>81q=v=P3aZ?W@KHchqu>D*3 z=Ojn{-_T&y|8DrR-OhbI)N?6+6E{g``>wUM)%vaZ8$O5YLZh|)qg}J>4@lbm>KE9x z|JgrEM*Q(DSx@*ANBRJ^1BoHAHnC+|u%CD~>?_RQFwS-ab;ORG`T=ITU7vty_p4t( zI}oDGCjL45lf(YQ_}AA*t^bJ|@nt`ddwW>cq6bW8*cul`ozF2sQ$KXSZx^=H6T37% z*N&z`T_E<4aqym;oSa7?=d>UHiv@qt0oo$lgVX~O>jnCBa<=t3Hd-E@HX!w-zjMEnIOuh_`%C(FIQ zXFBOX1pWtRmib9~?jbX)3tI^WJ3Afn>zLDOCTXACNMHx(nq^ znlQIROub?uzl)b1$n)IzgRToA@jsXp?SC)$yOq1!^?2xk+dJg?gVO=4PZ)0h9Xv2| z@n>I~V?FHibB+(~U7YFE<+v|29{9)Ne;%9;Nc+FNCt2`scJcpWSL7^Lj@@$Y4afJn z4v=&F?d=^{1KMJp`$o?3A^ujsHxXecoXvD|Y)Ik>m^Tbyuc_7lBmR(i+K>M=HwF9u zjg6@PhbGqlf}3C;JZG&_;d_|s?YB3omvOHD3(qf8zx?G&m9cl9b?#SVW0N(uuj@d( ze8BYf1J?t$2L{JlG5&M#z@bO)CI$XY*7y(F0lSFKu= zqFx35%a*;Qo_!WN@YIi>1OE>EgF1k-Pf!;$h0h8R9TFaG{?Bes&`X**Kj;)sKCiZK|6HX2|6ly#HTB9Xuc~D) zycnSa^aHF5xF>|?{|V;t^#A&^MSQ=B&pPzRn!Zy$xtw*&hn(_II~@o$P+{}X$z{n)l`o7%YXS85G(U?p|n=Y}6Z zdw^|1_6xkM|BN^Oe4ZQja^1vU;#24s1aMX@bzlrM{4MCf-%N8I@QOdzb5jR0GBWTz zxA)Y>4I9UGKOMPBVR@Gu7yL~ghq@r}_{lFV9rzY> z05t7&K=4O9AgcYd4aW5y_+Eh8x^*ks`4e^E)mN8W{Q~w4IX=KKA`^=T)&6(tQ`q$f zByE!Yf~3*`ulUn9(*M&396x@1TnA7WtX;d#Y7??wz`kK8d_TvCc#nxca5&AKH~eW|CO#}f`deueQWrQr zKp$%7^POz;ex#ducZxgdMZEn0`vp(zZfjqh*1F^e=`{=1Wfu7l95~=3J&3=SkpD+J zz_$Hohrd%9I;GzaoTI(NDPLy#{m=pS4Hu`k4L`NFw*8%b1xwz@C|OqKna%p1)(q4*&Y~>wTYo`ssbsf!TyVb;k{RxgI|q;5STwJ?QDOt130WIB?+5 z;lP1|2Lr%<4A>8U_St8(@4WNQ;+;Ep&j0x1kNxlkKGp~K?Y01s(X8hLOMvrv?J~4Q;BTx<;Kmc~TV=6kz?#kpTRCVAG~e0boA{ z?1vGq-L-4iV(7&D%*<^6u3dQvZxgn*v{>Ums0SR||7O|Elz9Ag;7*`-=x!kQ+P%O> zmq$l7LkB+Vz7weKx)f+@?+i3FHY$8)TNM`Pg!utC{H2U|-k;ML`gBcao+bZIIxE-RxFWzE0~jwF!@SXjoa&~(E^j=Y(bU|0ucNbb6yu`-*h%0h>i*yU_O~iKJ6i#B zYaR`HK7}cSUw!rB{AZuNoiH6}3*nD;z^wECx=y%ZFV~%P zfaw_f4BWYE;oo!gc){1J>rVZby1KedoQZl5Yve~UZx;CIqmKf4d3gcOoqYJ=hn60E z{`n5fslOZ8y*oXSm31fp?8jbu>H6@}rMGKg_lrUEL6#20+Xu9@wp#T+>H%B)Mebzh zI@V5MuQEF60P)AQF`SLulb4tOXzQ7-`IVKGk5yM!XC6Fw@ZRRln@7?13t+4_fOS;V z1C^ehuF&3B_+EfIbSN*db!$!F<(IDqfc@CgrG3NCKHC?e19SL)>DgcFcsgNjrt9}a z3)|^H82>ZGKkrfa{69E+`0za7{n&4Q^P5bB@4@enqJJMiKOlfGY<;uJ%{_{BwWVsu zjz;y`YrTPG%ld%7MPtA}tOE$ozu)+qb zbf6A(!2gIl{%s-u-x}Tj&(!w6lRoP>M+@sZVB-JPS6`tIzsnlS1NQUa=O07AKNDj+ z_s|X)McokS?CcKY=3WeJ+}MY8^@Hm5*9X<|<UGUq?!otFPEiD~r4_pnr`|dzs&6>f$>eWM7Q#*({ zz)lDHMuGo8{O!Lj*#2v6wQc`7l}$3HN3&xu*K}LA3+wnF%g=w5_!F;Ht5*5S%gcRe z@B45r^t{~MV~Z21eJf9}28pI}~{9tsz|p0`LQYkRKRDd*CMefpMQ4 zY;TK>zn5}JhTl+MnNQ)9&9fcup)~aU?~EZlihln2WBJGaMDV8$l$MtIVEewDoCChi zn~%&xegD|I@AhZD{q~)EpwV^fB6J`CKM;T)2&`B!FoO2L4YUWA!~P%Pv*P(D_@f^< zYwUk2b(y(tG!zej2eAHW3^a;yzHZ`=b#M_-aZWEaHPx4yS>oHX=lr}48wMYP?PsoE zf9D>^P6tL_e|?~L`SSkdFTdRXF!}|55b?k`L}nxYT*EB;*LijU_py`P5r1b#E!gKC zq5yaR>*z>h80#5E->w$@zdyzr*$D_$R-Qt8unYK~^S$%Vz`S+q?l|ed$m-RDy(?D^ zEdRwX1|A0fe&9dhc~ec8ZTQpwvoFK7ia4*uTC2+Ssw90JTeoxmR*2_Y3SoakfP^zq z19;{b)_$I z;71+cTd`u$_IXoD>8faCKfd!eH}*#2v8n;rGPZvQ6!wEwBteJSi7efkAu zWn~=q@j2PQU=^tYLw^9w7Xtr(1OEru{~xVcGvrT68St%Hb4PB8JD;uiPr?3eeSYc5 z(j`^EzPzm5k1>8%>;=ei9T@T<>?c77{HO!`uz%nB^>^i`3E>^>?N2=;OzPy#PubfW4zVFa*4b{SXP^A^Azd_&EM;v!eee{$KFd{XN6d z&Lih54&kE zFDtXKuYlbb78LqW_a_3Pgg!HjfBWpgpY4C*KY8psEbOac_ZIerg+BQIgyNK_hj(^# zSoJ^JfwRW`b^LXk_u})hx49kl{F3Ub>IIb*6@JX)_@D=gf+yk69ml_YR^iX@*u+}j zhq12?Vt*pZv8Qs)X8d*Amv$bxU0W{#`^rlHiNX^;tm{g+eJ1(^ot+{7-!c2_-}Lv< zutz<=1b8n%AKx0=shI)TTlPOo@s~LOFLqy1UJ=5+Dun&Sd7VVtX`-Ir+1VLv|8;cC zs`{V!d%+%Uyd|}?`>HB`5yo_DZRYe6h0XnV?wJt&Xa~$n{Miq*tM5@B?Zmz=jC~QX zue~3T+}C%^;U77N%W*woUtL+bt<+uuVc?J@yA+dzm7lGGf#GX^mM)tas`k-!GG3cAHv^A3=BodN9Vl46zaK z0r6gg_`C;4gJb-zC5hi=294s|_13x__Jsziage__VZ%e^?s1UV+gRrlJYm0TldioTcI}#+EJHWFCf^-`E zbPCTR3Q}_l&n4s8q&hVtK2A+3JiA&ae#>f{8d93@ZBu^VG(@X_`x{uR@ps_Cc0THR zwE1VWojVEFb@oii|98?4SYyHPlNLuEFr&E0g*Klbd-MHl+Q32BU%)*kI$@85zYY72EyDRD@C_kq z0dMj1OsXKA_Vau~o>8e2&#WG&WZxD}lKZm@9#~5|}H2xe}OpC7=@i7Z@+JMSPhVA05BGkk{Sf+8GFs@b`1^ zZ~@Mh$DBS)Cwq9vu+Mo$+;7BvW;_K6ntl5g8S#ZioCcu(o*B^N%zz$e2J|>H5D`x? z;){& zncqcUMar{iG*aG_KxBC+6;WOm+?srrI%siAU9`C6&a^llj=%BpS!BdhjJOpY|9$ZF z?rACE6Ip0~(|rx@{IG+dvBULTH!ULTH!ULUR>7l!Va`f^by9$)tV E0i9@WlK=n! literal 0 HcmV?d00001 diff --git a/Plugins/Discord/source/Plugin.Discord/Art/favicon.ico b/Plugins/Discord/source/Plugin.Discord/Art/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1ce0205a3ac78c1b8258bf04410eee49827f6c7a GIT binary patch literal 15086 zcmeI3U1(d^9l(!2GOuf>g`3oI>{yCz*_I_+viu?WOO_pf${*Dp%IN5W*ce-34}J+5 zWe|-%xRgQIL*K&07!7+^F=KR$QD%pB>nIC)NkZt0otH7T1=*knL1Lf%j&!ect;n*Z z>cgeJ@bmxPbN=W2&po=n_uT*ge}$+LHNs>PNQ?MMtq{XP2#Y0k{5#5jO_|H39b1K% zyC_6AI#kgjRgMq>y@r=lB#$fb=R#xK#aCau`bXr=e9bpCce*cKwmxpW-0>TTvmeT( zkE*I_am8%=eoJe29At~R`?v6*xwZS9liIggd#kT@^uA|lcSr;K<~I9{+H>E^+Purw zZ|dxJ{mI(plm=q$oqd6-s;aDQ?Y)Dx9>>6=9{YgQvDpV6cK5h#*&3>TUH#rbpUblc z@(}0n$Bw=sbxFLi4GxdLH83>xXZUb%XzZ5*gQNQ(b=(6Z@AbP!s$XbGfp^5W>>2UN zV~7uXCf=;6;lev+8=sgC!`|4q|9h}FJ~8z_jwSLj$L^EnY0^J44|ctK|J3Y_si|2B z>v~zPa(-c{c5ZI*&g|Txgb#CVr}UPVR~rILE1v`cD-xbAE-o+U(|%>;brbv)-T`}c zb;jw=r^}plr4&N$!47Nzm*qi?5YOs_NYo1vHq(vzPerdE9>ZPu6jWPvpRs@+)cq;B zag|m2K9d^AH|uX>bGP8$8i3pIV`wPX3|wq%tL2{lUHBJBh{LbSVV~zf1J8pGUTbQP z!xPvBsi6D@_2)`8j;5B*CZ0>TFEv}Ag2dLl*c@X2e)724`jC1@vF-Exs^wWW(tM@s zK1c%}QEq8&?Ygd&{rmEjE?+V2H#N75D;C=u@QCNCOe0SD7qG3$TgJkYJ2&TH5Taw!ZrI>V->!^}|?4 zXWu_SW`niGXYJNLU*o0LuWnfDfpv@@zGN-44>AjzjMLiD=`6~_i}laTta*NJ>v8Xa zJPy`Gaad=JMZ4cQdWRZVgRR>ggK>~qc)~b;X0yAC@>@Gyo@&-?)?VlE=ODAN-`h9z zC2KnWpl7mZzcp(|AAIKQ_sXoeJa_w?!(;gxW{r7{b)^G-2w`{|rYLVghFEJRKxQG{ zKQMBK^=RGs^RMJRGl^XlV|5EmM!z|-e-hdPw`Z==&xSNH7`EM%{q@LjXR1Ca1J?-w@gCGS)0U!3qwf2`H1 zdPSV~kPpzw`?tYLqs-y=&pZUFaSz#kRIBhK-iywG4>I1Trl#i_ zr>E!cg4FmA(qFj6+*{Vv)QH*H#jcszg?k_kypek^g=_NO_saZy;G?JcDVmy!?7C z@44f!0a17WU%)zay!6s{GBqoD+3ITWJXpaFmZBRt>o7S3^7-~DyERfC;6Hct4d0$wnQD&<8heOkg}SpdRmwv*(359Bw)9m{mJJ>m7f9^sR+TL zpQ7W;Pr-DYZQr07z$fH~u##W5(`L-KKpKdA9nJyYm0(Px>K7)h(!(CaEG z@j06CH%35aU|S#C<(5vRXVlr$>nbJjsao(CMnPsFf}S7Nj5_!@D?0zJmL5Mo*BHpW zG25c}YBR!EaiC9`}X;+g+b~N3l692=*QHBbiH&* zU6i=8OV=%zB$gZk2{E;Ql3jEnn!ajdq^$F86u(^!n**^L~&Si0V40S==2jeKttx!#*v&bP|@KEi1pKY0rl z@)n{XGeQ2ssl9&4E%1}$5CeHE3rpeCIRE5qtZ8}vD%U?Fi-F}B=a}<7pj_ukzDERP zR+dyw$VyK4-g1pw*&$~{eIpTu7|0B$^03k?=eY8k?Lnl zr1}ku$)sNLdn=V6Rh6`#5H(4;O9qsx-$`acVI`RfrN($ul^>`&zTFz98FE*kj@%Kb zBe#SwD;ZV^R4I8gB~e$$J1U7vh%yWsdp- PWlrh+0-d literal 0 HcmV?d00001 diff --git a/Plugins/Discord/source/Plugin.Discord/Config.Designer.cs b/Plugins/Discord/source/Plugin.Discord/Config.Designer.cs new file mode 100644 index 00000000..fc944820 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Config.Designer.cs @@ -0,0 +1,158 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ServerManagerTool.Plugin.Discord { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.8.0.0")] + internal sealed partial class Config : global::System.Configuration.ApplicationSettingsBase { + + private static Config defaultInstance = ((Config)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Config()))); + + public static Config Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("http://servermanager.azurewebsites.net/api/plugin/call/{0}/{1}/")] + public string PluginCallUrlFormat { + get { + return ((string)(this["PluginCallUrlFormat"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("http://whatismyip.akamai.com/")] + public string PublicIPCheckUrl { + get { + return ((string)(this["PublicIPCheckUrl"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("12")] + public int CallHomeDelay { + get { + return ((int)(this["CallHomeDelay"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("5000")] + public int RequestTimeout { + get { + return ((int)(this["RequestTimeout"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("1B745000-6389-4770-9509-C6A05E209323")] + public string PluginCode { + get { + return ((string)(this["PluginCode"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("Discord Plugin")] + public string PluginName { + get { + return ((string)(this["PluginName"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discor" + + "d/latest.zip")] + public string LatestDownloadUrl { + get { + return ((string)(this["LatestDownloadUrl"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discor" + + "d/latest.txt")] + public string LatestVersionUrl { + get { + return ((string)(this["LatestVersionUrl"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discor" + + "d/beta/latest.zip")] + public string LatestBetaDownloadUrl { + get { + return ((string)(this["LatestBetaDownloadUrl"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discor" + + "d/beta/latest.txt")] + public string LatestBetaVersionUrl { + get { + return ((string)(this["LatestBetaVersionUrl"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("ServerManager.Plugin.Discord.zip")] + public string PluginZipFilename { + get { + return ((string)(this["PluginZipFilename"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("_discordplugin.cfg")] + public string ConfigFile { + get { + return ((string)(this["ConfigFile"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discor" + + "d/VersionFeed.xml")] + public string VersionFeedUrl { + get { + return ((string)(this["VersionFeedUrl"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discor" + + "d/beta/VersionFeed.xml")] + public string VersionBetaFeedUrl { + get { + return ((string)(this["VersionBetaFeedUrl"])); + } + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Config.settings b/Plugins/Discord/source/Plugin.Discord/Config.settings new file mode 100644 index 00000000..e95f2570 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Config.settings @@ -0,0 +1,48 @@ + + + + + + http://servermanager.azurewebsites.net/api/plugin/call/{0}/{1}/ + + + http://whatismyip.akamai.com/ + + + 12 + + + 5000 + + + 1B745000-6389-4770-9509-C6A05E209323 + + + Discord Plugin + + + https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/latest.zip + + + https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/latest.txt + + + https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/latest.zip + + + https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/latest.txt + + + ServerManager.Plugin.Discord.zip + + + _discordplugin.cfg + + + https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/VersionFeed.xml + + + https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/VersionFeed.xml + + + \ No newline at end of file diff --git a/Plugins/Discord/source/Plugin.Discord/DiscordPlugin.cs b/Plugins/Discord/source/Plugin.Discord/DiscordPlugin.cs new file mode 100644 index 00000000..f3d3cb91 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/DiscordPlugin.cs @@ -0,0 +1,260 @@ +using ServerManagerTool.Plugin.Common; +using ServerManagerTool.Plugin.Discord.Windows; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Windows; + +namespace ServerManagerTool.Plugin.Discord +{ + public sealed class DiscordPlugin : IAlertPlugin, IBeta + { + private const int MAX_MESSAGE_LENGTH = 1980; // 2000 minus some formatting characters + + private Object lockObject = new Object(); + + public DiscordPlugin() + { + BetaEnabled = false; + } + + private DiscordPluginConfig PluginConfig + { + get; + set; + } + + public bool BetaEnabled + { + get; + set; + } + + public bool Enabled => true; + + public string PluginCode => Config.Default.PluginCode; + + public string PluginName => Config.Default.PluginName; + + public Version PluginVersion + { + get + { + try + { + return Assembly.GetExecutingAssembly().GetName().Version; + } + catch + { + return new Version(); + } + } + } + + public bool HasConfigForm => true; + + private async Task CallHomeAsync() + { + try + { + var publicIP = await NetworkUtils.DiscoverPublicIPAsync(); + await NetworkUtils.PerformCallToAPIAsync(PluginCode, publicIP); +#if DEBUG + var logFile = Path.Combine(PluginHelper.PluginFolder, "DiscordApiCalls.log"); + File.AppendAllLines(logFile, new[] { "CallHomeAsync successful" }, Encoding.Unicode); +#endif + } + catch (Exception ex) + { + Debug.WriteLine($"Failed calling home {ex.Message}"); +#if DEBUG + var logFile = Path.Combine(PluginHelper.PluginFolder, "DiscordErrors.log"); + File.AppendAllLines(logFile, new[] { $"Failed calling home {ex.Message}" }, Encoding.Unicode); +#endif + } + } + + public void HandleAlert(AlertType alertType, string profileName, string alertMessage) + { + if (string.IsNullOrWhiteSpace(alertMessage)) + return; + + lock (lockObject) + { + var configProfiles = PluginConfig.ConfigProfiles.Where(cp => cp.IsEnabled + && cp.AlertTypes.Any(pn => pn.Value.Equals(alertType)) + && cp.ProfileNames.Any(pn => pn.Value.Equals(profileName, StringComparison.OrdinalIgnoreCase)) + && !string.IsNullOrWhiteSpace(cp.DiscordWebhookUrl)); + if (configProfiles == null || configProfiles.Count() == 0) + { +#if DEBUG + var logFile = Path.Combine(PluginHelper.PluginFolder, "DiscordErrors.log"); + File.AppendAllLines(logFile, new[] { $"{alertType}; {profileName} - {alertMessage.Replace(Environment.NewLine, " ")} (No config profiles found)" }, Encoding.Unicode); +#endif + return; + } + + foreach (var configProfile in configProfiles) + { + HandleAlert(configProfile, alertType, profileName, alertMessage); + } + } + } + + internal void HandleAlert(ConfigProfile configProfile, AlertType alertType, string profileName, string alertMessage) + { + if (configProfile == null || string.IsNullOrWhiteSpace(configProfile.DiscordWebhookUrl) || string.IsNullOrWhiteSpace(alertMessage)) + return; + + // remove any bad characters + var formattedProfileName = profileName?.Replace("&", "_") ?? string.Empty; + var formattedAlertMessage = alertMessage?.Replace("&", "_") ?? string.Empty; + + // check if we need to add the profile name to the message + if (configProfile.PrefixMessageWithProfileName && !string.IsNullOrWhiteSpace(formattedProfileName)) + formattedAlertMessage = $"({formattedProfileName}) {formattedAlertMessage}"; + + // check if the message is too long + if (formattedAlertMessage.Length > MAX_MESSAGE_LENGTH) + formattedAlertMessage = $"{formattedAlertMessage.Substring(0, MAX_MESSAGE_LENGTH - 3)}..."; + + // check if we need to apply any styles to the message + if (configProfile.MessageCodeBlock) + formattedAlertMessage = $"```{formattedAlertMessage}```"; + if (configProfile.MessageBold) + formattedAlertMessage = $"**{formattedAlertMessage}**"; + if (configProfile.MessageItalic) + formattedAlertMessage = $"*{formattedAlertMessage}*"; + if (configProfile.MessageUnderlined) + formattedAlertMessage = $"__{formattedAlertMessage}__"; + formattedAlertMessage = HttpUtility.UrlEncode(formattedAlertMessage); + + var postData = string.Empty; + + if (configProfile.DiscordUseTTS) + postData += $"&tts={configProfile.DiscordUseTTS}"; + if (!string.IsNullOrWhiteSpace(configProfile.DiscordBotName)) + postData += $"&username={configProfile.DiscordBotName.Replace("&", "_")}"; + postData += $"&content={formattedAlertMessage}"; + + try + { + var data = Encoding.UTF8.GetBytes(postData); + + var url = configProfile.DiscordWebhookUrl; + url = url.Trim(); + if (url.EndsWith("/")) + url = url.Substring(0, url.Length - 1); + + var httpRequest = WebRequest.Create($"{url}?wait=true"); + httpRequest.Timeout = Config.Default.RequestTimeout; + httpRequest.Method = "POST"; + httpRequest.ContentType = "application/x-www-form-urlencoded"; + httpRequest.ContentLength = data.Length; + + using (var stream = httpRequest.GetRequestStream()) + { + stream.Write(data, 0, data.Length); + } + + var httpResponse = (HttpWebResponse)httpRequest.GetResponse(); + var responseString = new StreamReader(httpResponse.GetResponseStream()).ReadToEnd(); + if (httpResponse.StatusCode == HttpStatusCode.OK) + { + Debug.WriteLine($"{nameof(HandleAlert)}\r\nResponse: {responseString}"); +#if DEBUG + var logFile = Path.Combine(PluginHelper.PluginFolder, "DiscordSuccess.log"); + File.AppendAllLines(logFile, new[] { $"{alertType}; {profileName} - {alertMessage.Replace(Environment.NewLine, " ")} ({responseString})" }, Encoding.Unicode); +#endif + } + else + { + Debug.WriteLine($"{nameof(HandleAlert)}\r\n{httpResponse.StatusCode}: {responseString}"); +#if DEBUG + var logFile = Path.Combine(PluginHelper.PluginFolder, "DiscordErrors.log"); + File.AppendAllLines(logFile, new[] { $"{alertType}; {profileName} - {alertMessage.Replace(Environment.NewLine, " ")} ({responseString})" }, Encoding.Unicode); +#endif + } + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(HandleAlert)}\r\n{ex.Message}"); +#if DEBUG + var logFile = Path.Combine(PluginHelper.PluginFolder, "DiscordExceptions.log"); + File.AppendAllLines(logFile, new[] { $"{alertType}; {profileName} - {alertMessage.Replace(Environment.NewLine, " ")} ({ex.Message})" }, Encoding.Unicode); +#endif + } + } + + public void Initialize() + { + LoadConfig(); + + if (PluginConfig.LastCallHome.AddHours(Config.Default.CallHomeDelay) < DateTime.Now) + { + //CallHomeAsync().DoNotWait(); + + PluginConfig.LastCallHome = DateTime.Now; + SaveConfig(); + } + } + + private void LoadConfig() + { + try + { + PluginConfig = null; + + var configFile = Path.Combine(PluginHelper.PluginFolder, Config.Default.ConfigFile); + PluginConfig = JsonUtils.DeserializeFromFile(configFile); + + if ((PluginConfig?.ConfigProfiles?.Count ?? 0) == 0) + { + PluginConfig = new DiscordPluginConfig(); + + SaveConfig(); + } + + PluginConfig?.CommitChanges(); + } + catch (Exception ex) + { + PluginConfig = new DiscordPluginConfig(); + Debug.WriteLine($"ERROR: {nameof(LoadConfig)}\r\n{ex.Message}"); + } + } + + public void OpenConfigForm(Window owner) + { + var window = new ConfigWindow(this, this.PluginConfig); + window.Owner = owner; + + var dialogResult = window.ShowDialog(); + if (dialogResult.HasValue && dialogResult.Value) + { + SaveConfig(); + LoadConfig(); + } + } + + private void SaveConfig() + { + try + { + var configFile = Path.Combine(PluginHelper.PluginFolder, Config.Default.ConfigFile); + JsonUtils.SerializeToFile(PluginConfig, configFile); + PluginConfig?.CommitChanges(); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(SaveConfig)}\r\n{ex.Message}"); + } + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Globalization/en-US/en-US.xaml b/Plugins/Discord/source/Plugin.Discord/Globalization/en-US/en-US.xaml new file mode 100644 index 00000000..8b6f079e --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Globalization/en-US/en-US.xaml @@ -0,0 +1,119 @@ + + + + Cancel + Close + OK + Save + + (Beta Mode) + A new version of the plugin is available. + Download the latest version. + + + + Discord Plugin Configuration + + View the Patch Notes. + Add Profile + Delete All Profiles + Delete Profile + Edit Profile + + Name + + Add Action Error + An error occurred while trying to add a profile. + Confirm Delete All Action + Click 'Yes' to confirm you want to delete all. + Delete All Action Error + An error occurred while trying to delete all profiles. + Confirm Close Action + Click 'Yes' to confirm you want to close the form. If you have any unsaved changes they will be lost. + Confirm Delete Action + Click 'Yes' to confirm you want to perform the delete. + Delete Action Error + An error occurred while trying to delete the profile. + Download Action Error + An error occurred while trying to perform the download. + Download Successful + The latest version has been saved to your desktop. + Edit Action Error + An error occurred while trying to edit a profile. + Save Action Error + An error occurred while trying to perform the save. + + + + Profile Configuration + + Name: + The name of your config profile. + Enabled + Is the config profile enabled. + Webhook Url: + This is your Webhook Url provided by discord. + Bot Name: + This will override the name you setup in your webhook. Leave blank to use default. + Use Text to Speech (TTS) + If enabled, will make the bot announce with Text to Speech, otherwise will turn off Text to Speech. + Prefix Message with Server Manager Profile Name + If enabled, the alert message will be sent prefixed with the server manager profile name. + Message Options + Bold + If enabled, will show the message in bold. + Underline + If enabled, will show the message with an underline. + Italic + If enabled, will show the message in italic. + Embedded Code Block + CIf enabled, will show the message in an embedded code block. + Profile Names + Alert Types + + Add Server Manager Profile Name + Delete All Server Manager Profile Names + Delete Server Manager Profile Name + + Add Alert Type + Delete All Alert Types + Delete Alert Type + + Server Manager Profile Name + Alert Type + + Test + + Add Action Error + An error occurred while trying to add an alert type. + An error occurred while trying to add a profile name. + Confirm Delete All Action + Click 'Yes' to confirm you want to delete all. + Delete All Action Error + An error occurred while trying to delete all alert types. + An error occurred while trying to delete all profile names. + Confirm Close Action + Click 'Yes' to confirm you want to close the form. If you have any unsaved changes they will be lost. + Confirm Delete Action + Click 'Yes' to confirm you want to perform the delete. + Delete Action Error + An error occurred while trying to delete the alert type. + An error occurred while trying to delete the profile name. + Test Action Error + An error occurred while trying to test the config profile. + The profile is not enabled and cannot be tested. + + + + Discord Plugin Version Details + Load Feed Error + + Version: + Select the version to view details. + + + \ No newline at end of file diff --git a/Plugins/Discord/source/Plugin.Discord/Interfaces/IBindable.cs b/Plugins/Discord/source/Plugin.Discord/Interfaces/IBindable.cs new file mode 100644 index 00000000..2978db18 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Interfaces/IBindable.cs @@ -0,0 +1,15 @@ +namespace ServerManagerTool.Plugin.Discord +{ + internal interface IBindable + { + bool HasChanges { get; set; } + + bool HasAnyChanges { get; } + + void CommitChanges(); + + void BeginUpdate(); + + void EndUpdate(); + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Lib/BrowserBehavior.cs b/Plugins/Discord/source/Plugin.Discord/Lib/BrowserBehavior.cs new file mode 100644 index 00000000..6b8e2099 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Lib/BrowserBehavior.cs @@ -0,0 +1,32 @@ +using System.Windows; +using System.Windows.Controls; + +namespace ServerManagerTool.Plugin.Discord +{ + public static class BrowserBehavior + { + public static readonly DependencyProperty HtmlProperty = DependencyProperty.RegisterAttached("Html", typeof(string), typeof(BrowserBehavior), new FrameworkPropertyMetadata(OnHtmlChanged)); + + [AttachedPropertyBrowsableForType(typeof(WebBrowser))] + public static string GetHtml(WebBrowser d) + { + return (string)d.GetValue(HtmlProperty); + } + + public static void SetHtml(WebBrowser d, string value) + { + d.SetValue(HtmlProperty, value); + } + + static void OnHtmlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (e.NewValue == null) + return; + + if (d is WebBrowser wb) + { + wb.NavigateToString(e.NewValue as string); + } + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/AlertTypeValue.cs b/Plugins/Discord/source/Plugin.Discord/Models/AlertTypeValue.cs new file mode 100644 index 00000000..5d408bd0 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/AlertTypeValue.cs @@ -0,0 +1,55 @@ +using ServerManagerTool.Plugin.Common; +using System.Runtime.Serialization; + +namespace ServerManagerTool.Plugin.Discord +{ + [DataContract] + internal class AlertTypeValue : Bindable + { + public AlertTypeValue() + : base() + { + Value = AlertType.Error; + OriginalValue = Value; + HasChanges = false; + } + + public AlertTypeValue(AlertType value) + : base() + { + Value = value; + OriginalValue = Value; + HasChanges = !Value.Equals(OriginalValue); + } + + public AlertTypeValue(AlertType value, AlertType originalValue) + : base() + { + Value = value; + OriginalValue = originalValue; + HasChanges = !Value.Equals(OriginalValue); + } + + [DataMember] + public AlertType Value + { + get { return Get(); } + set { Set(value); } + } + + public AlertType OriginalValue + { + get; + set; + } + + public override bool HasAnyChanges => base.HasChanges && !Value.Equals(OriginalValue); + + public override void CommitChanges() + { + base.CommitChanges(); + + OriginalValue = Value; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/AlertTypeValueList.cs b/Plugins/Discord/source/Plugin.Discord/Models/AlertTypeValueList.cs new file mode 100644 index 00000000..bbe47fdf --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/AlertTypeValueList.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace ServerManagerTool.Plugin.Discord +{ + internal class AlertTypeValueList : List, IBindable, INotifyCollectionChanged + { + private bool _hasChanges = false; + + public bool HasChanges + { + get => _hasChanges; + set => _hasChanges = value; + } + + public bool HasAnyChanges => _hasChanges || this.Any(a => a?.HasAnyChanges ?? false); + + public void BeginUpdate() + { + } + + public void CommitChanges() + { + HasChanges = false; + + foreach (var alertType in this) + { + alertType.CommitChanges(); + } + } + + public void EndUpdate() + { + } + + #region INotifyCollectionChanged + public event NotifyCollectionChangedEventHandler CollectionChanged; + #endregion + + protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + public void NotifyAdd(AlertTypeValue item, bool setChanged = true) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + if (setChanged) + HasChanges = true; + } + + public void NotifyClear(bool setChanged = true) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + if (setChanged) + HasChanges = true; + } + + public void NotifyRemove(AlertTypeValue item, int index, bool setChanged = true) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + if (setChanged) + HasChanges = true; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/Bindable.cs b/Plugins/Discord/source/Plugin.Discord/Models/Bindable.cs new file mode 100644 index 00000000..8b12e9f4 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/Bindable.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace ServerManagerTool.Plugin.Discord +{ + [DataContract] + internal class Bindable : INotifyPropertyChanged, IBindable + { + private Dictionary _properties = new Dictionary(); + + protected bool _isUpdating = false; + + public Bindable() + { + _properties = new Dictionary(); + } + + protected T Get([CallerMemberName] string name = null) + { + object value = null; + if (_properties?.TryGetValue(name, out value) ?? false) + return value == null ? default(T) : (T)value; + return default(T); + } + + protected void Set(T value, bool setChanged = true, [CallerMemberName] string name = null) + { + if (Equals(value, Get(name))) + return; + if (_properties == null) + _properties = new Dictionary(); + _properties[name] = value; + OnPropertyChanged(name); + if (!_isUpdating && setChanged) + HasChanges = true; + } + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #region INotifyPropertyChanged + public event PropertyChangedEventHandler PropertyChanged; + #endregion + + #region IBindable + public bool HasChanges + { + get { return Get(); } + set { Set(value, false); } + } + + public virtual bool HasAnyChanges => HasChanges || (_properties?.Any(p => (p.Value as IBindable)?.HasAnyChanges ?? false) ?? false); + + public virtual void CommitChanges() + { + HasChanges = false; + + if (_properties == null) + return; + + foreach (var property in _properties) + { + var bindable = property.Value as IBindable; + if (bindable != null) + bindable.CommitChanges(); + } + } + + public void BeginUpdate() + { + _isUpdating = true; + } + + public void EndUpdate() + { + _isUpdating = false; + } + #endregion + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/ConfigProfile.cs b/Plugins/Discord/source/Plugin.Discord/Models/ConfigProfile.cs new file mode 100644 index 00000000..2dfd109d --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/ConfigProfile.cs @@ -0,0 +1,188 @@ +using System.Runtime.Serialization; + +namespace ServerManagerTool.Plugin.Discord +{ + [DataContract] + internal sealed class ConfigProfile : Bindable + { + public ConfigProfile() + : base() + { + Name = "New Discord Profile"; + ProfileNames = new ProfileNameValueList(); + AlertTypes = new AlertTypeValueList(); + DiscordWebhookUrl = string.Empty; + DiscordBotName = string.Empty; + DiscordUseTTS = false; + PrefixMessageWithProfileName = false; + MessageBold = false; + MessageUnderlined = false; + MessageItalic = false; + MessageCodeBlock = false; + IsEnabled = true; + } + + [DataMember] + public string Name + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public ProfileNameValueList ProfileNames + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public AlertTypeValueList AlertTypes + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public string DiscordWebhookUrl + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public string DiscordBotName + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public bool DiscordUseTTS + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public bool PrefixMessageWithProfileName + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public bool IsEnabled + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public bool MessageBold + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public bool MessageUnderlined + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public bool MessageItalic + { + get { return Get(); } + set { Set(value); } + } + + [DataMember] + public bool MessageCodeBlock + { + get { return Get(); } + set { Set(value); } + } + + public ConfigProfile Clone() + { + var clone = new ConfigProfile(); + clone.Name = this.Name; + + foreach (var profileName in this.ProfileNames) + { + clone.ProfileNames.Add(new ProfileNameValue(profileName.Value, profileName.OriginalValue) { HasChanges = profileName.HasChanges }); + } + clone.ProfileNames.HasChanges = this.ProfileNames.HasChanges; + foreach (var alertType in this.AlertTypes) + { + clone.AlertTypes.Add(new AlertTypeValue(alertType.Value, alertType.OriginalValue) { HasChanges = alertType.HasChanges }); + } + clone.AlertTypes.HasChanges = this.AlertTypes.HasChanges; + + clone.DiscordWebhookUrl = this.DiscordWebhookUrl; + clone.DiscordBotName = this.DiscordBotName; + clone.DiscordUseTTS = this.DiscordUseTTS; + clone.PrefixMessageWithProfileName = this.PrefixMessageWithProfileName; + clone.MessageBold = this.MessageBold; + clone.MessageUnderlined = this.MessageUnderlined; + clone.MessageItalic = this.MessageItalic; + clone.MessageCodeBlock = this.MessageCodeBlock; + clone.IsEnabled = this.IsEnabled; + clone.HasChanges = this.HasChanges; + return clone; + } + + public void CopyFrom(ConfigProfile source) + { + if (source == null) + return; + + try + { + this.BeginUpdate(); + + this.Name = source.Name; + + this.ProfileNames.BeginUpdate(); + this.ProfileNames.Clear(); + foreach (var profileName in source.ProfileNames) + { + this.ProfileNames.Add(new ProfileNameValue(profileName.Value, profileName.OriginalValue)); + } + if (source.ProfileNames.HasChanges) + this.ProfileNames.HasChanges = true; + this.ProfileNames.EndUpdate(); + + this.AlertTypes.BeginUpdate(); + this.AlertTypes.Clear(); + foreach (var alertType in source.AlertTypes) + { + this.AlertTypes.Add(new AlertTypeValue(alertType.Value, alertType.OriginalValue)); + } + if (source.AlertTypes.HasChanges) + this.AlertTypes.HasChanges = true; + this.AlertTypes.EndUpdate(); + + this.DiscordWebhookUrl = source.DiscordWebhookUrl; + this.DiscordBotName = source.DiscordBotName; + this.DiscordUseTTS = source.DiscordUseTTS; + this.PrefixMessageWithProfileName = source.PrefixMessageWithProfileName; + this.MessageBold = source.MessageBold; + this.MessageUnderlined = source.MessageUnderlined; + this.MessageItalic = source.MessageItalic; + this.MessageCodeBlock = source.MessageCodeBlock; + this.IsEnabled = source.IsEnabled; + + if (source.HasChanges) + this.HasChanges = true; + } + finally + { + this.EndUpdate(); + } + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/DiscordPluginConfig.cs b/Plugins/Discord/source/Plugin.Discord/Models/DiscordPluginConfig.cs new file mode 100644 index 00000000..b0705f91 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/DiscordPluginConfig.cs @@ -0,0 +1,35 @@ +using System; +using System.Runtime.Serialization; + +namespace ServerManagerTool.Plugin.Discord +{ + [DataContract] + internal sealed class DiscordPluginConfig : Bindable + { + public DiscordPluginConfig() + : base() + { + LastCallHome = DateTime.MinValue; + ConfigProfiles = new ObservableList(); + } + + [DataMember] + public DateTime LastCallHome + { + get; + set; + } + + [DataMember] + public ObservableList ConfigProfiles + { + get { return Get>(); } + set { Set(value); } + } + + public override bool HasAnyChanges + { + get => base.HasChanges || (ConfigProfiles?.HasAnyChanges ?? false); + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/ObservableList.cs b/Plugins/Discord/source/Plugin.Discord/Models/ObservableList.cs new file mode 100644 index 00000000..8b8e1850 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/ObservableList.cs @@ -0,0 +1,131 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Runtime.Serialization; + +namespace ServerManagerTool.Plugin.Discord +{ + [DataContract] + internal class ObservableList : Bindable, IList, INotifyCollectionChanged + { + private List _listObject = null; + + public ObservableList() + : base() + { + _listObject = new List(); + CommitChanges(); + } + + public override bool HasAnyChanges => base.HasChanges || (_listObject?.Any(i => (i as IBindable)?.HasAnyChanges ?? false) ?? false); + + public override void CommitChanges() + { + base.CommitChanges(); + + if (_listObject == null) + return; + + foreach (T item in _listObject) + { + var bindable = item as IBindable; + if (bindable != null) + bindable.CommitChanges(); + } + } + + [DataMember] + internal List List + { + get => _listObject; + set => _listObject = value; + } + + #region IList + public T this[int index] + { + get => _listObject[index]; + set + { + T oldValue = _listObject[index]; + _listObject[index] = value; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue)); + } + } + + public int Count => _listObject.Count; + + public bool IsReadOnly => false; + + public void Add(T item) + { + _listObject.Add(item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + } + + public void Clear() + { + _listObject.Clear(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public bool Contains(T item) + { + return _listObject.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _listObject.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _listObject.GetEnumerator(); + } + + public int IndexOf(T item) + { + return _listObject.IndexOf(item); + } + + public void Insert(int index, T item) + { + _listObject.Insert(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + } + + public bool Remove(T item) + { + int index = _listObject.IndexOf(item); + var result = _listObject.Remove(item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + return result; + } + + public void RemoveAt(int index) + { + T item = _listObject[index]; + _listObject.RemoveAt(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _listObject.GetEnumerator(); + } + #endregion + + #region INotifyCollectionChanged + public event NotifyCollectionChangedEventHandler CollectionChanged; + + protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + if (!_isUpdating) + HasChanges = true; + } + #endregion + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/ProfileNameValue.cs b/Plugins/Discord/source/Plugin.Discord/Models/ProfileNameValue.cs new file mode 100644 index 00000000..e505f555 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/ProfileNameValue.cs @@ -0,0 +1,54 @@ +using System.Runtime.Serialization; + +namespace ServerManagerTool.Plugin.Discord +{ + [DataContract] + internal class ProfileNameValue : Bindable + { + public ProfileNameValue() + : base() + { + Value = string.Empty; + OriginalValue = Value; + HasChanges = false; + } + + public ProfileNameValue(string value) + : base() + { + Value = value; + OriginalValue = Value; + HasChanges = !Value.Equals(OriginalValue); + } + + public ProfileNameValue(string value, string originalValue) + : base() + { + Value = value; + OriginalValue = originalValue; + HasChanges = !Value.Equals(OriginalValue); + } + + [DataMember] + public string Value + { + get { return Get(); } + set { Set(value); } + } + + public string OriginalValue + { + get; + set; + } + + public override bool HasAnyChanges => base.HasChanges && !Value.Equals(OriginalValue); + + public override void CommitChanges() + { + base.CommitChanges(); + + OriginalValue = Value; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/ProfileNameValueList.cs b/Plugins/Discord/source/Plugin.Discord/Models/ProfileNameValueList.cs new file mode 100644 index 00000000..9101cabb --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/ProfileNameValueList.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace ServerManagerTool.Plugin.Discord +{ + internal class ProfileNameValueList : List, IBindable, INotifyCollectionChanged + { + private bool _hasChanges = false; + + public bool HasChanges + { + get => _hasChanges; + set => _hasChanges = value; + } + + public bool HasAnyChanges => _hasChanges || this.Any(p => p?.HasAnyChanges ?? false); + + public void BeginUpdate() + { + } + + public void CommitChanges() + { + HasChanges = false; + + foreach (var profileName in this) + { + profileName.CommitChanges(); + } + } + + public void EndUpdate() + { + } + + #region INotifyCollectionChanged + public event NotifyCollectionChangedEventHandler CollectionChanged; + #endregion + + protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + public void NotifyAdd(ProfileNameValue item, bool setChanged = true) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + if (setChanged) + HasChanges = true; + } + + public void NotifyClear(bool setChanged = true) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + if (setChanged) + HasChanges = true; + } + + public void NotifyRemove(ProfileNameValue item, int index, bool setChanged = true) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + if (setChanged) + HasChanges = true; + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/VersionFeed.cs b/Plugins/Discord/source/Plugin.Discord/Models/VersionFeed.cs new file mode 100644 index 00000000..db41f723 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/VersionFeed.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace ServerManagerTool.Plugin.Discord +{ + public class VersionFeed + { + public string Id { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string SubTitle { get; set; } = string.Empty; + public Uri Link { get; set; } = null; + public DateTimeOffset Updated { get; set; } = DateTimeOffset.Now; + + public List Entries { get; set; } = new List(); + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Models/VersionFeedEntry.cs b/Plugins/Discord/source/Plugin.Discord/Models/VersionFeedEntry.cs new file mode 100644 index 00000000..6883a4fd --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Models/VersionFeedEntry.cs @@ -0,0 +1,17 @@ +using System; + +namespace ServerManagerTool.Plugin.Discord +{ + public class VersionFeedEntry + { + public string Id { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public Uri Link { get; set; } = null; + public DateTimeOffset Updated { get; set; } = DateTimeOffset.Now; + public string Content { get; set; } = string.Empty; + public string Author { get; set; } = string.Empty; + + public bool IsCurrent { get; set; } = false; + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Plugin.Discord.csproj b/Plugins/Discord/source/Plugin.Discord/Plugin.Discord.csproj new file mode 100644 index 00000000..24bec9e6 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Plugin.Discord.csproj @@ -0,0 +1,73 @@ + + + %24/Development/ServerManagers/Main/Plugin.Discord + {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C} + https://dev.azure.com/bretthewitson + . + + + net462 + false + Art\favicon.ico + ServerManager.Plugin.Discord + ServerManagerTool.Plugin.Discord + + + none + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Config.settings + + + + + SettingsSingleFileGenerator + Config.Designer.cs + + + \ No newline at end of file diff --git a/Plugins/Discord/source/Plugin.Discord/Properties/AssemblyInfo.cs b/Plugins/Discord/source/Plugin.Discord/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..e63e3f68 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ServerManager Discord Plugin")] +[assembly: AssemblyDescription("A Discord plugin that can be used with the server managers.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Bletch1971")] +[assembly: AssemblyProduct("Server Managers")] +[assembly: AssemblyCopyright("Copyright © 2015-2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("936ef260-fecf-4e9e-a21e-092d65931c7d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.16.1")] +[assembly: AssemblyFileVersion("1.0.16.1")] diff --git a/Plugins/Discord/source/Plugin.Discord/Utils/NetworkUtils.cs b/Plugins/Discord/source/Plugin.Discord/Utils/NetworkUtils.cs new file mode 100644 index 00000000..0590e7d4 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Utils/NetworkUtils.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Threading.Tasks; + +namespace ServerManagerTool.Plugin.Discord +{ + internal static class NetworkUtils + { + public static async Task DiscoverPublicIPAsync() + { + using (var webClient = new WebClient()) + { + try + { + var publicIP = await webClient.DownloadStringTaskAsync(Config.Default.PublicIPCheckUrl); + + if (IPAddress.TryParse(publicIP, out IPAddress address)) + return address; + + return IPAddress.None; + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(DiscoverPublicIPAsync)}\r\n{ex.Message}"); + return IPAddress.None; + } + } + } + + public static async Task CheckLatestVersionAsync(bool betaEnabled) + { + try + { + using (var webClient = new WebClient()) + { + string latestVersion = null; + + if (betaEnabled) + latestVersion = await webClient.DownloadStringTaskAsync(Config.Default.LatestBetaVersionUrl); + else + latestVersion = await webClient.DownloadStringTaskAsync(Config.Default.LatestVersionUrl); + + if (Version.TryParse(latestVersion, out Version version)) + return version; + + return new Version(); + } + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(CheckLatestVersionAsync)}\r\n{ex.Message}"); + return new Version(); + } + } + + public static bool DownloadLatestVersion(string sourceUrl, string destinationFile) + { + try + { + using (var client = new WebClient()) + { + client.DownloadFile(sourceUrl, destinationFile); + return true; + } + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(DownloadLatestVersion)}\r\n{ex.Message}"); + return false; + } + } + + public static async Task PerformCallToAPIAsync(string pluginCode, IPAddress ipAddress) + { + try + { + using (var client = new WebClient()) + { + var url = string.Format(Config.Default.PluginCallUrlFormat, pluginCode, ipAddress); + await client.DownloadStringTaskAsync(url); + } + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(PerformCallToAPIAsync)} - {pluginCode}; {ipAddress}\r\n{ex.Message}"); + } + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Utils/TaskUtils.cs b/Plugins/Discord/source/Plugin.Discord/Utils/TaskUtils.cs new file mode 100644 index 00000000..52439873 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Utils/TaskUtils.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; + +namespace ServerManagerTool.Plugin.Discord +{ + internal static class TaskUtils + { + public static readonly Task FinishedTask = Task.FromResult(true); + + public static void DoNotWait(this Task task) + { + // Do nothing, let the task continue. Eliminates compiler warning about non-awaited tasks in an async method. + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Utils/VersionFeedUtils.cs b/Plugins/Discord/source/Plugin.Discord/Utils/VersionFeedUtils.cs new file mode 100644 index 00000000..3059e059 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Utils/VersionFeedUtils.cs @@ -0,0 +1,53 @@ +using System; +using System.ServiceModel.Syndication; +using System.Xml; + +namespace ServerManagerTool.Plugin.Discord +{ + public static class VersionFeedUtils + { + public static VersionFeed LoadVersionFeed(string inputUri, string currentVersion) + { + try + { + var reader = XmlReader.Create(inputUri); + var feed = SyndicationFeed.Load(reader); + + var versionFeed = new VersionFeed + { + Id = feed.Id, + Title = feed.Title?.Text, + SubTitle = feed.Description?.Text, + Link = feed.Links?[0].Uri, + Updated = feed.LastUpdatedTime.ToLocalTime(), + }; + + //Loop through all items in the SyndicationFeed + foreach (var item in feed.Items) + { + var textContent = item.Content as TextSyndicationContent; + + var versionFeedEntry = new VersionFeedEntry + { + Id = item.Id, + Title = item.Title?.Text, + Summary = item.Summary?.Text, + Link = item.Links?[0].Uri, + Updated = item.LastUpdatedTime.ToLocalTime(), + Content = textContent?.Text, + Author = item.Authors?[0].Name, + + IsCurrent = (item.Summary?.Text ?? string.Empty).Equals(currentVersion), + }; + versionFeed.Entries.Add(versionFeedEntry); + } + + return versionFeed; + } + catch (Exception) + { + return new VersionFeed(); + } + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/VersionFeed.xml b/Plugins/Discord/source/Plugin.Discord/VersionFeed.xml new file mode 100644 index 00000000..d2ece481 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/VersionFeed.xml @@ -0,0 +1,292 @@ + + + + urn:uuid:C93B24FC-C8DB-44F3-8B6F-A27A52E7AF9F + Discord Plugin Version Feed + This is the Discord Plugin release version feed. + + 2020-06-12T00:00:01Z + + + urn:uuid:A7158CB1-C5B2-4505-B171-CDD54918B227 + 1.0.16 (1.0.16.1) + 1.0.16.1 + + 2020-06-12T00:00:01Z + +
+

+ BUGFIX +
+

    +
  • Added url encoding so that special characters are sent to the webhook correctly.
  • +
  • Changed the text encoding from ASCII to UTF8, so that unicode languages should work correctly.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:6ADCC571-3933-44E1-91FB-4DC8F8B836F2 + 1.0.15 (1.0.15.1) + 1.0.15.1 + + 2020-03-03T00:00:01Z + +
+

+ CHANGE +
+

    +
  • Config Save - nows creates a backup file (.bak) of the config file before the new changes are saved.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:6ADCC571-3933-44E1-91FB-4DC8F8B836F2 + 1.0.14 (1.0.14.2) + 1.0.14.2 + + 2018-08-16T00:00:01Z + +
+

+ CHANGE +
+

    +
  • Download Location Changed - Have moved the server manager files to a new location for hosting.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:6ADCC571-3933-44E1-91FB-4DC8F8B836F2 + 1.0.13 (1.0.13.1) + 1.0.13.1 + + 2018-06-13T04:40:00Z + +
+

+ NEW +
+

    +
  • Message Options - Added new message options to display the messages in Bold, Italic, Underlined and Embedded Code Block.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:21C68E55-E915-4337-8CFB-7E96FE6967CB + 1.0.12 (1.0.12.1) + 1.0.12.1 + + 2018-05-24T00:20:00Z + +
+

+ NEW +
+

    +
  • Added version feed window.
  • +
+ CHANGE +
+
    +
  • DotNet Framework updated.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:C1ED0129-58CF-4AA4-8203-DDFA199E17DE + 1.0.11 (1.0.11.0) + 1.0.11.0 + + 2018-05-24T00:20:00Z + +
+

+ CHANGE +
+

    +
  • Icons - all icons have be cleaned up and updated.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:58894ADA-FDC4-4F5A-A904-B81D9130FD00 + 1.0.10 (1.0.10.0) + 1.0.10.0 + + 2018-05-24T00:20:00Z + +
+

+ BUGFIX +
+

    +
  • Attempt to fix the save config bug, where the config file is blank.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:20DB658C-44FA-4642-923B-A94926B0FDC3 + 1.0.9 (1.0.9.0) + 1.0.9.0 + + 2018-05-24T00:20:00Z + +
+

+ BUGFIX +
+

    +
  • Some minor bugfixes and code cleanup.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:3B0D107D-77DB-4E70-BE8A-336A20A55F8A + 1.0.8 (1.0.8.0) + 1.0.8.0 + + 2018-05-24T00:20:00Z + +
+

+ CHANGE +
+

    +
  • Changed the download location of the plugin.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:CDA4857D-FC4A-44AB-A11E-9CC1CDA11BCD + 1.0.7 (1.0.7.0) + 1.0.7.0 + + 2018-05-24T00:20:00Z + +
+

+ CHANGE +
+

    +
  • Minor code clean-up and tweaks.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:C6CEC6B2-E405-4DF6-B991-FC1D4EEDF3E8 + 1.0.6 (1.0.6.0) + 1.0.6.0 + + 2018-05-24T00:20:00Z + +
+

+ CHANGE +
+

    +
  • Have changed the storage host for the discord plugin.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ + + urn:uuid:9C4A9557-3A74-428F-A22C-727CBBFB6E91 + 1.0.5 (1.0.5.0) + 1.0.5.0 + + 2018-05-24T00:20:00Z + +
+

+ NEW +
+

    +
  • New discord plugin to send Server Manager alerts to one or more channels on your discord server.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ +
\ No newline at end of file diff --git a/Plugins/Discord/source/Plugin.Discord/VersionFeedBeta.xml b/Plugins/Discord/source/Plugin.Discord/VersionFeedBeta.xml new file mode 100644 index 00000000..1c08b222 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/VersionFeedBeta.xml @@ -0,0 +1,34 @@ + + + + urn:uuid:6BB39661-8638-425E-B5F7-0C75AACC1D26 + Discord Plugin Version Feed + This is the Discord Plugin beta version feed. + + 2020-06-12T00:00:01Z + + + urn:uuid:A7158CB1-C5B2-4505-B171-CDD54918B227 + 1.0.16 (1.0.16.1) + 1.0.16.1 + + 2020-06-12T00:00:01Z + +
+

+ BUGFIX +
+

    +
  • Added url encoding so that special characters are sent to the webhook correctly.
  • +
  • Changed the text encoding from ASCII to UTF8, so that unicode languages should work correctly.
  • +
+

+
+
+ + bletch + bletch1971@hotmail.com + +
+ +
\ No newline at end of file diff --git a/Plugins/Discord/source/Plugin.Discord/Windows/ConfigProfileWindow.xaml b/Plugins/Discord/source/Plugin.Discord/Windows/ConfigProfileWindow.xaml new file mode 100644 index 00000000..d28bd268 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Windows/ConfigProfileWindow.xaml @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Plugins/Discord/source/Plugin.Discord/Windows/ConfigProfileWindow.xaml.cs b/Plugins/Discord/source/Plugin.Discord/Windows/ConfigProfileWindow.xaml.cs new file mode 100644 index 00000000..4199e97f --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Windows/ConfigProfileWindow.xaml.cs @@ -0,0 +1,223 @@ +using ServerManagerTool.Plugin.Common; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace ServerManagerTool.Plugin.Discord.Windows +{ + /// + /// Interaction logic for ConfigProfileWindow.xaml + /// + public partial class ConfigProfileWindow : Window + { + private static readonly DependencyProperty ProfileProperty = DependencyProperty.Register(nameof(Profile), typeof(ConfigProfile), typeof(ConfigProfileWindow)); + + internal ConfigProfileWindow(DiscordPlugin plugin, ConfigProfile profile) + { + this.Plugin = plugin ?? new DiscordPlugin(); + this.OriginalProfile = profile; + this.Profile = profile.Clone(); + this.Profile.CommitChanges(); + + InitializeComponent(); + + if (plugin.BetaEnabled) + Title = $"{Title} {ResourceUtils.GetResourceString(this.Resources, "Global_BetaModeLabel")}"; + + this.DataContext = this; + } + + private ConfigProfile OriginalProfile + { + get; + set; + } + + private ConfigProfile Profile + { + get { return GetValue(ProfileProperty) as ConfigProfile; } + set { SetValue(ProfileProperty, value); } + } + + private DiscordPlugin Plugin + { + get; + set; + } + + private void ConfigProfileWindow_Closing(object sender, CancelEventArgs e) + { + if (DialogResult.HasValue && DialogResult.Value) + return; + + if (this.Profile.HasAnyChanges) + { + if (MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_CloseLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_CloseTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) + e.Cancel = true; + } + } + + private void ComboBox_PreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + var comboBox = sender as ComboBox; + if (comboBox == null) + return; + + if (comboBox.IsDropDownOpen) + return; + + e.Handled = true; + } + + private void Ok_Click(object sender, RoutedEventArgs e) + { + if (this.Profile.HasAnyChanges) + { + this.OriginalProfile.CopyFrom(this.Profile); + } + + DialogResult = true; + Close(); + } + + private void Test_Click(object sender, RoutedEventArgs e) + { + if (!Profile.IsEnabled) + { + MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_TestEnabledErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_TestErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + try + { + foreach (var profileName in Profile.ProfileNames) + { + foreach (var alertType in Profile.AlertTypes) + { + Plugin.HandleAlert(Profile, alertType.Value, profileName.Value, $"Test '{alertType.Value}' message for profile name '{profileName.Value}'."); + Task.Delay(1000).Wait(); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(Test_Click)}\r\n{ex.Message}"); + MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_TestErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_TestErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void AddAlertType_Click(object sender, RoutedEventArgs e) + { + try + { + var alertType = new AlertTypeValue(AlertType.Error); + + Profile.AlertTypes.Add(alertType); + Profile.AlertTypes.NotifyAdd(alertType); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(AddAlertType_Click)}\r\n{ex.Message}"); + MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_AddAlertTypeErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_AddErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void ClearAlertTypes_Click(object sender, RoutedEventArgs e) + { + if (MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_ClearLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_ClearTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) + return; + + try + { + if (Profile.AlertTypes.Count == 0) + return; + + Profile.AlertTypes.Clear(); + Profile.AlertTypes.NotifyClear(); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(ClearAlertTypes_Click)}\r\n{ex.Message}"); + MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_ClearAlertTypesErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_ClearErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void DeleteAlertType_Click(object sender, RoutedEventArgs e) + { + if (MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_DeleteLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_DeleteTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) + return; + + try + { + var alertType = ((AlertTypeValue)((Button)e.Source).DataContext); + var index = Profile.AlertTypes.IndexOf(alertType); + Profile.AlertTypes.Remove(alertType); + Profile.AlertTypes.NotifyRemove(alertType, index); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(DeleteAlertType_Click)}\r\n{ex.Message}"); + MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_DeleteAlertTypeErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_DeleteErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void AddProfileName_Click(object sender, RoutedEventArgs e) + { + try + { + var profileName = new ProfileNameValue(); + + Profile.ProfileNames.Add(profileName); + Profile.ProfileNames.NotifyAdd(profileName); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(AddProfileName_Click)}\r\n{ex.Message}"); + MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_AddProfileNameErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_AddErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void ClearProfileNames_Click(object sender, RoutedEventArgs e) + { + if (MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_ClearLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_ClearTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) + return; + + try + { + if (Profile.ProfileNames.Count == 0) + return; + + Profile.ProfileNames.Clear(); + Profile.ProfileNames.NotifyClear(); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(ClearProfileNames_Click)}\r\n{ex.Message}"); + MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_ClearProfileNamesErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_ClearErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void DeleteProfileName_Click(object sender, RoutedEventArgs e) + { + if (MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_DeleteLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_DeleteTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) + return; + + try + { + var profileName = ((ProfileNameValue)((Button)e.Source).DataContext); + var index = Profile.ProfileNames.IndexOf(profileName); + Profile.ProfileNames.Remove(profileName); + Profile.ProfileNames.NotifyRemove(profileName, index); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR: {nameof(DeleteProfileName_Click)}\r\n{ex.Message}"); + MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_DeleteProfileNameErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigProfileWindow_DeleteErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } +} diff --git a/Plugins/Discord/source/Plugin.Discord/Windows/ConfigWindow.xaml b/Plugins/Discord/source/Plugin.Discord/Windows/ConfigWindow.xaml new file mode 100644 index 00000000..e9a2f129 --- /dev/null +++ b/Plugins/Discord/source/Plugin.Discord/Windows/ConfigWindow.xaml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +