Discord Plugin Source

Added discord plugin source to github
This commit is contained in:
Brett Hewitson 2020-07-11 13:09:27 +10:00
parent 4a163627b8
commit 6f671a9d57
50 changed files with 3673 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -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

View file

@ -0,0 +1,15 @@
namespace ServerManagerTool.Plugin.Common
{
public enum AlertType
{
Error,
Shutdown,
ShutdownMessage,
ShutdownReason,
Startup,
Backup,
UpdateResults,
ServerStatusChange,
ModUpdateDetected,
}
}

View file

@ -0,0 +1,47 @@
using System;
using System.Linq;
using System.Windows.Markup;
using System.Windows.Media.Imaging;
namespace ServerManagerTool.Plugin.Common
{
/// <summary>
/// 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}"
/// </summary>
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;
}
}
}

View file

@ -0,0 +1,13 @@
namespace ServerManagerTool.Plugin.Common
{
public interface IAlertPlugin : IPlugin
{
/// <summary>
/// Handles the alert message passed for the profile.
/// </summary>
/// <param name="alertType">The type of alert message.</param>
/// <param name="profileName">The name of the profile the alert message is associated with.</param>
/// <param name="alertMessage">The message of the alert.</param>
void HandleAlert(AlertType alertType, string profileName, string alertMessage);
}
}

View file

@ -0,0 +1,11 @@
namespace ServerManagerTool.Plugin.Common
{
public interface IBeta
{
bool BetaEnabled
{
get;
set;
}
}
}

View file

@ -0,0 +1,56 @@
using System;
using System.Windows;
namespace ServerManagerTool.Plugin.Common
{
public interface IPlugin
{
/// <summary>
/// Gets a values indicating if the plugin can be used
/// </summary>
bool Enabled
{
get;
}
/// <summary>
/// Gets a value indicating the code of the plugin
/// </summary>
string PluginCode
{
get;
}
/// <summary>
/// Gets a value indicating the name of the plugin
/// </summary>
string PluginName
{
get;
}
/// <summary>
/// Gets a value indicating the version of the plugin
/// </summary>
Version PluginVersion
{
get;
}
/// <summary>
/// Gets a value that indicates if the plugin has a configuration form.
/// </summary>
bool HasConfigForm
{
get;
}
/// <summary>
/// Performs any initialization for the plugin.
/// </summary>
void Initialize();
/// <summary>
/// Opens the configuration form.
/// </summary>
/// <param name="owner">The owner window.</param>
void OpenConfigForm(Window owner);
}
}

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Globals">
<SccProjectName>%24/Development/ServerManagers/Main/Plugin.Common</SccProjectName>
<SccProvider>{4CA58AB2-18FA-4F8D-95D4-32DDF27D184C}</SccProvider>
<SccAuxPath>https://dev.azure.com/bretthewitson</SccAuxPath>
<SccLocalPath>.</SccLocalPath>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<RootNamespace>ServerManagerTool.Plugin.Common</RootNamespace>
<AssemblyName>ServerManager.Plugin.Common</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System.Xaml" />
<Reference Include="WindowsBase" />
</ItemGroup>
</Project>

View file

@ -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)
{
}
}
}

View file

@ -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<PluginItem>();
}
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<PluginItem> 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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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")]

View file

@ -0,0 +1,64 @@
using System.IO;
using System.Runtime.Serialization.Json;
namespace ServerManagerTool.Plugin.Common
{
public static class JsonUtils
{
public static T DeserializeFromFile<T>(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>(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();
}
}
}
}

View file

@ -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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,158 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
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"]));
}
}
}
}

View file

@ -0,0 +1,48 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="ServerManagerTool.Plugin.Discord" GeneratedClassName="Config">
<Profiles />
<Settings>
<Setting Name="PluginCallUrlFormat" Type="System.String" Scope="Application">
<Value Profile="(Default)">http://servermanager.azurewebsites.net/api/plugin/call/{0}/{1}/</Value>
</Setting>
<Setting Name="PublicIPCheckUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">http://whatismyip.akamai.com/</Value>
</Setting>
<Setting Name="CallHomeDelay" Type="System.Int32" Scope="Application">
<Value Profile="(Default)">12</Value>
</Setting>
<Setting Name="RequestTimeout" Type="System.Int32" Scope="Application">
<Value Profile="(Default)">5000</Value>
</Setting>
<Setting Name="PluginCode" Type="System.String" Scope="Application">
<Value Profile="(Default)">1B745000-6389-4770-9509-C6A05E209323</Value>
</Setting>
<Setting Name="PluginName" Type="System.String" Scope="Application">
<Value Profile="(Default)">Discord Plugin</Value>
</Setting>
<Setting Name="LatestDownloadUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/latest.zip</Value>
</Setting>
<Setting Name="LatestVersionUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/latest.txt</Value>
</Setting>
<Setting Name="LatestBetaDownloadUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/latest.zip</Value>
</Setting>
<Setting Name="LatestBetaVersionUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/latest.txt</Value>
</Setting>
<Setting Name="PluginZipFilename" Type="System.String" Scope="Application">
<Value Profile="(Default)">ServerManager.Plugin.Discord.zip</Value>
</Setting>
<Setting Name="ConfigFile" Type="System.String" Scope="Application">
<Value Profile="(Default)">_discordplugin.cfg</Value>
</Setting>
<Setting Name="VersionFeedUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/VersionFeed.xml</Value>
</Setting>
<Setting Name="VersionBetaFeedUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/VersionFeed.xml</Value>
</Setting>
</Settings>
</SettingsFile>

View file

@ -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<DiscordPluginConfig>(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}");
}
}
}
}

View file

