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 00000000..f5b8bc3e Binary files /dev/null and b/Plugins/Discord/source/Plugin.Discord/Art/Add.ico differ 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 00000000..81247582 Binary files /dev/null and b/Plugins/Discord/source/Plugin.Discord/Art/ChangeNotes.ico differ diff --git a/Plugins/Discord/source/Plugin.Discord/Art/Delete.ico b/Plugins/Discord/source/Plugin.Discord/Art/Delete.ico new file mode 100644 index 00000000..43936478 Binary files /dev/null and b/Plugins/Discord/source/Plugin.Discord/Art/Delete.ico differ diff --git a/Plugins/Discord/source/Plugin.Discord/Art/Download.ico b/Plugins/Discord/source/Plugin.Discord/Art/Download.ico new file mode 100644 index 00000000..e8713e50 Binary files /dev/null and b/Plugins/Discord/source/Plugin.Discord/Art/Download.ico differ diff --git a/Plugins/Discord/source/Plugin.Discord/Art/Edit.ico b/Plugins/Discord/source/Plugin.Discord/Art/Edit.ico new file mode 100644 index 00000000..fdafc83f Binary files /dev/null and b/Plugins/Discord/source/Plugin.Discord/Art/Edit.ico differ 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 00000000..1ce0205a Binary files /dev/null and b/Plugins/Discord/source/Plugin.Discord/Art/favicon.ico differ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +