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);
+
+ DataContractJsonSerializer 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);
+
+ DataContractJsonSerializer 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/Discord/source/Plugin.Discord/Windows/ConfigWindow.xaml.cs b/Plugins/Discord/source/Plugin.Discord/Windows/ConfigWindow.xaml.cs
new file mode 100644
index 00000000..050cb775
--- /dev/null
+++ b/Plugins/Discord/source/Plugin.Discord/Windows/ConfigWindow.xaml.cs
@@ -0,0 +1,271 @@
+using ServerManagerTool.Plugin.Common;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace ServerManagerTool.Plugin.Discord.Windows
+{
+ ///
+ /// Interaction logic for ConfigWindow.xaml
+ ///
+ public partial class ConfigWindow : Window
+ {
+ private static readonly DependencyProperty PluginConfigProperty = DependencyProperty.Register(nameof(PluginConfig), typeof(DiscordPluginConfig), typeof(ConfigWindow));
+ public static readonly DependencyProperty LatestVersionProperty = DependencyProperty.Register(nameof(LatestVersion), typeof(Version), typeof(ConfigWindow), new PropertyMetadata(new Version()));
+ public static readonly DependencyProperty NewVersionAvailableProperty = DependencyProperty.Register(nameof(NewVersionAvailable), typeof(bool), typeof(ConfigWindow), new PropertyMetadata(false));
+
+ internal ConfigWindow(DiscordPlugin plugin, DiscordPluginConfig pluginConfig)
+ {
+ this.Plugin = plugin ?? new DiscordPlugin();
+ this.PluginConfig = pluginConfig ?? new DiscordPluginConfig();
+
+ InitializeComponent();
+
+ if (plugin.BetaEnabled)
+ Title = $"{Title} {ResourceUtils.GetResourceString(this.Resources, "Global_BetaModeLabel")}";
+
+ this.DataContext = this;
+ }
+
+ private DiscordPlugin Plugin
+ {
+ get;
+ set;
+ }
+
+ private DiscordPluginConfig PluginConfig
+ {
+ get { return GetValue(PluginConfigProperty) as DiscordPluginConfig; }
+ set { SetValue(PluginConfigProperty, value); }
+ }
+
+ public Version LatestVersion
+ {
+ get { return (Version)GetValue(LatestVersionProperty); }
+ set { SetValue(LatestVersionProperty, value); }
+ }
+
+ public bool NewVersionAvailable
+ {
+ get { return (bool)GetValue(NewVersionAvailableProperty); }
+ set { SetValue(NewVersionAvailableProperty, value); }
+ }
+
+ private void ConfigWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+ {
+ if (DialogResult.HasValue && DialogResult.Value)
+ return;
+
+ if (PluginConfig.HasAnyChanges)
+ {
+ if (MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_CloseLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_CloseTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
+ e.Cancel = true;
+ }
+ }
+
+ private void ConfigWindow_Loaded(object sender, RoutedEventArgs e)
+ {
+ CheckLatestVersionAsync().DoNotWait();
+ }
+
+ private void DownloadPlugin_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ DownloadLatestVersion();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR: {nameof(DownloadPlugin_Click)}\r\n{ex.Message}");
+ MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_DownloadErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_DownloadErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void AddConfigProfile_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var profile = new ConfigProfile();
+
+ if (EditProfile(profile))
+ PluginConfig.ConfigProfiles.Add(profile);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR: {nameof(AddConfigProfile_Click)}\r\n{ex.Message}");
+ MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_AddErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_AddErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void ClearConfigProfiles_Click(object sender, RoutedEventArgs e)
+ {
+ if (MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_ClearLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_ClearTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
+ return;
+
+ try
+ {
+ if (PluginConfig.ConfigProfiles.Count == 0)
+ return;
+
+ PluginConfig.ConfigProfiles.Clear();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR: {nameof(ClearConfigProfiles_Click)}\r\n{ex.Message}");
+ MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_ClearErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_ClearErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void DeleteConfigProfile_Click(object sender, RoutedEventArgs e)
+ {
+ if (MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_DeleteLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_DeleteTitle"), MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
+ return;
+
+ try
+ {
+ var profile = ((ConfigProfile)((Button)e.Source).DataContext);
+ PluginConfig.ConfigProfiles.Remove(profile);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR: {nameof(DeleteConfigProfile_Click)}\r\n{ex.Message}");
+ MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_DeleteErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_DeleteErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void EditConfigProfile_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var profile = ((ConfigProfile)((Button)e.Source).DataContext);
+ EditProfile(profile);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR: {nameof(EditConfigProfile_Click)}\r\n{ex.Message}");
+ MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_EditErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_EditErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void PatchNotes_Click(object sender, RoutedEventArgs e)
+ {
+ var url = string.Empty;
+ if (Plugin.BetaEnabled)
+ url = Config.Default.VersionBetaFeedUrl;
+ else
+ url = Config.Default.VersionFeedUrl;
+
+ if (!string.IsNullOrWhiteSpace(url))
+ {
+ var window = new VersionFeedWindow(Plugin, url);
+ window.Owner = this;
+ window.ShowDialog();
+ this.BringIntoView();
+ }
+ }
+
+ private void Save_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ BackupExistingConfig();
+ SaveConfig();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR: {nameof(Save_Click)}\r\n{ex.Message}");
+ MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_SaveErrorLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_SaveErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void BackupExistingConfig()
+ {
+ var configFile = Path.Combine(PluginHelper.PluginFolder, Config.Default.ConfigFile);
+ if (!File.Exists(configFile))
+ return;
+
+ var backupFile = Path.ChangeExtension(configFile, "bak");
+
+ try
+ {
+ File.Copy(configFile, backupFile, true);
+ }
+ catch
+ {
+ // do nothing, just exit if cannot backup existing config file
+ throw;
+ }
+ }
+
+ private async Task CheckLatestVersionAsync()
+ {
+ try
+ {
+ var newVersion = await NetworkUtils.CheckLatestVersionAsync(Plugin.BetaEnabled);
+
+ this.LatestVersion = newVersion;
+ this.NewVersionAvailable = Plugin.PluginVersion < newVersion;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR: {nameof(CheckLatestVersionAsync)}\r\n{ex.Message}");
+ }
+ }
+
+ private void DownloadLatestVersion()
+ {
+ var cursor = this.Cursor;
+
+ try
+ {
+ this.Cursor = Cursors.Wait;
+ Task.Delay(500).Wait();
+
+ var latestZip = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), Config.Default.PluginZipFilename);
+
+ var sourceUrl = string.Empty;
+ if (Plugin.BetaEnabled)
+ sourceUrl = Config.Default.LatestBetaDownloadUrl;
+ else
+ sourceUrl = Config.Default.LatestDownloadUrl;
+
+ NetworkUtils.DownloadLatestVersion(sourceUrl, latestZip);
+
+ MessageBox.Show(ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_DownloadSuccessLabel"), ResourceUtils.GetResourceString(this.Resources, "ConfigWindow_DownloadSuccessTitle"), MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR: {nameof(DownloadLatestVersion)}\r\n{ex.Message}");
+ }
+ finally
+ {
+ this.Cursor = cursor;
+ }
+ }
+
+ private bool EditProfile(ConfigProfile profile)
+ {
+ if (profile == null)
+ return false;
+
+ var window = new ConfigProfileWindow(Plugin, profile);
+ window.Owner = this;
+
+ var dialogResult = window.ShowDialog();
+ this.BringIntoView();
+
+ return dialogResult.HasValue && dialogResult.Value;
+ }
+
+ private void SaveConfig()
+ {
+ var configFile = Path.Combine(PluginHelper.PluginFolder, Config.Default.ConfigFile);
+ JsonUtils.SerializeToFile(PluginConfig, configFile);
+ PluginConfig?.CommitChanges();
+ }
+ }
+}
diff --git a/Plugins/Discord/source/Plugin.Discord/Windows/VersionFeedWindow.xaml b/Plugins/Discord/source/Plugin.Discord/Windows/VersionFeedWindow.xaml
new file mode 100644
index 00000000..d79d3c10
--- /dev/null
+++ b/Plugins/Discord/source/Plugin.Discord/Windows/VersionFeedWindow.xaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/Discord/source/Plugin.Discord/Windows/VersionFeedWindow.xaml.cs b/Plugins/Discord/source/Plugin.Discord/Windows/VersionFeedWindow.xaml.cs
new file mode 100644
index 00000000..a30c9341
--- /dev/null
+++ b/Plugins/Discord/source/Plugin.Discord/Windows/VersionFeedWindow.xaml.cs
@@ -0,0 +1,81 @@
+using ServerManagerTool.Plugin.Common;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+
+namespace ServerManagerTool.Plugin.Discord.Windows
+{
+ ///
+ /// Interaction logic for VersionFeedWindow.xaml
+ ///
+ public partial class VersionFeedWindow : Window
+ {
+ public static readonly DependencyProperty FeedEntriesProperty = DependencyProperty.Register(nameof(FeedEntries), typeof(ObservableCollection), typeof(VersionFeedWindow), new PropertyMetadata(new ObservableCollection()));
+ public static readonly DependencyProperty SelectedFeedEntryProperty = DependencyProperty.Register(nameof(SelectedFeedEntry), typeof(VersionFeedEntry), typeof(VersionFeedWindow), new PropertyMetadata(null));
+
+ private string feedUri = string.Empty;
+
+ public VersionFeedWindow(DiscordPlugin plugin, string feedUri)
+ {
+ this.Plugin = plugin ?? new DiscordPlugin();
+ this.feedUri = feedUri;
+
+ InitializeComponent();
+
+ this.DataContext = this;
+ }
+
+ private DiscordPlugin Plugin
+ {
+ get;
+ set;
+ }
+
+ public ObservableCollection FeedEntries
+ {
+ get { return (ObservableCollection)GetValue(FeedEntriesProperty); }
+ set { SetValue(FeedEntriesProperty, value); }
+ }
+
+ public VersionFeedEntry SelectedFeedEntry
+ {
+ get { return (VersionFeedEntry)GetValue(SelectedFeedEntryProperty); }
+ set { SetValue(SelectedFeedEntryProperty, value); }
+ }
+
+ private void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ LoadFeed();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, ResourceUtils.GetResourceString(this.Resources, "VersionFeedWindow_Load_FailedTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void LoadFeed()
+ {
+ FeedEntries.Clear();
+
+ if (string.IsNullOrWhiteSpace(this.feedUri))
+ return;
+
+ var versionFeed = VersionFeedUtils.LoadVersionFeed(this.feedUri, Plugin.PluginVersion.ToString());
+ if (versionFeed == null)
+ return;
+
+ foreach (var entry in versionFeed.Entries)
+ {
+ if (entry == null)
+ continue;
+
+ FeedEntries.Add(entry);
+ }
+
+ SelectedFeedEntry = FeedEntries.OrderByDescending(e => e.Updated).FirstOrDefault();
+ }
+ }
+}
diff --git a/Plugins/Discord/source/Plugin.Discord/app.config b/Plugins/Discord/source/Plugin.Discord/app.config
new file mode 100644
index 00000000..a2c1109e
--- /dev/null
+++ b/Plugins/Discord/source/Plugin.Discord/app.config
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
diff --git a/Plugins/Discord/source/ServerManagerTool.Plugins.sln b/Plugins/Discord/source/ServerManagerTool.Plugins.sln
new file mode 100644
index 00000000..ad28bf27
--- /dev/null
+++ b/Plugins/Discord/source/ServerManagerTool.Plugins.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30225.117
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.Common", "Plugin.Common\Plugin.Common.csproj", "{FCFB17CF-BFE4-49AC-AB04-4990CACE6756}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.Discord", "Plugin.Discord\Plugin.Discord.csproj", "{ADD31DE3-6EFE-4BAA-932F-66E59F590E02}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {FCFB17CF-BFE4-49AC-AB04-4990CACE6756}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FCFB17CF-BFE4-49AC-AB04-4990CACE6756}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FCFB17CF-BFE4-49AC-AB04-4990CACE6756}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FCFB17CF-BFE4-49AC-AB04-4990CACE6756}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ADD31DE3-6EFE-4BAA-932F-66E59F590E02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ADD31DE3-6EFE-4BAA-932F-66E59F590E02}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ADD31DE3-6EFE-4BAA-932F-66E59F590E02}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ADD31DE3-6EFE-4BAA-932F-66E59F590E02}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9AB9121A-35E6-486F-9096-E2F08AD9CF18}
+ EndGlobalSection
+ GlobalSection(TeamFoundationVersionControl) = preSolution
+ SccNumberOfProjects = 3
+ SccEnterpriseProvider = {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C}
+ SccTeamFoundationServer = https://dev.azure.com/bretthewitson
+ SccLocalPath0 = .
+ SccProjectUniqueName1 = Plugin.Common\\Plugin.Common.csproj
+ SccProjectName1 = Plugin.Common
+ SccLocalPath1 = Plugin.Common
+ SccProjectUniqueName2 = Plugin.Discord\\Plugin.Discord.csproj
+ SccProjectName2 = Plugin.Discord
+ SccLocalPath2 = Plugin.Discord
+ EndGlobalSection
+EndGlobal