@ -0,0 +1,119 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
>
<!--#region Global -->
<sys:String x:Key="Global_CancelButtonLabel">Cancel</sys:String>
<sys:String x:Key="Global_CloseButtonLabel">Close</sys:String>
<sys:String x:Key="Global_OkButtonLabel">OK</sys:String>
<sys:String x:Key="Global_SaveButtonLabel">Save</sys:String>
<sys:String x:Key="Global_BetaModeLabel"> (Beta Mode)</sys:String>
<sys:String x:Key="Global_NewVersionAvailableLabel">A new version of the plugin is available.</sys:String>
<sys:String x:Key="Global_DownloadNewVersionTooltip">Download the latest version.</sys:String>
<!--#endregion-->
<!--#region Config Window -->
<sys:String x:Key="ConfigWindow_Title">Discord Plugin Configuration</sys:String>
<sys:String x:Key="ConfigWindow_PatchNotesTooltip">View the Patch Notes.</sys:String>
<sys:String x:Key="ConfigWindow_AddConfigProfileTooltip">Add Profile</sys:String>
<sys:String x:Key="ConfigWindow_ClearConfigProfilesTooltip">Delete All Profiles</sys:String>
<sys:String x:Key="ConfigWindow_DeleteConfigProfileTooltip">Delete Profile</sys:String>
<sys:String x:Key="ConfigWindow_EditConfigProfileTooltip">Edit Profile</sys:String>
<sys:String x:Key="ConfigWindow_NameColumnLabel">Name</sys:String>
<sys:String x:Key="ConfigWindow_AddErrorTitle">Add Action Error</sys:String>
<sys:String x:Key="ConfigWindow_AddErrorLabel">An error occurred while trying to add a profile.</sys:String>
<sys:String x:Key="ConfigWindow_ClearTitle">Confirm Delete All Action</sys:String>
<sys:String x:Key="ConfigWindow_ClearLabel">Click 'Yes' to confirm you want to delete all.</sys:String>
<sys:String x:Key="ConfigWindow_ClearErrorTitle">Delete All Action Error</sys:String>
<sys:String x:Key="ConfigWindow_ClearErrorLabel">An error occurred while trying to delete all profiles.</sys:String>
<sys:String x:Key="ConfigWindow_CloseTitle">Confirm Close Action</sys:String>
<sys:String x:Key="ConfigWindow_CloseLabel">Click 'Yes' to confirm you want to close the form. If you have any unsaved changes they will be lost.</sys:String>
<sys:String x:Key="ConfigWindow_DeleteTitle">Confirm Delete Action</sys:String>
<sys:String x:Key="ConfigWindow_DeleteLabel">Click 'Yes' to confirm you want to perform the delete.</sys:String>
<sys:String x:Key="ConfigWindow_DeleteErrorTitle">Delete Action Error</sys:String>
<sys:String x:Key="ConfigWindow_DeleteErrorLabel">An error occurred while trying to delete the profile.</sys:String>
<sys:String x:Key="ConfigWindow_DownloadErrorTitle">Download Action Error</sys:String>
<sys:String x:Key="ConfigWindow_DownloadErrorLabel">An error occurred while trying to perform the download.</sys:String>
<sys:String x:Key="ConfigWindow_DownloadSuccessTitle">Download Successful</sys:String>
<sys:String x:Key="ConfigWindow_DownloadSuccessLabel">The latest version has been saved to your desktop.</sys:String>
<sys:String x:Key="ConfigWindow_EditErrorTitle">Edit Action Error</sys:String>
<sys:String x:Key="ConfigWindow_EditErrorLabel">An error occurred while trying to edit a profile.</sys:String>
<sys:String x:Key="ConfigWindow_SaveErrorTitle">Save Action Error</sys:String>
<sys:String x:Key="ConfigWindow_SaveErrorLabel">An error occurred while trying to perform the save.</sys:String>
<!--#endregion-->
<!--#region Config Item Window -->
<sys:String x:Key="ConfigProfileWindow_Title">Profile Configuration</sys:String>
<sys:String x:Key="ConfigProfileWindow_NameLabel">Name:</sys:String>
<sys:String x:Key="ConfigProfileWindow_NameTooltip">The name of your config profile.</sys:String>
<sys:String x:Key="ConfigProfileWindow_IsEnabledLabel">Enabled</sys:String>
<sys:String x:Key="ConfigProfileWindow_IsEnabledTooltip">Is the config profile enabled.</sys:String>
<sys:String x:Key="ConfigProfileWindow_WebhookLabel">Webhook Url:</sys:String>
<sys:String x:Key="ConfigProfileWindow_WebhookTooltip">This is your Webhook Url provided by discord.</sys:String>
<sys:String x:Key="ConfigProfileWindow_BotNameLabel">Bot Name:</sys:String>
<sys:String x:Key="ConfigProfileWindow_BotNameTooltip">This will override the name you setup in your webhook. Leave blank to use default.</sys:String>
<sys:String x:Key="ConfigProfileWindow_UseTTSLabel">Use Text to Speech (TTS)</sys:String>
<sys:String x:Key="ConfigProfileWindow_UseTTSTooltip">If enabled, will make the bot announce with Text to Speech, otherwise will turn off Text to Speech.</sys:String>
<sys:String x:Key="ConfigProfileWindow_PrefixMessageWithProfileNameLabel">Prefix Message with Server Manager Profile Name</sys:String>
<sys:String x:Key="ConfigProfileWindow_PrefixMessageWithProfileNameTooltip">If enabled, the alert message will be sent prefixed with the server manager profile name.</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsLabel">Message Options</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsBoldLabel">Bold</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsBoldTooltip">If enabled, will show the message in bold.</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsUnderlineLabel">Underline</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsUnderlineTooltip">If enabled, will show the message with an underline.</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsItalicLabel">Italic</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsItalicTooltip">If enabled, will show the message in italic.</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsCodeBlockLabel">Embedded Code Block</sys:String>
<sys:String x:Key="ConfigProfileWindow_MessageOptionsCodeBlockTooltip">CIf enabled, will show the message in an embedded code block.</sys:String>
<sys:String x:Key="ConfigProfileWindow_ProfileNamesLabel">Profile Names</sys:String>
<sys:String x:Key="ConfigProfileWindow_AlertTypesLabel">Alert Types</sys:String>
<sys:String x:Key="ConfigProfileWindow_AddProfileNameTooltip">Add Server Manager Profile Name</sys:String>
<sys:String x:Key="ConfigProfileWindow_ClearProfileNamesTooltip">Delete All Server Manager Profile Names</sys:String>
<sys:String x:Key="ConfigProfileWindow_DeleteProfileNameTooltip">Delete Server Manager Profile Name</sys:String>
<sys:String x:Key="ConfigProfileWindow_AddAlertTypeTooltip">Add Alert Type</sys:String>
<sys:String x:Key="ConfigProfileWindow_ClearAlertTypesTooltip">Delete All Alert Types</sys:String>
<sys:String x:Key="ConfigProfileWindow_DeleteAlertTypeTooltip">Delete Alert Type</sys:String>
<sys:String x:Key="ConfigProfileWindow_ProfileNameColumnLabel">Server Manager Profile Name</sys:String>
<sys:String x:Key="ConfigProfileWindow_AlertTypeColumnLabel">Alert Type</sys:String>
<sys:String x:Key="ConfigProfileWindow_TestButtonLabel">Test</sys:String>
<sys:String x:Key="ConfigProfileWindow_AddErrorTitle">Add Action Error</sys:String>
<sys:String x:Key="ConfigProfileWindow_AddAlertTypeErrorLabel">An error occurred while trying to add an alert type.</sys:String>
<sys:String x:Key="ConfigProfileWindow_AddProfileNameErrorLabel">An error occurred while trying to add a profile name.</sys:String>
<sys:String x:Key="ConfigProfileWindow_ClearTitle">Confirm Delete All Action</sys:String>
<sys:String x:Key="ConfigProfileWindow_ClearLabel">Click 'Yes' to confirm you want to delete all.</sys:String>
<sys:String x:Key="ConfigProfileWindow_ClearErrorTitle">Delete All Action Error</sys:String>
<sys:String x:Key="ConfigProfileWindow_ClearAlertTypesErrorLabel">An error occurred while trying to delete all alert types.</sys:String>
<sys:String x:Key="ConfigProfileWindow_ClearProfileNamesErrorLabel">An error occurred while trying to delete all profile names.</sys:String>
<sys:String x:Key="ConfigProfileWindow_CloseTitle">Confirm Close Action</sys:String>
<sys:String x:Key="ConfigProfileWindow_CloseLabel">Click 'Yes' to confirm you want to close the form. If you have any unsaved changes they will be lost.</sys:String>
<sys:String x:Key="ConfigProfileWindow_DeleteTitle">Confirm Delete Action</sys:String>
<sys:String x:Key="ConfigProfileWindow_DeleteLabel">Click 'Yes' to confirm you want to perform the delete.</sys:String>
<sys:String x:Key="ConfigProfileWindow_DeleteErrorTitle">Delete Action Error</sys:String>
<sys:String x:Key="ConfigProfileWindow_DeleteAlertTypeErrorLabel">An error occurred while trying to delete the alert type.</sys:String>
<sys:String x:Key="ConfigProfileWindow_DeleteProfileNameErrorLabel">An error occurred while trying to delete the profile name.</sys:String>
<sys:String x:Key="ConfigProfileWindow_TestErrorTitle">Test Action Error</sys:String>
<sys:String x:Key="ConfigProfileWindow_TestErrorLabel">An error occurred while trying to test the config profile.</sys:String>
<sys:String x:Key="ConfigProfileWindow_TestEnabledErrorLabel">The profile is not enabled and cannot be tested.</sys:String>
<!--#endregion-->
<!--#region Version Feed Window -->
<sys:String x:Key="VersionFeedWindow_Title">Discord Plugin Version Details</sys:String>
<sys:String x:Key="VersionFeedWindow_Load_FailedTitle">Load Feed Error</sys:String>
<sys:String x:Key="VersionFeedWindow_VersionFilterLabel">Version:</sys:String>
<sys:String x:Key="VersionFeedWindow_VersionFilterTooltip">Select the version to view details.</sys:String>
<!--#endregion-->
</ResourceDictionary>

View file

@ -0,0 +1,15 @@
namespace ServerManagerTool.Plugin.Discord
{
internal interface IBindable
{
bool HasChanges { get; set; }
bool HasAnyChanges { get; }
void CommitChanges();
void BeginUpdate();
void EndUpdate();
}
}

View file

@ -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);
}
}
}
}

View file

@ -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<AlertType>(); }
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;
}
}
}

View file

@ -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<AlertTypeValue>, 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;
}
}
}

View file

@ -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<string, object> _properties = new Dictionary<string, object>();
protected bool _isUpdating = false;
public Bindable()
{
_properties = new Dictionary<string, object>();
}
protected T Get<T>([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>(T value, bool setChanged = true, [CallerMemberName] string name = null)
{
if (Equals(value, Get<T>(name)))
return;
if (_properties == null)
_properties = new Dictionary<string, object>();
_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<bool>(); }
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
}
}

View file

@ -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<string>(); }
set { Set(value); }
}
[DataMember]
public ProfileNameValueList ProfileNames
{
get { return Get<ProfileNameValueList>(); }
set { Set(value); }
}
[DataMember]
public AlertTypeValueList AlertTypes
{
get { return Get<AlertTypeValueList>(); }
set { Set(value); }
}
[DataMember]
public string DiscordWebhookUrl
{
get { return Get<string>(); }
set { Set(value); }
}
[DataMember]
public string DiscordBotName
{
get { return Get<string>(); }
set { Set(value); }
}
[DataMember]
public bool DiscordUseTTS
{
get { return Get<bool>(); }
set { Set(value); }
}
[DataMember]
public bool PrefixMessageWithProfileName
{
get { return Get<bool>(); }
set { Set(value); }
}
[DataMember]
public bool IsEnabled
{
get { return Get<bool>(); }
set { Set(value); }
}
[DataMember]
public bool MessageBold
{
get { return Get<bool>(); }
set { Set(value); }
}
[DataMember]
public bool MessageUnderlined
{
get { return Get<bool>(); }
set { Set(value); }
}
[DataMember]
public bool MessageItalic
{
get { return Get<bool>(); }
set { Set(value); }
}
[DataMember]
public bool MessageCodeBlock
{
get { return Get<bool>(); }
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();
}
}
}
}

View file

@ -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<ConfigProfile>();
}
[DataMember]
public DateTime LastCallHome
{
get;
set;
}
[DataMember]
public ObservableList<ConfigProfile> ConfigProfiles
{
get { return Get<ObservableList<ConfigProfile>>(); }
set { Set(value); }
}
public override bool HasAnyChanges
{
get => base.HasChanges || (ConfigProfiles?.HasAnyChanges ?? false);
}
}
}

View file

@ -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<T> : Bindable, IList<T>, INotifyCollectionChanged
{
private List<T> _listObject = null;
public ObservableList()
: base()
{
_listObject = new List<T>();
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<T> List
{
get => _listObject;
set => _listObject = value;
}
#region IList<T>
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<T> 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
}
}

View file

@ -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<string>(); }
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;
}
}
}

View file

@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ServerManagerTool.Plugin.Discord
{
internal class ProfileNameValueList : List<ProfileNameValue>, 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;
}
}
}

View file

@ -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<VersionFeedEntry> Entries { get; set; } = new List<VersionFeedEntry>();
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,73 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Globals">
<SccProjectName>%24/Development/ServerManagers/Main/Plugin.Discord</SccProjectName>
<SccProvider>{4CA58AB2-18FA-4F8D-95D4-32DDF27D184C}</SccProvider>
<SccAuxPath>https://dev.azure.com/bretthewitson</SccAuxPath>
<SccLocalPath>.</SccLocalPath>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<ApplicationIcon>Art\favicon.ico</ApplicationIcon>
<AssemblyName>ServerManager.Plugin.Discord</AssemblyName>
<RootNamespace>ServerManagerTool.Plugin.Discord</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Publish\**" />
<EmbeddedResource Remove="Publish\**" />
<None Remove="Publish\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Art\Add.ico" />
<None Remove="Art\ChangeNotes.ico" />
<None Remove="Art\Delete.ico" />
<None Remove="Art\Download.ico" />
<None Remove="Art\Edit.ico" />
<None Remove="Art\favicon.ico" />
<None Remove="Globalization\en-US\en-US.xaml" />
</ItemGroup>
<ItemGroup>
<Page Include="Windows\ConfigProfileWindow.xaml" />
<Page Include="Windows\ConfigWindow.xaml" />
<Page Include="Windows\VersionFeedWindow.xaml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Plugin.Common\Plugin.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System.Net.Http" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Web" />
<Reference Include="System.Xaml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Resource Include="Art\Add.ico" />
<Resource Include="Art\ChangeNotes.ico" />
<Resource Include="Art\Delete.ico" />
<Resource Include="Art\Download.ico" />
<Resource Include="Art\Edit.ico" />
<Resource Include="Art\favicon.ico" />
<Resource Include="Globalization\en-US\en-US.xaml" />
</ItemGroup>
<ItemGroup>
<Compile Update="Config.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Config.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="Config.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Config.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
</Project>

View file

@ -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")]

View file

@ -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<IPAddress> 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<Version> 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}");
}
}
}
}

View file

@ -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.
}
}
}

View file

@ -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();
}
}
}
}

View file

