From 46ddb4b47860961d642f677007a46d2ccb9df407 Mon Sep 17 00:00:00 2001 From: EnderIce2 <38536866+EnderIce2@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:32:33 +0300 Subject: [PATCH] Update the plugin for SDR# 1920 ( #14 ) --- .deepsource.toml | 7 - DiscordAPI/Configuration.cs | 32 - .../Converters/EnumSnakeCaseConverter.cs | 98 -- DiscordAPI/Converters/EnumValueAttribute.cs | 16 - DiscordAPI/EventType.cs | 34 - DiscordAPI/Exceptions/InvalidPipeException.cs | 16 - DiscordAPI/Helper/BackoffDelay.cs | 71 -- DiscordAPI/Helper/StringTools.cs | 73 -- DiscordAPI/IO/Handshake.cs | 23 - DiscordAPI/IO/INamedPipeClient.cs | 56 -- DiscordAPI/IO/Opcode.cs | 33 - DiscordAPI/IO/PipeFrame.cs | 204 ---- DiscordAPI/Logging/ConsoleLogger.cs | 101 -- DiscordAPI/Logging/FileLogger.cs | 92 -- DiscordAPI/Logging/ILogger.cs | 46 - DiscordAPI/Logging/NullLogger.cs | 58 -- DiscordAPI/Message/CloseMessage.cs | 26 - .../Message/ConnectionEstablishedMessage.cs | 24 - DiscordAPI/Message/ConnectionFailedMessage.cs | 24 - DiscordAPI/Message/ErrorMessage.cs | 76 -- DiscordAPI/Message/IMessage.cs | 29 - DiscordAPI/Message/JoinMessage.cs | 25 - DiscordAPI/Message/JoinRequestMessage.cs | 25 - DiscordAPI/Message/MessageType.cs | 64 -- DiscordAPI/Message/PresenceMessage.cs | 47 - DiscordAPI/Message/ReadyMessage.cs | 35 - DiscordAPI/Message/SpectateMessage.cs | 19 - DiscordAPI/Message/SubscribeMessage.cs | 40 - DiscordAPI/Message/UnsubscribeMsesage.cs | 39 - DiscordAPI/RPC/Commands/CloseCommand.cs | 34 - DiscordAPI/RPC/Commands/ICommand.cs | 13 - DiscordAPI/RPC/Commands/PresenceCommand.cs | 32 - DiscordAPI/RPC/Commands/RespondCommand.cs | 32 - DiscordAPI/RPC/Commands/SubscribeCommand.cs | 23 - DiscordAPI/RPC/Payload/ClosePayload.cs | 23 - DiscordAPI/RPC/Payload/Command.cs | 129 --- DiscordAPI/RPC/Payload/IPayload.cs | 35 - DiscordAPI/RPC/Payload/PayloadArgument.cs | 53 -- DiscordAPI/RPC/Payload/PayloadEvent.cs | 57 -- DiscordAPI/RPC/RpcConnection.cs | 869 ----------------- DiscordAPI/User.cs | 229 ----- Properties/AssemblyInfo.cs | 36 - Properties/Resources.Designer.cs | 73 -- Register.txt | 1 - Resources/gear.png | Bin 3358 -> 0 bytes SDR-RPC.sln | 31 + SDR-RPC/ControlPanel.Designer.cs | 199 ++++ SDR-RPC/ControlPanel.cs | 79 ++ .../ControlPanel.resx | 50 +- SDR-RPC/CreditsForm.Designer.cs | 156 ++++ SettingsForm.cs => SDR-RPC/CreditsForm.cs | 49 +- .../CreditsForm.resx | 50 +- SDR-RPC/DiscordAPI/Configuration.cs | 28 + .../Converters/EnumSnakeCaseConverter.cs | 94 ++ .../Converters/EnumValueAttribute.cs | 14 + .../DiscordAPI}/DiscordRpcClient.cs | 52 +- SDR-RPC/DiscordAPI/EventType.cs | 29 + {DiscordAPI => SDR-RPC/DiscordAPI}/Events.cs | 8 +- .../Exceptions/BadPresenceException.cs | 13 +- .../InvalidConfigurationException.cs | 13 +- .../Exceptions/InvalidPipeException.cs | 15 + .../Exceptions/StringOutOfRangeException.cs | 13 +- .../Exceptions/UninitializedException.cs | 5 +- SDR-RPC/DiscordAPI/Helper/BackoffDelay.cs | 75 ++ SDR-RPC/DiscordAPI/Helper/StringTools.cs | 72 ++ SDR-RPC/DiscordAPI/IO/Handshake.cs | 19 + SDR-RPC/DiscordAPI/IO/INamedPipeClient.cs | 51 + .../DiscordAPI}/IO/ManagedNamedPipeClient.cs | 18 +- SDR-RPC/DiscordAPI/IO/Opcode.cs | 33 + SDR-RPC/DiscordAPI/IO/PipeFrame.cs | 203 ++++ .../DiscordAPI}/LICENSE.txt | 0 SDR-RPC/DiscordAPI/Logging/ConsoleLogger.cs | 98 ++ SDR-RPC/DiscordAPI/Logging/FileLogger.cs | 85 ++ SDR-RPC/DiscordAPI/Logging/ILogger.cs | 41 + .../DiscordAPI}/Logging/LogLevel.cs | 29 +- SDR-RPC/DiscordAPI/Logging/NullLogger.cs | 53 ++ SDR-RPC/DiscordAPI/Message/CloseMessage.cs | 30 + .../Message/ConnectionEstablishedMessage.cs | 19 + .../Message/ConnectionFailedMessage.cs | 19 + SDR-RPC/DiscordAPI/Message/ErrorMessage.cs | 76 ++ SDR-RPC/DiscordAPI/Message/IMessage.cs | 31 + SDR-RPC/DiscordAPI/Message/JoinMessage.cs | 22 + .../DiscordAPI/Message/JoinRequestMessage.cs | 22 + SDR-RPC/DiscordAPI/Message/MessageType.cs | 63 ++ SDR-RPC/DiscordAPI/Message/PresenceMessage.cs | 49 + SDR-RPC/DiscordAPI/Message/ReadyMessage.cs | 34 + SDR-RPC/DiscordAPI/Message/SpectateMessage.cs | 14 + .../DiscordAPI/Message/SubscribeMessage.cs | 40 + .../DiscordAPI/Message/UnsubscribeMsesage.cs | 40 + .../DiscordAPI/RPC/Commands/CloseCommand.cs | 30 + SDR-RPC/DiscordAPI/RPC/Commands/ICommand.cs | 9 + .../RPC/Commands/PresenceCommand.cs | 28 + .../DiscordAPI/RPC/Commands/RespondCommand.cs | 28 + .../RPC/Commands/SubscribeCommand.cs | 19 + .../DiscordAPI/RPC/Payload/ClosePayload.cs | 19 + SDR-RPC/DiscordAPI/RPC/Payload/Command.cs | 125 +++ SDR-RPC/DiscordAPI/RPC/Payload/IPayload.cs | 36 + .../DiscordAPI/RPC/Payload/PayloadArgument.cs | 59 ++ .../DiscordAPI/RPC/Payload/PayloadEvent.cs | 55 ++ .../DiscordAPI}/RPC/Payload/ServerEvent.cs | 57 +- SDR-RPC/DiscordAPI/RPC/RpcConnection.cs | 877 ++++++++++++++++++ .../DiscordAPI}/Registry/IUriSchemeCreator.cs | 6 +- .../Registry/MacUriSchemeCreator.cs | 11 +- .../Registry/UnixUriSchemeCreator.cs | 13 +- .../DiscordAPI}/Registry/UriScheme.cs | 12 +- .../Registry/WindowsUriSchemeCreator.cs | 4 +- .../DiscordAPI}/RichPresence.cs | 68 +- SDR-RPC/DiscordAPI/User.cs | 233 +++++ .../DiscordAPI}/Web/WebRPC.cs | 0 SDR-RPC/HelpForm.Designer.cs | 128 +++ SDR-RPC/HelpForm.cs | 20 + .../Resources.resx => SDR-RPC/HelpForm.resx | 54 +- LogWriter.cs => SDR-RPC/LogWriter.cs | 2 + MainPlugin.cs => SDR-RPC/MainPlugin.cs | 95 +- SDR-RPC/Properties/AssemblyInfo.cs | 11 + SDR-RPC/Properties/launchSettings.json | 9 + SDR-RPC/SDR-RPC.csproj | 57 ++ SDRSHARP-LICENSE.txt | 23 + SDRSharpPlugin.DiscordRPC.csproj | 175 ---- SDRSharpPlugin.DiscordRPC.sln | 30 - SettingsForm.Designer.cs | 188 ---- SettingsPanel.Designer.cs | 107 --- SettingsPanel.cs | 46 - app.config | 11 - lib/SDRSharp.Common.dll | Bin 0 -> 15360 bytes lib/SDRSharp.PanView.dll | Bin 0 -> 117760 bytes lib/SDRSharp.Radio.dll | Bin 0 -> 49152 bytes packages.config | 4 - 128 files changed, 3922 insertions(+), 3933 deletions(-) delete mode 100644 .deepsource.toml delete mode 100644 DiscordAPI/Configuration.cs delete mode 100644 DiscordAPI/Converters/EnumSnakeCaseConverter.cs delete mode 100644 DiscordAPI/Converters/EnumValueAttribute.cs delete mode 100644 DiscordAPI/EventType.cs delete mode 100644 DiscordAPI/Exceptions/InvalidPipeException.cs delete mode 100644 DiscordAPI/Helper/BackoffDelay.cs delete mode 100644 DiscordAPI/Helper/StringTools.cs delete mode 100644 DiscordAPI/IO/Handshake.cs delete mode 100644 DiscordAPI/IO/INamedPipeClient.cs delete mode 100644 DiscordAPI/IO/Opcode.cs delete mode 100644 DiscordAPI/IO/PipeFrame.cs delete mode 100644 DiscordAPI/Logging/ConsoleLogger.cs delete mode 100644 DiscordAPI/Logging/FileLogger.cs delete mode 100644 DiscordAPI/Logging/ILogger.cs delete mode 100644 DiscordAPI/Logging/NullLogger.cs delete mode 100644 DiscordAPI/Message/CloseMessage.cs delete mode 100644 DiscordAPI/Message/ConnectionEstablishedMessage.cs delete mode 100644 DiscordAPI/Message/ConnectionFailedMessage.cs delete mode 100644 DiscordAPI/Message/ErrorMessage.cs delete mode 100644 DiscordAPI/Message/IMessage.cs delete mode 100644 DiscordAPI/Message/JoinMessage.cs delete mode 100644 DiscordAPI/Message/JoinRequestMessage.cs delete mode 100644 DiscordAPI/Message/MessageType.cs delete mode 100644 DiscordAPI/Message/PresenceMessage.cs delete mode 100644 DiscordAPI/Message/ReadyMessage.cs delete mode 100644 DiscordAPI/Message/SpectateMessage.cs delete mode 100644 DiscordAPI/Message/SubscribeMessage.cs delete mode 100644 DiscordAPI/Message/UnsubscribeMsesage.cs delete mode 100644 DiscordAPI/RPC/Commands/CloseCommand.cs delete mode 100644 DiscordAPI/RPC/Commands/ICommand.cs delete mode 100644 DiscordAPI/RPC/Commands/PresenceCommand.cs delete mode 100644 DiscordAPI/RPC/Commands/RespondCommand.cs delete mode 100644 DiscordAPI/RPC/Commands/SubscribeCommand.cs delete mode 100644 DiscordAPI/RPC/Payload/ClosePayload.cs delete mode 100644 DiscordAPI/RPC/Payload/Command.cs delete mode 100644 DiscordAPI/RPC/Payload/IPayload.cs delete mode 100644 DiscordAPI/RPC/Payload/PayloadArgument.cs delete mode 100644 DiscordAPI/RPC/Payload/PayloadEvent.cs delete mode 100644 DiscordAPI/RPC/RpcConnection.cs delete mode 100644 DiscordAPI/User.cs delete mode 100644 Properties/AssemblyInfo.cs delete mode 100644 Properties/Resources.Designer.cs delete mode 100644 Register.txt delete mode 100644 Resources/gear.png create mode 100644 SDR-RPC.sln create mode 100644 SDR-RPC/ControlPanel.Designer.cs create mode 100644 SDR-RPC/ControlPanel.cs rename SettingsForm.resx => SDR-RPC/ControlPanel.resx (93%) create mode 100644 SDR-RPC/CreditsForm.Designer.cs rename SettingsForm.cs => SDR-RPC/CreditsForm.cs (68%) rename SettingsPanel.resx => SDR-RPC/CreditsForm.resx (93%) create mode 100644 SDR-RPC/DiscordAPI/Configuration.cs create mode 100644 SDR-RPC/DiscordAPI/Converters/EnumSnakeCaseConverter.cs create mode 100644 SDR-RPC/DiscordAPI/Converters/EnumValueAttribute.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/DiscordRpcClient.cs (97%) create mode 100644 SDR-RPC/DiscordAPI/EventType.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/Events.cs (98%) rename {DiscordAPI => SDR-RPC/DiscordAPI}/Exceptions/BadPresenceException.cs (63%) rename {DiscordAPI => SDR-RPC/DiscordAPI}/Exceptions/InvalidConfigurationException.cs (63%) create mode 100644 SDR-RPC/DiscordAPI/Exceptions/InvalidPipeException.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/Exceptions/StringOutOfRangeException.cs (87%) rename {DiscordAPI => SDR-RPC/DiscordAPI}/Exceptions/UninitializedException.cs (90%) create mode 100644 SDR-RPC/DiscordAPI/Helper/BackoffDelay.cs create mode 100644 SDR-RPC/DiscordAPI/Helper/StringTools.cs create mode 100644 SDR-RPC/DiscordAPI/IO/Handshake.cs create mode 100644 SDR-RPC/DiscordAPI/IO/INamedPipeClient.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/IO/ManagedNamedPipeClient.cs (98%) create mode 100644 SDR-RPC/DiscordAPI/IO/Opcode.cs create mode 100644 SDR-RPC/DiscordAPI/IO/PipeFrame.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/LICENSE.txt (100%) create mode 100644 SDR-RPC/DiscordAPI/Logging/ConsoleLogger.cs create mode 100644 SDR-RPC/DiscordAPI/Logging/FileLogger.cs create mode 100644 SDR-RPC/DiscordAPI/Logging/ILogger.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/Logging/LogLevel.cs (61%) create mode 100644 SDR-RPC/DiscordAPI/Logging/NullLogger.cs create mode 100644 SDR-RPC/DiscordAPI/Message/CloseMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/ConnectionEstablishedMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/ConnectionFailedMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/ErrorMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/IMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/JoinMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/JoinRequestMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/MessageType.cs create mode 100644 SDR-RPC/DiscordAPI/Message/PresenceMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/ReadyMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/SpectateMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/SubscribeMessage.cs create mode 100644 SDR-RPC/DiscordAPI/Message/UnsubscribeMsesage.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Commands/CloseCommand.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Commands/ICommand.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Commands/PresenceCommand.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Commands/RespondCommand.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Commands/SubscribeCommand.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Payload/ClosePayload.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Payload/Command.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Payload/IPayload.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Payload/PayloadArgument.cs create mode 100644 SDR-RPC/DiscordAPI/RPC/Payload/PayloadEvent.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/RPC/Payload/ServerEvent.cs (80%) create mode 100644 SDR-RPC/DiscordAPI/RPC/RpcConnection.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/Registry/IUriSchemeCreator.cs (87%) rename {DiscordAPI => SDR-RPC/DiscordAPI}/Registry/MacUriSchemeCreator.cs (92%) rename {DiscordAPI => SDR-RPC/DiscordAPI}/Registry/UnixUriSchemeCreator.cs (95%) rename {DiscordAPI => SDR-RPC/DiscordAPI}/Registry/UriScheme.cs (94%) rename {DiscordAPI => SDR-RPC/DiscordAPI}/Registry/WindowsUriSchemeCreator.cs (99%) rename {DiscordAPI => SDR-RPC/DiscordAPI}/RichPresence.cs (97%) create mode 100644 SDR-RPC/DiscordAPI/User.cs rename {DiscordAPI => SDR-RPC/DiscordAPI}/Web/WebRPC.cs (100%) create mode 100644 SDR-RPC/HelpForm.Designer.cs create mode 100644 SDR-RPC/HelpForm.cs rename Properties/Resources.resx => SDR-RPC/HelpForm.resx (87%) rename LogWriter.cs => SDR-RPC/LogWriter.cs (92%) rename MainPlugin.cs => SDR-RPC/MainPlugin.cs (80%) create mode 100644 SDR-RPC/Properties/AssemblyInfo.cs create mode 100644 SDR-RPC/Properties/launchSettings.json create mode 100644 SDR-RPC/SDR-RPC.csproj create mode 100644 SDRSHARP-LICENSE.txt delete mode 100644 SDRSharpPlugin.DiscordRPC.csproj delete mode 100644 SDRSharpPlugin.DiscordRPC.sln delete mode 100644 SettingsForm.Designer.cs delete mode 100644 SettingsPanel.Designer.cs delete mode 100644 SettingsPanel.cs delete mode 100644 app.config create mode 100644 lib/SDRSharp.Common.dll create mode 100644 lib/SDRSharp.PanView.dll create mode 100644 lib/SDRSharp.Radio.dll delete mode 100644 packages.config diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index c1e2484..0000000 --- a/.deepsource.toml +++ /dev/null @@ -1,7 +0,0 @@ -version = 1 - -exclude_patterns = ["DiscordAPI/**"] - -[[analyzers]] -name = "csharp" -enabled = true \ No newline at end of file diff --git a/DiscordAPI/Configuration.cs b/DiscordAPI/Configuration.cs deleted file mode 100644 index 33378c3..0000000 --- a/DiscordAPI/Configuration.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC -{ - /// - /// Configuration of the current RPC connection - /// - public class Configuration - { - /// - /// The Discord API endpoint that should be used. - /// - [JsonProperty("api_endpoint")] - public string ApiEndpoint { get; set; } - - /// - /// The CDN endpoint - /// - [JsonProperty("cdn_host")] - public string CdnHost { get; set; } - - /// - /// The type of enviroment the connection on. Usually Production. - /// - [JsonProperty("enviroment")] - public string Enviroment { get; set; } - } -} diff --git a/DiscordAPI/Converters/EnumSnakeCaseConverter.cs b/DiscordAPI/Converters/EnumSnakeCaseConverter.cs deleted file mode 100644 index d6cecd5..0000000 --- a/DiscordAPI/Converters/EnumSnakeCaseConverter.cs +++ /dev/null @@ -1,98 +0,0 @@ -using DiscordRPC.Helper; -using Newtonsoft.Json; -using System; -using System.Linq; -using System.Reflection; - -namespace DiscordRPC.Converters -{ - /// - /// Converts enums with the into Json friendly terms. - /// - internal class EnumSnakeCaseConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType.IsEnum; - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.Value == null) return null; - - object val = null; - if (TryParseEnum(objectType, (string)reader.Value, out val)) - return val; - - return existingValue; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var enumtype = value.GetType(); - var name = Enum.GetName(enumtype, value); - - //Get each member and look for hte correct one - var members = enumtype.GetMembers(BindingFlags.Public | BindingFlags.Static); - foreach (var m in members) - { - if (m.Name.Equals(name)) - { - var attributes = m.GetCustomAttributes(typeof(EnumValueAttribute), true); - if (attributes.Length > 0) - { - name = ((EnumValueAttribute)attributes[0]).Value; - } - } - } - - writer.WriteValue(name); - } - - - public bool TryParseEnum(Type enumType, string str, out object obj) - { - //Make sure the string isn;t null - if (str == null) - { - obj = null; - return false; - } - - //Get the real type - Type type = enumType; - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - type = type.GetGenericArguments().First(); - - //Make sure its actually a enum - if (!type.IsEnum) - { - obj = null; - return false; - } - - - //Get each member and look for hte correct one - var members = type.GetMembers(BindingFlags.Public | BindingFlags.Static); - foreach (var m in members) - { - var attributes = m.GetCustomAttributes(typeof(EnumValueAttribute), true); - foreach(var a in attributes) - { - var enumval = (EnumValueAttribute)a; - if (str.Equals(enumval.Value)) - { - obj = Enum.Parse(type, m.Name, ignoreCase: true); - - return true; - } - } - } - - //We failed - obj = null; - return false; - } - - } -} diff --git a/DiscordAPI/Converters/EnumValueAttribute.cs b/DiscordAPI/Converters/EnumValueAttribute.cs deleted file mode 100644 index 35a8c08..0000000 --- a/DiscordAPI/Converters/EnumValueAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Converters -{ - internal class EnumValueAttribute : Attribute - { - public string Value { get; set; } - public EnumValueAttribute(string value) - { - this.Value = value; - } - } -} diff --git a/DiscordAPI/EventType.cs b/DiscordAPI/EventType.cs deleted file mode 100644 index 61a8aea..0000000 --- a/DiscordAPI/EventType.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC -{ - /// - /// The type of event receieved by the RPC. A flag type that can be combined. - /// - [System.Flags] - public enum EventType - { - /// - /// No event - /// - None = 0, - - /// - /// Called when the Discord Client wishes to enter a game to spectate - /// - Spectate = 0x1, - - /// - /// Called when the Discord Client wishes to enter a game to play. - /// - Join = 0x2, - - /// - /// Called when another Discord Client has requested permission to join this game. - /// - JoinRequest = 0x4 - } -} diff --git a/DiscordAPI/Exceptions/InvalidPipeException.cs b/DiscordAPI/Exceptions/InvalidPipeException.cs deleted file mode 100644 index 2637e2a..0000000 --- a/DiscordAPI/Exceptions/InvalidPipeException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Exceptions -{ - /// - /// The exception that is thrown when a error occurs while communicating with a pipe or when a connection attempt fails. - /// - [System.Obsolete("Not actually used anywhere")] - public class InvalidPipeException : Exception - { - internal InvalidPipeException(string message) : base(message) { } - } -} diff --git a/DiscordAPI/Helper/BackoffDelay.cs b/DiscordAPI/Helper/BackoffDelay.cs deleted file mode 100644 index bb10254..0000000 --- a/DiscordAPI/Helper/BackoffDelay.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Helper -{ - - internal class BackoffDelay - { - /// - /// The maximum time the backoff can reach - /// - public int Maximum { get; private set; } - - /// - /// The minimum time the backoff can start at - /// - public int Minimum { get; private set; } - - /// - /// The current time of the backoff - /// - public int Current { get { return _current; } } - private int _current; - - /// - /// The current number of failures - /// - public int Fails { get { return _fails; } } - private int _fails; - - /// - /// The random generator - /// - public Random Random { get; set; } - - private BackoffDelay() { } - public BackoffDelay(int min, int max) : this(min, max, new Random()) { } - public BackoffDelay(int min, int max, Random random) - { - this.Minimum = min; - this.Maximum = max; - - this._current = min; - this._fails = 0; - this.Random = random; - } - - /// - /// Resets the backoff - /// - public void Reset() - { - _fails = 0; - _current = Minimum; - } - - public int NextDelay() - { - //Increment the failures - _fails++; - - double diff = (Maximum - Minimum) / 100f; - _current = (int)Math.Floor(diff * _fails) + Minimum; - - - return Math.Min(Math.Max(_current, Minimum), Maximum); - } - } -} diff --git a/DiscordAPI/Helper/StringTools.cs b/DiscordAPI/Helper/StringTools.cs deleted file mode 100644 index 01e57de..0000000 --- a/DiscordAPI/Helper/StringTools.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Helper -{ - /// - /// Collectin of helpful string extensions - /// - public static class StringTools - { - /// - /// Will return null if the string is whitespace, otherwise it will return the string. - /// - /// The string to check - /// Null if the string is empty, otherwise the string - public static string GetNullOrString(this string str) - { - return str.Length == 0 || string.IsNullOrEmpty(str.Trim()) ? null : str; - } - - /// - /// Does the string fit within the given amount of bytes? Uses UTF8 encoding. - /// - /// The string to check - /// The maximum number of bytes the string can take up - /// True if the string fits within the number of bytes - public static bool WithinLength(this string str, int bytes) - { - return str.WithinLength(bytes, Encoding.UTF8); - } - - /// - /// Does the string fit within the given amount of bytes? - /// - /// The string to check - /// The maximum number of bytes the string can take up - /// The encoding to count the bytes with - /// True if the string fits within the number of bytes - public static bool WithinLength(this string str, int bytes, Encoding encoding) - { - return encoding.GetByteCount(str) <= bytes; - } - - - /// - /// Converts the string into UpperCamelCase (Pascal Case). - /// - /// The string to convert - /// - public static string ToCamelCase(this string str) - { - if (str == null) return null; - - return str.ToLower() - .Split(new[] { "_", " " }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => char.ToUpper(s[0]) + s.Substring(1, s.Length - 1)) - .Aggregate(string.Empty, (s1, s2) => s1 + s2); - } - - /// - /// Converts the string into UPPER_SNAKE_CASE - /// - /// The string to convert - /// - public static string ToSnakeCase(this string str) - { - if (str == null) return null; - var concat = string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString()).ToArray()); - return concat.ToUpper(); - } - } -} diff --git a/DiscordAPI/IO/Handshake.cs b/DiscordAPI/IO/Handshake.cs deleted file mode 100644 index 1aa1204..0000000 --- a/DiscordAPI/IO/Handshake.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.IO -{ - internal class Handshake - { - /// - /// Version of the IPC API we are using - /// - [JsonProperty("v")] - public int Version { get; set; } - - /// - /// The ID of the app. - /// - [JsonProperty("client_id")] - public string ClientID { get; set; } - } -} diff --git a/DiscordAPI/IO/INamedPipeClient.cs b/DiscordAPI/IO/INamedPipeClient.cs deleted file mode 100644 index 5f171cb..0000000 --- a/DiscordAPI/IO/INamedPipeClient.cs +++ /dev/null @@ -1,56 +0,0 @@ -using DiscordRPC.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.IO -{ - /// - /// Pipe Client used to communicate with Discord. - /// - public interface INamedPipeClient : IDisposable - { - - /// - /// The logger for the Pipe client to use - /// - ILogger Logger { get; set; } - - /// - /// Is the pipe client currently connected? - /// - bool IsConnected { get; } - - /// - /// The pipe the client is currently connected too - /// - int ConnectedPipe { get; } - - /// - /// Attempts to connect to the pipe. If 0-9 is passed to pipe, it should try to only connect to the specified pipe. If -1 is passed, the pipe will find the first available pipe. - /// - /// If -1 is passed, the pipe will find the first available pipe, otherwise it connects to the pipe that was supplied - /// - bool Connect(int pipe); - - /// - /// Reads a frame if there is one available. Returns false if there is none. This should be non blocking (aka use a Peek first). - /// - /// The frame that has been read. Will be default(PipeFrame) if it fails to read - /// Returns true if a frame has been read, otherwise false. - bool ReadFrame(out PipeFrame frame); - - /// - /// Writes the frame to the pipe. Returns false if any errors occur. - /// - /// The frame to be written - bool WriteFrame(PipeFrame frame); - - /// - /// Closes the connection - /// - void Close(); - - } -} diff --git a/DiscordAPI/IO/Opcode.cs b/DiscordAPI/IO/Opcode.cs deleted file mode 100644 index f38a213..0000000 --- a/DiscordAPI/IO/Opcode.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace DiscordRPC.IO -{ - /// - /// The operation code that the was sent under. This defines the type of frame and the data to expect. - /// - public enum Opcode : uint - { - /// - /// Initial handshake frame - /// - Handshake = 0, - - /// - /// Generic message frame - /// - Frame = 1, - - /// - /// Discord has closed the connection - /// - Close = 2, - - /// - /// Ping frame (not used?) - /// - Ping = 3, - - /// - /// Pong frame (not used?) - /// - Pong = 4 - } -} diff --git a/DiscordAPI/IO/PipeFrame.cs b/DiscordAPI/IO/PipeFrame.cs deleted file mode 100644 index 2ff6db2..0000000 --- a/DiscordAPI/IO/PipeFrame.cs +++ /dev/null @@ -1,204 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace DiscordRPC.IO -{ - /// - /// A frame received and sent to the Discord client for RPC communications. - /// - public struct PipeFrame - { - /// - /// The maxium size of a pipe frame (16kb). - /// - public static readonly int MAX_SIZE = 16 * 1024; - - /// - /// The opcode of the frame - /// - public Opcode Opcode { get; set; } - - /// - /// The length of the frame data - /// - public uint Length { get { return (uint) Data.Length; } } - - /// - /// The data in the frame - /// - public byte[] Data { get; set; } - - /// - /// The data represented as a string. - /// - public string Message - { - get { return GetMessage(); } - set { SetMessage(value); } - } - - /// - /// Creates a new pipe frame instance - /// - /// The opcode of the frame - /// The data of the frame that will be serialized as JSON - public PipeFrame(Opcode opcode, object data) - { - //Set the opcode and a temp field for data - Opcode = opcode; - Data = null; - - //Set the data - SetObject(data); - } - - /// - /// Gets the encoding used for the pipe frames - /// - public Encoding MessageEncoding { get { return Encoding.UTF8; } } - - /// - /// Sets the data based of a string - /// - /// - private void SetMessage(string str) { Data = MessageEncoding.GetBytes(str); } - - /// - /// Gets a string based of the data - /// - /// - private string GetMessage() { return MessageEncoding.GetString(Data); } - - /// - /// Serializes the object into json string then encodes it into . - /// - /// - public void SetObject(object obj) - { - string json = JsonConvert.SerializeObject(obj); - SetMessage(json); - } - - /// - /// Sets the opcodes and serializes the object into a json string. - /// - /// - /// - public void SetObject(Opcode opcode, object obj) - { - Opcode = opcode; - SetObject(obj); - } - - /// - /// Deserializes the data into the supplied type using JSON. - /// - /// The type to deserialize into - /// - public T GetObject() - { - string json = GetMessage(); - return JsonConvert.DeserializeObject(json); - } - - /// - /// Attempts to read the contents of the frame from the stream - /// - /// - /// - public bool ReadStream(Stream stream) - { - //Try to read the opcode - uint op; - if (!TryReadUInt32(stream, out op)) - return false; - - //Try to read the length - uint len; - if (!TryReadUInt32(stream, out len)) - return false; - - uint readsRemaining = len; - - //Read the contents - using (var mem = new MemoryStream()) - { - byte[] buffer = new byte[Min(2048, len)]; // read in chunks of 2KB - int bytesRead; - while ((bytesRead = stream.Read(buffer, 0, Min(buffer.Length, readsRemaining))) > 0) - { - readsRemaining -= len; - mem.Write(buffer, 0, bytesRead); - } - - byte[] result = mem.ToArray(); - if (result.LongLength != len) - return false; - - Opcode = (Opcode)op; - Data = result; - return true; - } - - //fun - //if (a != null) { do { yield return true; switch (a) { case 1: await new Task(); default: lock (obj) { foreach (b in c) { for (int d = 0; d < 1; d++) { a++; } } } while (a is typeof(int) || (new Class()) != null) } goto MY_LABEL; - - } - - /// - /// Returns minimum value between a int and a unsigned int - /// - private int Min(int a, uint b) - { - if (b >= a) return a; - return (int) b; - } - - /// - /// Attempts to read a UInt32 - /// - /// - /// - /// - private bool TryReadUInt32(Stream stream, out uint value) - { - //Read the bytes available to us - byte[] bytes = new byte[4]; - int cnt = stream.Read(bytes, 0, bytes.Length); - - //Make sure we actually have a valid value - if (cnt != 4) - { - value = default(uint); - return false; - } - - value = BitConverter.ToUInt32(bytes, 0); - return true; - } - - /// - /// Writes the frame into the target frame as one big byte block. - /// - /// - public void WriteStream(Stream stream) - { - //Get all the bytes - byte[] op = BitConverter.GetBytes((uint) Opcode); - byte[] len = BitConverter.GetBytes(Length); - - //Copy it all into a buffer - byte[] buff = new byte[op.Length + len.Length + Data.Length]; - op.CopyTo(buff, 0); - len.CopyTo(buff, op.Length); - Data.CopyTo(buff, op.Length + len.Length); - - //Write it to the stream - stream.Write(buff, 0, buff.Length); - } - } -} diff --git a/DiscordAPI/Logging/ConsoleLogger.cs b/DiscordAPI/Logging/ConsoleLogger.cs deleted file mode 100644 index d940b0c..0000000 --- a/DiscordAPI/Logging/ConsoleLogger.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Logging -{ - /// - /// Logs the outputs to the console using - /// - public class ConsoleLogger : ILogger - { - /// - /// The level of logging to apply to this logger. - /// - public LogLevel Level { get; set; } - - /// - /// Should the output be coloured? - /// - public bool Coloured { get; set; } - - /// - /// A alias too - /// - public bool Colored { get { return Coloured; } set { Coloured = value; } } - - /// - /// Creates a new instance of a Console Logger. - /// - public ConsoleLogger() - { - this.Level = LogLevel.Info; - Coloured = false; - } - - /// - /// Creates a new instance of a Console Logger with a set log level - /// - /// - /// - public ConsoleLogger(LogLevel level, bool coloured = false) - { - Level = level; - Coloured = coloured; - } - - /// - /// Informative log messages - /// - /// - /// - public void Trace(string message, params object[] args) - { - if (Level > LogLevel.Trace) return; - - if (Coloured) Console.ForegroundColor = ConsoleColor.Gray; - Console.WriteLine("TRACE: " + message, args); - } - - /// - /// Informative log messages - /// - /// - /// - public void Info(string message, params object[] args) - { - if (Level > LogLevel.Info) return; - - if (Coloured) Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine("INFO: " + message, args); - } - - /// - /// Warning log messages - /// - /// - /// - public void Warning(string message, params object[] args) - { - if (Level > LogLevel.Warning) return; - - if (Coloured) Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("WARN: " + message, args); - } - - /// - /// Error log messsages - /// - /// - /// - public void Error(string message, params object[] args) - { - if (Level > LogLevel.Error) return; - - if (Coloured) Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("ERR : " + message, args); - } - - } -} diff --git a/DiscordAPI/Logging/FileLogger.cs b/DiscordAPI/Logging/FileLogger.cs deleted file mode 100644 index 9a16d5d..0000000 --- a/DiscordAPI/Logging/FileLogger.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Logging -{ - /// - /// Logs the outputs to a file - /// - public class FileLogger : ILogger - { - /// - /// The level of logging to apply to this logger. - /// - public LogLevel Level { get; set; } - - /// - /// Should the output be coloured? - /// - public string File { get; set; } - - private object filelock; - - /// - /// Creates a new instance of the file logger - /// - /// The path of the log file. - public FileLogger(string path) - : this(path, LogLevel.Info) { } - - /// - /// Creates a new instance of the file logger - /// - /// The path of the log file. - /// The level to assign to the logger. - public FileLogger(string path, LogLevel level) - { - Level = level; - File = path; - filelock = new object(); - } - - - /// - /// Informative log messages - /// - /// - /// - public void Trace(string message, params object[] args) - { - if (Level > LogLevel.Trace) return; - lock (filelock) System.IO.File.AppendAllText(File, "\r\nTRCE: " + (args.Length > 0 ? string.Format(message, args) : message)); - } - - /// - /// Informative log messages - /// - /// - /// - public void Info(string message, params object[] args) - { - if (Level > LogLevel.Info) return; - lock(filelock) System.IO.File.AppendAllText(File, "\r\nINFO: " + (args.Length > 0 ? string.Format(message, args) : message)); - } - - /// - /// Warning log messages - /// - /// - /// - public void Warning(string message, params object[] args) - { - if (Level > LogLevel.Warning) return; - lock (filelock) - System.IO.File.AppendAllText(File, "\r\nWARN: " + (args.Length > 0 ? string.Format(message, args) : message)); - } - - /// - /// Error log messsages - /// - /// - /// - public void Error(string message, params object[] args) - { - if (Level > LogLevel.Error) return; - lock (filelock) - System.IO.File.AppendAllText(File, "\r\nERR : " + (args.Length > 0 ? string.Format(message, args) : message)); - } - - } -} diff --git a/DiscordAPI/Logging/ILogger.cs b/DiscordAPI/Logging/ILogger.cs deleted file mode 100644 index 682d00e..0000000 --- a/DiscordAPI/Logging/ILogger.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Logging -{ - /// - /// Logging interface to log the internal states of the pipe. Logs are sent in a NON thread safe way. They can come from multiple threads and it is upto the ILogger to account for it. - /// - public interface ILogger - { - /// - /// The level of logging to apply to this logger. - /// - LogLevel Level { get; set; } - - /// - /// Debug trace messeages used for debugging internal elements. - /// - /// - /// - void Trace(string message, params object[] args); - - /// - /// Informative log messages - /// - /// - /// - void Info(string message, params object[] args); - - /// - /// Warning log messages - /// - /// - /// - void Warning(string message, params object[] args); - - /// - /// Error log messsages - /// - /// - /// - void Error(string message, params object[] args); - } -} diff --git a/DiscordAPI/Logging/NullLogger.cs b/DiscordAPI/Logging/NullLogger.cs deleted file mode 100644 index d5c04d7..0000000 --- a/DiscordAPI/Logging/NullLogger.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Logging -{ - /// - /// Ignores all log events - /// - public class NullLogger : ILogger - { - /// - /// The level of logging to apply to this logger. - /// - public LogLevel Level { get; set; } - - /// - /// Informative log messages - /// - /// - /// - public void Trace(string message, params object[] args) - { - //Null Logger, so no messages are acutally sent - } - - /// - /// Informative log messages - /// - /// - /// - public void Info(string message, params object[] args) - { - //Null Logger, so no messages are acutally sent - } - - /// - /// Warning log messages - /// - /// - /// - public void Warning(string message, params object[] args) - { - //Null Logger, so no messages are acutally sent - } - - /// - /// Error log messsages - /// - /// - /// - public void Error(string message, params object[] args) - { - //Null Logger, so no messages are acutally sent - } - } -} diff --git a/DiscordAPI/Message/CloseMessage.cs b/DiscordAPI/Message/CloseMessage.cs deleted file mode 100644 index 9ee7122..0000000 --- a/DiscordAPI/Message/CloseMessage.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace DiscordRPC.Message -{ - /// - /// Called when the IPC has closed. - /// - public class CloseMessage : IMessage - { - /// - /// The type of message - /// - public override MessageType Type { get { return MessageType.Close; } } - - /// - /// The reason for the close - /// - public string Reason { get; internal set; } - - /// - /// The closure code - /// - public int Code { get; internal set; } - - internal CloseMessage() { } - internal CloseMessage(string reason) { this.Reason = reason; } - } -} diff --git a/DiscordAPI/Message/ConnectionEstablishedMessage.cs b/DiscordAPI/Message/ConnectionEstablishedMessage.cs deleted file mode 100644 index 8315d92..0000000 --- a/DiscordAPI/Message/ConnectionEstablishedMessage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Message -{ - /// - /// The connection to the discord client was succesfull. This is called before . - /// - public class ConnectionEstablishedMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.ConnectionEstablished; } } - - /// - /// The pipe we ended up connecting too - /// - public int ConnectedPipe { get; internal set; } - } -} diff --git a/DiscordAPI/Message/ConnectionFailedMessage.cs b/DiscordAPI/Message/ConnectionFailedMessage.cs deleted file mode 100644 index 477e4d2..0000000 --- a/DiscordAPI/Message/ConnectionFailedMessage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Message -{ - /// - /// Failed to establish any connection with discord. Discord is potentially not running? - /// - public class ConnectionFailedMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.ConnectionFailed; } } - - /// - /// The pipe we failed to connect too. - /// - public int FailedPipe { get; internal set; } - } -} diff --git a/DiscordAPI/Message/ErrorMessage.cs b/DiscordAPI/Message/ErrorMessage.cs deleted file mode 100644 index b703b19..0000000 --- a/DiscordAPI/Message/ErrorMessage.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Newtonsoft.Json; - -namespace DiscordRPC.Message -{ - /// - /// Created when a error occurs within the ipc and it is sent to the client. - /// - public class ErrorMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.Error; } } - - /// - /// The Discord error code. - /// - [JsonProperty("code")] - public ErrorCode Code { get; internal set; } - - /// - /// The message associated with the error code. - /// - [JsonProperty("message")] - public string Message { get; internal set; } - - } - - /// - /// The error message received by discord. See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-errors for documentation - /// - public enum ErrorCode - { - //Pipe Error Codes - /// Pipe was Successful - Success = 0, - - ///The pipe had an exception - PipeException = 1, - - ///The pipe received corrupted data - ReadCorrupt = 2, - - //Custom Error Code - ///The functionality was not yet implemented - NotImplemented = 10, - - //Discord RPC error codes - ///Unkown Discord error - UnkownError = 1000, - - ///Invalid Payload received - InvalidPayload = 4000, - - ///Invalid command was sent - InvalidCommand = 4002, - - /// Invalid event was sent - InvalidEvent = 4004, - - /* - InvalidGuild = 4003, - InvalidChannel = 4005, - InvalidPermissions = 4006, - InvalidClientID = 4007, - InvalidOrigin = 4008, - InvalidToken = 4009, - InvalidUser = 4010, - OAuth2Error = 5000, - SelectChannelTimeout = 5001, - GetGuildTimeout = 5002, - SelectVoiceForceRequired = 5003, - CaptureShortcutAlreadyListening = 5004 - */ - } -} diff --git a/DiscordAPI/Message/IMessage.cs b/DiscordAPI/Message/IMessage.cs deleted file mode 100644 index 34522d8..0000000 --- a/DiscordAPI/Message/IMessage.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace DiscordRPC.Message -{ - /// - /// Messages received from discord. - /// - public abstract class IMessage - { - /// - /// The type of message received from discord - /// - public abstract MessageType Type { get; } - - /// - /// The time the message was created - /// - public DateTime TimeCreated { get { return _timecreated; } } - private DateTime _timecreated; - - /// - /// Creates a new instance of the message - /// - public IMessage() - { - _timecreated = DateTime.Now; - } - } -} diff --git a/DiscordAPI/Message/JoinMessage.cs b/DiscordAPI/Message/JoinMessage.cs deleted file mode 100644 index 21b4672..0000000 --- a/DiscordAPI/Message/JoinMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Message -{ - /// - /// Called when the Discord Client wishes for this process to join a game. D -> C. - /// - public class JoinMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.Join; } } - - /// - /// The to connect with. - /// - [JsonProperty("secret")] - public string Secret { get; internal set; } - } -} diff --git a/DiscordAPI/Message/JoinRequestMessage.cs b/DiscordAPI/Message/JoinRequestMessage.cs deleted file mode 100644 index 05db72e..0000000 --- a/DiscordAPI/Message/JoinRequestMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Message -{ - /// - /// Called when some other person has requested access to this game. C -> D -> C. - /// - public class JoinRequestMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.JoinRequest; } } - - /// - /// The discord user that is requesting access. - /// - [JsonProperty("user")] - public User User { get; internal set; } - } -} diff --git a/DiscordAPI/Message/MessageType.cs b/DiscordAPI/Message/MessageType.cs deleted file mode 100644 index 223291e..0000000 --- a/DiscordAPI/Message/MessageType.cs +++ /dev/null @@ -1,64 +0,0 @@ - -namespace DiscordRPC.Message -{ - /// - /// Type of message. - /// - public enum MessageType - { - /// - /// The Discord Client is ready to send and receive messages. - /// - Ready, - - /// - /// The connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again. - /// - Close, - - /// - /// A error has occured during the transmission of a message. For example, if a bad Rich Presence payload is sent, this event will be called explaining what went wrong. - /// - Error, - - /// - /// The Discord Client has updated the presence. - /// - PresenceUpdate, - - /// - /// The Discord Client has subscribed to an event. - /// - Subscribe, - - /// - /// The Discord Client has unsubscribed from an event. - /// - Unsubscribe, - - /// - /// The Discord Client wishes for this process to join a game. - /// - Join, - - /// - /// The Discord Client wishes for this process to spectate a game. - /// - Spectate, - - /// - /// Another discord user requests permission to join this game. - /// - JoinRequest, - - /// - /// The connection to the discord client was succesfull. This is called before . - /// - ConnectionEstablished, - - /// - /// Failed to establish any connection with discord. Discord is potentially not running? - /// - ConnectionFailed - } -} diff --git a/DiscordAPI/Message/PresenceMessage.cs b/DiscordAPI/Message/PresenceMessage.cs deleted file mode 100644 index 7f13701..0000000 --- a/DiscordAPI/Message/PresenceMessage.cs +++ /dev/null @@ -1,47 +0,0 @@ - - -namespace DiscordRPC.Message -{ - /// - /// Representation of the message received by discord when the presence has been updated. - /// - public class PresenceMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.PresenceUpdate; } } - - internal PresenceMessage() : this(null) { } - internal PresenceMessage(RichPresenceResponse rpr) - { - if (rpr == null) - { - Presence = null; - Name = "No Rich Presence"; - ApplicationID = ""; - } - else - { - Presence = (RichPresence)rpr; - Name = rpr.Name; - ApplicationID = rpr.ClientID; - } - } - - /// - /// The rich presence Discord has set - /// - public RichPresence Presence { get; internal set; } - - /// - /// The name of the application Discord has set it for - /// - public string Name { get; internal set; } - - /// - /// The ID of the application discord has set it for - /// - public string ApplicationID { get; internal set; } - } -} diff --git a/DiscordAPI/Message/ReadyMessage.cs b/DiscordAPI/Message/ReadyMessage.cs deleted file mode 100644 index c93ba57..0000000 --- a/DiscordAPI/Message/ReadyMessage.cs +++ /dev/null @@ -1,35 +0,0 @@ - - -using Newtonsoft.Json; - -namespace DiscordRPC.Message -{ - /// - /// Called when the ipc is ready to send arguments. - /// - public class ReadyMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.Ready; } } - - /// - /// The configuration of the connection - /// - [JsonProperty("config")] - public Configuration Configuration { get; set; } - - /// - /// User the connection belongs too - /// - [JsonProperty("user")] - public User User { get; set; } - - /// - /// The version of the RPC - /// - [JsonProperty("v")] - public int Version { get; set; } - } -} diff --git a/DiscordAPI/Message/SpectateMessage.cs b/DiscordAPI/Message/SpectateMessage.cs deleted file mode 100644 index 8a3d16b..0000000 --- a/DiscordAPI/Message/SpectateMessage.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Message -{ - /// - /// Called when the Discord Client wishes for this process to spectate a game. D -> C. - /// - public class SpectateMessage : JoinMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.Spectate; } } - } -} diff --git a/DiscordAPI/Message/SubscribeMessage.cs b/DiscordAPI/Message/SubscribeMessage.cs deleted file mode 100644 index ac694b0..0000000 --- a/DiscordAPI/Message/SubscribeMessage.cs +++ /dev/null @@ -1,40 +0,0 @@ -using DiscordRPC.RPC.Payload; - -namespace DiscordRPC.Message -{ - /// - /// Called as validation of a subscribe - /// - public class SubscribeMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.Subscribe; } } - - /// - /// The event that was subscribed too. - /// - public EventType Event { get; internal set; } - - internal SubscribeMessage(ServerEvent evt) - { - switch (evt) - { - default: - case ServerEvent.ActivityJoin: - Event = EventType.Join; - break; - - case ServerEvent.ActivityJoinRequest: - Event = EventType.JoinRequest; - break; - - case ServerEvent.ActivitySpectate: - Event = EventType.Spectate; - break; - - } - } - } -} diff --git a/DiscordAPI/Message/UnsubscribeMsesage.cs b/DiscordAPI/Message/UnsubscribeMsesage.cs deleted file mode 100644 index 30eb5e1..0000000 --- a/DiscordAPI/Message/UnsubscribeMsesage.cs +++ /dev/null @@ -1,39 +0,0 @@ -using DiscordRPC.RPC.Payload; - -namespace DiscordRPC.Message -{ - /// - /// Called as validation of a subscribe - /// - public class UnsubscribeMessage : IMessage - { - /// - /// The type of message received from discord - /// - public override MessageType Type { get { return MessageType.Unsubscribe; } } - - /// - /// The event that was subscribed too. - /// - public EventType Event { get; internal set; } - - internal UnsubscribeMessage(ServerEvent evt) - { - switch (evt) - { - default: - case ServerEvent.ActivityJoin: - Event = EventType.Join; - break; - - case ServerEvent.ActivityJoinRequest: - Event = EventType.JoinRequest; - break; - - case ServerEvent.ActivitySpectate: - Event = EventType.Spectate; - break; - } - } - } -} diff --git a/DiscordAPI/RPC/Commands/CloseCommand.cs b/DiscordAPI/RPC/Commands/CloseCommand.cs deleted file mode 100644 index a24867f..0000000 --- a/DiscordAPI/RPC/Commands/CloseCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using DiscordRPC.RPC.Payload; -using Newtonsoft.Json; - -namespace DiscordRPC.RPC.Commands -{ - internal class CloseCommand : ICommand - { - /// - /// The process ID - /// - [JsonProperty("pid")] - public int PID { get; set; } - - /// - /// The rich presence to be set. Can be null. - /// - [JsonProperty("close_reason")] - public string value = "Unity 5.5 doesn't handle thread aborts. Can you please close me discord?"; - - public IPayload PreparePayload(long nonce) - { - return new ArgumentPayload() - { - Command = Command.Dispatch, - Nonce = null, - Arguments = null - }; - } - } -} diff --git a/DiscordAPI/RPC/Commands/ICommand.cs b/DiscordAPI/RPC/Commands/ICommand.cs deleted file mode 100644 index 6eeedb2..0000000 --- a/DiscordAPI/RPC/Commands/ICommand.cs +++ /dev/null @@ -1,13 +0,0 @@ -using DiscordRPC.RPC.Payload; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.RPC.Commands -{ - internal interface ICommand - { - IPayload PreparePayload(long nonce); - } -} diff --git a/DiscordAPI/RPC/Commands/PresenceCommand.cs b/DiscordAPI/RPC/Commands/PresenceCommand.cs deleted file mode 100644 index cdffa48..0000000 --- a/DiscordAPI/RPC/Commands/PresenceCommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using DiscordRPC.RPC.Payload; -using Newtonsoft.Json; - -namespace DiscordRPC.RPC.Commands -{ - internal class PresenceCommand : ICommand - { - /// - /// The process ID - /// - [JsonProperty("pid")] - public int PID { get; set; } - - /// - /// The rich presence to be set. Can be null. - /// - [JsonProperty("activity")] - public RichPresence Presence { get; set; } - - public IPayload PreparePayload(long nonce) - { - return new ArgumentPayload(this, nonce) - { - Command = Command.SetActivity - }; - } - } -} diff --git a/DiscordAPI/RPC/Commands/RespondCommand.cs b/DiscordAPI/RPC/Commands/RespondCommand.cs deleted file mode 100644 index 94dda99..0000000 --- a/DiscordAPI/RPC/Commands/RespondCommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using DiscordRPC.RPC.Payload; -using Newtonsoft.Json; - -namespace DiscordRPC.RPC.Commands -{ - internal class RespondCommand : ICommand - { - /// - /// The user ID that we are accepting / rejecting - /// - [JsonProperty("user_id")] - public string UserID { get; set; } - - /// - /// If true, the user will be allowed to connect. - /// - [JsonIgnore] - public bool Accept { get; set; } - - public IPayload PreparePayload(long nonce) - { - return new ArgumentPayload(this, nonce) - { - Command = Accept ? Command.SendActivityJoinInvite : Command.CloseActivityJoinRequest - }; - } - } -} diff --git a/DiscordAPI/RPC/Commands/SubscribeCommand.cs b/DiscordAPI/RPC/Commands/SubscribeCommand.cs deleted file mode 100644 index 69c9cdb..0000000 --- a/DiscordAPI/RPC/Commands/SubscribeCommand.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using DiscordRPC.RPC.Payload; - -namespace DiscordRPC.RPC.Commands -{ - internal class SubscribeCommand : ICommand - { - public ServerEvent Event { get; set; } - public bool IsUnsubscribe { get; set; } - - public IPayload PreparePayload(long nonce) - { - return new EventPayload(nonce) - { - Command = IsUnsubscribe ? Command.Unsubscribe : Command.Subscribe, - Event = Event - }; - } - } -} diff --git a/DiscordAPI/RPC/Payload/ClosePayload.cs b/DiscordAPI/RPC/Payload/ClosePayload.cs deleted file mode 100644 index 122e7f5..0000000 --- a/DiscordAPI/RPC/Payload/ClosePayload.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.RPC.Payload -{ - internal class ClosePayload : IPayload - { - /// - /// The close code the discord gave us - /// - [JsonProperty("code")] - public int Code { get; set; } - - /// - /// The close reason discord gave us - /// - [JsonProperty("message")] - public string Reason { get; set; } - } -} diff --git a/DiscordAPI/RPC/Payload/Command.cs b/DiscordAPI/RPC/Payload/Command.cs deleted file mode 100644 index 1863fb1..0000000 --- a/DiscordAPI/RPC/Payload/Command.cs +++ /dev/null @@ -1,129 +0,0 @@ -using DiscordRPC.Converters; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.RPC.Payload -{ - /// - /// The possible commands that can be sent and received by the server. - /// - internal enum Command - { - /// - /// event dispatch - /// - [EnumValue("DISPATCH")] - Dispatch, - - /// - /// Called to set the activity - /// - [EnumValue("SET_ACTIVITY")] - SetActivity, - - /// - /// used to subscribe to an RPC event - /// - [EnumValue("SUBSCRIBE")] - Subscribe, - - /// - /// used to unsubscribe from an RPC event - /// - [EnumValue("UNSUBSCRIBE")] - Unsubscribe, - - /// - /// Used to accept join requests. - /// - [EnumValue("SEND_ACTIVITY_JOIN_INVITE")] - SendActivityJoinInvite, - - /// - /// Used to reject join requests. - /// - [EnumValue("CLOSE_ACTIVITY_JOIN_REQUEST")] - CloseActivityJoinRequest, - - /// - /// used to authorize a new client with your app - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - Authorize, - - /// - /// used to authenticate an existing client with your app - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - Authenticate, - - /// - /// used to retrieve guild information from the client - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - GetGuild, - - /// - /// used to retrieve a list of guilds from the client - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - GetGuilds, - - /// - /// used to retrieve channel information from the client - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - GetChannel, - - /// - /// used to retrieve a list of channels for a guild from the client - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - GetChannels, - - - /// - /// used to change voice settings of users in voice channels - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - SetUserVoiceSettings, - - /// - /// used to join or leave a voice channel, group dm, or dm - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - SelectVoiceChannel, - - /// - /// used to get the current voice channel the client is in - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - GetSelectedVoiceChannel, - - /// - /// used to join or leave a text channel, group dm, or dm - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - SelectTextChannel, - - /// - /// used to retrieve the client's voice settings - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - GetVoiceSettings, - - /// - /// used to set the client's voice settings - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - SetVoiceSettings, - - /// - /// used to capture a keyboard shortcut entered by the user RPC Events - /// - [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] - CaptureShortcut - } -} diff --git a/DiscordAPI/RPC/Payload/IPayload.cs b/DiscordAPI/RPC/Payload/IPayload.cs deleted file mode 100644 index 48b1f14..0000000 --- a/DiscordAPI/RPC/Payload/IPayload.cs +++ /dev/null @@ -1,35 +0,0 @@ -using DiscordRPC.Converters; -using Newtonsoft.Json; - -namespace DiscordRPC.RPC.Payload -{ - /// - /// Base Payload that is received by both client and server - /// - internal abstract class IPayload - { - /// - /// The type of payload - /// - [JsonProperty("cmd"), JsonConverter(typeof(EnumSnakeCaseConverter))] - public Command Command { get; set; } - - /// - /// A incremental value to help identify payloads - /// - [JsonProperty("nonce")] - public string Nonce { get; set; } - - protected IPayload() { } - protected IPayload(long nonce) - { - Nonce = nonce.ToString(); - } - - public override string ToString() - { - return "Payload || Command: " + Command.ToString() + ", Nonce: " + (Nonce != null ? Nonce.ToString() : "NULL"); - } - } -} - diff --git a/DiscordAPI/RPC/Payload/PayloadArgument.cs b/DiscordAPI/RPC/Payload/PayloadArgument.cs deleted file mode 100644 index 2116259..0000000 --- a/DiscordAPI/RPC/Payload/PayloadArgument.cs +++ /dev/null @@ -1,53 +0,0 @@ -using DiscordRPC.Converters; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DiscordRPC.RPC.Payload -{ - /// - /// The payload that is sent by the client to discord for events such as setting the rich presence. - /// - /// SetPrecense - /// - /// - internal class ArgumentPayload : IPayload - { - /// - /// The data the server sent too us - /// - [JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)] - public JObject Arguments { get; set; } - - public ArgumentPayload() : base() { Arguments = null; } - public ArgumentPayload(long nonce) : base(nonce) { Arguments = null; } - public ArgumentPayload(object args, long nonce) : base(nonce) - { - SetObject(args); - } - - /// - /// Sets the obejct stored within the data. - /// - /// - public void SetObject(object obj) - { - Arguments = JObject.FromObject(obj); - } - - /// - /// Gets the object stored within the Data - /// - /// - /// - public T GetObject() - { - return Arguments.ToObject(); - } - - public override string ToString() - { - return "Argument " + base.ToString(); - } - } -} - diff --git a/DiscordAPI/RPC/Payload/PayloadEvent.cs b/DiscordAPI/RPC/Payload/PayloadEvent.cs deleted file mode 100644 index 5ec94f9..0000000 --- a/DiscordAPI/RPC/Payload/PayloadEvent.cs +++ /dev/null @@ -1,57 +0,0 @@ -using DiscordRPC.Converters; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DiscordRPC.RPC.Payload -{ - /// - /// Used for Discord IPC Events - /// - internal class EventPayload : IPayload - { - /// - /// The data the server sent too us - /// - [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] - public JObject Data { get; set; } - - /// - /// The type of event the server sent - /// - [JsonProperty("evt"), JsonConverter(typeof(EnumSnakeCaseConverter))] - public ServerEvent? Event { get; set; } - - /// - /// Creates a payload with empty data - /// - public EventPayload() : base() { Data = null; } - - /// - /// Creates a payload with empty data and a set nonce - /// - /// - public EventPayload(long nonce) : base(nonce) { Data = null; } - - /// - /// Gets the object stored within the Data - /// - /// - /// - public T GetObject() - { - if (Data == null) return default(T); - return Data.ToObject(); - } - - /// - /// Converts the object into a human readable string - /// - /// - public override string ToString() - { - return "Event " + base.ToString() + ", Event: " + (Event.HasValue ? Event.ToString() : "N/A"); - } - } - - -} diff --git a/DiscordAPI/RPC/RpcConnection.cs b/DiscordAPI/RPC/RpcConnection.cs deleted file mode 100644 index 17cd57a..0000000 --- a/DiscordAPI/RPC/RpcConnection.cs +++ /dev/null @@ -1,869 +0,0 @@ -using DiscordRPC.Helper; -using DiscordRPC.Message; -using DiscordRPC.IO; -using DiscordRPC.RPC.Commands; -using DiscordRPC.RPC.Payload; -using System; -using System.Collections.Generic; -using System.Threading; -using Newtonsoft.Json; -using DiscordRPC.Logging; -using DiscordRPC.Events; - -namespace DiscordRPC.RPC -{ - /// - /// Communicates between the client and discord through RPC - /// - internal class RpcConnection : IDisposable - { - /// - /// Version of the RPC Protocol - /// - public static readonly int VERSION = 1; - - /// - /// The rate of poll to the discord pipe. - /// - public static readonly int POLL_RATE = 1000; - - /// - /// Should we send a null presence on the fairwells? - /// - private static readonly bool CLEAR_ON_SHUTDOWN = true; - - /// - /// Should we work in a lock step manner? This option is semi-obsolete and may not work as expected. - /// - private static readonly bool LOCK_STEP = false; - - /// - /// The logger used by the RPC connection - /// - public ILogger Logger - { - get { return _logger; } - set - { - _logger = value; - if (namedPipe != null) - namedPipe.Logger = value; - } - } - private ILogger _logger; - - /// - /// Called when a message is received from the RPC and is about to be enqueued. This is cross-thread and will execute on the RPC thread. - /// - public event OnRpcMessageEvent OnRpcMessage; - - #region States - - /// - /// The current state of the RPC connection - /// - public RpcState State { get { var tmp = RpcState.Disconnected; lock (l_states) tmp = _state; return tmp; } } - private RpcState _state; - private readonly object l_states = new object(); - - /// - /// The configuration received by the Ready - /// - public Configuration Configuration { get { Configuration tmp = null; lock (l_config) tmp = _configuration; return tmp; } } - private Configuration _configuration = null; - private readonly object l_config = new object(); - - private volatile bool aborting = false; - private volatile bool shutdown = false; - - /// - /// Indicates if the RPC connection is still running in the background - /// - public bool IsRunning { get { return thread != null; } } - - /// - /// Forces the to call instead, safely saying goodbye to Discord. - /// This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate. - /// - public bool ShutdownOnly { get; set; } - - #endregion - - #region Privates - - private string applicationID; //ID of the Discord APP - private int processID; //ID of the process to track - - private long nonce; //Current command index - - private Thread thread; //The current thread - private INamedPipeClient namedPipe; - - private int targetPipe; //The pipe to taget. Leave as -1 for any available pipe. - - private readonly object l_rtqueue = new object(); //Lock for the send queue - private readonly uint _maxRtQueueSize; - private Queue _rtqueue; //The send queue - - private readonly object l_rxqueue = new object(); //Lock for the receive queue - private readonly uint _maxRxQueueSize; //The max size of the RX queue - private Queue _rxqueue; //The receive queue - - private AutoResetEvent queueUpdatedEvent = new AutoResetEvent(false); - private BackoffDelay delay; //The backoff delay before reconnecting. - #endregion - - /// - /// Creates a new instance of the RPC. - /// - /// The ID of the Discord App - /// The ID of the currently running process - /// The target pipe to connect too - /// The pipe client we shall use. - /// The maximum size of the out queue - /// The maximum size of the in queue - public RpcConnection(string applicationID, int processID, int targetPipe, INamedPipeClient client, uint maxRxQueueSize = 128, uint maxRtQueueSize = 512) - { - this.applicationID = applicationID; - this.processID = processID; - this.targetPipe = targetPipe; - this.namedPipe = client; - this.ShutdownOnly = true; - - //Assign a default logger - Logger = new ConsoleLogger(); - - delay = new BackoffDelay(500, 60 * 1000); - _maxRtQueueSize = maxRtQueueSize; - _rtqueue = new Queue((int)_maxRtQueueSize + 1); - - _maxRxQueueSize = maxRxQueueSize; - _rxqueue = new Queue((int)_maxRxQueueSize + 1); - - nonce = 0; - } - - - private long GetNextNonce() - { - nonce += 1; - return nonce; - } - - #region Queues - /// - /// Enqueues a command - /// - /// The command to enqueue - internal void EnqueueCommand(ICommand command) - { - Logger.Trace("Enqueue Command: " + command.GetType().FullName); - - //We cannot add anything else if we are aborting or shutting down. - if (aborting || shutdown) return; - - //Enqueue the set presence argument - lock (l_rtqueue) - { - //If we are too big drop the last element - if (_rtqueue.Count == _maxRtQueueSize) - { - Logger.Error("Too many enqueued commands, dropping oldest one. Maybe you are pushing new presences to fast?"); - _rtqueue.Dequeue(); - } - - //Enqueue the message - _rtqueue.Enqueue(command); - } - } - - /// - /// Adds a message to the message queue. Does not copy the message, so besure to copy it yourself or dereference it. - /// - /// The message to add - private void EnqueueMessage(IMessage message) - { - //Invoke the message - try - { - if (OnRpcMessage != null) - OnRpcMessage.Invoke(this, message); - } - catch (Exception e) - { - Logger.Error("Unhandled Exception while processing event: {0}", e.GetType().FullName); - Logger.Error(e.Message); - Logger.Error(e.StackTrace); - } - - //Small queue sizes should just ignore messages - if (_maxRxQueueSize <= 0) - { - Logger.Trace("Enqueued Message, but queue size is 0."); - return; - } - - //Large queue sizes should keep the queue in check - Logger.Trace("Enqueue Message: " + message.Type); - lock (l_rxqueue) - { - //If we are too big drop the last element - if (_rxqueue.Count == _maxRxQueueSize) - { - Logger.Warning("Too many enqueued messages, dropping oldest one."); - _rxqueue.Dequeue(); - } - - //Enqueue the message - _rxqueue.Enqueue(message); - } - } - - /// - /// Dequeues a single message from the event stack. Returns null if none are available. - /// - /// - internal IMessage DequeueMessage() - { - //Logger.Trace("Deque Message"); - lock (l_rxqueue) - { - //We have nothing, so just return null. - if (_rxqueue.Count == 0) return null; - - //Get the value and remove it from the list at the same time - return _rxqueue.Dequeue(); - } - } - - /// - /// Dequeues all messages from the event stack. - /// - /// - internal IMessage[] DequeueMessages() - { - //Logger.Trace("Deque Multiple Messages"); - lock (l_rxqueue) - { - //Copy the messages into an array - IMessage[] messages = _rxqueue.ToArray(); - - //Clear the entire queue - _rxqueue.Clear(); - - //return the array - return messages; - } - } - #endregion - - /// - /// Main thread loop - /// - private void MainLoop() - { - //initialize the pipe - Logger.Info("RPC Connection Started"); - if (Logger.Level <= LogLevel.Trace) - { - Logger.Trace("============================"); - Logger.Trace("Assembly: " + System.Reflection.Assembly.GetAssembly(typeof(RichPresence)).FullName); - Logger.Trace("Pipe: " + namedPipe.GetType().FullName); - Logger.Trace("Platform: " + Environment.OSVersion.ToString()); - Logger.Trace("applicationID: " + applicationID); - Logger.Trace("targetPipe: " + targetPipe); - Logger.Trace("POLL_RATE: " + POLL_RATE); - Logger.Trace("_maxRtQueueSize: " + _maxRtQueueSize); - Logger.Trace("_maxRxQueueSize: " + _maxRxQueueSize); - Logger.Trace("============================"); - } - - //Forever trying to connect unless the abort signal is sent - //Keep Alive Loop - while (!aborting && !shutdown) - { - try - { - //Wrap everything up in a try get - //Dispose of the pipe if we have any (could be broken) - if (namedPipe == null) - { - Logger.Error("Something bad has happened with our pipe client!"); - aborting = true; - return; - } - - //Connect to a new pipe - Logger.Trace("Connecting to the pipe through the {0}", namedPipe.GetType().FullName); - if (namedPipe.Connect(targetPipe)) - { - #region Connected - //We connected to a pipe! Reset the delay - Logger.Trace("Connected to the pipe. Attempting to establish handshake..."); - EnqueueMessage(new ConnectionEstablishedMessage() { ConnectedPipe = namedPipe.ConnectedPipe }); - - //Attempt to establish a handshake - EstablishHandshake(); - Logger.Trace("Connection Established. Starting reading loop..."); - - //Continously iterate, waiting for the frame - //We want to only stop reading if the inside tells us (mainloop), if we are aborting (abort) or the pipe disconnects - // We dont want to exit on a shutdown, as we still have information - PipeFrame frame; - bool mainloop = true; - while (mainloop && !aborting && !shutdown && namedPipe.IsConnected) - { - #region Read Loop - - //Iterate over every frame we have queued up, processing its contents - if (namedPipe.ReadFrame(out frame)) - { - #region Read Payload - Logger.Trace("Read Payload: {0}", frame.Opcode); - - //Do some basic processing on the frame - switch (frame.Opcode) - { - //We have been told by discord to close, so we will consider it an abort - case Opcode.Close: - - ClosePayload close = frame.GetObject(); - Logger.Warning("We have been told to terminate by discord: ({0}) {1}", close.Code, close.Reason); - EnqueueMessage(new CloseMessage() { Code = close.Code, Reason = close.Reason }); - mainloop = false; - break; - - //We have pinged, so we will flip it and respond back with pong - case Opcode.Ping: - Logger.Trace("PING"); - frame.Opcode = Opcode.Pong; - namedPipe.WriteFrame(frame); - break; - - //We have ponged? I have no idea if Discord actually sends ping/pongs. - case Opcode.Pong: - Logger.Trace("PONG"); - break; - - //A frame has been sent, we should deal with that - case Opcode.Frame: - if (shutdown) - { - //We are shutting down, so skip it - Logger.Warning("Skipping frame because we are shutting down."); - break; - } - - if (frame.Data == null) - { - //We have invalid data, thats not good. - Logger.Error("We received no data from the frame so we cannot get the event payload!"); - break; - } - - //We have a frame, so we are going to process the payload and add it to the stack - EventPayload response = null; - try { response = frame.GetObject(); } catch (Exception e) - { - Logger.Error("Failed to parse event! " + e.Message); - Logger.Error("Data: " + frame.Message); - } - - if (response != null) ProcessFrame(response); - break; - - - default: - case Opcode.Handshake: - //We have a invalid opcode, better terminate to be safe - Logger.Error("Invalid opcode: {0}", frame.Opcode); - mainloop = false; - break; - } - - #endregion - } - - if (!aborting && namedPipe.IsConnected) - { - //Process the entire command queue we have left - ProcessCommandQueue(); - - //Wait for some time, or until a command has been queued up - queueUpdatedEvent.WaitOne(POLL_RATE); - } - - #endregion - } - #endregion - - Logger.Trace("Left main read loop for some reason. Aborting: {0}, Shutting Down: {1}", aborting, shutdown); - } - else - { - Logger.Error("Failed to connect for some reason."); - EnqueueMessage(new ConnectionFailedMessage() { FailedPipe = targetPipe }); - } - - //If we are not aborting, we have to wait a bit before trying to connect again - if (!aborting && !shutdown) - { - //We have disconnected for some reason, either a failed pipe or a bad reading, - // so we are going to wait a bit before doing it again - long sleep = delay.NextDelay(); - - Logger.Trace("Waiting {0}ms before attempting to connect again", sleep); - Thread.Sleep(delay.NextDelay()); - } - } - //catch(InvalidPipeException e) - //{ - // Logger.Error("Invalid Pipe Exception: {0}", e.Message); - //} - catch (Exception e) - { - Logger.Error("Unhandled Exception: {0}", e.GetType().FullName); - Logger.Error(e.Message); - Logger.Error(e.StackTrace); - } - finally - { - //Disconnect from the pipe because something bad has happened. An exception has been thrown or the main read loop has terminated. - if (namedPipe.IsConnected) - { - //Terminate the pipe - Logger.Trace("Closing the named pipe."); - namedPipe.Close(); - } - - //Update our state - SetConnectionState(RpcState.Disconnected); - } - } - - //We have disconnected, so dispose of the thread and the pipe. - Logger.Trace("Left Main Loop"); - if (namedPipe != null) - namedPipe.Dispose(); - - Logger.Info("Thread Terminated, no longer performing RPC connection."); - } - - #region Reading - - /// Handles the response from the pipe and calls appropriate events and changes states. - /// The response received by the server. - private void ProcessFrame(EventPayload response) - { - Logger.Info("Handling Response. Cmd: {0}, Event: {1}", response.Command, response.Event); - - //Check if it is an error - if (response.Event.HasValue && response.Event.Value == ServerEvent.Error) - { - //We have an error - Logger.Error("Error received from the RPC"); - - //Create the event objetc and push it to the queue - ErrorMessage err = response.GetObject(); - Logger.Error("Server responded with an error message: ({0}) {1}", err.Code.ToString(), err.Message); - - //Enqueue the messsage and then end - EnqueueMessage(err); - return; - } - - //Check if its a handshake - if (State == RpcState.Connecting) - { - if (response.Command == Command.Dispatch && response.Event.HasValue && response.Event.Value == ServerEvent.Ready) - { - Logger.Info("Connection established with the RPC"); - SetConnectionState(RpcState.Connected); - delay.Reset(); - - //Prepare the object - ReadyMessage ready = response.GetObject(); - lock (l_config) - { - _configuration = ready.Configuration; - ready.User.SetConfiguration(_configuration); - } - - //Enqueue the message - EnqueueMessage(ready); - return; - } - } - - if (State == RpcState.Connected) - { - switch(response.Command) - { - //We were sent a dispatch, better process it - case Command.Dispatch: - ProcessDispatch(response); - break; - - //We were sent a Activity Update, better enqueue it - case Command.SetActivity: - if (response.Data == null) - { - EnqueueMessage(new PresenceMessage()); - } - else - { - RichPresenceResponse rp = response.GetObject(); - EnqueueMessage(new PresenceMessage(rp)); - } - break; - - case Command.Unsubscribe: - case Command.Subscribe: - - //Prepare a serializer that can account for snake_case enums. - JsonSerializer serializer = new JsonSerializer(); - serializer.Converters.Add(new Converters.EnumSnakeCaseConverter()); - - //Go through the data, looking for the evt property, casting it to a server event - var evt = response.GetObject().Event.Value; - - //Enqueue the appropriate message. - if (response.Command == Command.Subscribe) - EnqueueMessage(new SubscribeMessage(evt)); - else - EnqueueMessage(new UnsubscribeMessage(evt)); - - break; - - - case Command.SendActivityJoinInvite: - Logger.Trace("Got invite response ack."); - break; - - case Command.CloseActivityJoinRequest: - Logger.Trace("Got invite response reject ack."); - break; - - //we have no idea what we were sent - default: - Logger.Error("Unkown frame was received! {0}", response.Command); - return; - } - return; - } - - Logger.Trace("Received a frame while we are disconnected. Ignoring. Cmd: {0}, Event: {1}", response.Command, response.Event); - } - - private void ProcessDispatch(EventPayload response) - { - if (response.Command != Command.Dispatch) return; - if (!response.Event.HasValue) return; - - switch(response.Event.Value) - { - //We are to join the server - case ServerEvent.ActivitySpectate: - var spectate = response.GetObject(); - EnqueueMessage(spectate); - break; - - case ServerEvent.ActivityJoin: - var join = response.GetObject(); - EnqueueMessage(join); - break; - - case ServerEvent.ActivityJoinRequest: - var request = response.GetObject(); - EnqueueMessage(request); - break; - - //Unkown dispatch event received. We should just ignore it. - default: - Logger.Warning("Ignoring {0}", response.Event.Value); - break; - } - } - - #endregion - - #region Writting - - private void ProcessCommandQueue() - { - //Logger.Info("Checking command queue"); - - //We are not ready yet, dont even try - if (State != RpcState.Connected) - return; - - //We are aborting, so we will just log a warning so we know this is probably only going to send the CLOSE - if (aborting) - Logger.Warning("We have been told to write a queue but we have also been aborted."); - - //Prepare some variabels we will clone into with locks - bool needsWriting = true; - ICommand item = null; - - //Continue looping until we dont need anymore messages - while (needsWriting && namedPipe.IsConnected) - { - lock (l_rtqueue) - { - //Pull the value and update our writing needs - // If we have nothing to write, exit the loop - needsWriting = _rtqueue.Count > 0; - if (!needsWriting) break; - - //Peek at the item - item = _rtqueue.Peek(); - } - - //BReak out of the loop as soon as we send this item - if (shutdown || (!aborting && LOCK_STEP)) - needsWriting = false; - - //Prepare the payload - IPayload payload = item.PreparePayload(GetNextNonce()); - Logger.Trace("Attempting to send payload: " + payload.Command); - - //Prepare the frame - PipeFrame frame = new PipeFrame(); - if (item is CloseCommand) - { - //We have been sent a close frame. We better just send a handwave - //Send it off to the server - SendHandwave(); - - //Queue the item - Logger.Trace("Handwave sent, ending queue processing."); - lock (l_rtqueue) _rtqueue.Dequeue(); - - //Stop sending any more messages - return; - } - else - { - if (aborting) - { - //We are aborting, so just dequeue the message and dont bother sending it - Logger.Warning("- skipping frame because of abort."); - lock (l_rtqueue) _rtqueue.Dequeue(); - } - else - { - //Prepare the frame - frame.SetObject(Opcode.Frame, payload); - - //Write it and if it wrote perfectly fine, we will dequeue it - Logger.Trace("Sending payload: " + payload.Command); - if (namedPipe.WriteFrame(frame)) - { - //We sent it, so now dequeue it - Logger.Trace("Sent Successfully."); - lock (l_rtqueue) _rtqueue.Dequeue(); - } - else - { - //Something went wrong, so just giveup and wait for the next time around. - Logger.Warning("Something went wrong during writing!"); - return; - } - } - } - } - } - - #endregion - - #region Connection - - /// - /// Establishes the handshake with the server. - /// - /// - private void EstablishHandshake() - { - Logger.Trace("Attempting to establish a handshake..."); - - //We are establishing a lock and not releasing it until we sent the handshake message. - // We need to set the key, and it would not be nice if someone did things between us setting the key. - - //Check its state - if (State != RpcState.Disconnected) - { - Logger.Error("State must be disconnected in order to start a handshake!"); - return; - } - - //Send it off to the server - Logger.Trace("Sending Handshake..."); - if (!namedPipe.WriteFrame(new PipeFrame(Opcode.Handshake, new Handshake() { Version = VERSION, ClientID = applicationID }))) - { - Logger.Error("Failed to write a handshake."); - return; - } - - //This has to be done outside the lock - SetConnectionState(RpcState.Connecting); - } - - /// - /// Establishes a fairwell with the server by sending a handwave. - /// - private void SendHandwave() - { - Logger.Info("Attempting to wave goodbye..."); - - //Check its state - if (State == RpcState.Disconnected) - { - Logger.Error("State must NOT be disconnected in order to send a handwave!"); - return; - } - - //Send the handwave - if (!namedPipe.WriteFrame(new PipeFrame(Opcode.Close, new Handshake() { Version = VERSION, ClientID = applicationID }))) - { - Logger.Error("failed to write a handwave."); - return; - } - } - - - /// - /// Attempts to connect to the pipe. Returns true on success - /// - /// - public bool AttemptConnection() - { - Logger.Info("Attempting a new connection"); - - //The thread mustn't exist already - if (thread != null) - { - Logger.Error("Cannot attempt a new connection as the previous connection thread is not null!"); - return false; - } - - //We have to be in the disconnected state - if (State != RpcState.Disconnected) - { - Logger.Warning("Cannot attempt a new connection as the previous connection hasn't changed state yet."); - return false; - } - - if (aborting) - { - Logger.Error("Cannot attempt a new connection while aborting!"); - return false; - } - - //Start the thread up - thread = new Thread(MainLoop); - thread.Name = "Discord IPC Thread"; - thread.IsBackground = true; - thread.Start(); - - return true; - } - - /// - /// Sets the current state of the pipe, locking the l_states object for thread saftey. - /// - /// The state to set it too. - private void SetConnectionState(RpcState state) - { - Logger.Trace("Setting the connection state to {0}", state.ToString().ToSnakeCase().ToUpperInvariant()); - lock (l_states) - { - _state = state; - } - } - - /// - /// Closes the connection and disposes of resources. This will not force termination, but instead allow Discord disconnect us after we say goodbye. - /// This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate. - /// - public void Shutdown() - { - //Enable the flag - Logger.Trace("Initiated shutdown procedure"); - shutdown = true; - - //Clear the commands and enqueue the close - lock(l_rtqueue) - { - _rtqueue.Clear(); - if (CLEAR_ON_SHUTDOWN) _rtqueue.Enqueue(new PresenceCommand() { PID = processID, Presence = null }); - _rtqueue.Enqueue(new CloseCommand()); - } - - //Trigger the event - queueUpdatedEvent.Set(); - } - - /// - /// Closes the connection and disposes of resources. - /// - public void Close() - { - if (thread == null) - { - Logger.Error("Cannot close as it is not available!"); - return; - } - - if (aborting) - { - Logger.Error("Cannot abort as it has already been aborted"); - return; - } - - //Set the abort state - if (ShutdownOnly) - { - Shutdown(); - return; - } - - //Terminate - Logger.Trace("Updating Abort State..."); - aborting = true; - queueUpdatedEvent.Set(); - } - - - /// - /// Closes the connection and disposes resources. Identical to but ignores the "ShutdownOnly" value. - /// - public void Dispose() - { - ShutdownOnly = false; - Close(); - } - #endregion - - } - - /// - /// State of the RPC connection - /// - internal enum RpcState - { - /// - /// Disconnected from the discord client - /// - Disconnected, - - /// - /// Connecting to the discord client. The handshake has been sent and we are awaiting the ready event - /// - Connecting, - - /// - /// We are connect to the client and can send and receive messages. - /// - Connected - } -} \ No newline at end of file diff --git a/DiscordAPI/User.cs b/DiscordAPI/User.cs deleted file mode 100644 index 29e9631..0000000 --- a/DiscordAPI/User.cs +++ /dev/null @@ -1,229 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC -{ - /// - /// Object representing a Discord user. This is used for join requests. - /// - public class User - { - /// - /// Possible formats for avatars - /// - public enum AvatarFormat - { - /// - /// Portable Network Graphics format (.png) - /// Losses format that supports transparent avatars. Most recommended for stationary formats with wide support from many libraries. - /// - PNG, - - /// - /// Joint Photographic Experts Group format (.jpeg) - /// The format most cameras use. Lossy and does not support transparent avatars. - /// - JPEG, - - /// - /// WebP format (.webp) - /// Picture only version of WebM. Pronounced "weeb p". - /// - WebP, - - /// - /// Graphics Interchange Format (.gif) - /// Animated avatars that Discord Nitro users are able to use. If the user doesn't have an animated avatar, then it will just be a single frame gif. - /// - GIF //Gif, as in gift. - } - - /// - /// Possible square sizes of avatars. - /// - public enum AvatarSize - { - /// 16 x 16 pixels. - x16 = 16, - /// 32 x 32 pixels. - x32 = 32, - /// 64 x 64 pixels. - x64 = 64, - /// 128 x 128 pixels. - x128 = 128, - /// 256 x 256 pixels. - x256 = 256, - /// 512 x 512 pixels. - x512 = 512, - /// 1024 x 1024 pixels. - x1024 = 1024, - /// 2048 x 2048 pixels. - x2048 = 2048 - } - - /// - /// The snowflake ID of the user. - /// - [JsonProperty("id")] - public ulong ID { get; private set; } - - /// - /// The username of the player. - /// - [JsonProperty("username")] - public string Username { get; private set; } - - /// - /// The discriminator of the user. - /// - [JsonProperty("discriminator")] - public int Discriminator { get; private set; } - - /// - /// The avatar hash of the user. Too get a URL for the avatar, use the . This can be null if the user has no avatar. The will account for this and return the discord default. - /// - [JsonProperty("avatar")] - public string Avatar { get; private set; } - - /// - /// The flags on a users account, often represented as a badge. - /// - [JsonProperty("flags")] - public Flag Flags { get; private set; } - - /// - /// A flag on the user account - /// - [Flags] - public enum Flag - { - /// No flag - None = 0, - - /// Staff of Discord. - Employee = 1 << 0, - - /// Partners of Discord. - Partner = 1 << 1, - - /// Original HypeSquad which organise events. - HypeSquad = 1 << 2, - - /// Bug Hunters that found and reported bugs in Discord. - BugHunter = 1 << 3, - - //These 2 are mistery types - //A = 1 << 4, - //B = 1 << 5, - - /// The HypeSquad House of Bravery. - HouseBravery = 1 << 6, - - /// The HypeSquad House of Brilliance. - HouseBrilliance = 1 << 7, - - /// The HypeSquad House of Balance (the best one). - HouseBalance = 1 << 8, - - /// Early Supporter of Discord and had Nitro before the store was released. - EarlySupporter = 1 << 9, - - /// Apart of a team. - /// Unclear if it is reserved for members that share a team with the current application. - /// - TeamUser = 1 << 10 - } - - /// - /// The premium type of the user. - /// - [JsonProperty("premium_type")] - public PremiumType Premium { get; private set; } - - /// - /// Type of premium - /// - public enum PremiumType - { - /// No subscription to any forms of Nitro. - None = 0, - - /// Nitro Classic subscription. Has chat perks and animated avatars. - NitroClassic = 1, - - /// Nitro subscription. Has chat perks, animated avatars, server boosting, and access to free Nitro Games. - Nitro = 2 - } - - /// - /// The endpoint for the CDN. Normally cdn.discordapp.com. - /// - public string CdnEndpoint { get; private set; } - - /// - /// Creates a new User instance. - /// - internal User() - { - CdnEndpoint = "cdn.discordapp.com"; - } - - /// - /// Updates the URL paths to the appropriate configuration - /// - /// The configuration received by the OnReady event. - internal void SetConfiguration(Configuration configuration) - { - this.CdnEndpoint = configuration.CdnHost; - } - - /// - /// Gets a URL that can be used to download the user's avatar. If the user has not yet set their avatar, it will return the default one that discord is using. The default avatar only supports the format. - /// - /// The format of the target avatar - /// The optional size of the avatar you wish for. Defaults to x128. - /// - public string GetAvatarURL(AvatarFormat format, AvatarSize size = AvatarSize.x128) - { - //Prepare the endpoint - string endpoint = "/avatars/" + ID + "/" + Avatar; - - //The user has no avatar, so we better replace it with the default - if (string.IsNullOrEmpty(Avatar)) - { - //Make sure we are only using PNG - if (format != AvatarFormat.PNG) - throw new BadImageFormatException("The user has no avatar and the requested format " + format.ToString() + " is not supported. (Only supports PNG)."); - - //Get the endpoint - int descrim = Discriminator % 5; - endpoint = "/embed/avatars/" + descrim; - } - - //Finish of the endpoint - return string.Format("https://{0}{1}{2}?size={3}", this.CdnEndpoint, endpoint, GetAvatarExtension(format), (int)size); - } - - /// - /// Returns the file extension of the specified format. - /// - /// The format to get the extention off - /// Returns a period prefixed file extension. - public string GetAvatarExtension(AvatarFormat format) - { - return "." + format.ToString().ToLowerInvariant(); - } - - /// - /// Formats the user into username#discriminator - /// - /// - public override string ToString() - { - return Username + "#" + Discriminator.ToString("D4"); - } - } -} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs deleted file mode 100644 index 726ca15..0000000 --- a/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -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("Show on Discord what are you listening on AIRSPY SDR#")] -[assembly: AssemblyDescription("Show on Discord what are you listening on AIRSPY SDR#")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SDR# Discord RPC Plugin")] -[assembly: AssemblyCopyright("Copyright © EnderIce2 2023")] -[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("72e8628f-ba39-4915-bf3c-dd48bf477d30")] - -// 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.*")] -[assembly: AssemblyVersion("1.2.0.2")] -[assembly: AssemblyFileVersion("1.2.0.2")] diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs deleted file mode 100644 index adfd7a9..0000000 --- a/Properties/Resources.Designer.cs +++ /dev/null @@ -1,73 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace DiscordRPC.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DiscordRPC.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap gear { - get { - object obj = ResourceManager.GetObject("gear", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - } -} diff --git a/Register.txt b/Register.txt deleted file mode 100644 index b63e255..0000000 --- a/Register.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Resources/gear.png b/Resources/gear.png deleted file mode 100644 index 8cc997600a2f463eef4aafed38e560846c3e555f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3358 zcmZ`+XE5A-wEpYX1|ixiiL!_!ST*X4WJO;=h#p4opiu9=oI;}m1o#=UQp+@ukiZ?Oh(&gxO4w|fiW`k%|U$aQN-pSTP~gUBgb z)BMOU2EYB`)X!u6uu2t8=c&_J7^#uKvBm9~{^5D0z_@7g@=PAiJn!LKwV`H@b&)H- zli#O5(!@GAw(ig$rl$q81pJx{2$m3him?m;>L6G1)GS1E6yJ$kib9PDY$tV1TC>)z z6XVJ$rf&puNPWvm3u@Ye^|}M@hwAb$SC@gVN&q|VI5yN}t(6SHT)n)r`p@wPY*Q-V zo-XMf?w55M)cbqofIEpPj1cgE=+4LwYSa)JJpuAV50pxYO9ws?gYI_SRQR=#5laDz z-KmP`9zsA$5{eP`Qx~sGL3y_+i?6;Sl0ua<>N$#MjOwYB_YD@f^2}YRposfz zNNqc9Cj6`SO-|G4Cq5?xxZ{BnpaqaKh9g%{+>1s!_TR;CN82BJp@Nfegbfj+dE%Wl zD#cww42gxX-MA%fFeMAI9KRLkIOy4QF-1mCh4As50IERNoCgJM81tfv8^nXZtdvl^ z?h4KNPDjAC(GCx+mG*!x^Z&EgPUP~t+o1Q5!76$6K9)d3SkD=J{P4S4YgP9sGvkhV z8PVh>nPljLqLI1exF*zUXUg`?PFeb%nkH_ue2Dj{sXe^dX&3@(NglOHv^CpuKQ^H9 zE(;;~hZv7~>Bp%E%E}a;ksyn)X_D+K_XA5J(}`4o?8UmP3PoL~ zC6Q7@2#F_2?P907Royp7_l>x3_v$*ZZ7GW!s0EGT@%z{y z+J$$`V_3!g=1kjc)1t;)f9Pf!?ej@2grcurT!FS;z|dDeff{eh5<43t&-PA)RIk47U(J>o1j|$VZ<}!Qq-wX)t$h(9-n!>h&hY3*J8-yq&hj1 z(vwLp#C|zEba0&RmpZl5>27qV{Vku%!nf(D&q70u*IMjq@%vTiqVS!1mpLxU91eYu zyg5OqE|R*@nfjTO`eH@LdL_nX{s}rwW%VKN5q_VsUoOi!Var$gh)VuNJBEgDC3s$7 z!wRfO+0bgk0cKdg%QnSpUCLmepl8^$)j>_g~i2d+u4{(T>25OE=ca@$iEnX(n#w$0`iYEh-9x~=*x_zI4xvu}9SKF0i zhSHY|$->_SC&soE&nCfNe1-`3?Lg`ddTSYnt(f{}LOZ;}K1NC zeyv~J(CoxOM9W(`j*@4z8FYef_VDIKhut9a+gDCYRW?~3L%54FZLJ)6$M%6uOFF^0 zH*W9n=+NUsz9O>3n)kvh!slvBglW;a7~!vc`pL?H7VIMqp!Rz3sf0=73Iinq&lGrK z23nEHjEp4CL`*;`o}#O+$kVs{TiUIwt*(zyl63)>xPH&4`*=PbyNGBP+nMb>bzv|Q z5ak*e3d*TMidGzOpsCpri_H6(@YsV&OS-bLcW)O2q{4rG;=EfhCcv){@!J*lp8*aU z5>v^eF7(I}=6k{cVUXI?CK;6YvCAViS!X6(+?P$ozL(=F25GdZ@ri0fxRbk5{cocEZ+dBygWZ6F98`~2S^NQ#B;I!D!Ee3`W)O@opvbJEtnw)msh4h=|x@KRG$uMHkyM>tPZl+zfdwX3x4k-PsKR{3Fq4IO z(cpr%kx*T2o*TCeBKp;Fz}OqYAkDVUZtUtI(ZyR$QVQ=KWzNKoB-o*i-m#BeoARd3 zh`lpCPu^DQO0P(|)NEgRV3ul_Wkp>&LlO&pq|t4grjMH{4^;| zxiy+}d=}YKZ8u?IPG&?uUQVSSQXPjXo54@7Dz%jbct|n%d3b~)oW5k}bfN6%&UZ(X zv|!$_Dh8`!32xjEghBttp7fcRu0ZVz%E_wndqf~f2AuZJ(1n=lJ`t6JXrzU5x$(yd zdO^p;k2$?1^PDI8Skyx1@gJD-)IeVmLEt^!kCbckGEP zH-{wKv4ybI{yf5ZQMTJ!nww;y@3e>PYv~+LJg(YdPXirUBgg4u(@FFXkK|c&FTMT{ zUa-ZZC2nc8%CRc;6Y-eJ9|%o6Ap^T%{)6+ag)6;PDXeU6n>6&iGT?FA6eaK}EUy&DPe|b)qQntomsTORf zA=F;^HZYmPS=mRe{A_o{Q6%Muy48vk#^l)+{TUwlX)(N6;3GKqGBYTV_9R=?W~GZA z^nTvzxu>!*S2d!jIKE}Lva6+wBtaeWOCv8SwrxoB!rB)6qCn}TmTMchAb{s$e*4+?;g8<~EMW9l=}GJ)2C8vh71RGp z1dGNTOQEYrhY#3=l)hT)?L$jc$PA}Oort{ zFkrcOo!QlTMsdZrrAg|qB~7rM)_3u$oDmaHUM=i4j5PI!l4NP*zsW7a>UAG!#nwpk z{vP>BQ`eY(QYli4kyv&VcFHFonRdDDlFzg^?RpTqTDf)>N$F<}OZ4XQ6l5>~+=;$NZlhs6MuZ%pO2uHi3FLXGnv=)vm3A|2t+K%(TLp-E9n$ zMko1~q8-MhzT5u{d$>3KJG-PBX?M#W(pf$_9fnyBI2Dp@7kNPu@VhcYNV7Y@N^9d_ zFez}7l~{DT=x&lRiQ-FWzz;i53iF=Oo+T+t3O1=3vy^1C*8n-q#MM(99F8BKw8jKR(I!xF8W6n^Np0}jGYE;GwJ6;aYZxy0KaH`=#1X9Vrbk z+=ga95eaP()KkX_b@8JRk-Tr3gGorHuSWy^t6?+cDKC&Kolk_A_x-` z6cv8~6O|E_mJyYB2!qMMV9rx7Z%&f`BXD_R<6!6a{|h)G_bzV)kiQf3-q?BiSi0K+ kK0ZD|4z5lf)|NA&PyZ<+u#WdywRi3R$904ZQRng9R* diff --git a/SDR-RPC.sln b/SDR-RPC.sln new file mode 100644 index 0000000..4205dbd --- /dev/null +++ b/SDR-RPC.sln @@ -0,0 +1,31 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDR-RPC", "SDR-RPC\SDR-RPC.csproj", "{7249303D-EB57-44E6-B270-942A0D54A8AB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7249303D-EB57-44E6-B270-942A0D54A8AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7249303D-EB57-44E6-B270-942A0D54A8AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7249303D-EB57-44E6-B270-942A0D54A8AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7249303D-EB57-44E6-B270-942A0D54A8AB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7ECEC65D-47BE-487D-BC90-9BC261B61569} + EndGlobalSection + GlobalSection(SubversionScc) = preSolution + Svn-Managed = True + Manager = AnkhSVN - Subversion Support for Visual Studio + EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection +EndGlobal diff --git a/SDR-RPC/ControlPanel.Designer.cs b/SDR-RPC/ControlPanel.Designer.cs new file mode 100644 index 0000000..900cc5b --- /dev/null +++ b/SDR-RPC/ControlPanel.Designer.cs @@ -0,0 +1,199 @@ +namespace EnderIce2.SDRSharpPlugin +{ + partial class ControlPanel + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + groupBox1 = new System.Windows.Forms.GroupBox(); + helpBtn = new System.Windows.Forms.Button(); + creditsBtn = new System.Windows.Forms.Button(); + dbgCheckBox = new System.Windows.Forms.CheckBox(); + IDtxtBox = new System.Windows.Forms.TextBox(); + label2 = new System.Windows.Forms.Label(); + groupBox2 = new System.Windows.Forms.GroupBox(); + statusLbl = new System.Windows.Forms.Label(); + versionLbl = new System.Windows.Forms.Label(); + tableLayoutPanel1.SuspendLayout(); + groupBox1.SuspendLayout(); + groupBox2.SuspendLayout(); + SuspendLayout(); + // + // tableLayoutPanel1 + // + tableLayoutPanel1.AutoScroll = true; + tableLayoutPanel1.ColumnCount = 1; + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + tableLayoutPanel1.Controls.Add(groupBox1, 0, 1); + tableLayoutPanel1.Controls.Add(groupBox2, 0, 0); + tableLayoutPanel1.Controls.Add(versionLbl, 0, 2); + tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; + tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); + tableLayoutPanel1.MinimumSize = new System.Drawing.Size(200, 200); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 3; + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 18F)); + tableLayoutPanel1.Size = new System.Drawing.Size(200, 200); + tableLayoutPanel1.TabIndex = 2; + // + // groupBox1 + // + groupBox1.AutoSize = true; + groupBox1.Controls.Add(helpBtn); + groupBox1.Controls.Add(creditsBtn); + groupBox1.Controls.Add(dbgCheckBox); + groupBox1.Controls.Add(IDtxtBox); + groupBox1.Controls.Add(label2); + groupBox1.Dock = System.Windows.Forms.DockStyle.Fill; + groupBox1.Location = new System.Drawing.Point(3, 53); + groupBox1.Name = "groupBox1"; + groupBox1.Size = new System.Drawing.Size(194, 126); + groupBox1.TabIndex = 0; + groupBox1.TabStop = false; + groupBox1.Text = "Settings"; + // + // helpBtn + // + helpBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; + helpBtn.Location = new System.Drawing.Point(6, 71); + helpBtn.Name = "helpBtn"; + helpBtn.Size = new System.Drawing.Size(67, 23); + helpBtn.TabIndex = 2; + helpBtn.Text = "Help"; + helpBtn.UseVisualStyleBackColor = true; + helpBtn.Click += helpBtn_Click; + // + // creditsBtn + // + creditsBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; + creditsBtn.Location = new System.Drawing.Point(79, 71); + creditsBtn.Name = "creditsBtn"; + creditsBtn.Size = new System.Drawing.Size(67, 23); + creditsBtn.TabIndex = 3; + creditsBtn.Text = "Credits"; + creditsBtn.UseVisualStyleBackColor = true; + creditsBtn.Click += creditsBtn_Click; + // + // dbgCheckBox + // + dbgCheckBox.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; + dbgCheckBox.AutoSize = true; + dbgCheckBox.Location = new System.Drawing.Point(6, 100); + dbgCheckBox.Name = "dbgCheckBox"; + dbgCheckBox.Size = new System.Drawing.Size(150, 19); + dbgCheckBox.TabIndex = 4; + dbgCheckBox.Text = "Enable logging (debug)"; + dbgCheckBox.UseVisualStyleBackColor = true; + dbgCheckBox.CheckedChanged += dbgCheckBox_CheckedChanged; + // + // IDtxtBox + // + IDtxtBox.BorderStyle = System.Windows.Forms.BorderStyle.None; + IDtxtBox.Location = new System.Drawing.Point(6, 37); + IDtxtBox.MaxLength = 18; + IDtxtBox.Name = "IDtxtBox"; + IDtxtBox.PlaceholderText = "765213507321856078"; + IDtxtBox.Size = new System.Drawing.Size(117, 16); + IDtxtBox.TabIndex = 1; + IDtxtBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Center; + IDtxtBox.KeyDown += IDtxtBox_KeyDown; + // + // label2 + // + label2.AutoSize = true; + label2.Location = new System.Drawing.Point(6, 19); + label2.Name = "label2"; + label2.Size = new System.Drawing.Size(97, 15); + label2.TabIndex = 0; + label2.Text = "Custom Client ID"; + // + // groupBox2 + // + groupBox2.AutoSize = true; + groupBox2.Controls.Add(statusLbl); + groupBox2.Dock = System.Windows.Forms.DockStyle.Fill; + groupBox2.Location = new System.Drawing.Point(3, 3); + groupBox2.Name = "groupBox2"; + groupBox2.Size = new System.Drawing.Size(194, 44); + groupBox2.TabIndex = 3; + groupBox2.TabStop = false; + groupBox2.Text = "Status"; + // + // statusLbl + // + statusLbl.Dock = System.Windows.Forms.DockStyle.Fill; + statusLbl.Location = new System.Drawing.Point(3, 19); + statusLbl.Name = "statusLbl"; + statusLbl.Size = new System.Drawing.Size(188, 22); + statusLbl.TabIndex = 1; + statusLbl.Text = "Status Label"; + statusLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // versionLbl + // + versionLbl.AutoSize = true; + versionLbl.Dock = System.Windows.Forms.DockStyle.Fill; + versionLbl.Location = new System.Drawing.Point(3, 182); + versionLbl.Name = "versionLbl"; + versionLbl.Size = new System.Drawing.Size(194, 18); + versionLbl.TabIndex = 4; + versionLbl.Text = "v0.0.0.0"; + versionLbl.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + versionLbl.DoubleClick += versionLbl_DoubleClick; + // + // ControlPanel + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + AutoScroll = true; + AutoScrollMinSize = new System.Drawing.Size(200, 200); + Controls.Add(tableLayoutPanel1); + Name = "ControlPanel"; + Size = new System.Drawing.Size(200, 200); + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + groupBox1.ResumeLayout(false); + groupBox1.PerformLayout(); + groupBox2.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.TextBox IDtxtBox; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.CheckBox dbgCheckBox; + private System.Windows.Forms.Button helpBtn; + private System.Windows.Forms.Button creditsBtn; + private System.Windows.Forms.GroupBox groupBox2; + private System.Windows.Forms.Label statusLbl; + private System.Windows.Forms.Label versionLbl; + } +} diff --git a/SDR-RPC/ControlPanel.cs b/SDR-RPC/ControlPanel.cs new file mode 100644 index 0000000..961d5a9 --- /dev/null +++ b/SDR-RPC/ControlPanel.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using SDRSharp.Common; +using SDRSharp.Radio; +using DiscordRPC; +using DiscordRPC.Logging; +using DiscordRPC.Message; +using static System.Windows.Forms.VisualStyles.VisualStyleElement; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.Button; +using EnderIce2.SDRSharpPlugin; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.ToolTip; + +namespace EnderIce2.SDRSharpPlugin +{ + public partial class ControlPanel : UserControl + { + private string _ChangeStatus; + private ISharpControl _control; + + public string ChangeStatus + { + get => _ChangeStatus; + set + { + _ChangeStatus = value; + statusLbl.Text = value; + LogWriter.WriteToFile(value); + } + } + + public ControlPanel(ISharpControl control) + { + _control = control; + InitializeComponent(); + versionLbl.Text = $"v{Assembly.GetExecutingAssembly().GetName().Version.ToString()}"; + IDtxtBox.Text = Utils.GetStringSetting("ClientID"); + dbgCheckBox.Checked = Utils.GetBooleanSetting("LogRPC", false); + } + + private void IDtxtBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode != Keys.Enter) + return; + + IDtxtBox.Text.Replace(" ", "").Replace("\n", "").Replace("\r", ""); + + if (IDtxtBox.Text.All(char.IsWhiteSpace)) + { + IDtxtBox.Text = "765213507321856078"; + } + + Utils.SaveSetting("ClientID", IDtxtBox.Text); + ChangeStatus = "Configuration Updated"; + e.Handled = true; + e.SuppressKeyPress = true; + } + + private void dbgCheckBox_CheckedChanged(object sender, EventArgs e) => Utils.SaveSetting("LogRPC", dbgCheckBox.Checked); + + private void versionLbl_DoubleClick(object sender, EventArgs e) + { + Process.Start(new ProcessStartInfo("https://enderice2.github.io/SDR-RPC/") { UseShellExecute = true }); + } + + private void helpBtn_Click(object sender, EventArgs e) => new HelpForm().ShowDialog(); + + private void creditsBtn_Click(object sender, EventArgs e) => new CreditsForm().ShowDialog(); + } +} \ No newline at end of file diff --git a/SettingsForm.resx b/SDR-RPC/ControlPanel.resx similarity index 93% rename from SettingsForm.resx rename to SDR-RPC/ControlPanel.resx index 1af7de1..af32865 100644 --- a/SettingsForm.resx +++ b/SDR-RPC/ControlPanel.resx @@ -1,17 +1,17 @@  - diff --git a/SDR-RPC/CreditsForm.Designer.cs b/SDR-RPC/CreditsForm.Designer.cs new file mode 100644 index 0000000..35234fe --- /dev/null +++ b/SDR-RPC/CreditsForm.Designer.cs @@ -0,0 +1,156 @@ +namespace EnderIce2.SDRSharpPlugin +{ + partial class CreditsForm +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + groupBox1 = new System.Windows.Forms.GroupBox(); + lacheeLic = new System.Windows.Forms.LinkLabel(); + lacheeSrc = new System.Windows.Forms.LinkLabel(); + groupBox2 = new System.Windows.Forms.GroupBox(); + jamesLic = new System.Windows.Forms.LinkLabel(); + jamesSrc = new System.Windows.Forms.LinkLabel(); + groupBox1.SuspendLayout(); + groupBox2.SuspendLayout(); + SuspendLayout(); + // + // groupBox1 + // + groupBox1.Controls.Add(lacheeLic); + groupBox1.Controls.Add(lacheeSrc); + groupBox1.ForeColor = System.Drawing.Color.White; + groupBox1.Location = new System.Drawing.Point(12, 12); + groupBox1.Name = "groupBox1"; + groupBox1.Size = new System.Drawing.Size(171, 47); + groupBox1.TabIndex = 0; + groupBox1.TabStop = false; + groupBox1.Text = "Lachee/discord-rpc-csharp"; + // + // lacheeLic + // + lacheeLic.ActiveLinkColor = System.Drawing.Color.White; + lacheeLic.AutoSize = true; + lacheeLic.DisabledLinkColor = System.Drawing.Color.Silver; + lacheeLic.LinkColor = System.Drawing.Color.White; + lacheeLic.Location = new System.Drawing.Point(6, 19); + lacheeLic.Name = "lacheeLic"; + lacheeLic.Size = new System.Drawing.Size(71, 15); + lacheeLic.TabIndex = 1; + lacheeLic.TabStop = true; + lacheeLic.Text = "View license"; + lacheeLic.VisitedLinkColor = System.Drawing.Color.FromArgb(224, 224, 224); + lacheeLic.LinkClicked += lacheeLic_LinkClicked; + // + // lacheeSrc + // + lacheeSrc.ActiveLinkColor = System.Drawing.Color.White; + lacheeSrc.AutoSize = true; + lacheeSrc.DisabledLinkColor = System.Drawing.Color.Silver; + lacheeSrc.LinkColor = System.Drawing.Color.White; + lacheeSrc.Location = new System.Drawing.Point(83, 19); + lacheeSrc.Name = "lacheeSrc"; + lacheeSrc.Size = new System.Drawing.Size(70, 15); + lacheeSrc.TabIndex = 0; + lacheeSrc.TabStop = true; + lacheeSrc.Text = "View source"; + lacheeSrc.VisitedLinkColor = System.Drawing.Color.FromArgb(224, 224, 224); + lacheeSrc.LinkClicked += lacheeSrc_LinkClicked; + // + // groupBox2 + // + groupBox2.Controls.Add(jamesLic); + groupBox2.Controls.Add(jamesSrc); + groupBox2.ForeColor = System.Drawing.Color.White; + groupBox2.Location = new System.Drawing.Point(12, 65); + groupBox2.Name = "groupBox2"; + groupBox2.Size = new System.Drawing.Size(171, 47); + groupBox2.TabIndex = 1; + groupBox2.TabStop = false; + groupBox2.Text = "JamesNK/Newtonsoft.Json"; + // + // jamesLic + // + jamesLic.ActiveLinkColor = System.Drawing.Color.White; + jamesLic.AutoSize = true; + jamesLic.DisabledLinkColor = System.Drawing.Color.Silver; + jamesLic.LinkColor = System.Drawing.Color.White; + jamesLic.Location = new System.Drawing.Point(6, 19); + jamesLic.Name = "jamesLic"; + jamesLic.Size = new System.Drawing.Size(71, 15); + jamesLic.TabIndex = 3; + jamesLic.TabStop = true; + jamesLic.Text = "View license"; + jamesLic.VisitedLinkColor = System.Drawing.Color.FromArgb(224, 224, 224); + jamesLic.LinkClicked += jamesLic_LinkClicked; + // + // jamesSrc + // + jamesSrc.ActiveLinkColor = System.Drawing.Color.White; + jamesSrc.AutoSize = true; + jamesSrc.DisabledLinkColor = System.Drawing.Color.Silver; + jamesSrc.LinkColor = System.Drawing.Color.White; + jamesSrc.Location = new System.Drawing.Point(83, 19); + jamesSrc.Name = "jamesSrc"; + jamesSrc.Size = new System.Drawing.Size(70, 15); + jamesSrc.TabIndex = 2; + jamesSrc.TabStop = true; + jamesSrc.Text = "View source"; + jamesSrc.VisitedLinkColor = System.Drawing.Color.FromArgb(224, 224, 224); + jamesSrc.LinkClicked += jamesSrc_LinkClicked; + // + // CreditsForm + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + BackColor = System.Drawing.Color.FromArgb(28, 28, 28); + ClientSize = new System.Drawing.Size(196, 123); + Controls.Add(groupBox2); + Controls.Add(groupBox1); + ForeColor = System.Drawing.Color.White; + FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + MaximizeBox = false; + MinimizeBox = false; + Name = "CreditsForm"; + StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + Text = "Credits"; + groupBox1.ResumeLayout(false); + groupBox1.PerformLayout(); + groupBox2.ResumeLayout(false); + groupBox2.PerformLayout(); + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.GroupBox groupBox2; + private System.Windows.Forms.LinkLabel lacheeLic; + private System.Windows.Forms.LinkLabel lacheeSrc; + private System.Windows.Forms.LinkLabel jamesLic; + private System.Windows.Forms.LinkLabel jamesSrc; + } +} \ No newline at end of file diff --git a/SettingsForm.cs b/SDR-RPC/CreditsForm.cs similarity index 68% rename from SettingsForm.cs rename to SDR-RPC/CreditsForm.cs index b3ddf53..7bcb78f 100644 --- a/SettingsForm.cs +++ b/SDR-RPC/CreditsForm.cs @@ -1,46 +1,41 @@ -using SDRSharp.Radio; -using System; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; using System.Windows.Forms; namespace EnderIce2.SDRSharpPlugin { - public partial class SettingsForm : Form + public partial class CreditsForm : Form { - public SettingsForm() + public CreditsForm() { InitializeComponent(); - textBox1.Text = Utils.GetStringSetting("ClientID"); - checkBox1.Checked = Utils.GetBooleanSetting("LogRPC", false); } - private void Button2_Click(object sender, EventArgs e) - { - Process.Start(new ProcessStartInfo("https://ko-fi.com/enderice2") { UseShellExecute = true }); - Close(); - } - - private void Button1_Click(object sender, EventArgs e) => Close(); - - private void Button5_Click(object sender, EventArgs e) + private void lacheeLic_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { MessageBox.Show("MIT License\n\nCopyright (c) 2018 Lachee\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.", "discord-rpc-csharp"); + } + + private void lacheeSrc_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + Process.Start(new ProcessStartInfo("https://github.com/Lachee/discord-rpc-csharp") { UseShellExecute = true }); + } + + private void jamesLic_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { MessageBox.Show("The MIT License (MIT)\n\nCopyright(c) 2007 James Newton-King\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", "Newtonsoft.Json"); } - private void CheckBox1_CheckedChanged(object sender, EventArgs e) => Utils.SaveSetting("LogRPC", checkBox1.Checked); - - private void Button4_Click(object sender, EventArgs e) + private void jamesSrc_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { - textBox1.Text.Replace(" ", "").Replace("\n", "").Replace("\r", ""); - Utils.SaveSetting("ClientID", textBox1.Text); - label1.Text = $"Configuration Updated.\nNew ID: {Utils.GetStringSetting("ClientID")}"; - } - - private void Button3_Click(object sender, EventArgs e) - { - Utils.SaveSetting("ClientID", "765213507321856078"); - textBox1.Text = "765213507321856078"; + Process.Start(new ProcessStartInfo("https://github.com/JamesNK/Newtonsoft.Json") { UseShellExecute = true }); } } } diff --git a/SettingsPanel.resx b/SDR-RPC/CreditsForm.resx similarity index 93% rename from SettingsPanel.resx rename to SDR-RPC/CreditsForm.resx index 1af7de1..af32865 100644 --- a/SettingsPanel.resx +++ b/SDR-RPC/CreditsForm.resx @@ -1,17 +1,17 @@  - diff --git a/SDR-RPC/DiscordAPI/Configuration.cs b/SDR-RPC/DiscordAPI/Configuration.cs new file mode 100644 index 0000000..6611932 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Configuration.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace DiscordRPC +{ + /// + /// Configuration of the current RPC connection + /// + public class Configuration + { + /// + /// The Discord API endpoint that should be used. + /// + [JsonProperty("api_endpoint")] + public string ApiEndpoint { get; set; } + + /// + /// The CDN endpoint + /// + [JsonProperty("cdn_host")] + public string CdnHost { get; set; } + + /// + /// The type of enviroment the connection on. Usually Production. + /// + [JsonProperty("enviroment")] + public string Enviroment { get; set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Converters/EnumSnakeCaseConverter.cs b/SDR-RPC/DiscordAPI/Converters/EnumSnakeCaseConverter.cs new file mode 100644 index 0000000..1ba9177 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Converters/EnumSnakeCaseConverter.cs @@ -0,0 +1,94 @@ +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Reflection; + +namespace DiscordRPC.Converters +{ + /// + /// Converts enums with the into Json friendly terms. + /// + internal class EnumSnakeCaseConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType.IsEnum; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.Value == null) return null; + + object val = null; + if (TryParseEnum(objectType, (string)reader.Value, out val)) + return val; + + return existingValue; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var enumtype = value.GetType(); + var name = Enum.GetName(enumtype, value); + + //Get each member and look for hte correct one + var members = enumtype.GetMembers(BindingFlags.Public | BindingFlags.Static); + foreach (var m in members) + { + if (m.Name.Equals(name)) + { + var attributes = m.GetCustomAttributes(typeof(EnumValueAttribute), true); + if (attributes.Length > 0) + { + name = ((EnumValueAttribute)attributes[0]).Value; + } + } + } + + writer.WriteValue(name); + } + + public bool TryParseEnum(Type enumType, string str, out object obj) + { + //Make sure the string isn;t null + if (str == null) + { + obj = null; + return false; + } + + //Get the real type + Type type = enumType; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + type = type.GetGenericArguments().First(); + + //Make sure its actually a enum + if (!type.IsEnum) + { + obj = null; + return false; + } + + //Get each member and look for hte correct one + var members = type.GetMembers(BindingFlags.Public | BindingFlags.Static); + foreach (var m in members) + { + var attributes = m.GetCustomAttributes(typeof(EnumValueAttribute), true); + foreach (var a in attributes) + { + var enumval = (EnumValueAttribute)a; + if (str.Equals(enumval.Value)) + { + obj = Enum.Parse(type, m.Name, ignoreCase: true); + + return true; + } + } + } + + //We failed + obj = null; + return false; + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Converters/EnumValueAttribute.cs b/SDR-RPC/DiscordAPI/Converters/EnumValueAttribute.cs new file mode 100644 index 0000000..2238a19 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Converters/EnumValueAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace DiscordRPC.Converters +{ + internal class EnumValueAttribute : Attribute + { + public string Value { get; set; } + + public EnumValueAttribute(string value) + { + this.Value = value; + } + } +} \ No newline at end of file diff --git a/DiscordAPI/DiscordRpcClient.cs b/SDR-RPC/DiscordAPI/DiscordRpcClient.cs similarity index 97% rename from DiscordAPI/DiscordRpcClient.cs rename to SDR-RPC/DiscordAPI/DiscordRpcClient.cs index 9bc4798..ed400bf 100644 --- a/DiscordAPI/DiscordRpcClient.cs +++ b/SDR-RPC/DiscordAPI/DiscordRpcClient.cs @@ -10,7 +10,6 @@ using System; namespace DiscordRPC { - /// /// A Discord RPC Client which is used to send Rich Presence updates and receive Join / Spectate events. /// @@ -18,7 +17,6 @@ namespace DiscordRPC { #region Properties - /// /// Gets a value indicating if the client has registered a URI Scheme. If this is false, Join / Spectate events will fail. /// To register a URI Scheme, call . @@ -41,7 +39,7 @@ namespace DiscordRPC public int ProcessID { get; private set; } /// - /// The maximum size of the message queue received from Discord. + /// The maximum size of the message queue received from Discord. /// public int MaxQueueSize { get; private set; } @@ -62,10 +60,11 @@ namespace DiscordRPC if (connection != null) connection.Logger = value; } } + private ILogger _logger; /// - /// Indicates if the client will automatically invoke the events without having to be called. + /// Indicates if the client will automatically invoke the events without having to be called. /// public bool AutoEvents { get; private set; } @@ -73,7 +72,8 @@ namespace DiscordRPC /// Skips sending presences that are identical to the current one. /// public bool SkipIdenticalPresence { get; set; } - #endregion + + #endregion Properties /// /// The pipe the discord client is on, ranging from 0 to 9. Use -1 to scan through all pipes. @@ -121,6 +121,7 @@ namespace DiscordRPC if (connection != null) connection.ShutdownOnly = value; } } + private bool _shutdownOnly = true; private object _sync = new object(); @@ -196,7 +197,8 @@ namespace DiscordRPC /// The RPC Connection has sent a message. Called before any other event and executed from the RPC Thread. /// public event OnRpcMessageEvent OnRpcMessage; - #endregion + + #endregion Events #region Initialization @@ -253,9 +255,10 @@ namespace DiscordRPC }; } - #endregion + #endregion Initialization #region Message Handling + /// /// Dequeues all the messages from Discord, processes them and then invoke appropriate event handlers. This will process the message and update the internal state before invoking the events. Returns the messages that were invoked in the order they were invoked. /// This method cannot be used if is enabled. @@ -420,7 +423,8 @@ namespace DiscordRPC break; } } - #endregion + + #endregion Message Handling /// /// Respond to a Join Request. All requests will timeout after 30 seconds. @@ -487,6 +491,7 @@ namespace DiscordRPC } #region Updates + /// /// Updates only the of the and sends the updated presence to Discord. Returns the newly edited Rich Presence. /// @@ -505,6 +510,7 @@ namespace DiscordRPC } return CurrentPresence; } + /// /// Updates only the of the and sends the updated presence to Discord. Returns the newly edited Rich Presence. /// @@ -523,6 +529,7 @@ namespace DiscordRPC } return CurrentPresence; } + /// /// Updates only the of the and sends the updated presence to Discord. Returns the newly edited Rich Presence. /// @@ -542,6 +549,7 @@ namespace DiscordRPC SetPresence(CurrentPresence); return CurrentPresence; } + /// /// Updates the of the and sends the update presence to Discord. Returns the newly edited Rich Presence. /// Will return null if no presence exists and will throw a new if the Party does not exist. @@ -560,6 +568,7 @@ namespace DiscordRPC try { UpdatePartySize(size, CurrentPresence.Party.Max); } catch (Exception) { throw; } return CurrentPresence; } + /// /// Updates the of the and sends the update presence to Discord. Returns the newly edited Rich Presence. /// Will return null if no presence exists and will throw a new if the Party does not exist. @@ -656,7 +665,8 @@ namespace DiscordRPC /// Sets the start time of the to now and sends the updated presence to Discord. /// /// Updated Rich Presence - public RichPresence UpdateStartTime() { try { return UpdateStartTime(DateTime.UtcNow); } catch (Exception) { throw; } } + public RichPresence UpdateStartTime() + { try { return UpdateStartTime(DateTime.UtcNow); } catch (Exception) { throw; } } /// /// Sets the start time of the and sends the updated presence to Discord. @@ -683,7 +693,8 @@ namespace DiscordRPC /// Sets the end time of the to now and sends the updated presence to Discord. /// /// Updated Rich Presence - public RichPresence UpdateEndTime() { try { return UpdateEndTime(DateTime.UtcNow); } catch (Exception) { throw; } } + public RichPresence UpdateEndTime() + { try { return UpdateEndTime(DateTime.UtcNow); } catch (Exception) { throw; } } /// /// Sets the end time of the and sends the updated presence to Discord. @@ -724,7 +735,8 @@ namespace DiscordRPC SetPresence(CurrentPresence); return CurrentPresence; } - #endregion + + #endregion Updates /// /// Clears the Rich Presence. Use this just before disposal to prevent ghosting. @@ -764,21 +776,24 @@ namespace DiscordRPC /// Requires the UriScheme to be registered. /// /// The event type to subscribe to - public void Subscribe(EventType type) { SetSubscription(Subscription | type); } + public void Subscribe(EventType type) + { SetSubscription(Subscription | type); } /// - /// + /// /// /// [System.Obsolete("Replaced with Unsubscribe", true)] - public void Unubscribe(EventType type) { SetSubscription(Subscription & ~type); } + public void Unubscribe(EventType type) + { SetSubscription(Subscription & ~type); } /// /// Unsubscribe from the event sent by discord. Used for Join / Spectate feature. /// Requires the UriScheme to be registered. /// /// The event type to unsubscribe from - public void Unsubscribe(EventType type) { SetSubscription(Subscription & ~type); } + public void Unsubscribe(EventType type) + { SetSubscription(Subscription & ~type); } /// /// Sets the subscription to the events sent from Discord. @@ -811,7 +826,7 @@ namespace DiscordRPC /// Represents if the unsubscribe payload should be sent instead. private void SubscribeToTypes(EventType type, bool isUnsubscribe) { - //Because of SetSubscription, this can actually be none as there is no differences. + //Because of SetSubscription, this can actually be none as there is no differences. //If that is the case, we should just stop here if (type == EventType.None) return; @@ -840,7 +855,7 @@ namespace DiscordRPC connection.EnqueueCommand(new SubscribeCommand() { Event = RPC.Payload.ServerEvent.ActivityJoinRequest, IsUnsubscribe = isUnsubscribe }); } - #endregion + #endregion Subscriptions /// /// Resends the current presence and subscription. This is used when Ready is called to keep the current state within discord. @@ -896,6 +911,5 @@ namespace DiscordRPC if (IsInitialized) Deinitialize(); IsDisposed = true; } - } -} +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/EventType.cs b/SDR-RPC/DiscordAPI/EventType.cs new file mode 100644 index 0000000..3c4626e --- /dev/null +++ b/SDR-RPC/DiscordAPI/EventType.cs @@ -0,0 +1,29 @@ +namespace DiscordRPC +{ + /// + /// The type of event receieved by the RPC. A flag type that can be combined. + /// + [System.Flags] + public enum EventType + { + /// + /// No event + /// + None = 0, + + /// + /// Called when the Discord Client wishes to enter a game to spectate + /// + Spectate = 0x1, + + /// + /// Called when the Discord Client wishes to enter a game to play. + /// + Join = 0x2, + + /// + /// Called when another Discord Client has requested permission to join this game. + /// + JoinRequest = 0x4 + } +} \ No newline at end of file diff --git a/DiscordAPI/Events.cs b/SDR-RPC/DiscordAPI/Events.cs similarity index 98% rename from DiscordAPI/Events.cs rename to SDR-RPC/DiscordAPI/Events.cs index b28c7fe..3612402 100644 --- a/DiscordAPI/Events.cs +++ b/SDR-RPC/DiscordAPI/Events.cs @@ -1,8 +1,4 @@ using DiscordRPC.Message; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace DiscordRPC.Events { @@ -69,7 +65,6 @@ namespace DiscordRPC.Events /// The arguments supplied with the event public delegate void OnJoinRequestedEvent(object sender, JoinRequestMessage args); - /// /// The connection to the discord client was succesfull. This is called before . /// @@ -84,11 +79,10 @@ namespace DiscordRPC.Events /// The arguments supplied with the event public delegate void OnConnectionFailedEvent(object sender, ConnectionFailedMessage args); - /// /// A RPC Message is received. /// /// The handler that sent this event /// The raw message from the RPC public delegate void OnRpcMessageEvent(object sender, IMessage msg); -} +} \ No newline at end of file diff --git a/DiscordAPI/Exceptions/BadPresenceException.cs b/SDR-RPC/DiscordAPI/Exceptions/BadPresenceException.cs similarity index 63% rename from DiscordAPI/Exceptions/BadPresenceException.cs rename to SDR-RPC/DiscordAPI/Exceptions/BadPresenceException.cs index f5acf6c..19fea54 100644 --- a/DiscordAPI/Exceptions/BadPresenceException.cs +++ b/SDR-RPC/DiscordAPI/Exceptions/BadPresenceException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace DiscordRPC.Exceptions { @@ -9,7 +6,9 @@ namespace DiscordRPC.Exceptions /// A BadPresenceException is thrown when invalid, incompatible or conflicting properties and is unable to be sent. /// public class BadPresenceException : Exception - { - internal BadPresenceException(string message) : base(message) { } - } -} + { + internal BadPresenceException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/DiscordAPI/Exceptions/InvalidConfigurationException.cs b/SDR-RPC/DiscordAPI/Exceptions/InvalidConfigurationException.cs similarity index 63% rename from DiscordAPI/Exceptions/InvalidConfigurationException.cs rename to SDR-RPC/DiscordAPI/Exceptions/InvalidConfigurationException.cs index 37f00e0..1b41b4f 100644 --- a/DiscordAPI/Exceptions/InvalidConfigurationException.cs +++ b/SDR-RPC/DiscordAPI/Exceptions/InvalidConfigurationException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace DiscordRPC.Exceptions { @@ -9,7 +6,9 @@ namespace DiscordRPC.Exceptions /// A InvalidConfigurationException is thrown when trying to perform a action that conflicts with the current configuration. /// public class InvalidConfigurationException : Exception - { - internal InvalidConfigurationException(string message) : base(message) { } - } -} + { + internal InvalidConfigurationException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Exceptions/InvalidPipeException.cs b/SDR-RPC/DiscordAPI/Exceptions/InvalidPipeException.cs new file mode 100644 index 0000000..cfca966 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Exceptions/InvalidPipeException.cs @@ -0,0 +1,15 @@ +using System; + +namespace DiscordRPC.Exceptions +{ + /// + /// The exception that is thrown when a error occurs while communicating with a pipe or when a connection attempt fails. + /// + [System.Obsolete("Not actually used anywhere")] + public class InvalidPipeException : Exception + { + internal InvalidPipeException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/DiscordAPI/Exceptions/StringOutOfRangeException.cs b/SDR-RPC/DiscordAPI/Exceptions/StringOutOfRangeException.cs similarity index 87% rename from DiscordAPI/Exceptions/StringOutOfRangeException.cs rename to SDR-RPC/DiscordAPI/Exceptions/StringOutOfRangeException.cs index e9a7b09..3045abc 100644 --- a/DiscordAPI/Exceptions/StringOutOfRangeException.cs +++ b/SDR-RPC/DiscordAPI/Exceptions/StringOutOfRangeException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace DiscordRPC.Exceptions { @@ -9,7 +6,7 @@ namespace DiscordRPC.Exceptions /// A StringOutOfRangeException is thrown when the length of a string exceeds the allowed limit. /// public class StringOutOfRangeException : Exception - { + { /// /// Maximum length the string is allowed to be. /// @@ -26,7 +23,7 @@ namespace DiscordRPC.Exceptions /// The custom message /// Minimum length the string can be /// Maximum length the string can be - internal StringOutOfRangeException(string message, int min, int max) : base(message) + internal StringOutOfRangeException(string message, int min, int max) : base(message) { MinimumLength = min; MaximumLength = max; @@ -37,14 +34,14 @@ namespace DiscordRPC.Exceptions /// /// /// - internal StringOutOfRangeException(int minumum, int max) + internal StringOutOfRangeException(int minumum, int max) : this("Length of string is out of range. Expected a value between " + minumum + " and " + max, minumum, max) { } /// /// Creates a new sting out of range exception with a range of 0 to max /// /// - internal StringOutOfRangeException(int max) + internal StringOutOfRangeException(int max) : this("Length of string is out of range. Expected a value with a maximum length of " + max, 0, max) { } } -} +} \ No newline at end of file diff --git a/DiscordAPI/Exceptions/UninitializedException.cs b/SDR-RPC/DiscordAPI/Exceptions/UninitializedException.cs similarity index 90% rename from DiscordAPI/Exceptions/UninitializedException.cs rename to SDR-RPC/DiscordAPI/Exceptions/UninitializedException.cs index a894f3f..ea38423 100644 --- a/DiscordAPI/Exceptions/UninitializedException.cs +++ b/SDR-RPC/DiscordAPI/Exceptions/UninitializedException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace DiscordRPC.Exceptions { @@ -21,4 +18,4 @@ namespace DiscordRPC.Exceptions /// internal UninitializedException() : this("Cannot perform action because the client has not been initialized yet or has been deinitialized.") { } } -} +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Helper/BackoffDelay.cs b/SDR-RPC/DiscordAPI/Helper/BackoffDelay.cs new file mode 100644 index 0000000..a4fe702 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Helper/BackoffDelay.cs @@ -0,0 +1,75 @@ +using System; + +namespace DiscordRPC.Helper +{ + internal class BackoffDelay + { + /// + /// The maximum time the backoff can reach + /// + public int Maximum { get; private set; } + + /// + /// The minimum time the backoff can start at + /// + public int Minimum { get; private set; } + + /// + /// The current time of the backoff + /// + public int Current + { get { return _current; } } + + private int _current; + + /// + /// The current number of failures + /// + public int Fails + { get { return _fails; } } + + private int _fails; + + /// + /// The random generator + /// + public Random Random { get; set; } + + private BackoffDelay() + { } + + public BackoffDelay(int min, int max) : this(min, max, new Random()) + { + } + + public BackoffDelay(int min, int max, Random random) + { + this.Minimum = min; + this.Maximum = max; + + this._current = min; + this._fails = 0; + this.Random = random; + } + + /// + /// Resets the backoff + /// + public void Reset() + { + _fails = 0; + _current = Minimum; + } + + public int NextDelay() + { + //Increment the failures + _fails++; + + double diff = (Maximum - Minimum) / 100f; + _current = (int)Math.Floor(diff * _fails) + Minimum; + + return Math.Min(Math.Max(_current, Minimum), Maximum); + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Helper/StringTools.cs b/SDR-RPC/DiscordAPI/Helper/StringTools.cs new file mode 100644 index 0000000..84569e6 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Helper/StringTools.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using System.Text; + +namespace DiscordRPC.Helper +{ + /// + /// Collectin of helpful string extensions + /// + public static class StringTools + { + /// + /// Will return null if the string is whitespace, otherwise it will return the string. + /// + /// The string to check + /// Null if the string is empty, otherwise the string + public static string GetNullOrString(this string str) + { + return str.Length == 0 || string.IsNullOrEmpty(str.Trim()) ? null : str; + } + + /// + /// Does the string fit within the given amount of bytes? Uses UTF8 encoding. + /// + /// The string to check + /// The maximum number of bytes the string can take up + /// True if the string fits within the number of bytes + public static bool WithinLength(this string str, int bytes) + { + return str.WithinLength(bytes, Encoding.UTF8); + } + + /// + /// Does the string fit within the given amount of bytes? + /// + /// The string to check + /// The maximum number of bytes the string can take up + /// The encoding to count the bytes with + /// True if the string fits within the number of bytes + public static bool WithinLength(this string str, int bytes, Encoding encoding) + { + return encoding.GetByteCount(str) <= bytes; + } + + /// + /// Converts the string into UpperCamelCase (Pascal Case). + /// + /// The string to convert + /// + public static string ToCamelCase(this string str) + { + if (str == null) return null; + + return str.ToLower() + .Split(new[] { "_", " " }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => char.ToUpper(s[0]) + s.Substring(1, s.Length - 1)) + .Aggregate(string.Empty, (s1, s2) => s1 + s2); + } + + /// + /// Converts the string into UPPER_SNAKE_CASE + /// + /// The string to convert + /// + public static string ToSnakeCase(this string str) + { + if (str == null) return null; + var concat = string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString()).ToArray()); + return concat.ToUpper(); + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/IO/Handshake.cs b/SDR-RPC/DiscordAPI/IO/Handshake.cs new file mode 100644 index 0000000..f4d7035 --- /dev/null +++ b/SDR-RPC/DiscordAPI/IO/Handshake.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace DiscordRPC.IO +{ + internal class Handshake + { + /// + /// Version of the IPC API we are using + /// + [JsonProperty("v")] + public int Version { get; set; } + + /// + /// The ID of the app. + /// + [JsonProperty("client_id")] + public string ClientID { get; set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/IO/INamedPipeClient.cs b/SDR-RPC/DiscordAPI/IO/INamedPipeClient.cs new file mode 100644 index 0000000..c2c16b3 --- /dev/null +++ b/SDR-RPC/DiscordAPI/IO/INamedPipeClient.cs @@ -0,0 +1,51 @@ +using DiscordRPC.Logging; +using System; + +namespace DiscordRPC.IO +{ + /// + /// Pipe Client used to communicate with Discord. + /// + public interface INamedPipeClient : IDisposable + { + /// + /// The logger for the Pipe client to use + /// + ILogger Logger { get; set; } + + /// + /// Is the pipe client currently connected? + /// + bool IsConnected { get; } + + /// + /// The pipe the client is currently connected too + /// + int ConnectedPipe { get; } + + /// + /// Attempts to connect to the pipe. If 0-9 is passed to pipe, it should try to only connect to the specified pipe. If -1 is passed, the pipe will find the first available pipe. + /// + /// If -1 is passed, the pipe will find the first available pipe, otherwise it connects to the pipe that was supplied + /// + bool Connect(int pipe); + + /// + /// Reads a frame if there is one available. Returns false if there is none. This should be non blocking (aka use a Peek first). + /// + /// The frame that has been read. Will be default(PipeFrame) if it fails to read + /// Returns true if a frame has been read, otherwise false. + bool ReadFrame(out PipeFrame frame); + + /// + /// Writes the frame to the pipe. Returns false if any errors occur. + /// + /// The frame to be written + bool WriteFrame(PipeFrame frame); + + /// + /// Closes the connection + /// + void Close(); + } +} \ No newline at end of file diff --git a/DiscordAPI/IO/ManagedNamedPipeClient.cs b/SDR-RPC/DiscordAPI/IO/ManagedNamedPipeClient.cs similarity index 98% rename from DiscordAPI/IO/ManagedNamedPipeClient.cs rename to SDR-RPC/DiscordAPI/IO/ManagedNamedPipeClient.cs index 1e0312d..94c24a9 100644 --- a/DiscordAPI/IO/ManagedNamedPipeClient.cs +++ b/SDR-RPC/DiscordAPI/IO/ManagedNamedPipeClient.cs @@ -1,11 +1,9 @@ -using System; +using DiscordRPC.Logging; +using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using DiscordRPC.Logging; +using System.IO; using System.IO.Pipes; using System.Threading; -using System.IO; namespace DiscordRPC.IO { @@ -17,7 +15,7 @@ namespace DiscordRPC.IO /// /// Name format of the pipe /// - const string PIPE_NAME = @"discord-ipc-{0}"; + private const string PIPE_NAME = @"discord-ipc-{0}"; /// /// The logger for the Pipe client to use @@ -44,7 +42,8 @@ namespace DiscordRPC.IO /// /// The pipe we are currently connected too. /// - public int ConnectedPipe { get { return _connectedPipe; } } + public int ConnectedPipe + { get { return _connectedPipe; } } private int _connectedPipe; private NamedPipeClientStream _stream; @@ -378,7 +377,7 @@ namespace DiscordRPC.IO return; } - //flush and dispose + //flush and dispose try { //Wait for the stream object to become available. @@ -469,6 +468,7 @@ namespace DiscordRPC.IO { default: return null; + case PlatformID.Unix: return "snap.discord/"; } @@ -506,4 +506,4 @@ namespace DiscordRPC.IO } } } -} +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/IO/Opcode.cs b/SDR-RPC/DiscordAPI/IO/Opcode.cs new file mode 100644 index 0000000..ba57e8b --- /dev/null +++ b/SDR-RPC/DiscordAPI/IO/Opcode.cs @@ -0,0 +1,33 @@ +namespace DiscordRPC.IO +{ + /// + /// The operation code that the was sent under. This defines the type of frame and the data to expect. + /// + public enum Opcode : uint + { + /// + /// Initial handshake frame + /// + Handshake = 0, + + /// + /// Generic message frame + /// + Frame = 1, + + /// + /// Discord has closed the connection + /// + Close = 2, + + /// + /// Ping frame (not used?) + /// + Ping = 3, + + /// + /// Pong frame (not used?) + /// + Pong = 4 + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/IO/PipeFrame.cs b/SDR-RPC/DiscordAPI/IO/PipeFrame.cs new file mode 100644 index 0000000..27531c6 --- /dev/null +++ b/SDR-RPC/DiscordAPI/IO/PipeFrame.cs @@ -0,0 +1,203 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.Text; + +namespace DiscordRPC.IO +{ + /// + /// A frame received and sent to the Discord client for RPC communications. + /// + public struct PipeFrame + { + /// + /// The maxium size of a pipe frame (16kb). + /// + public static readonly int MAX_SIZE = 16 * 1024; + + /// + /// The opcode of the frame + /// + public Opcode Opcode { get; set; } + + /// + /// The length of the frame data + /// + public uint Length { get { return (uint)Data.Length; } } + + /// + /// The data in the frame + /// + public byte[] Data { get; set; } + + /// + /// The data represented as a string. + /// + public string Message + { + get { return GetMessage(); } + set { SetMessage(value); } + } + + /// + /// Creates a new pipe frame instance + /// + /// The opcode of the frame + /// The data of the frame that will be serialized as JSON + public PipeFrame(Opcode opcode, object data) + { + //Set the opcode and a temp field for data + Opcode = opcode; + Data = null; + + //Set the data + SetObject(data); + } + + /// + /// Gets the encoding used for the pipe frames + /// + public Encoding MessageEncoding { get { return Encoding.UTF8; } } + + /// + /// Sets the data based of a string + /// + /// + private void SetMessage(string str) + { Data = MessageEncoding.GetBytes(str); } + + /// + /// Gets a string based of the data + /// + /// + private string GetMessage() + { return MessageEncoding.GetString(Data); } + + /// + /// Serializes the object into json string then encodes it into . + /// + /// + public void SetObject(object obj) + { + string json = JsonConvert.SerializeObject(obj); + SetMessage(json); + } + + /// + /// Sets the opcodes and serializes the object into a json string. + /// + /// + /// + public void SetObject(Opcode opcode, object obj) + { + Opcode = opcode; + SetObject(obj); + } + + /// + /// Deserializes the data into the supplied type using JSON. + /// + /// The type to deserialize into + /// + public T GetObject() + { + string json = GetMessage(); + return JsonConvert.DeserializeObject(json); + } + + /// + /// Attempts to read the contents of the frame from the stream + /// + /// + /// + public bool ReadStream(Stream stream) + { + //Try to read the opcode + uint op; + if (!TryReadUInt32(stream, out op)) + return false; + + //Try to read the length + uint len; + if (!TryReadUInt32(stream, out len)) + return false; + + uint readsRemaining = len; + + //Read the contents + using (var mem = new MemoryStream()) + { + byte[] buffer = new byte[Min(2048, len)]; // read in chunks of 2KB + int bytesRead; + while ((bytesRead = stream.Read(buffer, 0, Min(buffer.Length, readsRemaining))) > 0) + { + readsRemaining -= len; + mem.Write(buffer, 0, bytesRead); + } + + byte[] result = mem.ToArray(); + if (result.LongLength != len) + return false; + + Opcode = (Opcode)op; + Data = result; + return true; + } + + //fun + //if (a != null) { do { yield return true; switch (a) { case 1: await new Task(); default: lock (obj) { foreach (b in c) { for (int d = 0; d < 1; d++) { a++; } } } while (a is typeof(int) || (new Class()) != null) } goto MY_LABEL; + } + + /// + /// Returns minimum value between a int and a unsigned int + /// + private int Min(int a, uint b) + { + if (b >= a) return a; + return (int)b; + } + + /// + /// Attempts to read a UInt32 + /// + /// + /// + /// + private bool TryReadUInt32(Stream stream, out uint value) + { + //Read the bytes available to us + byte[] bytes = new byte[4]; + int cnt = stream.Read(bytes, 0, bytes.Length); + + //Make sure we actually have a valid value + if (cnt != 4) + { + value = default(uint); + return false; + } + + value = BitConverter.ToUInt32(bytes, 0); + return true; + } + + /// + /// Writes the frame into the target frame as one big byte block. + /// + /// + public void WriteStream(Stream stream) + { + //Get all the bytes + byte[] op = BitConverter.GetBytes((uint)Opcode); + byte[] len = BitConverter.GetBytes(Length); + + //Copy it all into a buffer + byte[] buff = new byte[op.Length + len.Length + Data.Length]; + op.CopyTo(buff, 0); + len.CopyTo(buff, op.Length); + Data.CopyTo(buff, op.Length + len.Length); + + //Write it to the stream + stream.Write(buff, 0, buff.Length); + } + } +} \ No newline at end of file diff --git a/DiscordAPI/LICENSE.txt b/SDR-RPC/DiscordAPI/LICENSE.txt similarity index 100% rename from DiscordAPI/LICENSE.txt rename to SDR-RPC/DiscordAPI/LICENSE.txt diff --git a/SDR-RPC/DiscordAPI/Logging/ConsoleLogger.cs b/SDR-RPC/DiscordAPI/Logging/ConsoleLogger.cs new file mode 100644 index 0000000..201fb2f --- /dev/null +++ b/SDR-RPC/DiscordAPI/Logging/ConsoleLogger.cs @@ -0,0 +1,98 @@ +using System; + +namespace DiscordRPC.Logging +{ + /// + /// Logs the outputs to the console using + /// + public class ConsoleLogger : ILogger + { + /// + /// The level of logging to apply to this logger. + /// + public LogLevel Level { get; set; } + + /// + /// Should the output be coloured? + /// + public bool Coloured { get; set; } + + /// + /// A alias too + /// + public bool Colored + { get { return Coloured; } set { Coloured = value; } } + + /// + /// Creates a new instance of a Console Logger. + /// + public ConsoleLogger() + { + this.Level = LogLevel.Info; + Coloured = false; + } + + /// + /// Creates a new instance of a Console Logger with a set log level + /// + /// + /// + public ConsoleLogger(LogLevel level, bool coloured = false) + { + Level = level; + Coloured = coloured; + } + + /// + /// Informative log messages + /// + /// + /// + public void Trace(string message, params object[] args) + { + if (Level > LogLevel.Trace) return; + + if (Coloured) Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine("TRACE: " + message, args); + } + + /// + /// Informative log messages + /// + /// + /// + public void Info(string message, params object[] args) + { + if (Level > LogLevel.Info) return; + + if (Coloured) Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine("INFO: " + message, args); + } + + /// + /// Warning log messages + /// + /// + /// + public void Warning(string message, params object[] args) + { + if (Level > LogLevel.Warning) return; + + if (Coloured) Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("WARN: " + message, args); + } + + /// + /// Error log messsages + /// + /// + /// + public void Error(string message, params object[] args) + { + if (Level > LogLevel.Error) return; + + if (Coloured) Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("ERR : " + message, args); + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Logging/FileLogger.cs b/SDR-RPC/DiscordAPI/Logging/FileLogger.cs new file mode 100644 index 0000000..b226e3e --- /dev/null +++ b/SDR-RPC/DiscordAPI/Logging/FileLogger.cs @@ -0,0 +1,85 @@ +namespace DiscordRPC.Logging +{ + /// + /// Logs the outputs to a file + /// + public class FileLogger : ILogger + { + /// + /// The level of logging to apply to this logger. + /// + public LogLevel Level { get; set; } + + /// + /// Should the output be coloured? + /// + public string File { get; set; } + + private object filelock; + + /// + /// Creates a new instance of the file logger + /// + /// The path of the log file. + public FileLogger(string path) + : this(path, LogLevel.Info) { } + + /// + /// Creates a new instance of the file logger + /// + /// The path of the log file. + /// The level to assign to the logger. + public FileLogger(string path, LogLevel level) + { + Level = level; + File = path; + filelock = new object(); + } + + /// + /// Informative log messages + /// + /// + /// + public void Trace(string message, params object[] args) + { + if (Level > LogLevel.Trace) return; + lock (filelock) System.IO.File.AppendAllText(File, "\r\nTRCE: " + (args.Length > 0 ? string.Format(message, args) : message)); + } + + /// + /// Informative log messages + /// + /// + /// + public void Info(string message, params object[] args) + { + if (Level > LogLevel.Info) return; + lock (filelock) System.IO.File.AppendAllText(File, "\r\nINFO: " + (args.Length > 0 ? string.Format(message, args) : message)); + } + + /// + /// Warning log messages + /// + /// + /// + public void Warning(string message, params object[] args) + { + if (Level > LogLevel.Warning) return; + lock (filelock) + System.IO.File.AppendAllText(File, "\r\nWARN: " + (args.Length > 0 ? string.Format(message, args) : message)); + } + + /// + /// Error log messsages + /// + /// + /// + public void Error(string message, params object[] args) + { + if (Level > LogLevel.Error) return; + lock (filelock) + System.IO.File.AppendAllText(File, "\r\nERR : " + (args.Length > 0 ? string.Format(message, args) : message)); + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Logging/ILogger.cs b/SDR-RPC/DiscordAPI/Logging/ILogger.cs new file mode 100644 index 0000000..1014031 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Logging/ILogger.cs @@ -0,0 +1,41 @@ +namespace DiscordRPC.Logging +{ + /// + /// Logging interface to log the internal states of the pipe. Logs are sent in a NON thread safe way. They can come from multiple threads and it is upto the ILogger to account for it. + /// + public interface ILogger + { + /// + /// The level of logging to apply to this logger. + /// + LogLevel Level { get; set; } + + /// + /// Debug trace messeages used for debugging internal elements. + /// + /// + /// + void Trace(string message, params object[] args); + + /// + /// Informative log messages + /// + /// + /// + void Info(string message, params object[] args); + + /// + /// Warning log messages + /// + /// + /// + void Warning(string message, params object[] args); + + /// + /// Error log messsages + /// + /// + /// + void Error(string message, params object[] args); + } +} \ No newline at end of file diff --git a/DiscordAPI/Logging/LogLevel.cs b/SDR-RPC/DiscordAPI/Logging/LogLevel.cs similarity index 61% rename from DiscordAPI/Logging/LogLevel.cs rename to SDR-RPC/DiscordAPI/Logging/LogLevel.cs index af689e5..71fb72b 100644 --- a/DiscordAPI/Logging/LogLevel.cs +++ b/SDR-RPC/DiscordAPI/Logging/LogLevel.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DiscordRPC.Logging +namespace DiscordRPC.Logging { - /// - /// Level of logging to use. - /// - public enum LogLevel - { + /// + /// Level of logging to use. + /// + public enum LogLevel + { /// /// Trace, Info, Warning and Errors are logged /// @@ -30,9 +25,9 @@ namespace DiscordRPC.Logging /// Error = 4, - /// - /// Nothing is logged - /// - None = 256 - } -} + /// + /// Nothing is logged + /// + None = 256 + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Logging/NullLogger.cs b/SDR-RPC/DiscordAPI/Logging/NullLogger.cs new file mode 100644 index 0000000..475444b --- /dev/null +++ b/SDR-RPC/DiscordAPI/Logging/NullLogger.cs @@ -0,0 +1,53 @@ +namespace DiscordRPC.Logging +{ + /// + /// Ignores all log events + /// + public class NullLogger : ILogger + { + /// + /// The level of logging to apply to this logger. + /// + public LogLevel Level { get; set; } + + /// + /// Informative log messages + /// + /// + /// + public void Trace(string message, params object[] args) + { + //Null Logger, so no messages are acutally sent + } + + /// + /// Informative log messages + /// + /// + /// + public void Info(string message, params object[] args) + { + //Null Logger, so no messages are acutally sent + } + + /// + /// Warning log messages + /// + /// + /// + public void Warning(string message, params object[] args) + { + //Null Logger, so no messages are acutally sent + } + + /// + /// Error log messsages + /// + /// + /// + public void Error(string message, params object[] args) + { + //Null Logger, so no messages are acutally sent + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/CloseMessage.cs b/SDR-RPC/DiscordAPI/Message/CloseMessage.cs new file mode 100644 index 0000000..f99c23b --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/CloseMessage.cs @@ -0,0 +1,30 @@ +namespace DiscordRPC.Message +{ + /// + /// Called when the IPC has closed. + /// + public class CloseMessage : IMessage + { + /// + /// The type of message + /// + public override MessageType Type + { get { return MessageType.Close; } } + + /// + /// The reason for the close + /// + public string Reason { get; internal set; } + + /// + /// The closure code + /// + public int Code { get; internal set; } + + internal CloseMessage() + { } + + internal CloseMessage(string reason) + { this.Reason = reason; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/ConnectionEstablishedMessage.cs b/SDR-RPC/DiscordAPI/Message/ConnectionEstablishedMessage.cs new file mode 100644 index 0000000..14b5637 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/ConnectionEstablishedMessage.cs @@ -0,0 +1,19 @@ +namespace DiscordRPC.Message +{ + /// + /// The connection to the discord client was succesfull. This is called before . + /// + public class ConnectionEstablishedMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.ConnectionEstablished; } } + + /// + /// The pipe we ended up connecting too + /// + public int ConnectedPipe { get; internal set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/ConnectionFailedMessage.cs b/SDR-RPC/DiscordAPI/Message/ConnectionFailedMessage.cs new file mode 100644 index 0000000..ac15ef3 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/ConnectionFailedMessage.cs @@ -0,0 +1,19 @@ +namespace DiscordRPC.Message +{ + /// + /// Failed to establish any connection with discord. Discord is potentially not running? + /// + public class ConnectionFailedMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.ConnectionFailed; } } + + /// + /// The pipe we failed to connect too. + /// + public int FailedPipe { get; internal set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/ErrorMessage.cs b/SDR-RPC/DiscordAPI/Message/ErrorMessage.cs new file mode 100644 index 0000000..d9d4cd4 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/ErrorMessage.cs @@ -0,0 +1,76 @@ +using Newtonsoft.Json; + +namespace DiscordRPC.Message +{ + /// + /// Created when a error occurs within the ipc and it is sent to the client. + /// + public class ErrorMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.Error; } } + + /// + /// The Discord error code. + /// + [JsonProperty("code")] + public ErrorCode Code { get; internal set; } + + /// + /// The message associated with the error code. + /// + [JsonProperty("message")] + public string Message { get; internal set; } + } + + /// + /// The error message received by discord. See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-errors for documentation + /// + public enum ErrorCode + { + //Pipe Error Codes + /// Pipe was Successful + Success = 0, + + ///The pipe had an exception + PipeException = 1, + + ///The pipe received corrupted data + ReadCorrupt = 2, + + //Custom Error Code + ///The functionality was not yet implemented + NotImplemented = 10, + + //Discord RPC error codes + ///Unkown Discord error + UnkownError = 1000, + + ///Invalid Payload received + InvalidPayload = 4000, + + ///Invalid command was sent + InvalidCommand = 4002, + + /// Invalid event was sent + InvalidEvent = 4004, + + /* + InvalidGuild = 4003, + InvalidChannel = 4005, + InvalidPermissions = 4006, + InvalidClientID = 4007, + InvalidOrigin = 4008, + InvalidToken = 4009, + InvalidUser = 4010, + OAuth2Error = 5000, + SelectChannelTimeout = 5001, + GetGuildTimeout = 5002, + SelectVoiceForceRequired = 5003, + CaptureShortcutAlreadyListening = 5004 + */ + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/IMessage.cs b/SDR-RPC/DiscordAPI/Message/IMessage.cs new file mode 100644 index 0000000..9693b00 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/IMessage.cs @@ -0,0 +1,31 @@ +using System; + +namespace DiscordRPC.Message +{ + /// + /// Messages received from discord. + /// + public abstract class IMessage + { + /// + /// The type of message received from discord + /// + public abstract MessageType Type { get; } + + /// + /// The time the message was created + /// + public DateTime TimeCreated + { get { return _timecreated; } } + + private DateTime _timecreated; + + /// + /// Creates a new instance of the message + /// + public IMessage() + { + _timecreated = DateTime.Now; + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/JoinMessage.cs b/SDR-RPC/DiscordAPI/Message/JoinMessage.cs new file mode 100644 index 0000000..c8edfb4 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/JoinMessage.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace DiscordRPC.Message +{ + /// + /// Called when the Discord Client wishes for this process to join a game. D -> C. + /// + public class JoinMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.Join; } } + + /// + /// The to connect with. + /// + [JsonProperty("secret")] + public string Secret { get; internal set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/JoinRequestMessage.cs b/SDR-RPC/DiscordAPI/Message/JoinRequestMessage.cs new file mode 100644 index 0000000..1ea7e60 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/JoinRequestMessage.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace DiscordRPC.Message +{ + /// + /// Called when some other person has requested access to this game. C -> D -> C. + /// + public class JoinRequestMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.JoinRequest; } } + + /// + /// The discord user that is requesting access. + /// + [JsonProperty("user")] + public User User { get; internal set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/MessageType.cs b/SDR-RPC/DiscordAPI/Message/MessageType.cs new file mode 100644 index 0000000..803cbb4 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/MessageType.cs @@ -0,0 +1,63 @@ +namespace DiscordRPC.Message +{ + /// + /// Type of message. + /// + public enum MessageType + { + /// + /// The Discord Client is ready to send and receive messages. + /// + Ready, + + /// + /// The connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again. + /// + Close, + + /// + /// A error has occured during the transmission of a message. For example, if a bad Rich Presence payload is sent, this event will be called explaining what went wrong. + /// + Error, + + /// + /// The Discord Client has updated the presence. + /// + PresenceUpdate, + + /// + /// The Discord Client has subscribed to an event. + /// + Subscribe, + + /// + /// The Discord Client has unsubscribed from an event. + /// + Unsubscribe, + + /// + /// The Discord Client wishes for this process to join a game. + /// + Join, + + /// + /// The Discord Client wishes for this process to spectate a game. + /// + Spectate, + + /// + /// Another discord user requests permission to join this game. + /// + JoinRequest, + + /// + /// The connection to the discord client was succesfull. This is called before . + /// + ConnectionEstablished, + + /// + /// Failed to establish any connection with discord. Discord is potentially not running? + /// + ConnectionFailed + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/PresenceMessage.cs b/SDR-RPC/DiscordAPI/Message/PresenceMessage.cs new file mode 100644 index 0000000..a4cd2f7 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/PresenceMessage.cs @@ -0,0 +1,49 @@ +namespace DiscordRPC.Message +{ + /// + /// Representation of the message received by discord when the presence has been updated. + /// + public class PresenceMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.PresenceUpdate; } } + + internal PresenceMessage() : this(null) + { + } + + internal PresenceMessage(RichPresenceResponse rpr) + { + if (rpr == null) + { + Presence = null; + Name = "No Rich Presence"; + ApplicationID = ""; + } + else + { + Presence = (RichPresence)rpr; + Name = rpr.Name; + ApplicationID = rpr.ClientID; + } + } + + /// + /// The rich presence Discord has set + /// + public RichPresence Presence { get; internal set; } + + /// + /// The name of the application Discord has set it for + /// + public string Name { get; internal set; } + + /// + /// The ID of the application discord has set it for + /// + public string ApplicationID { get; internal set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/ReadyMessage.cs b/SDR-RPC/DiscordAPI/Message/ReadyMessage.cs new file mode 100644 index 0000000..fde7427 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/ReadyMessage.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace DiscordRPC.Message +{ + /// + /// Called when the ipc is ready to send arguments. + /// + public class ReadyMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.Ready; } } + + /// + /// The configuration of the connection + /// + [JsonProperty("config")] + public Configuration Configuration { get; set; } + + /// + /// User the connection belongs too + /// + [JsonProperty("user")] + public User User { get; set; } + + /// + /// The version of the RPC + /// + [JsonProperty("v")] + public int Version { get; set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/SpectateMessage.cs b/SDR-RPC/DiscordAPI/Message/SpectateMessage.cs new file mode 100644 index 0000000..69b1890 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/SpectateMessage.cs @@ -0,0 +1,14 @@ +namespace DiscordRPC.Message +{ + /// + /// Called when the Discord Client wishes for this process to spectate a game. D -> C. + /// + public class SpectateMessage : JoinMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.Spectate; } } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/SubscribeMessage.cs b/SDR-RPC/DiscordAPI/Message/SubscribeMessage.cs new file mode 100644 index 0000000..7b1e688 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/SubscribeMessage.cs @@ -0,0 +1,40 @@ +using DiscordRPC.RPC.Payload; + +namespace DiscordRPC.Message +{ + /// + /// Called as validation of a subscribe + /// + public class SubscribeMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.Subscribe; } } + + /// + /// The event that was subscribed too. + /// + public EventType Event { get; internal set; } + + internal SubscribeMessage(ServerEvent evt) + { + switch (evt) + { + default: + case ServerEvent.ActivityJoin: + Event = EventType.Join; + break; + + case ServerEvent.ActivityJoinRequest: + Event = EventType.JoinRequest; + break; + + case ServerEvent.ActivitySpectate: + Event = EventType.Spectate; + break; + } + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/Message/UnsubscribeMsesage.cs b/SDR-RPC/DiscordAPI/Message/UnsubscribeMsesage.cs new file mode 100644 index 0000000..2b59db9 --- /dev/null +++ b/SDR-RPC/DiscordAPI/Message/UnsubscribeMsesage.cs @@ -0,0 +1,40 @@ +using DiscordRPC.RPC.Payload; + +namespace DiscordRPC.Message +{ + /// + /// Called as validation of a subscribe + /// + public class UnsubscribeMessage : IMessage + { + /// + /// The type of message received from discord + /// + public override MessageType Type + { get { return MessageType.Unsubscribe; } } + + /// + /// The event that was subscribed too. + /// + public EventType Event { get; internal set; } + + internal UnsubscribeMessage(ServerEvent evt) + { + switch (evt) + { + default: + case ServerEvent.ActivityJoin: + Event = EventType.Join; + break; + + case ServerEvent.ActivityJoinRequest: + Event = EventType.JoinRequest; + break; + + case ServerEvent.ActivitySpectate: + Event = EventType.Spectate; + break; + } + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Commands/CloseCommand.cs b/SDR-RPC/DiscordAPI/RPC/Commands/CloseCommand.cs new file mode 100644 index 0000000..2635757 --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Commands/CloseCommand.cs @@ -0,0 +1,30 @@ +using DiscordRPC.RPC.Payload; +using Newtonsoft.Json; + +namespace DiscordRPC.RPC.Commands +{ + internal class CloseCommand : ICommand + { + /// + /// The process ID + /// + [JsonProperty("pid")] + public int PID { get; set; } + + /// + /// The rich presence to be set. Can be null. + /// + [JsonProperty("close_reason")] + public string value = "Unity 5.5 doesn't handle thread aborts. Can you please close me discord?"; + + public IPayload PreparePayload(long nonce) + { + return new ArgumentPayload() + { + Command = Command.Dispatch, + Nonce = null, + Arguments = null + }; + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Commands/ICommand.cs b/SDR-RPC/DiscordAPI/RPC/Commands/ICommand.cs new file mode 100644 index 0000000..58ece0a --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Commands/ICommand.cs @@ -0,0 +1,9 @@ +using DiscordRPC.RPC.Payload; + +namespace DiscordRPC.RPC.Commands +{ + internal interface ICommand + { + IPayload PreparePayload(long nonce); + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Commands/PresenceCommand.cs b/SDR-RPC/DiscordAPI/RPC/Commands/PresenceCommand.cs new file mode 100644 index 0000000..0c24108 --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Commands/PresenceCommand.cs @@ -0,0 +1,28 @@ +using DiscordRPC.RPC.Payload; +using Newtonsoft.Json; + +namespace DiscordRPC.RPC.Commands +{ + internal class PresenceCommand : ICommand + { + /// + /// The process ID + /// + [JsonProperty("pid")] + public int PID { get; set; } + + /// + /// The rich presence to be set. Can be null. + /// + [JsonProperty("activity")] + public RichPresence Presence { get; set; } + + public IPayload PreparePayload(long nonce) + { + return new ArgumentPayload(this, nonce) + { + Command = Command.SetActivity + }; + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Commands/RespondCommand.cs b/SDR-RPC/DiscordAPI/RPC/Commands/RespondCommand.cs new file mode 100644 index 0000000..56217bb --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Commands/RespondCommand.cs @@ -0,0 +1,28 @@ +using DiscordRPC.RPC.Payload; +using Newtonsoft.Json; + +namespace DiscordRPC.RPC.Commands +{ + internal class RespondCommand : ICommand + { + /// + /// The user ID that we are accepting / rejecting + /// + [JsonProperty("user_id")] + public string UserID { get; set; } + + /// + /// If true, the user will be allowed to connect. + /// + [JsonIgnore] + public bool Accept { get; set; } + + public IPayload PreparePayload(long nonce) + { + return new ArgumentPayload(this, nonce) + { + Command = Accept ? Command.SendActivityJoinInvite : Command.CloseActivityJoinRequest + }; + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Commands/SubscribeCommand.cs b/SDR-RPC/DiscordAPI/RPC/Commands/SubscribeCommand.cs new file mode 100644 index 0000000..350fa78 --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Commands/SubscribeCommand.cs @@ -0,0 +1,19 @@ +using DiscordRPC.RPC.Payload; + +namespace DiscordRPC.RPC.Commands +{ + internal class SubscribeCommand : ICommand + { + public ServerEvent Event { get; set; } + public bool IsUnsubscribe { get; set; } + + public IPayload PreparePayload(long nonce) + { + return new EventPayload(nonce) + { + Command = IsUnsubscribe ? Command.Unsubscribe : Command.Subscribe, + Event = Event + }; + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Payload/ClosePayload.cs b/SDR-RPC/DiscordAPI/RPC/Payload/ClosePayload.cs new file mode 100644 index 0000000..a8797b6 --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Payload/ClosePayload.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace DiscordRPC.RPC.Payload +{ + internal class ClosePayload : IPayload + { + /// + /// The close code the discord gave us + /// + [JsonProperty("code")] + public int Code { get; set; } + + /// + /// The close reason discord gave us + /// + [JsonProperty("message")] + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Payload/Command.cs b/SDR-RPC/DiscordAPI/RPC/Payload/Command.cs new file mode 100644 index 0000000..ab36fc9 --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Payload/Command.cs @@ -0,0 +1,125 @@ +using DiscordRPC.Converters; +using System; + +namespace DiscordRPC.RPC.Payload +{ + /// + /// The possible commands that can be sent and received by the server. + /// + internal enum Command + { + /// + /// event dispatch + /// + [EnumValue("DISPATCH")] + Dispatch, + + /// + /// Called to set the activity + /// + [EnumValue("SET_ACTIVITY")] + SetActivity, + + /// + /// used to subscribe to an RPC event + /// + [EnumValue("SUBSCRIBE")] + Subscribe, + + /// + /// used to unsubscribe from an RPC event + /// + [EnumValue("UNSUBSCRIBE")] + Unsubscribe, + + /// + /// Used to accept join requests. + /// + [EnumValue("SEND_ACTIVITY_JOIN_INVITE")] + SendActivityJoinInvite, + + /// + /// Used to reject join requests. + /// + [EnumValue("CLOSE_ACTIVITY_JOIN_REQUEST")] + CloseActivityJoinRequest, + + /// + /// used to authorize a new client with your app + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + Authorize, + + /// + /// used to authenticate an existing client with your app + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + Authenticate, + + /// + /// used to retrieve guild information from the client + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + GetGuild, + + /// + /// used to retrieve a list of guilds from the client + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + GetGuilds, + + /// + /// used to retrieve channel information from the client + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + GetChannel, + + /// + /// used to retrieve a list of channels for a guild from the client + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + GetChannels, + + /// + /// used to change voice settings of users in voice channels + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + SetUserVoiceSettings, + + /// + /// used to join or leave a voice channel, group dm, or dm + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + SelectVoiceChannel, + + /// + /// used to get the current voice channel the client is in + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + GetSelectedVoiceChannel, + + /// + /// used to join or leave a text channel, group dm, or dm + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + SelectTextChannel, + + /// + /// used to retrieve the client's voice settings + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + GetVoiceSettings, + + /// + /// used to set the client's voice settings + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + SetVoiceSettings, + + /// + /// used to capture a keyboard shortcut entered by the user RPC Events + /// + [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] + CaptureShortcut + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Payload/IPayload.cs b/SDR-RPC/DiscordAPI/RPC/Payload/IPayload.cs new file mode 100644 index 0000000..293ed75 --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Payload/IPayload.cs @@ -0,0 +1,36 @@ +using DiscordRPC.Converters; +using Newtonsoft.Json; + +namespace DiscordRPC.RPC.Payload +{ + /// + /// Base Payload that is received by both client and server + /// + internal abstract class IPayload + { + /// + /// The type of payload + /// + [JsonProperty("cmd"), JsonConverter(typeof(EnumSnakeCaseConverter))] + public Command Command { get; set; } + + /// + /// A incremental value to help identify payloads + /// + [JsonProperty("nonce")] + public string Nonce { get; set; } + + protected IPayload() + { } + + protected IPayload(long nonce) + { + Nonce = nonce.ToString(); + } + + public override string ToString() + { + return "Payload || Command: " + Command.ToString() + ", Nonce: " + (Nonce != null ? Nonce.ToString() : "NULL"); + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Payload/PayloadArgument.cs b/SDR-RPC/DiscordAPI/RPC/Payload/PayloadArgument.cs new file mode 100644 index 0000000..d639b1f --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Payload/PayloadArgument.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DiscordRPC.RPC.Payload +{ + /// + /// The payload that is sent by the client to discord for events such as setting the rich presence. + /// + /// SetPrecense + /// + /// + internal class ArgumentPayload : IPayload + { + /// + /// The data the server sent too us + /// + [JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)] + public JObject Arguments { get; set; } + + public ArgumentPayload() : base() + { + Arguments = null; + } + + public ArgumentPayload(long nonce) : base(nonce) + { + Arguments = null; + } + + public ArgumentPayload(object args, long nonce) : base(nonce) + { + SetObject(args); + } + + /// + /// Sets the obejct stored within the data. + /// + /// + public void SetObject(object obj) + { + Arguments = JObject.FromObject(obj); + } + + /// + /// Gets the object stored within the Data + /// + /// + /// + public T GetObject() + { + return Arguments.ToObject(); + } + + public override string ToString() + { + return "Argument " + base.ToString(); + } + } +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/Payload/PayloadEvent.cs b/SDR-RPC/DiscordAPI/RPC/Payload/PayloadEvent.cs new file mode 100644 index 0000000..79d612a --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/Payload/PayloadEvent.cs @@ -0,0 +1,55 @@ +using DiscordRPC.Converters; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DiscordRPC.RPC.Payload +{ + /// + /// Used for Discord IPC Events + /// + internal class EventPayload : IPayload + { + /// + /// The data the server sent too us + /// + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public JObject Data { get; set; } + + /// + /// The type of event the server sent + /// + [JsonProperty("evt"), JsonConverter(typeof(EnumSnakeCaseConverter))] + public ServerEvent? Event { get; set; } + + /// + /// Creates a payload with empty data + /// + public EventPayload() : base() { Data = null; } + + /// + /// Creates a payload with empty data and a set nonce + /// + /// + public EventPayload(long nonce) : base(nonce) { Data = null; } + + /// + /// Gets the object stored within the Data + /// + /// + /// + public T GetObject() + { + if (Data == null) return default(T); + return Data.ToObject(); + } + + /// + /// Converts the object into a human readable string + /// + /// + public override string ToString() + { + return "Event " + base.ToString() + ", Event: " + (Event.HasValue ? Event.ToString() : "N/A"); + } + } +} \ No newline at end of file diff --git a/DiscordAPI/RPC/Payload/ServerEvent.cs b/SDR-RPC/DiscordAPI/RPC/Payload/ServerEvent.cs similarity index 80% rename from DiscordAPI/RPC/Payload/ServerEvent.cs rename to SDR-RPC/DiscordAPI/RPC/Payload/ServerEvent.cs index f91bd75..57f9a84 100644 --- a/DiscordAPI/RPC/Payload/ServerEvent.cs +++ b/SDR-RPC/DiscordAPI/RPC/Payload/ServerEvent.cs @@ -1,6 +1,4 @@ using DiscordRPC.Converters; -using System; -using System.Runtime.Serialization; namespace DiscordRPC.RPC.Payload { @@ -8,37 +6,36 @@ namespace DiscordRPC.RPC.Payload /// See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-events for documentation /// internal enum ServerEvent - { + { + /// + /// Sent when the server is ready to accept messages + /// + [EnumValue("READY")] + Ready, - /// - /// Sent when the server is ready to accept messages - /// - [EnumValue("READY")] - Ready, + /// + /// Sent when something bad has happened + /// + [EnumValue("ERROR")] + Error, - /// - /// Sent when something bad has happened - /// - [EnumValue("ERROR")] - Error, + /// + /// Join Event + /// + [EnumValue("ACTIVITY_JOIN")] + ActivityJoin, - /// - /// Join Event - /// - [EnumValue("ACTIVITY_JOIN")] - ActivityJoin, + /// + /// Spectate Event + /// + [EnumValue("ACTIVITY_SPECTATE")] + ActivitySpectate, - /// - /// Spectate Event - /// - [EnumValue("ACTIVITY_SPECTATE")] - ActivitySpectate, - - /// - /// Request Event - /// - [EnumValue("ACTIVITY_JOIN_REQUEST")] - ActivityJoinRequest, + /// + /// Request Event + /// + [EnumValue("ACTIVITY_JOIN_REQUEST")] + ActivityJoinRequest, #if INCLUDE_FULL_RPC //Old things that are obsolete @@ -92,4 +89,4 @@ namespace DiscordRPC.RPC.Payload CaptureShortcutChange #endif } -} +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/RPC/RpcConnection.cs b/SDR-RPC/DiscordAPI/RPC/RpcConnection.cs new file mode 100644 index 0000000..e9b8fff --- /dev/null +++ b/SDR-RPC/DiscordAPI/RPC/RpcConnection.cs @@ -0,0 +1,877 @@ +using DiscordRPC.Events; +using DiscordRPC.Helper; +using DiscordRPC.IO; +using DiscordRPC.Logging; +using DiscordRPC.Message; +using DiscordRPC.RPC.Commands; +using DiscordRPC.RPC.Payload; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace DiscordRPC.RPC +{ + /// + /// Communicates between the client and discord through RPC + /// + internal class RpcConnection : IDisposable + { + /// + /// Version of the RPC Protocol + /// + public static readonly int VERSION = 1; + + /// + /// The rate of poll to the discord pipe. + /// + public static readonly int POLL_RATE = 1000; + + /// + /// Should we send a null presence on the fairwells? + /// + private static readonly bool CLEAR_ON_SHUTDOWN = true; + + /// + /// Should we work in a lock step manner? This option is semi-obsolete and may not work as expected. + /// + private static readonly bool LOCK_STEP = false; + + /// + /// The logger used by the RPC connection + /// + public ILogger Logger + { + get { return _logger; } + set + { + _logger = value; + if (namedPipe != null) + namedPipe.Logger = value; + } + } + + private ILogger _logger; + + /// + /// Called when a message is received from the RPC and is about to be enqueued. This is cross-thread and will execute on the RPC thread. + /// + public event OnRpcMessageEvent OnRpcMessage; + + #region States + + /// + /// The current state of the RPC connection + /// + public RpcState State + { get { var tmp = RpcState.Disconnected; lock (l_states) tmp = _state; return tmp; } } + + private RpcState _state; + private readonly object l_states = new object(); + + /// + /// The configuration received by the Ready + /// + public Configuration Configuration + { get { Configuration tmp = null; lock (l_config) tmp = _configuration; return tmp; } } + + private Configuration _configuration = null; + private readonly object l_config = new object(); + + private volatile bool aborting = false; + private volatile bool shutdown = false; + + /// + /// Indicates if the RPC connection is still running in the background + /// + public bool IsRunning + { get { return thread != null; } } + + /// + /// Forces the to call instead, safely saying goodbye to Discord. + /// This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate. + /// + public bool ShutdownOnly { get; set; } + + #endregion States + + #region Privates + + private string applicationID; //ID of the Discord APP + private int processID; //ID of the process to track + + private long nonce; //Current command index + + private Thread thread; //The current thread + private INamedPipeClient namedPipe; + + private int targetPipe; //The pipe to taget. Leave as -1 for any available pipe. + + private readonly object l_rtqueue = new object(); //Lock for the send queue + private readonly uint _maxRtQueueSize; + private Queue _rtqueue; //The send queue + + private readonly object l_rxqueue = new object(); //Lock for the receive queue + private readonly uint _maxRxQueueSize; //The max size of the RX queue + private Queue _rxqueue; //The receive queue + + private AutoResetEvent queueUpdatedEvent = new AutoResetEvent(false); + private BackoffDelay delay; //The backoff delay before reconnecting. + + #endregion Privates + + /// + /// Creates a new instance of the RPC. + /// + /// The ID of the Discord App + /// The ID of the currently running process + /// The target pipe to connect too + /// The pipe client we shall use. + /// The maximum size of the out queue + /// The maximum size of the in queue + public RpcConnection(string applicationID, int processID, int targetPipe, INamedPipeClient client, uint maxRxQueueSize = 128, uint maxRtQueueSize = 512) + { + this.applicationID = applicationID; + this.processID = processID; + this.targetPipe = targetPipe; + this.namedPipe = client; + this.ShutdownOnly = true; + + //Assign a default logger + Logger = new ConsoleLogger(); + + delay = new BackoffDelay(500, 60 * 1000); + _maxRtQueueSize = maxRtQueueSize; + _rtqueue = new Queue((int)_maxRtQueueSize + 1); + + _maxRxQueueSize = maxRxQueueSize; + _rxqueue = new Queue((int)_maxRxQueueSize + 1); + + nonce = 0; + } + + private long GetNextNonce() + { + nonce += 1; + return nonce; + } + + #region Queues + + /// + /// Enqueues a command + /// + /// The command to enqueue + internal void EnqueueCommand(ICommand command) + { + Logger.Trace("Enqueue Command: " + command.GetType().FullName); + + //We cannot add anything else if we are aborting or shutting down. + if (aborting || shutdown) return; + + //Enqueue the set presence argument + lock (l_rtqueue) + { + //If we are too big drop the last element + if (_rtqueue.Count == _maxRtQueueSize) + { + Logger.Error("Too many enqueued commands, dropping oldest one. Maybe you are pushing new presences to fast?"); + _rtqueue.Dequeue(); + } + + //Enqueue the message + _rtqueue.Enqueue(command); + } + } + + /// + /// Adds a message to the message queue. Does not copy the message, so besure to copy it yourself or dereference it. + /// + /// The message to add + private void EnqueueMessage(IMessage message) + { + //Invoke the message + try + { + if (OnRpcMessage != null) + OnRpcMessage.Invoke(this, message); + } + catch (Exception e) + { + Logger.Error("Unhandled Exception while processing event: {0}", e.GetType().FullName); + Logger.Error(e.Message); + Logger.Error(e.StackTrace); + } + + //Small queue sizes should just ignore messages + if (_maxRxQueueSize <= 0) + { + Logger.Trace("Enqueued Message, but queue size is 0."); + return; + } + + //Large queue sizes should keep the queue in check + Logger.Trace("Enqueue Message: " + message.Type); + lock (l_rxqueue) + { + //If we are too big drop the last element + if (_rxqueue.Count == _maxRxQueueSize) + { + Logger.Warning("Too many enqueued messages, dropping oldest one."); + _rxqueue.Dequeue(); + } + + //Enqueue the message + _rxqueue.Enqueue(message); + } + } + + /// + /// Dequeues a single message from the event stack. Returns null if none are available. + /// + /// + internal IMessage DequeueMessage() + { + //Logger.Trace("Deque Message"); + lock (l_rxqueue) + { + //We have nothing, so just return null. + if (_rxqueue.Count == 0) return null; + + //Get the value and remove it from the list at the same time + return _rxqueue.Dequeue(); + } + } + + /// + /// Dequeues all messages from the event stack. + /// + /// + internal IMessage[] DequeueMessages() + { + //Logger.Trace("Deque Multiple Messages"); + lock (l_rxqueue) + { + //Copy the messages into an array + IMessage[] messages = _rxqueue.ToArray(); + + //Clear the entire queue + _rxqueue.Clear(); + + //return the array + return messages; + } + } + + #endregion Queues + + /// + /// Main thread loop + /// + private void MainLoop() + { + //initialize the pipe + Logger.Info("RPC Connection Started"); + if (Logger.Level <= LogLevel.Trace) + { + Logger.Trace("============================"); + Logger.Trace("Assembly: " + System.Reflection.Assembly.GetAssembly(typeof(RichPresence)).FullName); + Logger.Trace("Pipe: " + namedPipe.GetType().FullName); + Logger.Trace("Platform: " + Environment.OSVersion.ToString()); + Logger.Trace("applicationID: " + applicationID); + Logger.Trace("targetPipe: " + targetPipe); + Logger.Trace("POLL_RATE: " + POLL_RATE); + Logger.Trace("_maxRtQueueSize: " + _maxRtQueueSize); + Logger.Trace("_maxRxQueueSize: " + _maxRxQueueSize); + Logger.Trace("============================"); + } + + //Forever trying to connect unless the abort signal is sent + //Keep Alive Loop + while (!aborting && !shutdown) + { + try + { + //Wrap everything up in a try get + //Dispose of the pipe if we have any (could be broken) + if (namedPipe == null) + { + Logger.Error("Something bad has happened with our pipe client!"); + aborting = true; + return; + } + + //Connect to a new pipe + Logger.Trace("Connecting to the pipe through the {0}", namedPipe.GetType().FullName); + if (namedPipe.Connect(targetPipe)) + { + #region Connected + + //We connected to a pipe! Reset the delay + Logger.Trace("Connected to the pipe. Attempting to establish handshake..."); + EnqueueMessage(new ConnectionEstablishedMessage() { ConnectedPipe = namedPipe.ConnectedPipe }); + + //Attempt to establish a handshake + EstablishHandshake(); + Logger.Trace("Connection Established. Starting reading loop..."); + + //Continously iterate, waiting for the frame + //We want to only stop reading if the inside tells us (mainloop), if we are aborting (abort) or the pipe disconnects + // We dont want to exit on a shutdown, as we still have information + PipeFrame frame; + bool mainloop = true; + while (mainloop && !aborting && !shutdown && namedPipe.IsConnected) + { + #region Read Loop + + //Iterate over every frame we have queued up, processing its contents + if (namedPipe.ReadFrame(out frame)) + { + #region Read Payload + + Logger.Trace("Read Payload: {0}", frame.Opcode); + + //Do some basic processing on the frame + switch (frame.Opcode) + { + //We have been told by discord to close, so we will consider it an abort + case Opcode.Close: + + ClosePayload close = frame.GetObject(); + Logger.Warning("We have been told to terminate by discord: ({0}) {1}", close.Code, close.Reason); + EnqueueMessage(new CloseMessage() { Code = close.Code, Reason = close.Reason }); + mainloop = false; + break; + + //We have pinged, so we will flip it and respond back with pong + case Opcode.Ping: + Logger.Trace("PING"); + frame.Opcode = Opcode.Pong; + namedPipe.WriteFrame(frame); + break; + + //We have ponged? I have no idea if Discord actually sends ping/pongs. + case Opcode.Pong: + Logger.Trace("PONG"); + break; + + //A frame has been sent, we should deal with that + case Opcode.Frame: + if (shutdown) + { + //We are shutting down, so skip it + Logger.Warning("Skipping frame because we are shutting down."); + break; + } + + if (frame.Data == null) + { + //We have invalid data, thats not good. + Logger.Error("We received no data from the frame so we cannot get the event payload!"); + break; + } + + //We have a frame, so we are going to process the payload and add it to the stack + EventPayload response = null; + try { response = frame.GetObject(); } + catch (Exception e) + { + Logger.Error("Failed to parse event! " + e.Message); + Logger.Error("Data: " + frame.Message); + } + + if (response != null) ProcessFrame(response); + break; + + default: + case Opcode.Handshake: + //We have a invalid opcode, better terminate to be safe + Logger.Error("Invalid opcode: {0}", frame.Opcode); + mainloop = false; + break; + } + + #endregion Read Payload + } + + if (!aborting && namedPipe.IsConnected) + { + //Process the entire command queue we have left + ProcessCommandQueue(); + + //Wait for some time, or until a command has been queued up + queueUpdatedEvent.WaitOne(POLL_RATE); + } + + #endregion Read Loop + } + + #endregion Connected + + Logger.Trace("Left main read loop for some reason. Aborting: {0}, Shutting Down: {1}", aborting, shutdown); + } + else + { + Logger.Error("Failed to connect for some reason."); + EnqueueMessage(new ConnectionFailedMessage() { FailedPipe = targetPipe }); + } + + //If we are not aborting, we have to wait a bit before trying to connect again + if (!aborting && !shutdown) + { + //We have disconnected for some reason, either a failed pipe or a bad reading, + // so we are going to wait a bit before doing it again + long sleep = delay.NextDelay(); + + Logger.Trace("Waiting {0}ms before attempting to connect again", sleep); + Thread.Sleep(delay.NextDelay()); + } + } + //catch(InvalidPipeException e) + //{ + // Logger.Error("Invalid Pipe Exception: {0}", e.Message); + //} + catch (Exception e) + { + Logger.Error("Unhandled Exception: {0}", e.GetType().FullName); + Logger.Error(e.Message); + Logger.Error(e.StackTrace); + } + finally + { + //Disconnect from the pipe because something bad has happened. An exception has been thrown or the main read loop has terminated. + if (namedPipe.IsConnected) + { + //Terminate the pipe + Logger.Trace("Closing the named pipe."); + namedPipe.Close(); + } + + //Update our state + SetConnectionState(RpcState.Disconnected); + } + } + + //We have disconnected, so dispose of the thread and the pipe. + Logger.Trace("Left Main Loop"); + if (namedPipe != null) + namedPipe.Dispose(); + + Logger.Info("Thread Terminated, no longer performing RPC connection."); + } + + #region Reading + + /// Handles the response from the pipe and calls appropriate events and changes states. + /// The response received by the server. + private void ProcessFrame(EventPayload response) + { + Logger.Info("Handling Response. Cmd: {0}, Event: {1}", response.Command, response.Event); + + //Check if it is an error + if (response.Event.HasValue && response.Event.Value == ServerEvent.Error) + { + //We have an error + Logger.Error("Error received from the RPC"); + + //Create the event objetc and push it to the queue + ErrorMessage err = response.GetObject(); + Logger.Error("Server responded with an error message: ({0}) {1}", err.Code.ToString(), err.Message); + + //Enqueue the messsage and then end + EnqueueMessage(err); + return; + } + + //Check if its a handshake + if (State == RpcState.Connecting) + { + if (response.Command == Command.Dispatch && response.Event.HasValue && response.Event.Value == ServerEvent.Ready) + { + Logger.Info("Connection established with the RPC"); + SetConnectionState(RpcState.Connected); + delay.Reset(); + + //Prepare the object + ReadyMessage ready = response.GetObject(); + lock (l_config) + { + _configuration = ready.Configuration; + ready.User.SetConfiguration(_configuration); + } + + //Enqueue the message + EnqueueMessage(ready); + return; + } + } + + if (State == RpcState.Connected) + { + switch (response.Command) + { + //We were sent a dispatch, better process it + case Command.Dispatch: + ProcessDispatch(response); + break; + + //We were sent a Activity Update, better enqueue it + case Command.SetActivity: + if (response.Data == null) + { + EnqueueMessage(new PresenceMessage()); + } + else + { + RichPresenceResponse rp = response.GetObject(); + EnqueueMessage(new PresenceMessage(rp)); + } + break; + + case Command.Unsubscribe: + case Command.Subscribe: + + //Prepare a serializer that can account for snake_case enums. + JsonSerializer serializer = new JsonSerializer(); + serializer.Converters.Add(new Converters.EnumSnakeCaseConverter()); + + //Go through the data, looking for the evt property, casting it to a server event + var evt = response.GetObject().Event.Value; + + //Enqueue the appropriate message. + if (response.Command == Command.Subscribe) + EnqueueMessage(new SubscribeMessage(evt)); + else + EnqueueMessage(new UnsubscribeMessage(evt)); + + break; + + case Command.SendActivityJoinInvite: + Logger.Trace("Got invite response ack."); + break; + + case Command.CloseActivityJoinRequest: + Logger.Trace("Got invite response reject ack."); + break; + + //we have no idea what we were sent + default: + Logger.Error("Unkown frame was received! {0}", response.Command); + return; + } + return; + } + + Logger.Trace("Received a frame while we are disconnected. Ignoring. Cmd: {0}, Event: {1}", response.Command, response.Event); + } + + private void ProcessDispatch(EventPayload response) + { + if (response.Command != Command.Dispatch) return; + if (!response.Event.HasValue) return; + + switch (response.Event.Value) + { + //We are to join the server + case ServerEvent.ActivitySpectate: + var spectate = response.GetObject(); + EnqueueMessage(spectate); + break; + + case ServerEvent.ActivityJoin: + var join = response.GetObject(); + EnqueueMessage(join); + break; + + case ServerEvent.ActivityJoinRequest: + var request = response.GetObject(); + EnqueueMessage(request); + break; + + //Unkown dispatch event received. We should just ignore it. + default: + Logger.Warning("Ignoring {0}", response.Event.Value); + break; + } + } + + #endregion Reading + + #region Writting + + private void ProcessCommandQueue() + { + //Logger.Info("Checking command queue"); + + //We are not ready yet, dont even try + if (State != RpcState.Connected) + return; + + //We are aborting, so we will just log a warning so we know this is probably only going to send the CLOSE + if (aborting) + Logger.Warning("We have been told to write a queue but we have also been aborted."); + + //Prepare some variabels we will clone into with locks + bool needsWriting = true; + ICommand item = null; + + //Continue looping until we dont need anymore messages + while (needsWriting && namedPipe.IsConnected) + { + lock (l_rtqueue) + { + //Pull the value and update our writing needs + // If we have nothing to write, exit the loop + needsWriting = _rtqueue.Count > 0; + if (!needsWriting) break; + + //Peek at the item + item = _rtqueue.Peek(); + } + + //BReak out of the loop as soon as we send this item + if (shutdown || (!aborting && LOCK_STEP)) + needsWriting = false; + + //Prepare the payload + IPayload payload = item.PreparePayload(GetNextNonce()); + Logger.Trace("Attempting to send payload: " + payload.Command); + + //Prepare the frame + PipeFrame frame = new PipeFrame(); + if (item is CloseCommand) + { + //We have been sent a close frame. We better just send a handwave + //Send it off to the server + SendHandwave(); + + //Queue the item + Logger.Trace("Handwave sent, ending queue processing."); + lock (l_rtqueue) _rtqueue.Dequeue(); + + //Stop sending any more messages + return; + } + else + { + if (aborting) + { + //We are aborting, so just dequeue the message and dont bother sending it + Logger.Warning("- skipping frame because of abort."); + lock (l_rtqueue) _rtqueue.Dequeue(); + } + else + { + //Prepare the frame + frame.SetObject(Opcode.Frame, payload); + + //Write it and if it wrote perfectly fine, we will dequeue it + Logger.Trace("Sending payload: " + payload.Command); + if (namedPipe.WriteFrame(frame)) + { + //We sent it, so now dequeue it + Logger.Trace("Sent Successfully."); + lock (l_rtqueue) _rtqueue.Dequeue(); + } + else + { + //Something went wrong, so just giveup and wait for the next time around. + Logger.Warning("Something went wrong during writing!"); + return; + } + } + } + } + } + + #endregion Writting + + #region Connection + + /// + /// Establishes the handshake with the server. + /// + /// + private void EstablishHandshake() + { + Logger.Trace("Attempting to establish a handshake..."); + + //We are establishing a lock and not releasing it until we sent the handshake message. + // We need to set the key, and it would not be nice if someone did things between us setting the key. + + //Check its state + if (State != RpcState.Disconnected) + { + Logger.Error("State must be disconnected in order to start a handshake!"); + return; + } + + //Send it off to the server + Logger.Trace("Sending Handshake..."); + if (!namedPipe.WriteFrame(new PipeFrame(Opcode.Handshake, new Handshake() { Version = VERSION, ClientID = applicationID }))) + { + Logger.Error("Failed to write a handshake."); + return; + } + + //This has to be done outside the lock + SetConnectionState(RpcState.Connecting); + } + + /// + /// Establishes a fairwell with the server by sending a handwave. + /// + private void SendHandwave() + { + Logger.Info("Attempting to wave goodbye..."); + + //Check its state + if (State == RpcState.Disconnected) + { + Logger.Error("State must NOT be disconnected in order to send a handwave!"); + return; + } + + //Send the handwave + if (!namedPipe.WriteFrame(new PipeFrame(Opcode.Close, new Handshake() { Version = VERSION, ClientID = applicationID }))) + { + Logger.Error("failed to write a handwave."); + return; + } + } + + /// + /// Attempts to connect to the pipe. Returns true on success + /// + /// + public bool AttemptConnection() + { + Logger.Info("Attempting a new connection"); + + //The thread mustn't exist already + if (thread != null) + { + Logger.Error("Cannot attempt a new connection as the previous connection thread is not null!"); + return false; + } + + //We have to be in the disconnected state + if (State != RpcState.Disconnected) + { + Logger.Warning("Cannot attempt a new connection as the previous connection hasn't changed state yet."); + return false; + } + + if (aborting) + { + Logger.Error("Cannot attempt a new connection while aborting!"); + return false; + } + + //Start the thread up + thread = new Thread(MainLoop); + thread.Name = "Discord IPC Thread"; + thread.IsBackground = true; + thread.Start(); + + return true; + } + + /// + /// Sets the current state of the pipe, locking the l_states object for thread saftey. + /// + /// The state to set it too. + private void SetConnectionState(RpcState state) + { + Logger.Trace("Setting the connection state to {0}", state.ToString().ToSnakeCase().ToUpperInvariant()); + lock (l_states) + { + _state = state; + } + } + + /// + /// Closes the connection and disposes of resources. This will not force termination, but instead allow Discord disconnect us after we say goodbye. + /// This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate. + /// + public void Shutdown() + { + //Enable the flag + Logger.Trace("Initiated shutdown procedure"); + shutdown = true; + + //Clear the commands and enqueue the close + lock (l_rtqueue) + { + _rtqueue.Clear(); + if (CLEAR_ON_SHUTDOWN) _rtqueue.Enqueue(new PresenceCommand() { PID = processID, Presence = null }); + _rtqueue.Enqueue(new CloseCommand()); + } + + //Trigger the event + queueUpdatedEvent.Set(); + } + + /// + /// Closes the connection and disposes of resources. + /// + public void Close() + { + if (thread == null) + { + Logger.Error("Cannot close as it is not available!"); + return; + } + + if (aborting) + { + Logger.Error("Cannot abort as it has already been aborted"); + return; + } + + //Set the abort state + if (ShutdownOnly) + { + Shutdown(); + return; + } + + //Terminate + Logger.Trace("Updating Abort State..."); + aborting = true; + queueUpdatedEvent.Set(); + } + + /// + /// Closes the connection and disposes resources. Identical to but ignores the "ShutdownOnly" value. + /// + public void Dispose() + { + ShutdownOnly = false; + Close(); + } + + #endregion Connection + } + + /// + /// State of the RPC connection + /// + internal enum RpcState + { + /// + /// Disconnected from the discord client + /// + Disconnected, + + /// + /// Connecting to the discord client. The handshake has been sent and we are awaiting the ready event + /// + Connecting, + + /// + /// We are connect to the client and can send and receive messages. + /// + Connected + } +} \ No newline at end of file diff --git a/DiscordAPI/Registry/IUriSchemeCreator.cs b/SDR-RPC/DiscordAPI/Registry/IUriSchemeCreator.cs similarity index 87% rename from DiscordAPI/Registry/IUriSchemeCreator.cs rename to SDR-RPC/DiscordAPI/Registry/IUriSchemeCreator.cs index cf5a117..1b2be39 100644 --- a/DiscordAPI/Registry/IUriSchemeCreator.cs +++ b/SDR-RPC/DiscordAPI/Registry/IUriSchemeCreator.cs @@ -1,6 +1,4 @@ -using DiscordRPC.Logging; - -namespace DiscordRPC.Registry +namespace DiscordRPC.Registry { internal interface IUriSchemeCreator { @@ -11,4 +9,4 @@ namespace DiscordRPC.Registry /// The register context. bool RegisterUriScheme(UriSchemeRegister register); } -} +} \ No newline at end of file diff --git a/DiscordAPI/Registry/MacUriSchemeCreator.cs b/SDR-RPC/DiscordAPI/Registry/MacUriSchemeCreator.cs similarity index 92% rename from DiscordAPI/Registry/MacUriSchemeCreator.cs rename to SDR-RPC/DiscordAPI/Registry/MacUriSchemeCreator.cs index b4d939b..5819bb1 100644 --- a/DiscordAPI/Registry/MacUriSchemeCreator.cs +++ b/SDR-RPC/DiscordAPI/Registry/MacUriSchemeCreator.cs @@ -1,16 +1,12 @@ using DiscordRPC.Logging; -using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; namespace DiscordRPC.Registry { internal class MacUriSchemeCreator : IUriSchemeCreator { private ILogger logger; + public MacUriSchemeCreator(ILogger logger) { this.logger = logger; @@ -27,7 +23,7 @@ namespace DiscordRPC.Registry logger.Error("Failed to register because the application could not be located."); return false; } - + logger.Trace("Registering Steam Command"); //Prepare the command @@ -49,6 +45,5 @@ namespace DiscordRPC.Registry logger.Trace("Registered {0}, {1}", filepath + "/" + register.ApplicationID + ".json", command); return true; } - } -} +} \ No newline at end of file diff --git a/DiscordAPI/Registry/UnixUriSchemeCreator.cs b/SDR-RPC/DiscordAPI/Registry/UnixUriSchemeCreator.cs similarity index 95% rename from DiscordAPI/Registry/UnixUriSchemeCreator.cs rename to SDR-RPC/DiscordAPI/Registry/UnixUriSchemeCreator.cs index 25c1351..8e87266 100644 --- a/DiscordAPI/Registry/UnixUriSchemeCreator.cs +++ b/SDR-RPC/DiscordAPI/Registry/UnixUriSchemeCreator.cs @@ -1,16 +1,14 @@ using DiscordRPC.Logging; using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; namespace DiscordRPC.Registry { internal class UnixUriSchemeCreator : IUriSchemeCreator { private ILogger logger; + public UnixUriSchemeCreator(ILogger logger) { this.logger = logger; @@ -45,9 +43,8 @@ namespace DiscordRPC.Registry command = exe; } - //Prepare the file - string desktopFileFormat = + string desktopFileFormat = @"[Desktop Entry] Name=Game {0} Exec={1} %u @@ -55,7 +52,7 @@ Type=Application NoDisplay=true Categories=Discord;Games; MimeType=x-scheme-handler/discord-{2}"; - + string file = string.Format(desktopFileFormat, register.ApplicationID, command, register.ApplicationID); //Prepare the path @@ -91,9 +88,9 @@ MimeType=x-scheme-handler/discord-{2}"; //Run the process and wait for response Process process = Process.Start("xdg-mime", arguments); process.WaitForExit(); - + //Return if succesful return process.ExitCode >= 0; } } -} +} \ No newline at end of file diff --git a/DiscordAPI/Registry/UriScheme.cs b/SDR-RPC/DiscordAPI/Registry/UriScheme.cs similarity index 94% rename from DiscordAPI/Registry/UriScheme.cs rename to SDR-RPC/DiscordAPI/Registry/UriScheme.cs index 2113d96..b31d77c 100644 --- a/DiscordAPI/Registry/UriScheme.cs +++ b/SDR-RPC/DiscordAPI/Registry/UriScheme.cs @@ -5,7 +5,7 @@ using System.Diagnostics; namespace DiscordRPC.Registry { internal class UriSchemeRegister - { + { /// /// The ID of the Discord App to register /// @@ -19,7 +19,8 @@ namespace DiscordRPC.Registry /// /// Is this register using steam? /// - public bool UsingSteamApp { get { return !string.IsNullOrEmpty(SteamAppID) && SteamAppID != ""; } } + public bool UsingSteamApp + { get { return !string.IsNullOrEmpty(SteamAppID) && SteamAppID != ""; } } /// /// The full executable path of the application. @@ -27,6 +28,7 @@ namespace DiscordRPC.Registry public string ExecutablePath { get; set; } private ILogger _logger; + public UriSchemeRegister(ILogger logger, string applicationID, string steamAppID = null, string executable = null) { _logger = logger; @@ -42,7 +44,7 @@ namespace DiscordRPC.Registry { //Get the creator IUriSchemeCreator creator = null; - switch(Environment.OSVersion.Platform) + switch (Environment.OSVersion.Platform) { case PlatformID.Win32Windows: case PlatformID.Win32S: @@ -56,7 +58,7 @@ namespace DiscordRPC.Registry _logger.Trace("Creating Unix Scheme Creator"); creator = new UnixUriSchemeCreator(_logger); break; - + case PlatformID.MacOSX: _logger.Trace("Creating MacOSX Scheme Creator"); creator = new MacUriSchemeCreator(_logger); @@ -86,4 +88,4 @@ namespace DiscordRPC.Registry return Process.GetCurrentProcess().MainModule.FileName; } } -} +} \ No newline at end of file diff --git a/DiscordAPI/Registry/WindowsUriSchemeCreator.cs b/SDR-RPC/DiscordAPI/Registry/WindowsUriSchemeCreator.cs similarity index 99% rename from DiscordAPI/Registry/WindowsUriSchemeCreator.cs rename to SDR-RPC/DiscordAPI/Registry/WindowsUriSchemeCreator.cs index cef4312..a41dcd7 100644 --- a/DiscordAPI/Registry/WindowsUriSchemeCreator.cs +++ b/SDR-RPC/DiscordAPI/Registry/WindowsUriSchemeCreator.cs @@ -6,6 +6,7 @@ namespace DiscordRPC.Registry internal class WindowsUriSchemeCreator : IUriSchemeCreator { private ILogger logger; + public WindowsUriSchemeCreator(ILogger logger) { this.logger = logger; @@ -39,7 +40,6 @@ namespace DiscordRPC.Registry string steam = GetSteamLocation(); if (steam != null) command = string.Format("\"{0}\" steam://rungameid/{1}", steam, register.SteamAppID); - } //Okay, now actually register it @@ -84,4 +84,4 @@ namespace DiscordRPC.Registry } } } -} +} \ No newline at end of file diff --git a/DiscordAPI/RichPresence.cs b/SDR-RPC/DiscordAPI/RichPresence.cs similarity index 97% rename from DiscordAPI/RichPresence.cs rename to SDR-RPC/DiscordAPI/RichPresence.cs index f1d3c99..c3c7cd8 100644 --- a/DiscordAPI/RichPresence.cs +++ b/SDR-RPC/DiscordAPI/RichPresence.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; -using System; +using DiscordRPC.Exceptions; using DiscordRPC.Helper; +using Newtonsoft.Json; +using System; using System.Text; -using DiscordRPC.Exceptions; namespace DiscordRPC { @@ -27,6 +27,7 @@ namespace DiscordRPC throw new StringOutOfRangeException("State", 0, 128); } } + private string _state; /// @@ -43,6 +44,7 @@ namespace DiscordRPC throw new StringOutOfRangeException(128); } } + private string _details; /// @@ -184,6 +186,7 @@ namespace DiscordRPC } #region Has Checks + /// /// Does the Rich Presence have valid timestamps? /// @@ -219,9 +222,11 @@ namespace DiscordRPC { return Secrets != null && (Secrets.JoinSecret != null || Secrets.SpectateSecret != null); } - #endregion + + #endregion Has Checks #region Builder + /// /// Sets the state of the Rich Presence. See also . /// @@ -287,7 +292,8 @@ namespace DiscordRPC Secrets = secrets; return this; } - #endregion + + #endregion Builder /// /// Attempts to call on the string and return the result, if its within a valid length. @@ -409,7 +415,7 @@ namespace DiscordRPC public class Secrets { /// - /// The unique match code to distinguish different games/lobbies. Use to get an appropriately sized secret. + /// The unique match code to distinguish different games/lobbies. Use to get an appropriately sized secret. /// This cannot be null and must be supplied for the Join / Spectate feature to work. /// Max Length of 128 Bytes /// @@ -424,11 +430,12 @@ namespace DiscordRPC throw new StringOutOfRangeException(128); } } + private string _matchSecret; /// /// The secret data that will tell the client how to connect to the game to play. This could be a unique identifier for a fancy match maker or player id, lobby id, etc. - /// It is recommended to encrypt this information so its hard for people to replicate it. + /// It is recommended to encrypt this information so its hard for people to replicate it. /// Do NOT just use the IP address in this. That is a bad practice and can leave your players vulnerable! /// /// Max Length of 128 Bytes @@ -443,11 +450,12 @@ namespace DiscordRPC throw new StringOutOfRangeException(128); } } + private string _joinSecret; /// /// The secret data that will tell the client how to connect to the game to spectate. This could be a unique identifier for a fancy match maker or player id, lobby id, etc. - /// It is recommended to encrypt this information so its hard for people to replicate it. + /// It is recommended to encrypt this information so its hard for people to replicate it. /// Do NOT just use the IP address in this. That is a bad practice and can leave your players vulnerable! /// /// Max Length of 128 Bytes @@ -462,20 +470,22 @@ namespace DiscordRPC throw new StringOutOfRangeException(128); } } - private string _spectateSecret; + private string _spectateSecret; #region Statics /// /// The encoding the secret generator is using /// - public static Encoding Encoding { get { return Encoding.UTF8; } } + public static Encoding Encoding + { get { return Encoding.UTF8; } } /// /// The length of a secret in bytes. /// - public static int SecretLength { get { return 128; } } + public static int SecretLength + { get { return 128; } } /// /// Creates a new secret. This is NOT a cryptographic function and should NOT be used for sensitive information. This is mainly provided as a way to generate quick IDs. @@ -493,7 +503,6 @@ namespace DiscordRPC return Encoding.GetString(bytes); } - /// /// Creates a secret word using more readable friendly characters. Useful for debugging purposes. This is not a cryptographic function and should NOT be used for sensitive information. /// @@ -509,7 +518,8 @@ namespace DiscordRPC return secret; } - #endregion + + #endregion Statics } /// @@ -535,6 +545,7 @@ namespace DiscordRPC _largeimageID = null; } } + private string _largeimagekey; /// @@ -551,8 +562,8 @@ namespace DiscordRPC throw new StringOutOfRangeException(128); } } - private string _largeimagetext; + private string _largeimagetext; /// /// Name of the uploaded image for the small profile artwork. @@ -571,6 +582,7 @@ namespace DiscordRPC _smallimageID = null; } } + private string _smallimagekey; /// @@ -587,20 +599,25 @@ namespace DiscordRPC throw new StringOutOfRangeException(128); } } + private string _smallimagetext; /// /// The ID of the large image. This is only set after Update Presence and will automatically become null when is changed. /// [JsonIgnore] - public ulong? LargeImageID { get { return _largeimageID; } } + public ulong? LargeImageID + { get { return _largeimageID; } } + private ulong? _largeimageID; /// /// The ID of the small image. This is only set after Update Presence and will automatically become null when is changed. /// [JsonIgnore] - public ulong? SmallImageID { get { return _smallimageID; } } + public ulong? SmallImageID + { get { return _smallimageID; } } + private ulong? _smallimageID; /// @@ -646,14 +663,16 @@ namespace DiscordRPC public class Timestamps { /// A new timestamp that starts from the current time. - public static Timestamps Now { get { return new Timestamps(DateTime.UtcNow, end: null); } } + public static Timestamps Now + { get { return new Timestamps(DateTime.UtcNow, end: null); } } /// /// Creates a new timestamp starting at the current time and ending in the supplied timespan /// /// How long the Timestamp will last for in seconds. /// Returns a new timestamp with given duration. - public static Timestamps FromTimeSpan(double seconds) { return FromTimeSpan(TimeSpan.FromSeconds(seconds)); } + public static Timestamps FromTimeSpan(double seconds) + { return FromTimeSpan(TimeSpan.FromSeconds(seconds)); } /// /// Creates a new timestamp starting at current time and ending in the supplied timespan @@ -718,7 +737,6 @@ namespace DiscordRPC } } - /// /// Converts between DateTime and Milliseconds to give the Unix Epoch Time for the . /// @@ -758,7 +776,6 @@ namespace DiscordRPC var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); return Convert.ToUInt64((date - epoch).TotalMilliseconds); } - } /// @@ -768,11 +785,13 @@ namespace DiscordRPC public class Party { /// - /// A unique ID for the player's current party / lobby / group. If this is not supplied, they player will not be in a party and the rest of the information will not be sent. + /// A unique ID for the player's current party / lobby / group. If this is not supplied, they player will not be in a party and the rest of the information will not be sent. /// Max 128 Bytes /// [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public string ID { get { return _partyid; } set { _partyid = value.GetNullOrString(); } } + public string ID + { get { return _partyid; } set { _partyid = value.GetNullOrString(); } } + private string _partyid; /// @@ -810,11 +829,9 @@ namespace DiscordRPC Max = value[1]; } } - } } - /// /// A rich presence that has been parsed from the pipe as a response. /// @@ -831,6 +848,5 @@ namespace DiscordRPC /// [JsonProperty("name")] public string Name { get; private set; } - } -} +} \ No newline at end of file diff --git a/SDR-RPC/DiscordAPI/User.cs b/SDR-RPC/DiscordAPI/User.cs new file mode 100644 index 0000000..dfa1100 --- /dev/null +++ b/SDR-RPC/DiscordAPI/User.cs @@ -0,0 +1,233 @@ +using Newtonsoft.Json; +using System; + +namespace DiscordRPC +{ + /// + /// Object representing a Discord user. This is used for join requests. + /// + public class User + { + /// + /// Possible formats for avatars + /// + public enum AvatarFormat + { + /// + /// Portable Network Graphics format (.png) + /// Losses format that supports transparent avatars. Most recommended for stationary formats with wide support from many libraries. + /// + PNG, + + /// + /// Joint Photographic Experts Group format (.jpeg) + /// The format most cameras use. Lossy and does not support transparent avatars. + /// + JPEG, + + /// + /// WebP format (.webp) + /// Picture only version of WebM. Pronounced "weeb p". + /// + WebP, + + /// + /// Graphics Interchange Format (.gif) + /// Animated avatars that Discord Nitro users are able to use. If the user doesn't have an animated avatar, then it will just be a single frame gif. + /// + GIF //Gif, as in gift. + } + + /// + /// Possible square sizes of avatars. + /// + public enum AvatarSize + { + /// 16 x 16 pixels. + x16 = 16, + + /// 32 x 32 pixels. + x32 = 32, + + /// 64 x 64 pixels. + x64 = 64, + + /// 128 x 128 pixels. + x128 = 128, + + /// 256 x 256 pixels. + x256 = 256, + + /// 512 x 512 pixels. + x512 = 512, + + /// 1024 x 1024 pixels. + x1024 = 1024, + + /// 2048 x 2048 pixels. + x2048 = 2048 + } + + /// + /// The snowflake ID of the user. + /// + [JsonProperty("id")] + public ulong ID { get; private set; } + + /// + /// The username of the player. + /// + [JsonProperty("username")] + public string Username { get; private set; } + + /// + /// The discriminator of the user. + /// + [JsonProperty("discriminator")] + public int Discriminator { get; private set; } + + /// + /// The avatar hash of the user. Too get a URL for the avatar, use the . This can be null if the user has no avatar. The will account for this and return the discord default. + /// + [JsonProperty("avatar")] + public string Avatar { get; private set; } + + /// + /// The flags on a users account, often represented as a badge. + /// + [JsonProperty("flags")] + public Flag Flags { get; private set; } + + /// + /// A flag on the user account + /// + [Flags] + public enum Flag + { + /// No flag + None = 0, + + /// Staff of Discord. + Employee = 1 << 0, + + /// Partners of Discord. + Partner = 1 << 1, + + /// Original HypeSquad which organise events. + HypeSquad = 1 << 2, + + /// Bug Hunters that found and reported bugs in Discord. + BugHunter = 1 << 3, + + //These 2 are mistery types + //A = 1 << 4, + //B = 1 << 5, + + /// The HypeSquad House of Bravery. + HouseBravery = 1 << 6, + + /// The HypeSquad House of Brilliance. + HouseBrilliance = 1 << 7, + + /// The HypeSquad House of Balance (the best one). + HouseBalance = 1 << 8, + + /// Early Supporter of Discord and had Nitro before the store was released. + EarlySupporter = 1 << 9, + + /// Apart of a team. + /// Unclear if it is reserved for members that share a team with the current application. + /// + TeamUser = 1 << 10 + } + + /// + /// The premium type of the user. + /// + [JsonProperty("premium_type")] + public PremiumType Premium { get; private set; } + + /// + /// Type of premium + /// + public enum PremiumType + { + /// No subscription to any forms of Nitro. + None = 0, + + /// Nitro Classic subscription. Has chat perks and animated avatars. + NitroClassic = 1, + + /// Nitro subscription. Has chat perks, animated avatars, server boosting, and access to free Nitro Games. + Nitro = 2 + } + + /// + /// The endpoint for the CDN. Normally cdn.discordapp.com. + /// + public string CdnEndpoint { get; private set; } + + /// + /// Creates a new User instance. + /// + internal User() + { + CdnEndpoint = "cdn.discordapp.com"; + } + + /// + /// Updates the URL paths to the appropriate configuration + /// + /// The configuration received by the OnReady event. + internal void SetConfiguration(Configuration configuration) + { + this.CdnEndpoint = configuration.CdnHost; + } + + /// + /// Gets a URL that can be used to download the user's avatar. If the user has not yet set their avatar, it will return the default one that discord is using. The default avatar only supports the format. + /// + /// The format of the target avatar + /// The optional size of the avatar you wish for. Defaults to x128. + /// + public string GetAvatarURL(AvatarFormat format, AvatarSize size = AvatarSize.x128) + { + //Prepare the endpoint + string endpoint = "/avatars/" + ID + "/" + Avatar; + + //The user has no avatar, so we better replace it with the default + if (string.IsNullOrEmpty(Avatar)) + { + //Make sure we are only using PNG + if (format != AvatarFormat.PNG) + throw new BadImageFormatException("The user has no avatar and the requested format " + format.ToString() + " is not supported. (Only supports PNG)."); + + //Get the endpoint + int descrim = Discriminator % 5; + endpoint = "/embed/avatars/" + descrim; + } + + //Finish of the endpoint + return string.Format("https://{0}{1}{2}?size={3}", this.CdnEndpoint, endpoint, GetAvatarExtension(format), (int)size); + } + + /// + /// Returns the file extension of the specified format. + /// + /// The format to get the extention off + /// Returns a period prefixed file extension. + public string GetAvatarExtension(AvatarFormat format) + { + return "." + format.ToString().ToLowerInvariant(); + } + + /// + /// Formats the user into username#discriminator + /// + /// + public override string ToString() + { + return Username + "#" + Discriminator.ToString("D4"); + } + } +} \ No newline at end of file diff --git a/DiscordAPI/Web/WebRPC.cs b/SDR-RPC/DiscordAPI/Web/WebRPC.cs similarity index 100% rename from DiscordAPI/Web/WebRPC.cs rename to SDR-RPC/DiscordAPI/Web/WebRPC.cs diff --git a/SDR-RPC/HelpForm.Designer.cs b/SDR-RPC/HelpForm.Designer.cs new file mode 100644 index 0000000..b81ebec --- /dev/null +++ b/SDR-RPC/HelpForm.Designer.cs @@ -0,0 +1,128 @@ +namespace EnderIce2.SDRSharpPlugin +{ + partial class HelpForm +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + groupBox1 = new System.Windows.Forms.GroupBox(); + tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + groupBox4 = new System.Windows.Forms.GroupBox(); + groupBox3 = new System.Windows.Forms.GroupBox(); + groupBox2 = new System.Windows.Forms.GroupBox(); + tableLayoutPanel1.SuspendLayout(); + SuspendLayout(); + // + // groupBox1 + // + groupBox1.Dock = System.Windows.Forms.DockStyle.Fill; + groupBox1.ForeColor = System.Drawing.Color.White; + groupBox1.Location = new System.Drawing.Point(3, 3); + groupBox1.Name = "groupBox1"; + groupBox1.Size = new System.Drawing.Size(236, 74); + groupBox1.TabIndex = 0; + groupBox1.TabStop = false; + groupBox1.Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + // + // tableLayoutPanel1 + // + tableLayoutPanel1.AutoScroll = true; + tableLayoutPanel1.ColumnCount = 2; + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + tableLayoutPanel1.Controls.Add(groupBox4, 1, 1); + tableLayoutPanel1.Controls.Add(groupBox3, 0, 1); + tableLayoutPanel1.Controls.Add(groupBox2, 1, 0); + tableLayoutPanel1.Controls.Add(groupBox1, 0, 0); + tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; + tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 2; + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); + tableLayoutPanel1.Size = new System.Drawing.Size(484, 161); + tableLayoutPanel1.TabIndex = 1; + // + // groupBox4 + // + groupBox4.Dock = System.Windows.Forms.DockStyle.Fill; + groupBox4.ForeColor = System.Drawing.Color.White; + groupBox4.Location = new System.Drawing.Point(245, 83); + groupBox4.Name = "groupBox4"; + groupBox4.Size = new System.Drawing.Size(236, 75); + groupBox4.TabIndex = 3; + groupBox4.TabStop = false; + groupBox4.Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + // + // groupBox3 + // + groupBox3.Dock = System.Windows.Forms.DockStyle.Fill; + groupBox3.ForeColor = System.Drawing.Color.White; + groupBox3.Location = new System.Drawing.Point(3, 83); + groupBox3.Name = "groupBox3"; + groupBox3.Size = new System.Drawing.Size(236, 75); + groupBox3.TabIndex = 2; + groupBox3.TabStop = false; + groupBox3.Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + // + // groupBox2 + // + groupBox2.Dock = System.Windows.Forms.DockStyle.Fill; + groupBox2.ForeColor = System.Drawing.Color.White; + groupBox2.Location = new System.Drawing.Point(245, 3); + groupBox2.Name = "groupBox2"; + groupBox2.Size = new System.Drawing.Size(236, 74); + groupBox2.TabIndex = 1; + groupBox2.TabStop = false; + groupBox2.Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + // + // HelpForm + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + AutoScroll = true; + AutoScrollMinSize = new System.Drawing.Size(480, 115); + BackColor = System.Drawing.Color.FromArgb(28, 28, 28); + ClientSize = new System.Drawing.Size(484, 161); + Controls.Add(tableLayoutPanel1); + ForeColor = System.Drawing.Color.White; + HelpButton = true; + Name = "HelpForm"; + StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + Text = "Help"; + tableLayoutPanel1.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.GroupBox groupBox4; + private System.Windows.Forms.GroupBox groupBox3; + private System.Windows.Forms.GroupBox groupBox2; + } +} \ No newline at end of file diff --git a/SDR-RPC/HelpForm.cs b/SDR-RPC/HelpForm.cs new file mode 100644 index 0000000..243be8f --- /dev/null +++ b/SDR-RPC/HelpForm.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace EnderIce2.SDRSharpPlugin +{ + public partial class HelpForm : Form + { + public HelpForm() + { + InitializeComponent(); + } + } +} diff --git a/Properties/Resources.resx b/SDR-RPC/HelpForm.resx similarity index 87% rename from Properties/Resources.resx rename to SDR-RPC/HelpForm.resx index 4a91add..af32865 100644 --- a/Properties/Resources.resx +++ b/SDR-RPC/HelpForm.resx @@ -1,17 +1,17 @@  - @@ -117,8 +117,4 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\Resources\gear.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - \ No newline at end of file diff --git a/LogWriter.cs b/SDR-RPC/LogWriter.cs similarity index 92% rename from LogWriter.cs rename to SDR-RPC/LogWriter.cs index e516f91..606d87b 100644 --- a/LogWriter.cs +++ b/SDR-RPC/LogWriter.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; namespace EnderIce2.SDRSharpPlugin @@ -7,6 +8,7 @@ namespace EnderIce2.SDRSharpPlugin { public static void WriteToFile(string Message) { + Debug.WriteLine(Message); if (SDRSharp.Radio.Utils.GetBooleanSetting("LogRPC", false)) { string path = AppDomain.CurrentDomain.BaseDirectory + "\\RPCLogs\\"; diff --git a/MainPlugin.cs b/SDR-RPC/MainPlugin.cs similarity index 80% rename from MainPlugin.cs rename to SDR-RPC/MainPlugin.cs index 72ad3ff..9d509e0 100644 --- a/MainPlugin.cs +++ b/SDR-RPC/MainPlugin.cs @@ -1,30 +1,57 @@ -using DiscordRPC; -using DiscordRPC.Logging; -using DiscordRPC.Message; +using System.Windows.Forms; using SDRSharp.Common; using SDRSharp.Radio; -using System; -using System.Linq; using System.Reflection; +using System.Linq; +using System.Reflection.Metadata; using System.Threading.Tasks; -using System.Windows.Forms; +using System; +using DiscordRPC.Logging; +using DiscordRPC; +using DiscordRPC.Message; namespace EnderIce2.SDRSharpPlugin { - public class MainPlugin : ISharpPlugin + public class MainPlugin : ISharpPlugin, ICanLazyLoadGui, ISupportStatus, IExtendedNameProvider { - private SettingsPanel _controlPanel; + private ControlPanel _gui = null; + private ISharpControl _control; + + public string DisplayName => "Discord RPC"; + public string Category => "Misc"; + public string MenuItemName => DisplayName; + public bool IsActive => _gui != null && _gui.Visible; + private const LogLevel logLevel = LogLevel.Trace; private const int discordPipe = 0; - - private ISharpControl _control; private bool playedBefore; - private DiscordRpcClient client; private bool isConnected; - public bool HasGui => true; - public string DisplayName => "Discord RPC"; - public UserControl Gui => _controlPanel; + + public UserControl Gui + { + get + { + LoadGui(); + return _gui; + } + } + + public void LoadGui() + { + if (_gui == null) + { + _gui = new ControlPanel(_control); + } + } + + public void Initialize(ISharpControl control) + { + _control = control; + init(); + } + + public void Close() { } private readonly RichPresence presence = new RichPresence { @@ -35,14 +62,12 @@ namespace EnderIce2.SDRSharpPlugin LargeImageKey = "image_large", LargeImageText = "SDRSharp", SmallImageKey = "image_small", - SmallImageText = $"SDR-RPC Plugin v{Assembly.LoadFrom("SDR-RPC.dll").GetName().Version}" + SmallImageText = $"SDR-RPC Plugin v{Assembly.GetExecutingAssembly().GetName().Version.ToString()}" } }; - public void Initialize(ISharpControl control) + private void init() { - _controlPanel = new SettingsPanel(); - _control = control; if (Utils.GetBooleanSetting("EnableRPC", true)) { if (Utils.GetStringSetting("ClientID").All(char.IsWhiteSpace)) @@ -59,7 +84,8 @@ namespace EnderIce2.SDRSharpPlugin } catch (Exception ex) { - _controlPanel.ChangeStatus = $"RPC Error: {ex.Message}"; + if (_gui != null) + _gui.ChangeStatus = $"RPC Error: {ex.Message}"; LogWriter.WriteToFile("Error in DiscordRpcClient\n" + ex.ToString()); return; } @@ -83,7 +109,8 @@ namespace EnderIce2.SDRSharpPlugin } else { - _controlPanel.ChangeStatus = "RPC is disabled"; + if (_gui != null) + _gui.ChangeStatus = "RPC is disabled"; } LogWriter.WriteToFile("EOM Initialize"); } @@ -155,17 +182,12 @@ namespace EnderIce2.SDRSharpPlugin LogWriter.WriteToFile($"ObjectDisposedException exception for SetPresence\n{ex}"); } LogWriter.WriteToFile("SetPresence"); - _controlPanel.ChangeStatus = $"Presence Updated {DateTime.UtcNow}"; + if (_gui != null) + _gui.ChangeStatus = $"Presence Updated {DateTime.UtcNow}"; } } } - public void Close() - { - LogWriter.WriteToFile("Close called"); - client.Dispose(); - } - private void Client_OnPresenceUpdate(object sender, PresenceMessage args) { LogWriter.WriteToFile($"[RpcMessage] | Presence state: {args.Presence.State}"); @@ -178,30 +200,37 @@ namespace EnderIce2.SDRSharpPlugin private void OnConnectionFailed(object sender, ConnectionFailedMessage args) { - _controlPanel.ChangeStatus = $"RPC Connection Failed!\n{args.Type} | {args.FailedPipe}"; + if (_gui != null) + _gui.ChangeStatus = $"RPC Connection Failed!\n{args.Type} | {args.FailedPipe}"; isConnected = false; } private void OnConnectionEstablished(object sender, ConnectionEstablishedMessage args) { - _controlPanel.ChangeStatus = "RPC Connection Established!"; + if (_gui != null) + _gui.ChangeStatus = "RPC Connection Established!"; isConnected = true; } private void OnError(object sender, ErrorMessage args) { - _controlPanel.ChangeStatus = $"RPC Error:\n{args.Message}"; + if (_gui != null) + _gui.ChangeStatus = $"RPC Error:\n{args.Message}"; } private void OnClose(object sender, CloseMessage args) { - _controlPanel.ChangeStatus = "RPC Closed"; - Close(); + if (_gui != null) + _gui.ChangeStatus = "RPC Closed"; + LogWriter.WriteToFile("Close called"); + client.Dispose(); } private void OnReady(object sender, ReadyMessage args) { - _controlPanel.ChangeStatus = "RPC Ready"; + if (_gui != null) + _gui.ChangeStatus = "RPC Ready"; } + } } diff --git a/SDR-RPC/Properties/AssemblyInfo.cs b/SDR-RPC/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c8a01de --- /dev/null +++ b/SDR-RPC/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +using System.Reflection; +using System.Runtime.Versioning; + +[assembly: SupportedOSPlatform("windows7.0")] +[assembly: AssemblyTitle("SDR# Discord RPC Plugin")] +[assembly: AssemblyDescription("Show on Discord what are you listening on AIRSPY SDR#")] +[assembly: AssemblyProduct("SDR-RPC")] +[assembly: AssemblyCopyright("Copyright © EnderIce2 2024")] +[assembly: AssemblyCompany("EnderIce2")] +[assembly: AssemblyVersion("1.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] \ No newline at end of file diff --git a/SDR-RPC/Properties/launchSettings.json b/SDR-RPC/Properties/launchSettings.json new file mode 100644 index 0000000..c7258e2 --- /dev/null +++ b/SDR-RPC/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "SDRSharp.Diagnostics": { + "commandName": "Executable", + "executablePath": "..\\..\\bin\\SDRSharp.exe", + "workingDirectory": "..\\$(Configuration)\\$(TargetFramework)\\" + } + } +} \ No newline at end of file diff --git a/SDR-RPC/SDR-RPC.csproj b/SDR-RPC/SDR-RPC.csproj new file mode 100644 index 0000000..0884527 --- /dev/null +++ b/SDR-RPC/SDR-RPC.csproj @@ -0,0 +1,57 @@ + + + net8.0-windows + true + Library + AnyCPU + true + false + https://github.com/EnderIce2/SDR-RPC + LICENSE + README.md + Copyright © EnderIce2 2024 + https://enderice2.github.io/SDR-RPC/ + + Show on Discord what are you listening on AIRSPY SDR# + SDR-RPC + EnderIce2 + SDR# Discord RPC Plugin + git + $(VersionPrefix) + + + ..\Debug\ + true + + + ..\Release\ + true + + + + True + \ + + + True + \ + + + + + False + ..\lib\SDRSharp.Common.dll + + + False + ..\lib\SDRSharp.PanView.dll + + + False + ..\lib\SDRSharp.Radio.dll + + + + + + \ No newline at end of file diff --git a/SDRSHARP-LICENSE.txt b/SDRSHARP-LICENSE.txt new file mode 100644 index 0000000..a485ff2 --- /dev/null +++ b/SDRSHARP-LICENSE.txt @@ -0,0 +1,23 @@ +SDRSHARP REFERENCE LICENSE + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software. + +1. Definitions + +The terms "reproduce," "reproduction," and "distribution" have the same meaning here. +"You" means the licensee of the software, who is not engaged in designing, developing, or testing other software, that has the same or substantially the same features or functionality as the software. +"Your company" means the company you worked for when you downloaded the software. +"Reference use" means use of the software within your company as a reference for the sole purposes of developping and maintaining plugins for the accompagniting software. For clarity, "reference use" does NOT include (a) the right to use the software for purposes of designing, developing, or testing other software, that has the same or substantially the same features or functionality as the software, and (b) the right to distribute the software outside of your company. +"Plugin," "plugins" mean any additional software extensions to the accompanying software produced using the provided code examples. +"Licensed patents" means any Licensor patent claims which read directly on the software as distributed by the Licensor under this license. + +2. Grant of Rights + +(A) Copyright Grant- Subject to the terms of this license, the Licensor grants you a non-transferable, non-exclusive, worldwide, royalty-free copyright license to reproduce the software for reference use. +(B) Patent Grant- Subject to the terms of this license, the Licensor grants you a non-transferable, non-exclusive, worldwide, royalty-free patent license under licensed patents for reference use. + +3. Limitations + +(A) No Trademark License- This license does not grant you any rights to use the Licensor's name, logo, or trademarks. +(B) If you begin patent litigation against the Licensor over patents that you think may apply to the software (including a cross-claim or counterclaim in a lawsuit), your license to the software ends automatically. +(C) The software is licensed "as-is." You bear the risk of using it. The Licensor gives no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the Licensor excludes the implied warranties of merchantability, fitness for a particular purpose and non-infringement. diff --git a/SDRSharpPlugin.DiscordRPC.csproj b/SDRSharpPlugin.DiscordRPC.csproj deleted file mode 100644 index f57410a..0000000 --- a/SDRSharpPlugin.DiscordRPC.csproj +++ /dev/null @@ -1,175 +0,0 @@ - - - - - Debug - AnyCPU - {72E8628F-BA39-4915-BF3C-DD48BF477D30} - Library - Properties - DiscordRPC - SDR-RPC - v4.6 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - true - - 8.0 - x86 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - true - - 8.0 - x86 - - - false - - - - packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll - - - False - ..\..\sdrsharp-x86\SDRSharp.Common.dll - - - False - ..\..\sdrsharp-x86\SDRSharp.PanView.dll - - - False - ..\..\sdrsharp-x86\SDRSharp.Radio.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - UserControl - - - SettingsPanel.cs - - - Form - - - SettingsForm.cs - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - SettingsPanel.cs - - - SettingsForm.cs - - - - - - Always - - - - - - - - - - \ No newline at end of file diff --git a/SDRSharpPlugin.DiscordRPC.sln b/SDRSharpPlugin.DiscordRPC.sln deleted file mode 100644 index 941df0c..0000000 --- a/SDRSharpPlugin.DiscordRPC.sln +++ /dev/null @@ -1,30 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30517.126 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDRSharpPlugin.DiscordRPC", "SDRSharpPlugin.DiscordRPC.csproj", "{72E8628F-BA39-4915-BF3C-DD48BF477D30}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{446ABA84-7848-4219-AE53-4EE2132D0710}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {72E8628F-BA39-4915-BF3C-DD48BF477D30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72E8628F-BA39-4915-BF3C-DD48BF477D30}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72E8628F-BA39-4915-BF3C-DD48BF477D30}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72E8628F-BA39-4915-BF3C-DD48BF477D30}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {DA82DCCB-E6B0-4649-9A0C-CECB0AF41CDD} - EndGlobalSection -EndGlobal diff --git a/SettingsForm.Designer.cs b/SettingsForm.Designer.cs deleted file mode 100644 index cdd5d2f..0000000 --- a/SettingsForm.Designer.cs +++ /dev/null @@ -1,188 +0,0 @@ -namespace EnderIce2.SDRSharpPlugin -{ - partial class SettingsForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.button1 = new System.Windows.Forms.Button(); - this.label1 = new System.Windows.Forms.Label(); - this.button2 = new System.Windows.Forms.Button(); - this.checkBox1 = new System.Windows.Forms.CheckBox(); - this.textBox1 = new System.Windows.Forms.TextBox(); - this.label2 = new System.Windows.Forms.Label(); - this.button3 = new System.Windows.Forms.Button(); - this.button4 = new System.Windows.Forms.Button(); - this.button5 = new System.Windows.Forms.Button(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.button1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); - this.button1.FlatStyle = System.Windows.Forms.FlatStyle.Popup; - this.button1.Location = new System.Drawing.Point(200, 126); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 1; - this.button1.Text = "&OK"; - this.button1.UseVisualStyleBackColor = false; - this.button1.Click += new System.EventHandler(this.Button1_Click); - // - // label1 - // - this.label1.AutoSize = true; - this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F); - this.label1.Location = new System.Drawing.Point(6, 6); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(241, 24); - this.label1.TabIndex = 3; - this.label1.Text = "DiscordRPC Plugin Settings"; - // - // button2 - // - this.button2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.button2.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); - this.button2.FlatStyle = System.Windows.Forms.FlatStyle.Popup; - this.button2.Location = new System.Drawing.Point(12, 126); - this.button2.Name = "button2"; - this.button2.Size = new System.Drawing.Size(93, 23); - this.button2.TabIndex = 5; - this.button2.Text = "Support Me"; - this.button2.UseVisualStyleBackColor = false; - this.button2.Click += new System.EventHandler(this.Button2_Click); - // - // checkBox1 - // - this.checkBox1.AutoSize = true; - this.checkBox1.Location = new System.Drawing.Point(12, 105); - this.checkBox1.Name = "checkBox1"; - this.checkBox1.Size = new System.Drawing.Size(205, 17); - this.checkBox1.TabIndex = 6; - this.checkBox1.Text = "Enable Logging (for debugging)"; - this.checkBox1.UseVisualStyleBackColor = true; - this.checkBox1.CheckedChanged += new System.EventHandler(this.CheckBox1_CheckedChanged); - // - // textBox1 - // - this.textBox1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(25)))), ((int)(((byte)(25))))); - this.textBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.textBox1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244))))); - this.textBox1.Location = new System.Drawing.Point(12, 49); - this.textBox1.Name = "textBox1"; - this.textBox1.Size = new System.Drawing.Size(263, 21); - this.textBox1.TabIndex = 7; - // - // label2 - // - this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(13, 33); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(63, 13); - this.label2.TabIndex = 8; - this.label2.Text = "Client ID:"; - // - // button3 - // - this.button3.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); - this.button3.FlatStyle = System.Windows.Forms.FlatStyle.Popup; - this.button3.Location = new System.Drawing.Point(141, 76); - this.button3.Name = "button3"; - this.button3.Size = new System.Drawing.Size(64, 23); - this.button3.TabIndex = 9; - this.button3.Text = "Reset"; - this.button3.UseVisualStyleBackColor = false; - this.button3.Click += new System.EventHandler(this.Button3_Click); - // - // button4 - // - this.button4.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); - this.button4.FlatStyle = System.Windows.Forms.FlatStyle.Popup; - this.button4.Location = new System.Drawing.Point(211, 76); - this.button4.Name = "button4"; - this.button4.Size = new System.Drawing.Size(64, 23); - this.button4.TabIndex = 10; - this.button4.Text = "Apply"; - this.button4.UseVisualStyleBackColor = false; - this.button4.Click += new System.EventHandler(this.Button4_Click); - // - // button5 - // - this.button5.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.button5.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); - this.button5.FlatStyle = System.Windows.Forms.FlatStyle.Popup; - this.button5.Location = new System.Drawing.Point(111, 126); - this.button5.Name = "button5"; - this.button5.Size = new System.Drawing.Size(83, 23); - this.button5.TabIndex = 11; - this.button5.Text = "Licenses"; - this.button5.UseVisualStyleBackColor = false; - this.button5.Click += new System.EventHandler(this.Button5_Click); - // - // SettingsForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(28)))), ((int)(((byte)(28))))); - this.ClientSize = new System.Drawing.Size(287, 161); - this.Controls.Add(this.button5); - this.Controls.Add(this.button4); - this.Controls.Add(this.button3); - this.Controls.Add(this.label2); - this.Controls.Add(this.textBox1); - this.Controls.Add(this.checkBox1); - this.Controls.Add(this.button2); - this.Controls.Add(this.label1); - this.Controls.Add(this.button1); - this.Font = new System.Drawing.Font("Verdana", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238))); - this.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244))))); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.MaximizeBox = false; - this.MinimizeBox = false; - this.Name = "SettingsForm"; - this.ShowIcon = false; - this.ShowInTaskbar = false; - this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "SDRSharp Discord RPC Settings"; - this.TopMost = true; - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - private System.Windows.Forms.Button button1; - private System.Windows.Forms.Label label1; - private System.Windows.Forms.Button button2; - private System.Windows.Forms.CheckBox checkBox1; - private System.Windows.Forms.TextBox textBox1; - private System.Windows.Forms.Label label2; - private System.Windows.Forms.Button button3; - private System.Windows.Forms.Button button4; - private System.Windows.Forms.Button button5; - } -} \ No newline at end of file diff --git a/SettingsPanel.Designer.cs b/SettingsPanel.Designer.cs deleted file mode 100644 index c324852..0000000 --- a/SettingsPanel.Designer.cs +++ /dev/null @@ -1,107 +0,0 @@ -namespace EnderIce2.SDRSharpPlugin -{ - partial class SettingsPanel - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.checkBox1 = new System.Windows.Forms.CheckBox(); - this.label1 = new System.Windows.Forms.Label(); - this.button1 = new System.Windows.Forms.Button(); - this.SuspendLayout(); - // - // checkBox1 - // - this.checkBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.checkBox1.Checked = true; - this.checkBox1.CheckState = System.Windows.Forms.CheckState.Checked; - this.checkBox1.Font = new System.Drawing.Font("Verdana", 7F); - this.checkBox1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244))))); - this.checkBox1.Location = new System.Drawing.Point(3, 3); - this.checkBox1.Name = "checkBox1"; - this.checkBox1.Size = new System.Drawing.Size(153, 21); - this.checkBox1.TabIndex = 0; - this.checkBox1.Text = "Enable Discord RPC"; - this.checkBox1.UseVisualStyleBackColor = true; - this.checkBox1.CheckedChanged += new System.EventHandler(this.EnableRPC_CheckedChanged); - // - // label1 - // - this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.label1.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.label1.Font = new System.Drawing.Font("Verdana", 7F); - this.label1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244))))); - this.label1.Location = new System.Drawing.Point(0, 28); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(200, 22); - this.label1.TabIndex = 1; - this.label1.Text = "Loading status..."; - this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - // - // button1 - // - this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.button1.BackgroundImage = global::DiscordRPC.Properties.Resources.gear; - this.button1.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom; - this.button1.FlatAppearance.BorderSize = 0; - this.button1.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.button1.Font = new System.Drawing.Font("Verdana", 7F); - this.button1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244))))); - this.button1.Location = new System.Drawing.Point(175, 0); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(25, 25); - this.button1.TabIndex = 4; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.SettingsButton_Click); - // - // SettingsPanel - // - this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15))))); - this.Controls.Add(this.button1); - this.Controls.Add(this.label1); - this.Controls.Add(this.checkBox1); - this.Font = new System.Drawing.Font("Verdana", 7F); - this.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244))))); - this.MinimumSize = new System.Drawing.Size(200, 50); - this.Name = "SettingsPanel"; - this.Size = new System.Drawing.Size(200, 50); - this.Load += new System.EventHandler(this.SettingsPanel_Load); - this.ResumeLayout(false); - - } - -#endregion - - private System.Windows.Forms.CheckBox checkBox1; - private System.Windows.Forms.Label label1; - private System.Windows.Forms.Button button1; - } -} diff --git a/SettingsPanel.cs b/SettingsPanel.cs deleted file mode 100644 index be95f9a..0000000 --- a/SettingsPanel.cs +++ /dev/null @@ -1,46 +0,0 @@ -using SDRSharp.Radio; -using System; -using System.Windows.Forms; - -namespace EnderIce2.SDRSharpPlugin -{ - public partial class SettingsPanel : UserControl - { - private string _ChangeStatus; - public string ChangeStatus - { - get => _ChangeStatus; - set - { - _ChangeStatus = value; - label1.Text = value; - LogWriter.WriteToFile(value); - } - } - public SettingsPanel() - { - InitializeComponent(); - checkBox1.Checked = Utils.GetBooleanSetting("EnableRPC", true); - LogWriter.WriteToFile("User Control Loaded"); - } - - private void SettingsButton_Click(object sender, EventArgs e) => new SettingsForm().Show(); - - private void EnableRPC_CheckedChanged(object sender, EventArgs e) - { - Utils.SaveSetting("EnableRPC", checkBox1.Checked); - label1.Text = "Restart required"; - LogWriter.WriteToFile($"checkbox on SettingsPanel clicked {checkBox1.Checked}"); - } - - private void SettingsPanel_Load(object sender, EventArgs e) - { - // can't use bcz System.Drawing.Color is from dotnet core 5 - //MainPlugin._control.ThemeForeColor; - //BackColor = MainPlugin._control.ThemePanelColor; - //MainPlugin._control.ThemeBackColor; - BackColor = System.Drawing.Color.FromArgb(15, 15, 15); - ForeColor = System.Drawing.Color.FromArgb(244, 244, 244); - } - } -} \ No newline at end of file diff --git a/app.config b/app.config deleted file mode 100644 index 8ff5e18..0000000 --- a/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/lib/SDRSharp.Common.dll b/lib/SDRSharp.Common.dll new file mode 100644 index 0000000000000000000000000000000000000000..d13e9b71dd98bfc178195e94d01dea576e5048c4 GIT binary patch literal 15360 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 literal 0 HcmV?d00001 diff --git a/lib/SDRSharp.PanView.dll b/lib/SDRSharp.PanView.dll new file mode 100644 index 0000000000000000000000000000000000000000..558c71952dc3bc9bd7d22c05e6f5f0373dbc94ba GIT binary patch literal 117760 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�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 G4*Wj_D#1el literal 0 HcmV?d00001 diff --git a/lib/SDRSharp.Radio.dll b/lib/SDRSharp.Radio.dll new file mode 100644 index 0000000000000000000000000000000000000000..968ef43b8b99809949373a54b1e719cf14fd2d93 GIT binary patch literal 49152 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 m4zyQ0TBjJJGQEfT$CHZx?*#j>ltp - - - \ No newline at end of file