@ -0,0 +1,292 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>urn:uuid:C93B24FC-C8DB-44F3-8B6F-A27A52E7AF9F</id>
<title>Discord Plugin Version Feed</title>
<subtitle>This is the Discord Plugin release version feed.</subtitle>
<link href="" />
<updated>2020-06-12T00:00:01Z</updated>
<entry>
<id>urn:uuid:A7158CB1-C5B2-4505-B171-CDD54918B227</id>
<title>1.0.16 (1.0.16.1)</title>
<summary>1.0.16.1</summary>
<link href="" />
<updated>2020-06-12T00:00:01Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">BUGFIX</u>
<br/>
<ul>
<li>Added url encoding so that special characters are sent to the webhook correctly.</li>
<li>Changed the text encoding from ASCII to UTF8, so that unicode languages should work correctly.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:6ADCC571-3933-44E1-91FB-4DC8F8B836F2</id>
<title>1.0.15 (1.0.15.1)</title>
<summary>1.0.15.1</summary>
<link href="" />
<updated>2020-03-03T00:00:01Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">CHANGE</u>
<br/>
<ul>
<li>Config Save - nows creates a backup file (.bak) of the config file before the new changes are saved.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:6ADCC571-3933-44E1-91FB-4DC8F8B836F2</id>
<title>1.0.14 (1.0.14.2)</title>
<summary>1.0.14.2</summary>
<link href="" />
<updated>2018-08-16T00:00:01Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">CHANGE</u>
<br/>
<ul>
<li>Download Location Changed - Have moved the server manager files to a new location for hosting.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:6ADCC571-3933-44E1-91FB-4DC8F8B836F2</id>
<title>1.0.13 (1.0.13.1)</title>
<summary>1.0.13.1</summary>
<link href="" />
<updated>2018-06-13T04:40:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">NEW</u>
<br/>
<ul>
<li>Message Options - Added new message options to display the messages in Bold, Italic, Underlined and Embedded Code Block.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:21C68E55-E915-4337-8CFB-7E96FE6967CB</id>
<title>1.0.12 (1.0.12.1)</title>
<summary>1.0.12.1</summary>
<link href="" />
<updated>2018-05-24T00:20:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">NEW</u>
<br/>
<ul>
<li>Added version feed window.</li>
</ul>
<u style="font-size: .9em;">CHANGE</u>
<br/>
<ul>
<li>DotNet Framework updated.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:C1ED0129-58CF-4AA4-8203-DDFA199E17DE</id>
<title>1.0.11 (1.0.11.0)</title>
<summary>1.0.11.0</summary>
<link href="" />
<updated>2018-05-24T00:20:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">CHANGE</u>
<br/>
<ul>
<li>Icons - all icons have be cleaned up and updated.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:58894ADA-FDC4-4F5A-A904-B81D9130FD00</id>
<title>1.0.10 (1.0.10.0)</title>
<summary>1.0.10.0</summary>
<link href="" />
<updated>2018-05-24T00:20:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">BUGFIX</u>
<br/>
<ul>
<li>Attempt to fix the save config bug, where the config file is blank.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:20DB658C-44FA-4642-923B-A94926B0FDC3</id>
<title>1.0.9 (1.0.9.0)</title>
<summary>1.0.9.0</summary>
<link href="" />
<updated>2018-05-24T00:20:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">BUGFIX</u>
<br/>
<ul>
<li>Some minor bugfixes and code cleanup.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:3B0D107D-77DB-4E70-BE8A-336A20A55F8A</id>
<title>1.0.8 (1.0.8.0)</title>
<summary>1.0.8.0</summary>
<link href="" />
<updated>2018-05-24T00:20:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">CHANGE</u>
<br/>
<ul>
<li>Changed the download location of the plugin.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:CDA4857D-FC4A-44AB-A11E-9CC1CDA11BCD</id>
<title>1.0.7 (1.0.7.0)</title>
<summary>1.0.7.0</summary>
<link href="" />
<updated>2018-05-24T00:20:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">CHANGE</u>
<br/>
<ul>
<li>Minor code clean-up and tweaks.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:C6CEC6B2-E405-4DF6-B991-FC1D4EEDF3E8</id>
<title>1.0.6 (1.0.6.0)</title>
<summary>1.0.6.0</summary>
<link href="" />
<updated>2018-05-24T00:20:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">CHANGE</u>
<br/>
<ul>
<li>Have changed the storage host for the discord plugin.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
<entry>
<id>urn:uuid:9C4A9557-3A74-428F-A22C-727CBBFB6E91</id>
<title>1.0.5 (1.0.5.0)</title>
<summary>1.0.5.0</summary>
<link href="" />
<updated>2018-05-24T00:20:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">NEW</u>
<br/>
<ul>
<li>New discord plugin to send Server Manager alerts to one or more channels on your discord server.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
</feed>

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>urn:uuid:6BB39661-8638-425E-B5F7-0C75AACC1D26</id>
<title>Discord Plugin Version Feed</title>
<subtitle>This is the Discord Plugin beta version feed.</subtitle>
<link href="" />
<updated>2020-06-12T00:00:01Z</updated>
<entry>
<id>urn:uuid:A7158CB1-C5B2-4505-B171-CDD54918B227</id>
<title>1.0.16 (1.0.16.1)</title>
<summary>1.0.16.1</summary>
<link href="" />
<updated>2020-06-12T00:00:01Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
<p>
<u style="font-size: .9em;">BUGFIX</u>
<br/>
<ul>
<li>Added url encoding so that special characters are sent to the webhook correctly.</li>
<li>Changed the text encoding from ASCII to UTF8, so that unicode languages should work correctly.</li>
</ul>
</p>
</div>
</content>
<author>
<name>bletch</name>
<email>bletch1971@hotmail.com</email>
</author>
</entry>
</feed>

View file

@ -0,0 +1,228 @@
<Window x:Class="ServerManagerTool.Plugin.Discord.Windows.ConfigProfileWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:pc="clr-namespace:ServerManagerTool.Plugin.Common;assembly=ServerManager.Plugin.Common"
Title="{DynamicResource ConfigProfileWindow_Title}"
Icon="/ServerManager.Plugin.Discord;component/Art/favicon.ico"
Width="640" Height="520" MinWidth="640" MinHeight="480" ResizeMode="CanResizeWithGrip" WindowStyle="ToolWindow" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" Closing="ConfigProfileWindow_Closing">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/ServerManager.Plugin.Discord;component/Globalization/en-US/en-US.xaml"/>
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="BeigeBorder" Color="#FFD8CCBC"/>
<LinearGradientBrush x:Key="BeigeGradient" EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFECE1D4" Offset="1"/>
<GradientStop Color="#FFEAE8E6"/>
</LinearGradientBrush>
<ObjectDataProvider x:Key="AlertTypes" MethodName="GetValues" ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="pc:AlertType" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<Style x:Key="GroupBoxStyle" TargetType="GroupBox" BasedOn="{StaticResource {x:Type GroupBox}}">
<Setter Property="BorderBrush" Value="{StaticResource BeigeBorder}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid Background="{StaticResource BeigeGradient}">
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<DockPanel Margin="5" ScrollViewer.VerticalScrollBarVisibility="Auto">
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource ConfigProfileWindow_NameLabel}" ToolTip="{DynamicResource ConfigProfileWindow_NameTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Margin="1" Text="{Binding Profile.Name}" ToolTip="{DynamicResource ConfigProfileWindow_NameTooltip}" VerticalContentAlignment="Center" />
<CheckBox Grid.Row="0" Grid.Column="3" Margin="5,2,5,2" Content="{DynamicResource ConfigProfileWindow_IsEnabledLabel}" IsChecked="{Binding Profile.IsEnabled}" ToolTip="{DynamicResource ConfigProfileWindow_IsEnabledTooltip}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource ConfigProfileWindow_WebhookLabel}" ToolTip="{DynamicResource ConfigProfileWindow_WebhookTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="1" Text="{Binding Profile.DiscordWebhookUrl}" ToolTip="{DynamicResource ConfigProfileWindow_WebhookTooltip}" VerticalContentAlignment="Center" />
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource ConfigProfileWindow_BotNameLabel}" ToolTip="{DynamicResource ConfigProfileWindow_BotNameTooltip}"/>
<TextBox Grid.Row="2" Grid.Column="1" Margin="1" Text="{Binding Profile.DiscordBotName}" ToolTip="{DynamicResource ConfigProfileWindow_BotNameTooltip}" VerticalContentAlignment="Center" />
</Grid>
<GroupBox DockPanel.Dock="Top" HorizontalAlignment="Stretch" Style="{StaticResource GroupBoxStyle}">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<Label Content="{DynamicResource ConfigProfileWindow_MessageOptionsLabel}"/>
</StackPanel>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Row="0" Grid.Column="0" Margin="0,2,20,2" Content="{DynamicResource ConfigProfileWindow_PrefixMessageWithProfileNameLabel}" IsChecked="{Binding Profile.PrefixMessageWithProfileName}" ToolTip="{DynamicResource ConfigProfileWindow_PrefixMessageWithProfileNameTooltip}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<CheckBox Grid.Row="0" Grid.Column="1" Margin="10,2,20,2" Content="{DynamicResource ConfigProfileWindow_UseTTSLabel}" IsChecked="{Binding Profile.DiscordUseTTS}" ToolTip="{DynamicResource ConfigProfileWindow_UseTTSTooltip}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<CheckBox Grid.Row="1" Grid.Column="0" Margin="0,2,20,2" Content="{DynamicResource ConfigProfileWindow_MessageOptionsBoldLabel}" IsChecked="{Binding Profile.MessageBold}" ToolTip="{DynamicResource ConfigProfileWindow_MessageOptionsBoldTooltip}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<CheckBox Grid.Row="1" Grid.Column="1" Margin="10,2,20,2" Content="{DynamicResource ConfigProfileWindow_MessageOptionsUnderlineLabel}" IsChecked="{Binding Profile.MessageUnderlined}" ToolTip="{DynamicResource ConfigProfileWindow_MessageOptionsUnderlineTooltip}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<CheckBox Grid.Row="2" Grid.Column="0" Margin="0,2,20,2" Content="{DynamicResource ConfigProfileWindow_MessageOptionsItalicLabel}" IsChecked="{Binding Profile.MessageItalic}" ToolTip="{DynamicResource ConfigProfileWindow_MessageOptionsItalicTooltip}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<CheckBox Grid.Row="2" Grid.Column="1" Margin="10,2,20,2" Content="{DynamicResource ConfigProfileWindow_MessageOptionsCodeBlockLabel}" IsChecked="{Binding Profile.MessageCodeBlock}" ToolTip="{DynamicResource ConfigProfileWindow_MessageOptionsCodeBlockTooltip}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
</Grid>
</GroupBox>
<Grid DockPanel.Dock="Bottom" Margin="0,0,10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="{DynamicResource ConfigProfileWindow_TestButtonLabel}" Margin="5" MinWidth="75" Click="Test_Click"/>
<Button Grid.Column="2" Content="{DynamicResource Global_OkButtonLabel}" Margin="5" MinWidth="75" HorizontalAlignment="Right" Click="Ok_Click"/>
<Button Grid.Column="3" Content="{DynamicResource Global_CancelButtonLabel}" Margin="5" MinWidth="75" HorizontalAlignment="Left" IsCancel="True"/>
</Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="10"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<GroupBox Grid.Row="1" Grid.Column="0" HorizontalAlignment="Stretch" Style="{StaticResource GroupBoxStyle}">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<Label Content="{DynamicResource ConfigProfileWindow_ProfileNamesLabel}"/>
<Button Width="22" Height="22" Click="AddProfileName_Click" Margin="20,0,0,0" ToolTip="{DynamicResource ConfigProfileWindow_AddProfileNameTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Add.ico,Size=32}" />
</Button>
<Button Width="22" Height="22" Click="ClearProfileNames_Click" Margin="10,0,0,0" ToolTip="{DynamicResource ConfigProfileWindow_ClearProfileNamesTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Delete.ico,Size=32}" />
</Button>
</StackPanel>
</GroupBox.Header>
<DataGrid ItemsSource="{Binding Profile.ProfileNames}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserSortColumns="true" SelectionMode="Single" CanUserResizeColumns="False" CanUserResizeRows="False" RowHeaderWidth="25">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="{x:Static SystemColors.HighlightColor}"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="{x:Static SystemColors.HighlightTextColor}"/>
</Style.Resources>
</Style>
</DataGrid.Resources>
<DataGrid.HorizontalGridLinesBrush>
<SolidColorBrush Color="#FFB4B4B4"/>
</DataGrid.HorizontalGridLinesBrush>
<DataGrid.VerticalGridLinesBrush>
<SolidColorBrush Color="#FFB4B4B4"/>
</DataGrid.VerticalGridLinesBrush>
<DataGrid.Columns>
<DataGridTextColumn Width="*" Binding="{Binding Value}">
<DataGridTextColumn.Header>
<TextBlock Text="{DynamicResource ConfigProfileWindow_ProfileNameColumnLabel}"/>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTemplateColumn Width="30" CanUserReorder="False" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Width="22" Height="22" Margin="0" IsTabStop="False" HorizontalAlignment="Center" VerticalAlignment="Center" Click="DeleteProfileName_Click" ToolTip="{DynamicResource ConfigProfileWindow_DeleteProfileNameTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Delete.ico,Size=32}" />
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<GroupBox Grid.Row="1" Grid.Column="2" HorizontalAlignment="Stretch" Style="{StaticResource GroupBoxStyle}">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<Label Content="{DynamicResource ConfigProfileWindow_AlertTypesLabel}"/>
<Button Width="22" Height="22" Click="AddAlertType_Click" Margin="20,0,0,0" ToolTip="{DynamicResource ConfigProfileWindow_AddAlertTypeTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Add.ico,Size=32}" />
</Button>
<Button Width="22" Height="22" Click="ClearAlertTypes_Click" Margin="10,0,0,0" ToolTip="{DynamicResource ConfigProfileWindow_ClearAlertTypesTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Delete.ico,Size=32}" />
</Button>
</StackPanel>
</GroupBox.Header>
<DataGrid ItemsSource="{Binding Profile.AlertTypes}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserSortColumns="true" SelectionMode="Single" CanUserResizeColumns="False" CanUserResizeRows="False" RowHeaderWidth="25">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="{x:Static SystemColors.HighlightColor}"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="{x:Static SystemColors.HighlightTextColor}"/>
</Style.Resources>
</Style>
</DataGrid.Resources>
<DataGrid.HorizontalGridLinesBrush>
<SolidColorBrush Color="#FFB4B4B4"/>
</DataGrid.HorizontalGridLinesBrush>
<DataGrid.VerticalGridLinesBrush>
<SolidColorBrush Color="#FFB4B4B4"/>
</DataGrid.VerticalGridLinesBrush>
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.Header>
<TextBlock Text="{DynamicResource ConfigProfileWindow_AlertTypeColumnLabel}"/>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox IsEditable="False" ItemsSource="{Binding Source={StaticResource AlertTypes}}" SelectedValue="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" PreviewMouseWheel="ComboBox_PreviewMouseWheel"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="30" CanUserReorder="False" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Width="22" Height="22" Margin="0" IsTabStop="False" HorizontalAlignment="Center" VerticalAlignment="Center" Click="DeleteAlertType_Click" ToolTip="{DynamicResource ConfigProfileWindow_DeleteAlertTypeTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Delete.ico,Size=32}" />
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
</Grid>
</DockPanel>
</Grid>
</Window>

View file

@ -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
{
/// <summary>
/// Interaction logic for ConfigProfileWindow.xaml
/// </summary>
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);
}
}
}
}

View file

@ -0,0 +1,137 @@
<Window x:Class="ServerManagerTool.Plugin.Discord.Windows.ConfigWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pc="clr-namespace:ServerManagerTool.Plugin.Common;assembly=ServerManager.Plugin.Common"
Title="{DynamicResource ConfigWindow_Title}"
Icon="/ServerManager.Plugin.Discord;component/Art/favicon.ico"
Width="640" Height="480" MinWidth="640" MinHeight="480" ResizeMode="CanResizeWithGrip" WindowStyle="ToolWindow" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" Closing="ConfigWindow_Closing" Loaded="ConfigWindow_Loaded">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/ServerManager.Plugin.Discord;component/Globalization/en-US/en-US.xaml"/>
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="BeigeBorder" Color="#FFD8CCBC"/>
<LinearGradientBrush x:Key="BeigeGradient" EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFECE1D4" Offset="1"/>
<GradientStop Color="#FFEAE8E6"/>
</LinearGradientBrush>
<Style x:Key="GroupBoxStyle" TargetType="GroupBox" BasedOn="{StaticResource {x:Type GroupBox}}">
<Setter Property="BorderBrush" Value="{StaticResource BeigeBorder}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid Background="{StaticResource BeigeGradient}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<Style.Triggers>
<DataTrigger Binding="{Binding NewVersionAvailable}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="{DynamicResource Global_NewVersionAvailableLabel}" TextWrapping="Wrap" VerticalAlignment="Center" FontWeight="Bold"/>
<Button Width="22" Height="22" Click="DownloadPlugin_Click" Margin="10,0,0,0" ToolTip="{DynamicResource Global_DownloadNewVersionTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Download.ico,Size=32}" />
</Button>
</StackPanel>
<GroupBox Grid.Row="1" HorizontalAlignment="Stretch" Style="{StaticResource GroupBoxStyle}">
<GroupBox.Header>
<StackPanel Orientation="Horizontal" Margin="0,5,0,5">
<Button Width="22" Height="22" Click="PatchNotes_Click" ToolTip="{DynamicResource ConfigWindow_PatchNotesTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/ChangeNotes.ico,Size=32}" />
</Button>
<Button Width="22" Height="22" Click="AddConfigProfile_Click" Margin="20,0,0,0" ToolTip="{DynamicResource ConfigWindow_AddConfigProfileTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Add.ico,Size=32}" />
</Button>
<Button Width="22" Height="22" Click="ClearConfigProfiles_Click" Margin="10,0,0,0" ToolTip="{DynamicResource ConfigWindow_ClearConfigProfilesTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Delete.ico,Size=32}" />
</Button>
</StackPanel>
</GroupBox.Header>
<DataGrid ItemsSource="{Binding PluginConfig.ConfigProfiles}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserSortColumns="true" SelectionMode="Single" CanUserResizeColumns="False" CanUserResizeRows="False" RowHeaderWidth="25">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="{x:Static SystemColors.HighlightColor}"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="{x:Static SystemColors.HighlightTextColor}"/>
</Style.Resources>
</Style>
</DataGrid.Resources>
<DataGrid.HorizontalGridLinesBrush>
<SolidColorBrush Color="#FFB4B4B4"/>
</DataGrid.HorizontalGridLinesBrush>
<DataGrid.VerticalGridLinesBrush>
<SolidColorBrush Color="#FFB4B4B4"/>
</DataGrid.VerticalGridLinesBrush>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter Property="Background" Value="Beige" />
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Width="*" Binding="{Binding Name}" IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{DynamicResource ConfigWindow_NameColumnLabel}"/>
</DataGridTextColumn.Header>
<DataGridTextColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
<DataGridTemplateColumn Width="30" CanUserReorder="False" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Width="22" Height="22" Margin="0" IsTabStop="False" HorizontalAlignment="Center" VerticalAlignment="Center" Click="EditConfigProfile_Click" ToolTip="{DynamicResource ConfigWindow_EditConfigProfileTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Edit.ico,Size=32}" />
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="30" CanUserReorder="False" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Width="22" Height="22" Margin="0" IsTabStop="False" HorizontalAlignment="Center" VerticalAlignment="Center" Click="DeleteConfigProfile_Click" ToolTip="{DynamicResource ConfigWindow_DeleteConfigProfileTooltip}">
<Image Source="{pc:Icon Path=/ServerManager.Plugin.Discord;component/Art/Delete.ico,Size=32}" />
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,10,0">
<Button Content="{DynamicResource Global_SaveButtonLabel}" Margin="5" MinWidth="75" HorizontalAlignment="Right" Click="Save_Click"/>
<Button Content="{DynamicResource Global_CloseButtonLabel}" Margin="5" MinWidth="75" HorizontalAlignment="Left" IsCancel="True"/>
</StackPanel>
</Grid>
</Window>

View file

@ -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
{
/// <summary>
/// Interaction logic for ConfigWindow.xaml
/// </summary>
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();
}
}
}

View file

@ -0,0 +1,60 @@
<Window x:Class="ServerManagerTool.Plugin.Discord.Windows.VersionFeedWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ServerManagerTool.Plugin.Discord"
xmlns:pc="clr-namespace:ServerManagerTool.Plugin.Common;assembly=ServerManager.Plugin.Common"
MinWidth="400" MinHeight="400" Width="640" Height="480" WindowStyle="ToolWindow" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="../Art/favicon.ico" Title="{DynamicResource VersionFeedWindow_Title}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/ServerManager.Plugin.Discord;component/Globalization/en-US/en-US.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style x:Key="Version" TargetType="Label">
<Setter Property="Content" Value="{Binding Title}"/>
<Setter Property="FontWeight" Value="Normal"/>
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="#0066CC"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid Background="{DynamicResource GradientBackground}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Margin="5,5,0,0" Content="{DynamicResource VersionFeedWindow_VersionFilterLabel}"/>
<ComboBox Grid.Row="0" Grid.Column="1" Margin="5,5,5,0" ItemsSource="{Binding FeedEntries}" SelectedValue="{Binding SelectedFeedEntry}" ToolTip="{DynamicResource VersionFeedWindow_VersionFilterTooltip}">
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}" >
<Setter Property="Height" Value="20" />
</Style>
</ComboBox.ItemContainerStyle>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:VersionFeedEntry}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
<Label Padding="0,-1,0,-1" VerticalAlignment="Center" Style="{DynamicResource Version}"/>
<TextBlock Text="{Binding Updated, StringFormat= - {0:G}}" Margin="5,0,0,0" Padding="0,-1,0,-1" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<WebBrowser Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Margin="5" local:BrowserBehavior.Html="{Binding SelectedFeedEntry.Content}"/>
</Grid>
</Window>

View file

@ -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
{
/// <summary>
/// Interaction logic for VersionFeedWindow.xaml
/// </summary>
public partial class VersionFeedWindow : Window
{
public static readonly DependencyProperty FeedEntriesProperty = DependencyProperty.Register(nameof(FeedEntries), typeof(ObservableCollection<VersionFeedEntry>), typeof(VersionFeedWindow), new PropertyMetadata(new ObservableCollection<VersionFeedEntry>()));
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<VersionFeedEntry> FeedEntries
{
get { return (ObservableCollection<VersionFeedEntry>)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();
}
}
}

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<section name="ServerManagerTool.Plugin.Discord.Config" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false"/>
</sectionGroup>
</configSections>
<applicationSettings>
<ServerManagerTool.Plugin.Discord.Config>
<setting name="PluginCallUrlFormat" serializeAs="String">
<value>http://servermanager.azurewebsites.net/api/plugin/call/{0}/{1}/</value>
</setting>
<setting name="PublicIPCheckUrl" serializeAs="String">
<value>http://whatismyip.akamai.com/</value>
</setting>
<setting name="CallHomeDelay" serializeAs="String">
<value>12</value>
</setting>
<setting name="RequestTimeout" serializeAs="String">
<value>5000</value>
</setting>
<setting name="PluginCode" serializeAs="String">
<value>1B745000-6389-4770-9509-C6A05E209323</value>
</setting>
<setting name="PluginName" serializeAs="String">
<value>Discord Plugin</value>
</setting>
<setting name="LatestDownloadUrl" serializeAs="String">
<value>https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/latest.zip</value>
</setting>
<setting name="LatestVersionUrl" serializeAs="String">
<value>https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/latest.txt</value>
</setting>
<setting name="LatestBetaDownloadUrl" serializeAs="String">
<value>https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/latest.zip</value>
</setting>
<setting name="LatestBetaVersionUrl" serializeAs="String">
<value>https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/latest.txt</value>
</setting>
<setting name="PluginZipFilename" serializeAs="String">
<value>ServerManager.Plugin.Discord.zip</value>
</setting>
<setting name="ConfigFile" serializeAs="String">
<value>_discordplugin.cfg</value>
</setting>
<setting name="VersionFeedUrl" serializeAs="String">
<value>https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/VersionFeed.xml</value>
</setting>
<setting name="VersionBetaFeedUrl" serializeAs="String">
<value>https://raw.githubusercontent.com/Bletch1971/ServerManagers/master/Plugins/Discord/beta/VersionFeed.xml</value>
</setting>
</ServerManagerTool.Plugin.Discord.Config>
</applicationSettings>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/></startup></configuration>

View file

@ -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