mirror of
https://github.com/EnderIce2/SDR-RPC.git
synced 2025-05-27 20:04:27 +00:00
First commit
This commit is contained in:
parent
9d78d84acb
commit
b91db81473
32
DiscordAPI/Configuration.cs
Normal file
32
DiscordAPI/Configuration.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration of the current RPC connection
|
||||
/// </summary>
|
||||
public class Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// The Discord API endpoint that should be used.
|
||||
/// </summary>
|
||||
[JsonProperty("api_endpoint")]
|
||||
public string ApiEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CDN endpoint
|
||||
/// </summary>
|
||||
[JsonProperty("cdn_host")]
|
||||
public string CdnHost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of enviroment the connection on. Usually Production.
|
||||
/// </summary>
|
||||
[JsonProperty("enviroment")]
|
||||
public string Enviroment { get; set; }
|
||||
}
|
||||
}
|
98
DiscordAPI/Converters/EnumSnakeCaseConverter.cs
Normal file
98
DiscordAPI/Converters/EnumSnakeCaseConverter.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using DiscordRPC.Helper;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DiscordRPC.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts enums with the <see cref="EnumValueAttribute"/> into Json friendly terms.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
16
DiscordAPI/Converters/EnumValueAttribute.cs
Normal file
16
DiscordAPI/Converters/EnumValueAttribute.cs
Normal file
@ -0,0 +1,16 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
901
DiscordAPI/DiscordRpcClient.cs
Normal file
901
DiscordAPI/DiscordRpcClient.cs
Normal file
@ -0,0 +1,901 @@
|
||||
using DiscordRPC.Events;
|
||||
using DiscordRPC.Exceptions;
|
||||
using DiscordRPC.IO;
|
||||
using DiscordRPC.Logging;
|
||||
using DiscordRPC.Message;
|
||||
using DiscordRPC.Registry;
|
||||
using DiscordRPC.RPC;
|
||||
using DiscordRPC.RPC.Commands;
|
||||
using System;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A Discord RPC Client which is used to send Rich Presence updates and receive Join / Spectate events.
|
||||
/// </summary>
|
||||
public sealed class DiscordRpcClient : IDisposable
|
||||
{
|
||||
#region Properties
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating if the client has registered a URI Scheme. If this is false, Join / Spectate events will fail.
|
||||
/// <para>To register a URI Scheme, call <see cref="RegisterUriScheme(string, string)"/>.</para>
|
||||
/// </summary>
|
||||
public bool HasRegisteredUriScheme { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Application ID of the RPC Client.
|
||||
/// </summary>
|
||||
public string ApplicationID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Steam ID of the RPC Client. This value can be null if none was supplied.
|
||||
/// </summary>
|
||||
public string SteamID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the process used to run the RPC Client. Discord tracks this process ID and waits for its termination. Defaults to the current application process ID.
|
||||
/// </summary>
|
||||
public int ProcessID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size of the message queue received from Discord.
|
||||
/// </summary>
|
||||
public int MaxQueueSize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dispose state of the client object.
|
||||
/// </summary>
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The logger used this client and its associated components. <see cref="ILogger"/> are not called safely and can come from any thread. It is upto the <see cref="ILogger"/> to account for this and apply appropriate thread safe methods.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get { return _logger; }
|
||||
set
|
||||
{
|
||||
this._logger = value;
|
||||
if (connection != null) connection.Logger = value;
|
||||
}
|
||||
}
|
||||
private ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the client will automatically invoke the events without <see cref="Invoke"/> having to be called.
|
||||
/// </summary>
|
||||
public bool AutoEvents { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Skips sending presences that are identical to the current one.
|
||||
/// </summary>
|
||||
public bool SkipIdenticalPresence { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The pipe the discord client is on, ranging from 0 to 9. Use -1 to scan through all pipes.
|
||||
/// <para>This property can be used for testing multiple clients. For example, if a Discord Client was on pipe 0, the Discord Canary is most likely on pipe 1.</para>
|
||||
/// </summary>
|
||||
public int TargetPipe { get; private set; }
|
||||
|
||||
private RpcConnection connection;
|
||||
|
||||
/// <summary>
|
||||
/// The current presence that the client has. Gets set with <see cref="SetPresence(RichPresence)"/> and updated on <see cref="OnPresenceUpdate"/>.
|
||||
/// </summary>
|
||||
public RichPresence CurrentPresence { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current subscription to events. Gets set with <see cref="Subscribe(EventType)"/>, <see cref="UnsubscribeMessage"/> and updated on <see cref="OnSubscribe"/>, <see cref="OnUnsubscribe"/>.
|
||||
/// </summary>
|
||||
public EventType Subscription { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current discord user. This is updated with the ready event and will be null until the event is fired from the connection.
|
||||
/// </summary>
|
||||
public User CurrentUser { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current configuration the connection is using. Only becomes available after a ready event.
|
||||
/// </summary>
|
||||
public Configuration Configuration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Represents if the client has been <see cref="Initialize"/>
|
||||
/// </summary>
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Forces the connection to shutdown gracefully instead of just aborting the connection.
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
public bool ShutdownOnly
|
||||
{
|
||||
get { return _shutdownOnly; }
|
||||
set
|
||||
{
|
||||
_shutdownOnly = value;
|
||||
if (connection != null) connection.ShutdownOnly = value;
|
||||
}
|
||||
}
|
||||
private bool _shutdownOnly = true;
|
||||
private object _sync = new object();
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Called when the discord client is ready to send and receive messages.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnReadyEvent OnReady;
|
||||
|
||||
/// <summary>
|
||||
/// Called when connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnCloseEvent OnClose;
|
||||
|
||||
/// <summary>
|
||||
/// Called when 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.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnErrorEvent OnError;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has updated the presence.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnPresenceUpdateEvent OnPresenceUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has subscribed to an event.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnSubscribeEvent OnSubscribe;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has unsubscribed from an event.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnUnsubscribeEvent OnUnsubscribe;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to join a game.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnJoinEvent OnJoin;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to spectate a game.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnSpectateEvent OnSpectate;
|
||||
|
||||
/// <summary>
|
||||
/// Called when another discord user requests permission to join this game.
|
||||
/// <para>This event is not invoked untill <see cref="Invoke"/> is executed.</para>
|
||||
/// </summary>
|
||||
public event OnJoinRequestedEvent OnJoinRequested;
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the discord client was succesfull. This is called before <see cref="MessageType.Ready"/>.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnConnectionEstablishedEvent OnConnectionEstablished;
|
||||
|
||||
/// <summary>
|
||||
/// Failed to establish any connection with discord. Discord is potentially not running?
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnConnectionFailedEvent OnConnectionFailed;
|
||||
|
||||
/// <summary>
|
||||
/// The RPC Connection has sent a message. Called before any other event and executed from the RPC Thread.
|
||||
/// </summary>
|
||||
public event OnRpcMessageEvent OnRpcMessage;
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Discord RPC Client which can be used to send Rich Presence and receive Join / Spectate events.
|
||||
/// </summary>
|
||||
/// <param name="applicationID">The ID of the application created at discord's developers portal.</param>
|
||||
public DiscordRpcClient(string applicationID) : this(applicationID, -1) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Discord RPC Client which can be used to send Rich Presence and receive Join / Spectate events. This constructor exposes more advance features such as custom NamedPipeClients and Loggers.
|
||||
/// </summary>
|
||||
/// <param name="applicationID">The ID of the application created at discord's developers portal.</param>
|
||||
/// <param name="pipe">The pipe to connect too. If -1, then the client will scan for the first available instance of Discord.</param>
|
||||
/// <param name="logger">The logger used to report messages. If null, then a <see cref="NullLogger"/> will be created and logs will be ignored.</param>
|
||||
/// <param name="autoEvents">Should events be automatically invoked from the RPC Thread as they arrive from discord?</param>
|
||||
/// <param name="client">The pipe client to use and communicate to discord through. If null, the default <see cref="ManagedNamedPipeClient"/> will be used.</param>
|
||||
public DiscordRpcClient(string applicationID, int pipe = -1, ILogger logger = null, bool autoEvents = true, INamedPipeClient client = null)
|
||||
{
|
||||
//Make sure appID is NOT null.
|
||||
if (string.IsNullOrEmpty(applicationID))
|
||||
throw new ArgumentNullException("applicationID");
|
||||
|
||||
//Ensure we actually have json ahead of time. If statement is pointless, but its there just to ensure there is no unused warnings.
|
||||
var jsonConverterType = typeof(Newtonsoft.Json.JsonConverter);
|
||||
if (jsonConverterType == null) throw new Exception("JsonConverter Type Not Found");
|
||||
|
||||
//Store the properties
|
||||
ApplicationID = applicationID.Trim();
|
||||
TargetPipe = pipe;
|
||||
ProcessID = System.Diagnostics.Process.GetCurrentProcess().Id;
|
||||
HasRegisteredUriScheme = false;
|
||||
AutoEvents = autoEvents;
|
||||
SkipIdenticalPresence = true;
|
||||
|
||||
//Prepare the logger
|
||||
_logger = logger ?? new NullLogger();
|
||||
|
||||
//Create the RPC client, giving it the important details
|
||||
connection = new RpcConnection(ApplicationID, ProcessID, TargetPipe, client ?? new ManagedNamedPipeClient(), autoEvents ? 0 : 128U)
|
||||
{
|
||||
ShutdownOnly = _shutdownOnly,
|
||||
Logger = _logger
|
||||
};
|
||||
|
||||
//Subscribe to its event
|
||||
connection.OnRpcMessage += (sender, msg) =>
|
||||
{
|
||||
if (OnRpcMessage != null)
|
||||
OnRpcMessage.Invoke(this, msg);
|
||||
|
||||
if (AutoEvents)
|
||||
ProcessMessage(msg);
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message Handling
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>This method cannot be used if <see cref="AutoEvents"/> is enabled.</para>
|
||||
/// </summary>
|
||||
/// <returns>Returns the messages that were invoked and in the order they were invoked.</returns>
|
||||
public IMessage[] Invoke()
|
||||
{
|
||||
if (AutoEvents)
|
||||
{
|
||||
Logger.Error("Cannot Invoke client when AutomaticallyInvokeEvents has been set.");
|
||||
return new IMessage[0];
|
||||
//throw new InvalidOperationException("Cannot Invoke client when AutomaticallyInvokeEvents has been set.");
|
||||
}
|
||||
|
||||
//Dequeue all the messages and process them
|
||||
IMessage[] messages = connection.DequeueMessages();
|
||||
for (int i = 0; i < messages.Length; i++)
|
||||
{
|
||||
//Do a bit of pre-processing
|
||||
var message = messages[i];
|
||||
ProcessMessage(message);
|
||||
}
|
||||
|
||||
//Finally, return the messages
|
||||
return messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the message, updating our internal state and then invokes the events.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
private void ProcessMessage(IMessage message)
|
||||
{
|
||||
if (message == null) return;
|
||||
switch (message.Type)
|
||||
{
|
||||
//We got a update, so we will update our current presence
|
||||
case MessageType.PresenceUpdate:
|
||||
lock (_sync)
|
||||
{
|
||||
var pm = message as PresenceMessage;
|
||||
if (pm != null)
|
||||
{
|
||||
//We need to merge these presences together
|
||||
if (CurrentPresence == null)
|
||||
{
|
||||
CurrentPresence = pm.Presence;
|
||||
}
|
||||
else if (pm.Presence == null)
|
||||
{
|
||||
CurrentPresence = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentPresence.Merge(pm.Presence);
|
||||
}
|
||||
|
||||
//Update the message
|
||||
pm.Presence = CurrentPresence;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
//Update our configuration
|
||||
case MessageType.Ready:
|
||||
var rm = message as ReadyMessage;
|
||||
if (rm != null)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
Configuration = rm.Configuration;
|
||||
CurrentUser = rm.User;
|
||||
}
|
||||
|
||||
//Resend our presence and subscription
|
||||
SynchronizeState();
|
||||
}
|
||||
break;
|
||||
|
||||
//Update the request's CDN for the avatar helpers
|
||||
case MessageType.JoinRequest:
|
||||
if (Configuration != null)
|
||||
{
|
||||
//Update the User object within the join request if the current Cdn
|
||||
var jrm = message as JoinRequestMessage;
|
||||
if (jrm != null) jrm.User.SetConfiguration(Configuration);
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageType.Subscribe:
|
||||
lock (_sync)
|
||||
{
|
||||
var sub = message as SubscribeMessage;
|
||||
Subscription |= sub.Event;
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageType.Unsubscribe:
|
||||
lock (_sync)
|
||||
{
|
||||
var unsub = message as UnsubscribeMessage;
|
||||
Subscription &= ~unsub.Event;
|
||||
}
|
||||
break;
|
||||
|
||||
//We got a message we dont know what to do with.
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
//Invoke the appropriate methods
|
||||
switch (message.Type)
|
||||
{
|
||||
case MessageType.Ready:
|
||||
if (OnReady != null) OnReady.Invoke(this, message as ReadyMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Close:
|
||||
if (OnClose != null) OnClose.Invoke(this, message as CloseMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Error:
|
||||
if (OnError != null) OnError.Invoke(this, message as ErrorMessage);
|
||||
break;
|
||||
|
||||
case MessageType.PresenceUpdate:
|
||||
if (OnPresenceUpdate != null) OnPresenceUpdate.Invoke(this, message as PresenceMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Subscribe:
|
||||
if (OnSubscribe != null) OnSubscribe.Invoke(this, message as SubscribeMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Unsubscribe:
|
||||
if (OnUnsubscribe != null) OnUnsubscribe.Invoke(this, message as UnsubscribeMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Join:
|
||||
if (OnJoin != null) OnJoin.Invoke(this, message as JoinMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Spectate:
|
||||
if (OnSpectate != null) OnSpectate.Invoke(this, message as SpectateMessage);
|
||||
break;
|
||||
|
||||
case MessageType.JoinRequest:
|
||||
if (OnJoinRequested != null) OnJoinRequested.Invoke(this, message as JoinRequestMessage);
|
||||
break;
|
||||
|
||||
case MessageType.ConnectionEstablished:
|
||||
if (OnConnectionEstablished != null) OnConnectionEstablished.Invoke(this, message as ConnectionEstablishedMessage);
|
||||
break;
|
||||
|
||||
case MessageType.ConnectionFailed:
|
||||
if (OnConnectionFailed != null) OnConnectionFailed.Invoke(this, message as ConnectionFailedMessage);
|
||||
break;
|
||||
|
||||
default:
|
||||
//This in theory can never happen, but its a good idea as a reminder to update this part of the library if any new messages are implemented.
|
||||
Logger.Error("Message was queued with no appropriate handle! {0}", message.Type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Respond to a Join Request. All requests will timeout after 30 seconds.
|
||||
/// <para>Because of the 30 second timeout, it is recommended to call <seealso cref="Invoke"/> faster than every 15 seconds to give your users adequate time to respond to the request.</para>
|
||||
/// </summary>
|
||||
/// <param name="request">The request that is being responded too.</param>
|
||||
/// <param name="acceptRequest">Accept the join request.</param>
|
||||
public void Respond(JoinRequestMessage request, bool acceptRequest)
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
connection.EnqueueCommand(new RespondCommand() { Accept = acceptRequest, UserID = request.User.ID.ToString() });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Rich Presence.
|
||||
/// </summary>
|
||||
/// <param name="presence">The Rich Presence to set on the current Discord user.</param>
|
||||
public void SetPresence(RichPresence presence)
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
if (!IsInitialized)
|
||||
Logger.Warning("The client is not yet initialized, storing the presence as a state instead.");
|
||||
|
||||
//Send the event
|
||||
if (!presence)
|
||||
{
|
||||
//Clear the presence
|
||||
if (!SkipIdenticalPresence || CurrentPresence != null)
|
||||
connection.EnqueueCommand(new PresenceCommand() { PID = this.ProcessID, Presence = null });
|
||||
}
|
||||
else
|
||||
{
|
||||
//Send valid presence
|
||||
//Validate the presence with our settings
|
||||
if (presence.HasSecrets() && !HasRegisteredUriScheme)
|
||||
throw new BadPresenceException("Cannot send a presence with secrets as this object has not registered a URI scheme. Please enable the uri scheme registration in the DiscordRpcClient constructor.");
|
||||
|
||||
if (presence.HasParty() && presence.Party.Max < presence.Party.Size)
|
||||
throw new BadPresenceException("Presence maximum party size cannot be smaller than the current size.");
|
||||
|
||||
if (presence.HasSecrets() && !presence.HasParty())
|
||||
Logger.Warning("The presence has set the secrets but no buttons will show as there is no party available.");
|
||||
|
||||
//Send the presence, but only if we are not skipping
|
||||
if (!SkipIdenticalPresence || !presence.Matches(CurrentPresence))
|
||||
connection.EnqueueCommand(new PresenceCommand() { PID = this.ProcessID, Presence = presence.Clone() });
|
||||
}
|
||||
|
||||
//Update our local store
|
||||
lock (_sync) { CurrentPresence = presence != null ? presence.Clone() : null; }
|
||||
}
|
||||
|
||||
#region Updates
|
||||
/// <summary>
|
||||
/// Updates only the <see cref="RichPresence.Details"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// </summary>
|
||||
/// <param name="details">The details of the Rich Presence</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateDetails(string details)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
CurrentPresence.Details = details;
|
||||
SetPresence(CurrentPresence);
|
||||
}
|
||||
return CurrentPresence;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates only the <see cref="RichPresence.State"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// </summary>
|
||||
/// <param name="state">The state of the Rich Presence</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateState(string state)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
CurrentPresence.State = state;
|
||||
SetPresence(CurrentPresence);
|
||||
}
|
||||
return CurrentPresence;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates only the <see cref="RichPresence.Party"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// </summary>
|
||||
/// <param name="party">The party of the Rich Presence</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateParty(Party party)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
CurrentPresence.Party = party;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Party.Size"/> of the <see cref="CurrentPresence"/> and sends the update presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// <para>Will return null if no presence exists and will throw a new <see cref="NullReferenceException"/> if the Party does not exist.</para>
|
||||
/// </summary>
|
||||
/// <param name="size">The new size of the party. It cannot be greater than <see cref="Party.Max"/></param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdatePartySize(int size)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
if (CurrentPresence == null) return null;
|
||||
if (CurrentPresence.Party == null)
|
||||
throw new BadPresenceException("Cannot set the size of the party if the party does not exist");
|
||||
|
||||
try { UpdatePartySize(size, CurrentPresence.Party.Max); } catch (Exception) { throw; }
|
||||
return CurrentPresence;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Party.Size"/> of the <see cref="CurrentPresence"/> and sends the update presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// <para>Will return null if no presence exists and will throw a new <see cref="NullReferenceException"/> if the Party does not exist.</para>
|
||||
/// </summary>
|
||||
/// <param name="size">The new size of the party. It cannot be greater than <see cref="Party.Max"/></param>
|
||||
/// <param name="max">The new size of the party. It cannot be smaller than <see cref="Party.Size"/></param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdatePartySize(int size, int max)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
if (CurrentPresence == null) return null;
|
||||
if (CurrentPresence.Party == null)
|
||||
throw new BadPresenceException("Cannot set the size of the party if the party does not exist");
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
CurrentPresence.Party.Size = size;
|
||||
CurrentPresence.Party.Max = max;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the large <see cref="Assets"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Both <paramref name="key"/> and <paramref name="tooltip"/> are optional and will be ignored it null.
|
||||
/// </summary>
|
||||
/// <param name="key">Optional: The new key to set the asset too</param>
|
||||
/// <param name="tooltip">Optional: The new tooltip to display on the asset</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateLargeAsset(string key = null, string tooltip = null)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
if (CurrentPresence.Assets == null) CurrentPresence.Assets = new Assets();
|
||||
CurrentPresence.Assets.LargeImageKey = key ?? CurrentPresence.Assets.LargeImageKey;
|
||||
CurrentPresence.Assets.LargeImageText = tooltip ?? CurrentPresence.Assets.LargeImageText;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the small <see cref="Assets"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Both <paramref name="key"/> and <paramref name="tooltip"/> are optional and will be ignored it null.
|
||||
/// </summary>
|
||||
/// <param name="key">Optional: The new key to set the asset too</param>
|
||||
/// <param name="tooltip">Optional: The new tooltip to display on the asset</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateSmallAsset(string key = null, string tooltip = null)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
if (CurrentPresence.Assets == null) CurrentPresence.Assets = new Assets();
|
||||
CurrentPresence.Assets.SmallImageKey = key ?? CurrentPresence.Assets.SmallImageKey;
|
||||
CurrentPresence.Assets.SmallImageText = tooltip ?? CurrentPresence.Assets.SmallImageText;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Secrets"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Will override previous secret entirely.
|
||||
/// </summary>
|
||||
/// <param name="secrets">The new secret to send to discord.</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateSecrets(Secrets secrets)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
CurrentPresence.Secrets = secrets;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the start time of the <see cref="CurrentPresence"/> to now and sends the updated presence to Discord.
|
||||
/// </summary>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateStartTime() { try { return UpdateStartTime(DateTime.UtcNow); } catch (Exception) { throw; } }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the start time of the <see cref="CurrentPresence"/> and sends the updated presence to Discord.
|
||||
/// </summary>
|
||||
/// <param name="time">The new time for the start</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateStartTime(DateTime time)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
if (CurrentPresence.Timestamps == null) CurrentPresence.Timestamps = new Timestamps();
|
||||
CurrentPresence.Timestamps.Start = time;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the end time of the <see cref="CurrentPresence"/> to now and sends the updated presence to Discord.
|
||||
/// </summary>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateEndTime() { try { return UpdateEndTime(DateTime.UtcNow); } catch (Exception) { throw; } }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the end time of the <see cref="CurrentPresence"/> and sends the updated presence to Discord.
|
||||
/// </summary>
|
||||
/// <param name="time">The new time for the end</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateEndTime(DateTime time)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
if (CurrentPresence.Timestamps == null) CurrentPresence.Timestamps = new Timestamps();
|
||||
CurrentPresence.Timestamps.End = time;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the start and end time of <see cref="CurrentPresence"/> to null and sends it to Discord.
|
||||
/// </summary>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateClearTime()
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) return null;
|
||||
CurrentPresence.Timestamps = null;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Clears the Rich Presence. Use this just before disposal to prevent ghosting.
|
||||
/// </summary>
|
||||
public void ClearPresence()
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
//Just a wrapper function for sending null
|
||||
SetPresence(null);
|
||||
}
|
||||
|
||||
#region Subscriptions
|
||||
|
||||
/// <summary>
|
||||
/// Registers the application executable to a custom URI Scheme.
|
||||
/// <para>This is required for the Join and Spectate features. Discord will run this custom URI Scheme to launch your application when a user presses either of the buttons.</para>
|
||||
/// </summary>
|
||||
/// <param name="steamAppID">Optional Steam ID. If supplied, Discord will launch the game through steam instead of directly calling it.</param>
|
||||
/// <param name="executable">The path to the executable. If null, the path to the current executable will be used instead.</param>
|
||||
/// <returns></returns>
|
||||
public bool RegisterUriScheme(string steamAppID = null, string executable = null)
|
||||
{
|
||||
var urischeme = new UriSchemeRegister(_logger, ApplicationID, steamAppID, executable);
|
||||
return HasRegisteredUriScheme = urischeme.RegisterUriScheme();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to an event sent from discord. Used for Join / Spectate feature.
|
||||
/// <para>Requires the UriScheme to be registered.</para>
|
||||
/// </summary>
|
||||
/// <param name="type">The event type to subscribe to</param>
|
||||
public void Subscribe(EventType type) { SetSubscription(Subscription | type); }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
[System.Obsolete("Replaced with Unsubscribe", true)]
|
||||
public void Unubscribe(EventType type) { SetSubscription(Subscription & ~type); }
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from the event sent by discord. Used for Join / Spectate feature.
|
||||
/// <para>Requires the UriScheme to be registered.</para>
|
||||
/// </summary>
|
||||
/// <param name="type">The event type to unsubscribe from</param>
|
||||
public void Unsubscribe(EventType type) { SetSubscription(Subscription & ~type); }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the subscription to the events sent from Discord.
|
||||
/// <para>Requires the UriScheme to be registered.</para>
|
||||
/// </summary>
|
||||
/// <param name="type">The new subscription as a flag. Events selected in the flag will be subscribed too and the other events will be unsubscribed.</param>
|
||||
public void SetSubscription(EventType type)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
//Calculate what needs to be unsubscrinbed
|
||||
SubscribeToTypes(Subscription & ~type, true);
|
||||
SubscribeToTypes(~Subscription & type, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning("Client has not yet initialized, but events are being subscribed too. Storing them as state instead.");
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
Subscription = type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple helper function that will subscribe to the specified types in the flag.
|
||||
/// </summary>
|
||||
/// <param name="type">The flag to subscribe to</param>
|
||||
/// <param name="isUnsubscribe">Represents if the unsubscribe payload should be sent instead.</param>
|
||||
private void SubscribeToTypes(EventType type, bool isUnsubscribe)
|
||||
{
|
||||
//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;
|
||||
|
||||
//We cannot do anything if we are disposed or missing our connection.
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
//We dont have the Uri Scheme registered, we should throw a exception to tell the user.
|
||||
if (!HasRegisteredUriScheme)
|
||||
throw new InvalidConfigurationException("Cannot subscribe/unsubscribe to an event as this application has not registered a URI Scheme. Call RegisterUriScheme().");
|
||||
|
||||
//Add the subscribe command to be sent when the connection is able too
|
||||
if ((type & EventType.Spectate) == EventType.Spectate)
|
||||
connection.EnqueueCommand(new SubscribeCommand() { Event = RPC.Payload.ServerEvent.ActivitySpectate, IsUnsubscribe = isUnsubscribe });
|
||||
|
||||
if ((type & EventType.Join) == EventType.Join)
|
||||
connection.EnqueueCommand(new SubscribeCommand() { Event = RPC.Payload.ServerEvent.ActivityJoin, IsUnsubscribe = isUnsubscribe });
|
||||
|
||||
if ((type & EventType.JoinRequest) == EventType.JoinRequest)
|
||||
connection.EnqueueCommand(new SubscribeCommand() { Event = RPC.Payload.ServerEvent.ActivityJoinRequest, IsUnsubscribe = isUnsubscribe });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Resends the current presence and subscription. This is used when Ready is called to keep the current state within discord.
|
||||
/// </summary>
|
||||
public void SynchronizeState()
|
||||
{
|
||||
//Cannot sync over uninitialized connection
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
//Set the presence and if we have registered the uri scheme, resubscribe.
|
||||
SetPresence(CurrentPresence);
|
||||
if (HasRegisteredUriScheme)
|
||||
SubscribeToTypes(Subscription, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to initalize a connection to the Discord IPC.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool Initialize()
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (IsInitialized)
|
||||
throw new UninitializedException("Cannot initialize a client that is already initialized");
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
return IsInitialized = connection.AttemptConnection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to disconnect and deinitialize the IPC connection while retaining the settings.
|
||||
/// </summary>
|
||||
public void Deinitialize()
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException("Cannot deinitialize a client that has not been initalized.");
|
||||
|
||||
connection.Close();
|
||||
IsInitialized = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the connection to Discord and disposes of the object.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
if (IsInitialized) Deinitialize();
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
34
DiscordAPI/EventType.cs
Normal file
34
DiscordAPI/EventType.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of event receieved by the RPC. A flag type that can be combined.
|
||||
/// </summary>
|
||||
[System.Flags]
|
||||
public enum EventType
|
||||
{
|
||||
/// <summary>
|
||||
/// No event
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes to enter a game to spectate
|
||||
/// </summary>
|
||||
Spectate = 0x1,
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes to enter a game to play.
|
||||
/// </summary>
|
||||
Join = 0x2,
|
||||
|
||||
/// <summary>
|
||||
/// Called when another Discord Client has requested permission to join this game.
|
||||
/// </summary>
|
||||
JoinRequest = 0x4
|
||||
}
|
||||
}
|
94
DiscordAPI/Events.cs
Normal file
94
DiscordAPI/Events.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using DiscordRPC.Message;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the Discord Client is ready to send and receive messages.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnReadyEvent(object sender, ReadyMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnCloseEvent(object sender, CloseMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when 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.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnErrorEvent(object sender, ErrorMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has updated the presence.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnPresenceUpdateEvent(object sender, PresenceMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has subscribed to an event.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnSubscribeEvent(object sender, SubscribeMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has unsubscribed from an event.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnUnsubscribeEvent(object sender, UnsubscribeMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to join a game.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnJoinEvent(object sender, JoinMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to spectate a game.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnSpectateEvent(object sender, SpectateMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when another discord user requests permission to join this game.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnJoinRequestedEvent(object sender, JoinRequestMessage args);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the discord client was succesfull. This is called before <see cref="OnReadyEvent"/>.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnConnectionEstablishedEvent(object sender, ConnectionEstablishedMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Failed to establish any connection with discord. Discord is potentially not running?
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnConnectionFailedEvent(object sender, ConnectionFailedMessage args);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A RPC Message is received.
|
||||
/// </summary>
|
||||
/// <param name="sender">The handler that sent this event</param>
|
||||
/// <param name="msg">The raw message from the RPC</param>
|
||||
public delegate void OnRpcMessageEvent(object sender, IMessage msg);
|
||||
}
|
15
DiscordAPI/Exceptions/BadPresenceException.cs
Normal file
15
DiscordAPI/Exceptions/BadPresenceException.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// A BadPresenceException is thrown when invalid, incompatible or conflicting properties and is unable to be sent.
|
||||
/// </summary>
|
||||
public class BadPresenceException : Exception
|
||||
{
|
||||
internal BadPresenceException(string message) : base(message) { }
|
||||
}
|
||||
}
|
15
DiscordAPI/Exceptions/InvalidConfigurationException.cs
Normal file
15
DiscordAPI/Exceptions/InvalidConfigurationException.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// A InvalidConfigurationException is thrown when trying to perform a action that conflicts with the current configuration.
|
||||
/// </summary>
|
||||
public class InvalidConfigurationException : Exception
|
||||
{
|
||||
internal InvalidConfigurationException(string message) : base(message) { }
|
||||
}
|
||||
}
|
16
DiscordAPI/Exceptions/InvalidPipeException.cs
Normal file
16
DiscordAPI/Exceptions/InvalidPipeException.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when a error occurs while communicating with a pipe or when a connection attempt fails.
|
||||
/// </summary>
|
||||
[System.Obsolete("Not actually used anywhere")]
|
||||
public class InvalidPipeException : Exception
|
||||
{
|
||||
internal InvalidPipeException(string message) : base(message) { }
|
||||
}
|
||||
}
|
50
DiscordAPI/Exceptions/StringOutOfRangeException.cs
Normal file
50
DiscordAPI/Exceptions/StringOutOfRangeException.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// A StringOutOfRangeException is thrown when the length of a string exceeds the allowed limit.
|
||||
/// </summary>
|
||||
public class StringOutOfRangeException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum length the string is allowed to be.
|
||||
/// </summary>
|
||||
public int MaximumLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum length the string is allowed to be.
|
||||
/// </summary>
|
||||
public int MinimumLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new string out of range exception with a range of min to max and a custom message
|
||||
/// </summary>
|
||||
/// <param name="message">The custom message</param>
|
||||
/// <param name="min">Minimum length the string can be</param>
|
||||
/// <param name="max">Maximum length the string can be</param>
|
||||
internal StringOutOfRangeException(string message, int min, int max) : base(message)
|
||||
{
|
||||
MinimumLength = min;
|
||||
MaximumLength = max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sting out of range exception with a range of min to max
|
||||
/// </summary>
|
||||
/// <param name="minumum"></param>
|
||||
/// <param name="max"></param>
|
||||
internal StringOutOfRangeException(int minumum, int max)
|
||||
: this("Length of string is out of range. Expected a value between " + minumum + " and " + max, minumum, max) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sting out of range exception with a range of 0 to max
|
||||
/// </summary>
|
||||
/// <param name="max"></param>
|
||||
internal StringOutOfRangeException(int max)
|
||||
: this("Length of string is out of range. Expected a value with a maximum length of " + max, 0, max) { }
|
||||
}
|
||||
}
|
24
DiscordAPI/Exceptions/UninitializedException.cs
Normal file
24
DiscordAPI/Exceptions/UninitializedException.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when an action is performed on a client that has not yet been initialized
|
||||
/// </summary>
|
||||
public class UninitializedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new unintialized exception
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
internal UninitializedException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new uninitialized exception with default message.
|
||||
/// </summary>
|
||||
internal UninitializedException() : this("Cannot perform action because the client has not been initialized yet or has been deinitialized.") { }
|
||||
}
|
||||
}
|
71
DiscordAPI/Helper/BackoffDelay.cs
Normal file
71
DiscordAPI/Helper/BackoffDelay.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Helper
|
||||
{
|
||||
|
||||
internal class BackoffDelay
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum time the backoff can reach
|
||||
/// </summary>
|
||||
public int Maximum { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum time the backoff can start at
|
||||
/// </summary>
|
||||
public int Minimum { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current time of the backoff
|
||||
/// </summary>
|
||||
public int Current { get { return _current; } }
|
||||
private int _current;
|
||||
|
||||
/// <summary>
|
||||
/// The current number of failures
|
||||
/// </summary>
|
||||
public int Fails { get { return _fails; } }
|
||||
private int _fails;
|
||||
|
||||
/// <summary>
|
||||
/// The random generator
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the backoff
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
73
DiscordAPI/Helper/StringTools.cs
Normal file
73
DiscordAPI/Helper/StringTools.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Collectin of helpful string extensions
|
||||
/// </summary>
|
||||
public static class StringTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Will return null if the string is whitespace, otherwise it will return the string.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to check</param>
|
||||
/// <returns>Null if the string is empty, otherwise the string</returns>
|
||||
public static string GetNullOrString(this string str)
|
||||
{
|
||||
return str.Length == 0 || string.IsNullOrEmpty(str.Trim()) ? null : str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the string fit within the given amount of bytes? Uses UTF8 encoding.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to check</param>
|
||||
/// <param name="bytes">The maximum number of bytes the string can take up</param>
|
||||
/// <returns>True if the string fits within the number of bytes</returns>
|
||||
public static bool WithinLength(this string str, int bytes)
|
||||
{
|
||||
return str.WithinLength(bytes, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the string fit within the given amount of bytes?
|
||||
/// </summary>
|
||||
/// <param name="str">The string to check</param>
|
||||
/// <param name="bytes">The maximum number of bytes the string can take up</param>
|
||||
/// <param name="encoding">The encoding to count the bytes with</param>
|
||||
/// <returns>True if the string fits within the number of bytes</returns>
|
||||
public static bool WithinLength(this string str, int bytes, Encoding encoding)
|
||||
{
|
||||
return encoding.GetByteCount(str) <= bytes;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts the string into UpperCamelCase (Pascal Case).
|
||||
/// </summary>
|
||||
/// <param name="str">The string to convert</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the string into UPPER_SNAKE_CASE
|
||||
/// </summary>
|
||||
/// <param name="str">The string to convert</param>
|
||||
/// <returns></returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
23
DiscordAPI/IO/Handshake.cs
Normal file
23
DiscordAPI/IO/Handshake.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
internal class Handshake
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the IPC API we are using
|
||||
/// </summary>
|
||||
[JsonProperty("v")]
|
||||
public int Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the app.
|
||||
/// </summary>
|
||||
[JsonProperty("client_id")]
|
||||
public string ClientID { get; set; }
|
||||
}
|
||||
}
|
56
DiscordAPI/IO/INamedPipeClient.cs
Normal file
56
DiscordAPI/IO/INamedPipeClient.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using DiscordRPC.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Pipe Client used to communicate with Discord.
|
||||
/// </summary>
|
||||
public interface INamedPipeClient : IDisposable
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The logger for the Pipe client to use
|
||||
/// </summary>
|
||||
ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the pipe client currently connected?
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The pipe the client is currently connected too
|
||||
/// </summary>
|
||||
int ConnectedPipe { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="pipe">If -1 is passed, the pipe will find the first available pipe, otherwise it connects to the pipe that was supplied</param>
|
||||
/// <returns></returns>
|
||||
bool Connect(int pipe);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a frame if there is one available. Returns false if there is none. This should be non blocking (aka use a Peek first).
|
||||
/// </summary>
|
||||
/// <param name="frame">The frame that has been read. Will be <code>default(PipeFrame)</code> if it fails to read</param>
|
||||
/// <returns>Returns true if a frame has been read, otherwise false.</returns>
|
||||
bool ReadFrame(out PipeFrame frame);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the frame to the pipe. Returns false if any errors occur.
|
||||
/// </summary>
|
||||
/// <param name="frame">The frame to be written</param>
|
||||
bool WriteFrame(PipeFrame frame);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection
|
||||
/// </summary>
|
||||
void Close();
|
||||
|
||||
}
|
||||
}
|
509
DiscordAPI/IO/ManagedNamedPipeClient.cs
Normal file
509
DiscordAPI/IO/ManagedNamedPipeClient.cs
Normal file
@ -0,0 +1,509 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscordRPC.Logging;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// A named pipe client using the .NET framework <see cref="NamedPipeClientStream"/>
|
||||
/// </summary>
|
||||
public sealed class ManagedNamedPipeClient : INamedPipeClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Name format of the pipe
|
||||
/// </summary>
|
||||
const string PIPE_NAME = @"discord-ipc-{0}";
|
||||
|
||||
/// <summary>
|
||||
/// The logger for the Pipe client to use
|
||||
/// </summary>
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the client is connected
|
||||
/// </summary>
|
||||
public bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
//This will trigger if the stream is disabled. This should prevent the lock check
|
||||
if (_isClosed) return false;
|
||||
lock (l_stream)
|
||||
{
|
||||
//We cannot be sure its still connected, so lets double check
|
||||
return _stream != null && _stream.IsConnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The pipe we are currently connected too.
|
||||
/// </summary>
|
||||
public int ConnectedPipe { get { return _connectedPipe; } }
|
||||
|
||||
private int _connectedPipe;
|
||||
private NamedPipeClientStream _stream;
|
||||
|
||||
private byte[] _buffer = new byte[PipeFrame.MAX_SIZE];
|
||||
|
||||
private Queue<PipeFrame> _framequeue = new Queue<PipeFrame>();
|
||||
private object _framequeuelock = new object();
|
||||
|
||||
private volatile bool _isDisposed = false;
|
||||
private volatile bool _isClosed = true;
|
||||
|
||||
private object l_stream = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a Managed NamedPipe client. Doesn't connect to anything yet, just setups the values.
|
||||
/// </summary>
|
||||
public ManagedNamedPipeClient()
|
||||
{
|
||||
_buffer = new byte[PipeFrame.MAX_SIZE];
|
||||
Logger = new NullLogger();
|
||||
_stream = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the pipe
|
||||
/// </summary>
|
||||
/// <param name="pipe"></param>
|
||||
/// <returns></returns>
|
||||
public bool Connect(int pipe)
|
||||
{
|
||||
Logger.Trace("ManagedNamedPipeClient.Connection(" + pipe + ")");
|
||||
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException("NamedPipe");
|
||||
|
||||
if (pipe > 9)
|
||||
throw new ArgumentOutOfRangeException("pipe", "Argument cannot be greater than 9");
|
||||
|
||||
if (pipe < 0)
|
||||
{
|
||||
//Iterate until we connect to a pipe
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
if (AttemptConnection(i) || AttemptConnection(i, true))
|
||||
{
|
||||
BeginReadStream();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Attempt to connect to a specific pipe
|
||||
if (AttemptConnection(pipe) || AttemptConnection(pipe, true))
|
||||
{
|
||||
BeginReadStream();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//We failed to connect
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts a new connection
|
||||
/// </summary>
|
||||
/// <param name="pipe">The pipe number to connect too.</param>
|
||||
/// <param name="isSandbox">Should the connection to a sandbox be attempted?</param>
|
||||
/// <returns></returns>
|
||||
private bool AttemptConnection(int pipe, bool isSandbox = false)
|
||||
{
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException("_stream");
|
||||
|
||||
//If we are sandbox but we dont support sandbox, then skip
|
||||
string sandbox = isSandbox ? GetPipeSandbox() : "";
|
||||
if (isSandbox && sandbox == null)
|
||||
{
|
||||
Logger.Trace("Skipping sandbox connection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Prepare the pipename
|
||||
Logger.Trace("Connection Attempt " + pipe + " (" + sandbox + ")");
|
||||
string pipename = GetPipeName(pipe, sandbox);
|
||||
|
||||
try
|
||||
{
|
||||
//Create the client
|
||||
lock (l_stream)
|
||||
{
|
||||
Logger.Info("Attempting to connect to " + pipename);
|
||||
_stream = new NamedPipeClientStream(".", pipename, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
_stream.Connect(1000);
|
||||
|
||||
//Spin for a bit while we wait for it to finish connecting
|
||||
Logger.Trace("Waiting for connection...");
|
||||
do { Thread.Sleep(10); } while (!_stream.IsConnected);
|
||||
}
|
||||
|
||||
//Store the value
|
||||
Logger.Info("Connected to " + pipename);
|
||||
_connectedPipe = pipe;
|
||||
_isClosed = false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//Something happened, try again
|
||||
//TODO: Log the failure condition
|
||||
Logger.Error("Failed connection to {0}. {1}", pipename, e.Message);
|
||||
Close();
|
||||
}
|
||||
|
||||
Logger.Trace("Done. Result: {0}", _isClosed);
|
||||
return !_isClosed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a read. Can be executed in another thread.
|
||||
/// </summary>
|
||||
private void BeginReadStream()
|
||||
{
|
||||
if (_isClosed) return;
|
||||
try
|
||||
{
|
||||
lock (l_stream)
|
||||
{
|
||||
//Make sure the stream is valid
|
||||
if (_stream == null || !_stream.IsConnected) return;
|
||||
|
||||
Logger.Trace("Begining Read of {0} bytes", _buffer.Length);
|
||||
_stream.BeginRead(_buffer, 0, _buffer.Length, new AsyncCallback(EndReadStream), _stream.IsConnected);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Logger.Warning("Attempted to start reading from a disposed pipe");
|
||||
return;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
//The pipe has been closed
|
||||
Logger.Warning("Attempted to start reading from a closed pipe");
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("An exception occured while starting to read a stream: {0}", e.Message);
|
||||
Logger.Error(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a read. Can be executed in another thread.
|
||||
/// </summary>
|
||||
/// <param name="callback"></param>
|
||||
private void EndReadStream(IAsyncResult callback)
|
||||
{
|
||||
Logger.Trace("Ending Read");
|
||||
int bytes = 0;
|
||||
|
||||
try
|
||||
{
|
||||
//Attempt to read the bytes, catching for IO exceptions or dispose exceptions
|
||||
lock (l_stream)
|
||||
{
|
||||
//Make sure the stream is still valid
|
||||
if (_stream == null || !_stream.IsConnected) return;
|
||||
|
||||
//Read our btyes
|
||||
bytes = _stream.EndRead(callback);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
Logger.Warning("Attempted to end reading from a closed pipe");
|
||||
return;
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
Logger.Warning("Attempted to read from a null pipe");
|
||||
return;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Logger.Warning("Attemped to end reading from a disposed pipe");
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("An exception occured while ending a read of a stream: {0}", e.Message);
|
||||
Logger.Error(e.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
//How much did we read?
|
||||
Logger.Trace("Read {0} bytes", bytes);
|
||||
|
||||
//Did we read anything? If we did we should enqueue it.
|
||||
if (bytes > 0)
|
||||
{
|
||||
//Load it into a memory stream and read the frame
|
||||
using (MemoryStream memory = new MemoryStream(_buffer, 0, bytes))
|
||||
{
|
||||
try
|
||||
{
|
||||
PipeFrame frame = new PipeFrame();
|
||||
if (frame.ReadStream(memory))
|
||||
{
|
||||
Logger.Trace("Read a frame: {0}", frame.Opcode);
|
||||
|
||||
//Enqueue the stream
|
||||
lock (_framequeuelock)
|
||||
_framequeue.Enqueue(frame);
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: Enqueue a pipe close event here as we failed to read something.
|
||||
Logger.Error("Pipe failed to read from the data received by the stream.");
|
||||
Close();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("A exception has occured while trying to parse the pipe data: " + e.Message);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//If we read 0 bytes, its probably a broken pipe. However, I have only confirmed this is the case for MacOSX.
|
||||
// I have added this check here just so the Windows builds are not effected and continue to work as expected.
|
||||
if (IsUnix())
|
||||
{
|
||||
Logger.Error("Empty frame was read on " + Environment.OSVersion.ToString() + ", aborting.");
|
||||
Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning("Empty frame was read. Please send report to Lachee.");
|
||||
}
|
||||
}
|
||||
|
||||
//We are still connected, so continue to read
|
||||
if (!_isClosed && IsConnected)
|
||||
{
|
||||
Logger.Trace("Starting another read");
|
||||
BeginReadStream();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a frame, returning false if none are available
|
||||
/// </summary>
|
||||
/// <param name="frame"></param>
|
||||
/// <returns></returns>
|
||||
public bool ReadFrame(out PipeFrame frame)
|
||||
{
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException("_stream");
|
||||
|
||||
//Check the queue, returning the pipe if we have anything available. Otherwise null.
|
||||
lock (_framequeuelock)
|
||||
{
|
||||
if (_framequeue.Count == 0)
|
||||
{
|
||||
//We found nothing, so just default and return null
|
||||
frame = default(PipeFrame);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Return the dequed frame
|
||||
frame = _framequeue.Dequeue();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a frame to the pipe
|
||||
/// </summary>
|
||||
/// <param name="frame"></param>
|
||||
/// <returns></returns>
|
||||
public bool WriteFrame(PipeFrame frame)
|
||||
{
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException("_stream");
|
||||
|
||||
//Write the frame. We are assuming proper duplex connection here
|
||||
if (_isClosed || !IsConnected)
|
||||
{
|
||||
Logger.Error("Failed to write frame because the stream is closed");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//Write the pipe
|
||||
//This can only happen on the main thread so it should be fine.
|
||||
frame.WriteStream(_stream);
|
||||
return true;
|
||||
}
|
||||
catch (IOException io)
|
||||
{
|
||||
Logger.Error("Failed to write frame because of a IO Exception: {0}", io.Message);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Logger.Warning("Failed to write frame as the stream was already disposed");
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Logger.Warning("Failed to write frame because of a invalid operation");
|
||||
}
|
||||
|
||||
//We must have failed the try catch
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the pipe
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
//If we are already closed, jsut exit
|
||||
if (_isClosed)
|
||||
{
|
||||
Logger.Warning("Tried to close a already closed pipe.");
|
||||
return;
|
||||
}
|
||||
|
||||
//flush and dispose
|
||||
try
|
||||
{
|
||||
//Wait for the stream object to become available.
|
||||
lock (l_stream)
|
||||
{
|
||||
if (_stream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Stream isn't null, so flush it and then dispose of it.\
|
||||
// We are doing a catch here because it may throw an error during this process and we dont care if it fails.
|
||||
_stream.Flush();
|
||||
_stream.Dispose();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//We caught an error, but we dont care anyways because we are disposing of the stream.
|
||||
}
|
||||
|
||||
//Make the stream null and set our flag.
|
||||
_stream = null;
|
||||
_isClosed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//The stream is already null?
|
||||
Logger.Warning("Stream was closed, but no stream was available to begin with!");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
//ITs already been disposed
|
||||
Logger.Warning("Tried to dispose already disposed stream");
|
||||
}
|
||||
finally
|
||||
{
|
||||
//For good measures, we will mark the pipe as closed anyways
|
||||
_isClosed = true;
|
||||
_connectedPipe = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the stream
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
//Prevent double disposing
|
||||
if (_isDisposed) return;
|
||||
|
||||
//Close the stream (disposing of it too)
|
||||
if (!_isClosed) Close();
|
||||
|
||||
//Dispose of the stream if it hasnt been destroyed already.
|
||||
lock (l_stream)
|
||||
{
|
||||
if (_stream != null)
|
||||
{
|
||||
_stream.Dispose();
|
||||
_stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
//Set our dispose flag
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a platform specific path that Discord is hosting the IPC on.
|
||||
/// </summary>
|
||||
/// <param name="pipe">The pipe number.</param>
|
||||
/// <param name="sandbox">The sandbox the pipe is in. Leave blank for no sandbox.</param>
|
||||
/// <returns></returns>
|
||||
public static string GetPipeName(int pipe, string sandbox = "")
|
||||
{
|
||||
if (!IsUnix()) return sandbox + string.Format(PIPE_NAME, pipe);
|
||||
return Path.Combine(GetTemporaryDirectory(), sandbox + string.Format(PIPE_NAME, pipe));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the possible sandbox enviroment the pipe might be located within. If the platform doesn't support sandboxed Discord, then it will return null.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string GetPipeSandbox()
|
||||
{
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
default:
|
||||
return null;
|
||||
case PlatformID.Unix:
|
||||
return "snap.discord/";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the temporary path for the current enviroment. Only applicable for UNIX based systems.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static string GetTemporaryDirectory()
|
||||
{
|
||||
string temp = null;
|
||||
temp = temp ?? Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
|
||||
temp = temp ?? Environment.GetEnvironmentVariable("TMPDIR");
|
||||
temp = temp ?? Environment.GetEnvironmentVariable("TMP");
|
||||
temp = temp ?? Environment.GetEnvironmentVariable("TEMP");
|
||||
temp = temp ?? "/tmp";
|
||||
return temp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current OS platform is Unix based (Unix or MacOSX).
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static bool IsUnix()
|
||||
{
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
default:
|
||||
return false;
|
||||
|
||||
case PlatformID.Unix:
|
||||
case PlatformID.MacOSX:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
DiscordAPI/IO/Opcode.cs
Normal file
33
DiscordAPI/IO/Opcode.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// The operation code that the <see cref="PipeFrame"/> was sent under. This defines the type of frame and the data to expect.
|
||||
/// </summary>
|
||||
public enum Opcode : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// Initial handshake frame
|
||||
/// </summary>
|
||||
Handshake = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Generic message frame
|
||||
/// </summary>
|
||||
Frame = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Discord has closed the connection
|
||||
/// </summary>
|
||||
Close = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Ping frame (not used?)
|
||||
/// </summary>
|
||||
Ping = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Pong frame (not used?)
|
||||
/// </summary>
|
||||
Pong = 4
|
||||
}
|
||||
}
|
204
DiscordAPI/IO/PipeFrame.cs
Normal file
204
DiscordAPI/IO/PipeFrame.cs
Normal file
@ -0,0 +1,204 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// A frame received and sent to the Discord client for RPC communications.
|
||||
/// </summary>
|
||||
public struct PipeFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// The maxium size of a pipe frame (16kb).
|
||||
/// </summary>
|
||||
public static readonly int MAX_SIZE = 16 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// The opcode of the frame
|
||||
/// </summary>
|
||||
public Opcode Opcode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the frame data
|
||||
/// </summary>
|
||||
public uint Length { get { return (uint) Data.Length; } }
|
||||
|
||||
/// <summary>
|
||||
/// The data in the frame
|
||||
/// </summary>
|
||||
public byte[] Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data represented as a string.
|
||||
/// </summary>
|
||||
public string Message
|
||||
{
|
||||
get { return GetMessage(); }
|
||||
set { SetMessage(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new pipe frame instance
|
||||
/// </summary>
|
||||
/// <param name="opcode">The opcode of the frame</param>
|
||||
/// <param name="data">The data of the frame that will be serialized as JSON</param>
|
||||
public PipeFrame(Opcode opcode, object data)
|
||||
{
|
||||
//Set the opcode and a temp field for data
|
||||
Opcode = opcode;
|
||||
Data = null;
|
||||
|
||||
//Set the data
|
||||
SetObject(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the encoding used for the pipe frames
|
||||
/// </summary>
|
||||
public Encoding MessageEncoding { get { return Encoding.UTF8; } }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the data based of a string
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
private void SetMessage(string str) { Data = MessageEncoding.GetBytes(str); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string based of the data
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private string GetMessage() { return MessageEncoding.GetString(Data); }
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the object into json string then encodes it into <see cref="Data"/>.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public void SetObject(object obj)
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(obj);
|
||||
SetMessage(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the opcodes and serializes the object into a json string.
|
||||
/// </summary>
|
||||
/// <param name="opcode"></param>
|
||||
/// <param name="obj"></param>
|
||||
public void SetObject(Opcode opcode, object obj)
|
||||
{
|
||||
Opcode = opcode;
|
||||
SetObject(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the data into the supplied type using JSON.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetObject<T>()
|
||||
{
|
||||
string json = GetMessage();
|
||||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to read the contents of the frame from the stream
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns minimum value between a int and a unsigned int
|
||||
/// </summary>
|
||||
private int Min(int a, uint b)
|
||||
{
|
||||
if (b >= a) return a;
|
||||
return (int) b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to read a UInt32
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the frame into the target frame as one big byte block.
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
21
DiscordAPI/LICENSE.txt
Normal file
21
DiscordAPI/LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Lachee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
101
DiscordAPI/Logging/ConsoleLogger.cs
Normal file
101
DiscordAPI/Logging/ConsoleLogger.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs the outputs to the console using <see cref="Console.WriteLine()"/>
|
||||
/// </summary>
|
||||
public class ConsoleLogger : ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of logging to apply to this logger.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the output be coloured?
|
||||
/// </summary>
|
||||
public bool Coloured { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A alias too <see cref="Coloured"/>
|
||||
/// </summary>
|
||||
public bool Colored { get { return Coloured; } set { Coloured = value; } }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a Console Logger.
|
||||
/// </summary>
|
||||
public ConsoleLogger()
|
||||
{
|
||||
this.Level = LogLevel.Info;
|
||||
Coloured = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a Console Logger with a set log level
|
||||
/// </summary>
|
||||
/// <param name="level"></param>
|
||||
/// <param name="coloured"></param>
|
||||
public ConsoleLogger(LogLevel level, bool coloured = false)
|
||||
{
|
||||
Level = level;
|
||||
Coloured = coloured;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Trace(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Trace) return;
|
||||
|
||||
if (Coloured) Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.WriteLine("TRACE: " + message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Info(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Info) return;
|
||||
|
||||
if (Coloured) Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine("INFO: " + message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Warning(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Warning) return;
|
||||
|
||||
if (Coloured) Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("WARN: " + message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error log messsages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Error(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Error) return;
|
||||
|
||||
if (Coloured) Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("ERR : " + message, args);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
92
DiscordAPI/Logging/FileLogger.cs
Normal file
92
DiscordAPI/Logging/FileLogger.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs the outputs to a file
|
||||
/// </summary>
|
||||
public class FileLogger : ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of logging to apply to this logger.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the output be coloured?
|
||||
/// </summary>
|
||||
public string File { get; set; }
|
||||
|
||||
private object filelock;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the file logger
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the log file.</param>
|
||||
public FileLogger(string path)
|
||||
: this(path, LogLevel.Info) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the file logger
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the log file.</param>
|
||||
/// <param name="level">The level to assign to the logger.</param>
|
||||
public FileLogger(string path, LogLevel level)
|
||||
{
|
||||
Level = level;
|
||||
File = path;
|
||||
filelock = new object();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error log messsages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
46
DiscordAPI/Logging/ILogger.cs
Normal file
46
DiscordAPI/Logging/ILogger.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of logging to apply to this logger.
|
||||
/// </summary>
|
||||
LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Debug trace messeages used for debugging internal elements.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
void Trace(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
void Info(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Warning log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
void Warning(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Error log messsages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
void Error(string message, params object[] args);
|
||||
}
|
||||
}
|
38
DiscordAPI/Logging/LogLevel.cs
Normal file
38
DiscordAPI/Logging/LogLevel.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Level of logging to use.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Trace, Info, Warning and Errors are logged
|
||||
/// </summary>
|
||||
Trace = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Info, Warning and Errors are logged
|
||||
/// </summary>
|
||||
Info = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Warning and Errors are logged
|
||||
/// </summary>
|
||||
Warning = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Only Errors are logged
|
||||
/// </summary>
|
||||
Error = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Nothing is logged
|
||||
/// </summary>
|
||||
None = 256
|
||||
}
|
||||
}
|
58
DiscordAPI/Logging/NullLogger.cs
Normal file
58
DiscordAPI/Logging/NullLogger.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Ignores all log events
|
||||
/// </summary>
|
||||
public class NullLogger : ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of logging to apply to this logger.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Trace(string message, params object[] args)
|
||||
{
|
||||
//Null Logger, so no messages are acutally sent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Info(string message, params object[] args)
|
||||
{
|
||||
//Null Logger, so no messages are acutally sent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Warning(string message, params object[] args)
|
||||
{
|
||||
//Null Logger, so no messages are acutally sent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error log messsages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Error(string message, params object[] args)
|
||||
{
|
||||
//Null Logger, so no messages are acutally sent
|
||||
}
|
||||
}
|
||||
}
|
26
DiscordAPI/Message/CloseMessage.cs
Normal file
26
DiscordAPI/Message/CloseMessage.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the IPC has closed.
|
||||
/// </summary>
|
||||
public class CloseMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Close; } }
|
||||
|
||||
/// <summary>
|
||||
/// The reason for the close
|
||||
/// </summary>
|
||||
public string Reason { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The closure code
|
||||
/// </summary>
|
||||
public int Code { get; internal set; }
|
||||
|
||||
internal CloseMessage() { }
|
||||
internal CloseMessage(string reason) { this.Reason = reason; }
|
||||
}
|
||||
}
|
24
DiscordAPI/Message/ConnectionEstablishedMessage.cs
Normal file
24
DiscordAPI/Message/ConnectionEstablishedMessage.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection to the discord client was succesfull. This is called before <see cref="MessageType.Ready"/>.
|
||||
/// </summary>
|
||||
public class ConnectionEstablishedMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.ConnectionEstablished; } }
|
||||
|
||||
/// <summary>
|
||||
/// The pipe we ended up connecting too
|
||||
/// </summary>
|
||||
public int ConnectedPipe { get; internal set; }
|
||||
}
|
||||
}
|
24
DiscordAPI/Message/ConnectionFailedMessage.cs
Normal file
24
DiscordAPI/Message/ConnectionFailedMessage.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Failed to establish any connection with discord. Discord is potentially not running?
|
||||
/// </summary>
|
||||
public class ConnectionFailedMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.ConnectionFailed; } }
|
||||
|
||||
/// <summary>
|
||||
/// The pipe we failed to connect too.
|
||||
/// </summary>
|
||||
public int FailedPipe { get; internal set; }
|
||||
}
|
||||
}
|
76
DiscordAPI/Message/ErrorMessage.cs
Normal file
76
DiscordAPI/Message/ErrorMessage.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Created when a error occurs within the ipc and it is sent to the client.
|
||||
/// </summary>
|
||||
public class ErrorMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Error; } }
|
||||
|
||||
/// <summary>
|
||||
/// The Discord error code.
|
||||
/// </summary>
|
||||
[JsonProperty("code")]
|
||||
public ErrorCode Code { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message associated with the error code.
|
||||
/// </summary>
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; internal set; }
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The error message received by discord. See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-errors for documentation
|
||||
/// </summary>
|
||||
public enum ErrorCode
|
||||
{
|
||||
//Pipe Error Codes
|
||||
/// <summary> Pipe was Successful </summary>
|
||||
Success = 0,
|
||||
|
||||
///<summary>The pipe had an exception</summary>
|
||||
PipeException = 1,
|
||||
|
||||
///<summary>The pipe received corrupted data</summary>
|
||||
ReadCorrupt = 2,
|
||||
|
||||
//Custom Error Code
|
||||
///<summary>The functionality was not yet implemented</summary>
|
||||
NotImplemented = 10,
|
||||
|
||||
//Discord RPC error codes
|
||||
///<summary>Unkown Discord error</summary>
|
||||
UnkownError = 1000,
|
||||
|
||||
///<summary>Invalid Payload received</summary>
|
||||
InvalidPayload = 4000,
|
||||
|
||||
///<summary>Invalid command was sent</summary>
|
||||
InvalidCommand = 4002,
|
||||
|
||||
/// <summary>Invalid event was sent </summary>
|
||||
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
|
||||
*/
|
||||
}
|
||||
}
|
29
DiscordAPI/Message/IMessage.cs
Normal file
29
DiscordAPI/Message/IMessage.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Messages received from discord.
|
||||
/// </summary>
|
||||
public abstract class IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public abstract MessageType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the message was created
|
||||
/// </summary>
|
||||
public DateTime TimeCreated { get { return _timecreated; } }
|
||||
private DateTime _timecreated;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the message
|
||||
/// </summary>
|
||||
public IMessage()
|
||||
{
|
||||
_timecreated = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
25
DiscordAPI/Message/JoinMessage.cs
Normal file
25
DiscordAPI/Message/JoinMessage.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to join a game. D -> C.
|
||||
/// </summary>
|
||||
public class JoinMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Join; } }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Secrets.JoinSecret" /> to connect with.
|
||||
/// </summary>
|
||||
[JsonProperty("secret")]
|
||||
public string Secret { get; internal set; }
|
||||
}
|
||||
}
|
25
DiscordAPI/Message/JoinRequestMessage.cs
Normal file
25
DiscordAPI/Message/JoinRequestMessage.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when some other person has requested access to this game. C -> D -> C.
|
||||
/// </summary>
|
||||
public class JoinRequestMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.JoinRequest; } }
|
||||
|
||||
/// <summary>
|
||||
/// The discord user that is requesting access.
|
||||
/// </summary>
|
||||
[JsonProperty("user")]
|
||||
public User User { get; internal set; }
|
||||
}
|
||||
}
|
64
DiscordAPI/Message/MessageType.cs
Normal file
64
DiscordAPI/Message/MessageType.cs
Normal file
@ -0,0 +1,64 @@
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of message.
|
||||
/// </summary>
|
||||
public enum MessageType
|
||||
{
|
||||
/// <summary>
|
||||
/// The Discord Client is ready to send and receive messages.
|
||||
/// </summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Close,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client has updated the presence.
|
||||
/// </summary>
|
||||
PresenceUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client has subscribed to an event.
|
||||
/// </summary>
|
||||
Subscribe,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client has unsubscribed from an event.
|
||||
/// </summary>
|
||||
Unsubscribe,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client wishes for this process to join a game.
|
||||
/// </summary>
|
||||
Join,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client wishes for this process to spectate a game.
|
||||
/// </summary>
|
||||
Spectate,
|
||||
|
||||
/// <summary>
|
||||
/// Another discord user requests permission to join this game.
|
||||
/// </summary>
|
||||
JoinRequest,
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the discord client was succesfull. This is called before <see cref="Ready"/>.
|
||||
/// </summary>
|
||||
ConnectionEstablished,
|
||||
|
||||
/// <summary>
|
||||
/// Failed to establish any connection with discord. Discord is potentially not running?
|
||||
/// </summary>
|
||||
ConnectionFailed
|
||||
}
|
||||
}
|
47
DiscordAPI/Message/PresenceMessage.cs
Normal file
47
DiscordAPI/Message/PresenceMessage.cs
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Representation of the message received by discord when the presence has been updated.
|
||||
/// </summary>
|
||||
public class PresenceMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The rich presence Discord has set
|
||||
/// </summary>
|
||||
public RichPresence Presence { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the application Discord has set it for
|
||||
/// </summary>
|
||||
public string Name { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the application discord has set it for
|
||||
/// </summary>
|
||||
public string ApplicationID { get; internal set; }
|
||||
}
|
||||
}
|
35
DiscordAPI/Message/ReadyMessage.cs
Normal file
35
DiscordAPI/Message/ReadyMessage.cs
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the ipc is ready to send arguments.
|
||||
/// </summary>
|
||||
public class ReadyMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Ready; } }
|
||||
|
||||
/// <summary>
|
||||
/// The configuration of the connection
|
||||
/// </summary>
|
||||
[JsonProperty("config")]
|
||||
public Configuration Configuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User the connection belongs too
|
||||
/// </summary>
|
||||
[JsonProperty("user")]
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the RPC
|
||||
/// </summary>
|
||||
[JsonProperty("v")]
|
||||
public int Version { get; set; }
|
||||
}
|
||||
}
|
19
DiscordAPI/Message/SpectateMessage.cs
Normal file
19
DiscordAPI/Message/SpectateMessage.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to spectate a game. D -> C.
|
||||
/// </summary>
|
||||
public class SpectateMessage : JoinMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Spectate; } }
|
||||
}
|
||||
}
|
40
DiscordAPI/Message/SubscribeMessage.cs
Normal file
40
DiscordAPI/Message/SubscribeMessage.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using DiscordRPC.RPC.Payload;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called as validation of a subscribe
|
||||
/// </summary>
|
||||
public class SubscribeMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Subscribe; } }
|
||||
|
||||
/// <summary>
|
||||
/// The event that was subscribed too.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
DiscordAPI/Message/UnsubscribeMsesage.cs
Normal file
39
DiscordAPI/Message/UnsubscribeMsesage.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using DiscordRPC.RPC.Payload;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called as validation of a subscribe
|
||||
/// </summary>
|
||||
public class UnsubscribeMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Unsubscribe; } }
|
||||
|
||||
/// <summary>
|
||||
/// The event that was subscribed too.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
DiscordAPI/RPC/Commands/CloseCommand.cs
Normal file
34
DiscordAPI/RPC/Commands/CloseCommand.cs
Normal file
@ -0,0 +1,34 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The process ID
|
||||
/// </summary>
|
||||
[JsonProperty("pid")]
|
||||
public int PID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rich presence to be set. Can be null.
|
||||
/// </summary>
|
||||
[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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
13
DiscordAPI/RPC/Commands/ICommand.cs
Normal file
13
DiscordAPI/RPC/Commands/ICommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
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);
|
||||
}
|
||||
}
|
32
DiscordAPI/RPC/Commands/PresenceCommand.cs
Normal file
32
DiscordAPI/RPC/Commands/PresenceCommand.cs
Normal file
@ -0,0 +1,32 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The process ID
|
||||
/// </summary>
|
||||
[JsonProperty("pid")]
|
||||
public int PID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rich presence to be set. Can be null.
|
||||
/// </summary>
|
||||
[JsonProperty("activity")]
|
||||
public RichPresence Presence { get; set; }
|
||||
|
||||
public IPayload PreparePayload(long nonce)
|
||||
{
|
||||
return new ArgumentPayload(this, nonce)
|
||||
{
|
||||
Command = Command.SetActivity
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
32
DiscordAPI/RPC/Commands/RespondCommand.cs
Normal file
32
DiscordAPI/RPC/Commands/RespondCommand.cs
Normal file
@ -0,0 +1,32 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The user ID that we are accepting / rejecting
|
||||
/// </summary>
|
||||
[JsonProperty("user_id")]
|
||||
public string UserID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the user will be allowed to connect.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool Accept { get; set; }
|
||||
|
||||
public IPayload PreparePayload(long nonce)
|
||||
{
|
||||
return new ArgumentPayload(this, nonce)
|
||||
{
|
||||
Command = Accept ? Command.SendActivityJoinInvite : Command.CloseActivityJoinRequest
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
23
DiscordAPI/RPC/Commands/SubscribeCommand.cs
Normal file
23
DiscordAPI/RPC/Commands/SubscribeCommand.cs
Normal file
@ -0,0 +1,23 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
23
DiscordAPI/RPC/Payload/ClosePayload.cs
Normal file
23
DiscordAPI/RPC/Payload/ClosePayload.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
internal class ClosePayload : IPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The close code the discord gave us
|
||||
/// </summary>
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The close reason discord gave us
|
||||
/// </summary>
|
||||
[JsonProperty("message")]
|
||||
public string Reason { get; set; }
|
||||
}
|
||||
}
|
129
DiscordAPI/RPC/Payload/Command.cs
Normal file
129
DiscordAPI/RPC/Payload/Command.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using DiscordRPC.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// The possible commands that can be sent and received by the server.
|
||||
/// </summary>
|
||||
internal enum Command
|
||||
{
|
||||
/// <summary>
|
||||
/// event dispatch
|
||||
/// </summary>
|
||||
[EnumValue("DISPATCH")]
|
||||
Dispatch,
|
||||
|
||||
/// <summary>
|
||||
/// Called to set the activity
|
||||
/// </summary>
|
||||
[EnumValue("SET_ACTIVITY")]
|
||||
SetActivity,
|
||||
|
||||
/// <summary>
|
||||
/// used to subscribe to an RPC event
|
||||
/// </summary>
|
||||
[EnumValue("SUBSCRIBE")]
|
||||
Subscribe,
|
||||
|
||||
/// <summary>
|
||||
/// used to unsubscribe from an RPC event
|
||||
/// </summary>
|
||||
[EnumValue("UNSUBSCRIBE")]
|
||||
Unsubscribe,
|
||||
|
||||
/// <summary>
|
||||
/// Used to accept join requests.
|
||||
/// </summary>
|
||||
[EnumValue("SEND_ACTIVITY_JOIN_INVITE")]
|
||||
SendActivityJoinInvite,
|
||||
|
||||
/// <summary>
|
||||
/// Used to reject join requests.
|
||||
/// </summary>
|
||||
[EnumValue("CLOSE_ACTIVITY_JOIN_REQUEST")]
|
||||
CloseActivityJoinRequest,
|
||||
|
||||
/// <summary>
|
||||
/// used to authorize a new client with your app
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
Authorize,
|
||||
|
||||
/// <summary>
|
||||
/// used to authenticate an existing client with your app
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
Authenticate,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve guild information from the client
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetGuild,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve a list of guilds from the client
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetGuilds,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve channel information from the client
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetChannel,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve a list of channels for a guild from the client
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetChannels,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// used to change voice settings of users in voice channels
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
SetUserVoiceSettings,
|
||||
|
||||
/// <summary>
|
||||
/// used to join or leave a voice channel, group dm, or dm
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
SelectVoiceChannel,
|
||||
|
||||
/// <summary>
|
||||
/// used to get the current voice channel the client is in
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetSelectedVoiceChannel,
|
||||
|
||||
/// <summary>
|
||||
/// used to join or leave a text channel, group dm, or dm
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
SelectTextChannel,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve the client's voice settings
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetVoiceSettings,
|
||||
|
||||
/// <summary>
|
||||
/// used to set the client's voice settings
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
SetVoiceSettings,
|
||||
|
||||
/// <summary>
|
||||
/// used to capture a keyboard shortcut entered by the user RPC Events
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
CaptureShortcut
|
||||
}
|
||||
}
|
35
DiscordAPI/RPC/Payload/IPayload.cs
Normal file
35
DiscordAPI/RPC/Payload/IPayload.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using DiscordRPC.Converters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// Base Payload that is received by both client and server
|
||||
/// </summary>
|
||||
internal abstract class IPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of payload
|
||||
/// </summary>
|
||||
[JsonProperty("cmd"), JsonConverter(typeof(EnumSnakeCaseConverter))]
|
||||
public Command Command { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A incremental value to help identify payloads
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
53
DiscordAPI/RPC/Payload/PayloadArgument.cs
Normal file
53
DiscordAPI/RPC/Payload/PayloadArgument.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using DiscordRPC.Converters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// The payload that is sent by the client to discord for events such as setting the rich presence.
|
||||
/// <para>
|
||||
/// SetPrecense
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class ArgumentPayload : IPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The data the server sent too us
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the obejct stored within the data.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public void SetObject(object obj)
|
||||
{
|
||||
Arguments = JObject.FromObject(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object stored within the Data
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetObject<T>()
|
||||
{
|
||||
return Arguments.ToObject<T>();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "Argument " + base.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
57
DiscordAPI/RPC/Payload/PayloadEvent.cs
Normal file
57
DiscordAPI/RPC/Payload/PayloadEvent.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using DiscordRPC.Converters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for Discord IPC Events
|
||||
/// </summary>
|
||||
internal class EventPayload : IPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The data the server sent too us
|
||||
/// </summary>
|
||||
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public JObject Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of event the server sent
|
||||
/// </summary>
|
||||
[JsonProperty("evt"), JsonConverter(typeof(EnumSnakeCaseConverter))]
|
||||
public ServerEvent? Event { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a payload with empty data
|
||||
/// </summary>
|
||||
public EventPayload() : base() { Data = null; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a payload with empty data and a set nonce
|
||||
/// </summary>
|
||||
/// <param name="nonce"></param>
|
||||
public EventPayload(long nonce) : base(nonce) { Data = null; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object stored within the Data
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetObject<T>()
|
||||
{
|
||||
if (Data == null) return default(T);
|
||||
return Data.ToObject<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the object into a human readable string
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return "Event " + base.ToString() + ", Event: " + (Event.HasValue ? Event.ToString() : "N/A");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
95
DiscordAPI/RPC/Payload/ServerEvent.cs
Normal file
95
DiscordAPI/RPC/Payload/ServerEvent.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using DiscordRPC.Converters;
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-events for documentation
|
||||
/// </summary>
|
||||
internal enum ServerEvent
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Sent when the server is ready to accept messages
|
||||
/// </summary>
|
||||
[EnumValue("READY")]
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// Sent when something bad has happened
|
||||
/// </summary>
|
||||
[EnumValue("ERROR")]
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// Join Event
|
||||
/// </summary>
|
||||
[EnumValue("ACTIVITY_JOIN")]
|
||||
ActivityJoin,
|
||||
|
||||
/// <summary>
|
||||
/// Spectate Event
|
||||
/// </summary>
|
||||
[EnumValue("ACTIVITY_SPECTATE")]
|
||||
ActivitySpectate,
|
||||
|
||||
/// <summary>
|
||||
/// Request Event
|
||||
/// </summary>
|
||||
[EnumValue("ACTIVITY_JOIN_REQUEST")]
|
||||
ActivityJoinRequest,
|
||||
|
||||
#if INCLUDE_FULL_RPC
|
||||
//Old things that are obsolete
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("GUILD_STATUS")]
|
||||
GuildStatus,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("GUILD_CREATE")]
|
||||
GuildCreate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("CHANNEL_CREATE")]
|
||||
ChannelCreate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_CHANNEL_SELECT")]
|
||||
VoiceChannelSelect,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_STATE_CREATED")]
|
||||
VoiceStateCreated,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_STATE_UPDATED")]
|
||||
VoiceStateUpdated,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_STATE_DELETE")]
|
||||
VoiceStateDelete,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_SETTINGS_UPDATE")]
|
||||
VoiceSettingsUpdate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_CONNECTION_STATUS")]
|
||||
VoiceConnectionStatus,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("SPEAKING_START")]
|
||||
SpeakingStart,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("SPEAKING_STOP")]
|
||||
SpeakingStop,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("MESSAGE_CREATE")]
|
||||
MessageCreate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("MESSAGE_UPDATE")]
|
||||
MessageUpdate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("MESSAGE_DELETE")]
|
||||
MessageDelete,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("NOTIFICATION_CREATE")]
|
||||
NotificationCreate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("CAPTURE_SHORTCUT_CHANGE")]
|
||||
CaptureShortcutChange
|
||||
#endif
|
||||
}
|
||||
}
|
869
DiscordAPI/RPC/RpcConnection.cs
Normal file
869
DiscordAPI/RPC/RpcConnection.cs
Normal file
@ -0,0 +1,869 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Communicates between the client and discord through RPC
|
||||
/// </summary>
|
||||
internal class RpcConnection : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the RPC Protocol
|
||||
/// </summary>
|
||||
public static readonly int VERSION = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The rate of poll to the discord pipe.
|
||||
/// </summary>
|
||||
public static readonly int POLL_RATE = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Should we send a null presence on the fairwells?
|
||||
/// </summary>
|
||||
private static readonly bool CLEAR_ON_SHUTDOWN = true;
|
||||
|
||||
/// <summary>
|
||||
/// Should we work in a lock step manner? This option is semi-obsolete and may not work as expected.
|
||||
/// </summary>
|
||||
private static readonly bool LOCK_STEP = false;
|
||||
|
||||
/// <summary>
|
||||
/// The logger used by the RPC connection
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get { return _logger; }
|
||||
set
|
||||
{
|
||||
_logger = value;
|
||||
if (namedPipe != null)
|
||||
namedPipe.Logger = value;
|
||||
}
|
||||
}
|
||||
private ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public event OnRpcMessageEvent OnRpcMessage;
|
||||
|
||||
#region States
|
||||
|
||||
/// <summary>
|
||||
/// The current state of the RPC connection
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
/// The configuration received by the Ready
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the RPC connection is still running in the background
|
||||
/// </summary>
|
||||
public bool IsRunning { get { return thread != null; } }
|
||||
|
||||
/// <summary>
|
||||
/// Forces the <see cref="Close"/> to call <see cref="Shutdown"/> instead, safely saying goodbye to Discord.
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
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<ICommand> _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<IMessage> _rxqueue; //The receive queue
|
||||
|
||||
private AutoResetEvent queueUpdatedEvent = new AutoResetEvent(false);
|
||||
private BackoffDelay delay; //The backoff delay before reconnecting.
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the RPC.
|
||||
/// </summary>
|
||||
/// <param name="applicationID">The ID of the Discord App</param>
|
||||
/// <param name="processID">The ID of the currently running process</param>
|
||||
/// <param name="targetPipe">The target pipe to connect too</param>
|
||||
/// <param name="client">The pipe client we shall use.</param>
|
||||
/// <param name="maxRxQueueSize">The maximum size of the out queue</param>
|
||||
/// <param name="maxRtQueueSize">The maximum size of the in queue</param>
|
||||
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<ICommand>((int)_maxRtQueueSize + 1);
|
||||
|
||||
_maxRxQueueSize = maxRxQueueSize;
|
||||
_rxqueue = new Queue<IMessage>((int)_maxRxQueueSize + 1);
|
||||
|
||||
nonce = 0;
|
||||
}
|
||||
|
||||
|
||||
private long GetNextNonce()
|
||||
{
|
||||
nonce += 1;
|
||||
return nonce;
|
||||
}
|
||||
|
||||
#region Queues
|
||||
/// <summary>
|
||||
/// Enqueues a command
|
||||
/// </summary>
|
||||
/// <param name="command">The command to enqueue</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to the message queue. Does not copy the message, so besure to copy it yourself or dereference it.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to add</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues a single message from the event stack. Returns null if none are available.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues all messages from the event stack.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Main thread loop
|
||||
/// </summary>
|
||||
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<ClosePayload>();
|
||||
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<EventPayload>(); } 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
|
||||
|
||||
/// <summary>Handles the response from the pipe and calls appropriate events and changes states.</summary>
|
||||
/// <param name="response">The response received by the server.</param>
|
||||
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<ErrorMessage>();
|
||||
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<ReadyMessage>();
|
||||
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<RichPresenceResponse>();
|
||||
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<EventPayload>().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<SpectateMessage>();
|
||||
EnqueueMessage(spectate);
|
||||
break;
|
||||
|
||||
case ServerEvent.ActivityJoin:
|
||||
var join = response.GetObject<JoinMessage>();
|
||||
EnqueueMessage(join);
|
||||
break;
|
||||
|
||||
case ServerEvent.ActivityJoinRequest:
|
||||
var request = response.GetObject<JoinRequestMessage>();
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Establishes the handshake with the server.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Establishes a fairwell with the server by sending a handwave.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to connect to the pipe. Returns true on success
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current state of the pipe, locking the l_states object for thread saftey.
|
||||
/// </summary>
|
||||
/// <param name="state">The state to set it too.</param>
|
||||
private void SetConnectionState(RpcState state)
|
||||
{
|
||||
Logger.Trace("Setting the connection state to {0}", state.ToString().ToSnakeCase().ToUpperInvariant());
|
||||
lock (l_states)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection and disposes of resources. This will not force termination, but instead allow Discord disconnect us after we say goodbye.
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection and disposes of resources.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection and disposes resources. Identical to <see cref="Close"/> but ignores the "ShutdownOnly" value.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
ShutdownOnly = false;
|
||||
Close();
|
||||
}
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State of the RPC connection
|
||||
/// </summary>
|
||||
internal enum RpcState
|
||||
{
|
||||
/// <summary>
|
||||
/// Disconnected from the discord client
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
|
||||
/// <summary>
|
||||
/// Connecting to the discord client. The handshake has been sent and we are awaiting the ready event
|
||||
/// </summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>
|
||||
/// We are connect to the client and can send and receive messages.
|
||||
/// </summary>
|
||||
Connected
|
||||
}
|
||||
}
|
14
DiscordAPI/Registry/IUriSchemeCreator.cs
Normal file
14
DiscordAPI/Registry/IUriSchemeCreator.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using DiscordRPC.Logging;
|
||||
|
||||
namespace DiscordRPC.Registry
|
||||
{
|
||||
internal interface IUriSchemeCreator
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the URI scheme. If Steam ID is passed, the application will be launched through steam instead of directly.
|
||||
/// <para>Additional arguments can be supplied if required.</para>
|
||||
/// </summary>
|
||||
/// <param name="register">The register context.</param>
|
||||
bool RegisterUriScheme(UriSchemeRegister register);
|
||||
}
|
||||
}
|
54
DiscordAPI/Registry/MacUriSchemeCreator.cs
Normal file
54
DiscordAPI/Registry/MacUriSchemeCreator.cs
Normal file
@ -0,0 +1,54 @@
|
||||
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;
|
||||
}
|
||||
|
||||
public bool RegisterUriScheme(UriSchemeRegister register)
|
||||
{
|
||||
//var home = Environment.GetEnvironmentVariable("HOME");
|
||||
//if (string.IsNullOrEmpty(home)) return; //TODO: Log Error
|
||||
|
||||
string exe = register.ExecutablePath;
|
||||
if (string.IsNullOrEmpty(exe))
|
||||
{
|
||||
logger.Error("Failed to register because the application could not be located.");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Trace("Registering Steam Command");
|
||||
|
||||
//Prepare the command
|
||||
string command = exe;
|
||||
if (register.UsingSteamApp) command = "steam://rungameid/" + register.SteamAppID;
|
||||
else logger.Warning("This library does not fully support MacOS URI Scheme Registration.");
|
||||
|
||||
//get the folder ready
|
||||
string filepath = "~/Library/Application Support/discord/games";
|
||||
var directory = Directory.CreateDirectory(filepath);
|
||||
if (!directory.Exists)
|
||||
{
|
||||
logger.Error("Failed to register because {0} does not exist", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Write the contents to file
|
||||
File.WriteAllText(filepath + "/" + register.ApplicationID + ".json", "{ \"command\": \"" + command + "\" }");
|
||||
logger.Trace("Registered {0}, {1}", filepath + "/" + register.ApplicationID + ".json", command);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
99
DiscordAPI/Registry/UnixUriSchemeCreator.cs
Normal file
99
DiscordAPI/Registry/UnixUriSchemeCreator.cs
Normal file
@ -0,0 +1,99 @@
|
||||
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;
|
||||
}
|
||||
|
||||
public bool RegisterUriScheme(UriSchemeRegister register)
|
||||
{
|
||||
var home = Environment.GetEnvironmentVariable("HOME");
|
||||
if (string.IsNullOrEmpty(home))
|
||||
{
|
||||
logger.Error("Failed to register because the HOME variable was not set.");
|
||||
return false;
|
||||
}
|
||||
|
||||
string exe = register.ExecutablePath;
|
||||
if (string.IsNullOrEmpty(exe))
|
||||
{
|
||||
logger.Error("Failed to register because the application was not located.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Prepare the command
|
||||
string command = null;
|
||||
if (register.UsingSteamApp)
|
||||
{
|
||||
//A steam command isntead
|
||||
command = "xdg-open steam://rungameid/" + register.SteamAppID;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Just a regular discord command
|
||||
command = exe;
|
||||
}
|
||||
|
||||
|
||||
//Prepare the file
|
||||
string desktopFileFormat =
|
||||
@"[Desktop Entry]
|
||||
Name=Game {0}
|
||||
Exec={1} %u
|
||||
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
|
||||
string filename = "/discord-" + register.ApplicationID + ".desktop";
|
||||
string filepath = home + "/.local/share/applications";
|
||||
var directory = Directory.CreateDirectory(filepath);
|
||||
if (!directory.Exists)
|
||||
{
|
||||
logger.Error("Failed to register because {0} does not exist", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Write the file
|
||||
File.WriteAllText(filepath + filename, file);
|
||||
|
||||
//Register the Mime type
|
||||
if (!RegisterMime(register.ApplicationID))
|
||||
{
|
||||
logger.Error("Failed to register because the Mime failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Trace("Registered {0}, {1}, {2}", filepath + filename, file, command);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool RegisterMime(string appid)
|
||||
{
|
||||
//Format the arguments
|
||||
string format = "default discord-{0}.desktop x-scheme-handler/discord-{0}";
|
||||
string arguments = string.Format(format, appid);
|
||||
|
||||
//Run the process and wait for response
|
||||
Process process = Process.Start("xdg-mime", arguments);
|
||||
process.WaitForExit();
|
||||
|
||||
//Return if succesful
|
||||
return process.ExitCode >= 0;
|
||||
}
|
||||
}
|
||||
}
|
89
DiscordAPI/Registry/UriScheme.cs
Normal file
89
DiscordAPI/Registry/UriScheme.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using DiscordRPC.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DiscordRPC.Registry
|
||||
{
|
||||
internal class UriSchemeRegister
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the Discord App to register
|
||||
/// </summary>
|
||||
public string ApplicationID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional Steam App ID to register. If given a value, then the game will launch through steam instead of Discord.
|
||||
/// </summary>
|
||||
public string SteamAppID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this register using steam?
|
||||
/// </summary>
|
||||
public bool UsingSteamApp { get { return !string.IsNullOrEmpty(SteamAppID) && SteamAppID != ""; } }
|
||||
|
||||
/// <summary>
|
||||
/// The full executable path of the application.
|
||||
/// </summary>
|
||||
public string ExecutablePath { get; set; }
|
||||
|
||||
private ILogger _logger;
|
||||
public UriSchemeRegister(ILogger logger, string applicationID, string steamAppID = null, string executable = null)
|
||||
{
|
||||
_logger = logger;
|
||||
ApplicationID = applicationID.Trim();
|
||||
SteamAppID = steamAppID != null ? steamAppID.Trim() : null;
|
||||
ExecutablePath = executable ?? GetApplicationLocation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the URI scheme, using the correct creator for the correct platform
|
||||
/// </summary>
|
||||
public bool RegisterUriScheme()
|
||||
{
|
||||
//Get the creator
|
||||
IUriSchemeCreator creator = null;
|
||||
switch(Environment.OSVersion.Platform)
|
||||
{
|
||||
case PlatformID.Win32Windows:
|
||||
case PlatformID.Win32S:
|
||||
case PlatformID.Win32NT:
|
||||
case PlatformID.WinCE:
|
||||
_logger.Trace("Creating Windows Scheme Creator");
|
||||
creator = new WindowsUriSchemeCreator(_logger);
|
||||
break;
|
||||
|
||||
case PlatformID.Unix:
|
||||
_logger.Trace("Creating Unix Scheme Creator");
|
||||
creator = new UnixUriSchemeCreator(_logger);
|
||||
break;
|
||||
|
||||
case PlatformID.MacOSX:
|
||||
_logger.Trace("Creating MacOSX Scheme Creator");
|
||||
creator = new MacUriSchemeCreator(_logger);
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.Error("Unkown Platform: " + Environment.OSVersion.Platform);
|
||||
throw new PlatformNotSupportedException("Platform does not support registration.");
|
||||
}
|
||||
|
||||
//Regiser the app
|
||||
if (creator.RegisterUriScheme(this))
|
||||
{
|
||||
_logger.Info("URI scheme registered.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FileName for the currently executing application
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string GetApplicationLocation()
|
||||
{
|
||||
return Process.GetCurrentProcess().MainModule.FileName;
|
||||
}
|
||||
}
|
||||
}
|
87
DiscordAPI/Registry/WindowsUriSchemeCreator.cs
Normal file
87
DiscordAPI/Registry/WindowsUriSchemeCreator.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using DiscordRPC.Logging;
|
||||
using System;
|
||||
|
||||
namespace DiscordRPC.Registry
|
||||
{
|
||||
internal class WindowsUriSchemeCreator : IUriSchemeCreator
|
||||
{
|
||||
private ILogger logger;
|
||||
public WindowsUriSchemeCreator(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public bool RegisterUriScheme(UriSchemeRegister register)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX)
|
||||
{
|
||||
throw new PlatformNotSupportedException("URI schemes can only be registered on Windows");
|
||||
}
|
||||
|
||||
//Prepare our location
|
||||
string location = register.ExecutablePath;
|
||||
if (location == null)
|
||||
{
|
||||
logger.Error("Failed to register application because the location was null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Prepare the Scheme, Friendly name, default icon and default command
|
||||
string scheme = "discord-" + register.ApplicationID;
|
||||
string friendlyName = "Run game " + register.ApplicationID + " protocol";
|
||||
string defaultIcon = location;
|
||||
string command = location;
|
||||
|
||||
//We have a steam ID, so attempt to replce the command with a steam command
|
||||
if (register.UsingSteamApp)
|
||||
{
|
||||
//Try to get the steam location. If found, set the command to a run steam instead.
|
||||
string steam = GetSteamLocation();
|
||||
if (steam != null)
|
||||
command = string.Format("\"{0}\" steam://rungameid/{1}", steam, register.SteamAppID);
|
||||
|
||||
}
|
||||
|
||||
//Okay, now actually register it
|
||||
CreateUriScheme(scheme, friendlyName, defaultIcon, command);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the actual scheme
|
||||
/// </summary>
|
||||
/// <param name="scheme"></param>
|
||||
/// <param name="friendlyName"></param>
|
||||
/// <param name="defaultIcon"></param>
|
||||
/// <param name="command"></param>
|
||||
private void CreateUriScheme(string scheme, string friendlyName, string defaultIcon, string command)
|
||||
{
|
||||
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("SOFTWARE\\Classes\\" + scheme))
|
||||
{
|
||||
key.SetValue("", "URL:" + friendlyName);
|
||||
key.SetValue("URL Protocol", "");
|
||||
|
||||
using (var iconKey = key.CreateSubKey("DefaultIcon"))
|
||||
iconKey.SetValue("", defaultIcon);
|
||||
|
||||
using (var commandKey = key.CreateSubKey("shell\\open\\command"))
|
||||
commandKey.SetValue("", command);
|
||||
}
|
||||
|
||||
logger.Trace("Registered {0}, {1}, {2}", scheme, friendlyName, command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current location of the steam client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string GetSteamLocation()
|
||||
{
|
||||
using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("Software\\Valve\\Steam"))
|
||||
{
|
||||
if (key == null) return null;
|
||||
return key.GetValue("SteamExe") as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
836
DiscordAPI/RichPresence.cs
Normal file
836
DiscordAPI/RichPresence.cs
Normal file
@ -0,0 +1,836 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using DiscordRPC.Helper;
|
||||
using System.Text;
|
||||
using DiscordRPC.Exceptions;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// The Rich Presence structure that will be sent and received by Discord. Use this class to build your presence and update it appropriately.
|
||||
/// </summary>
|
||||
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
|
||||
[Serializable]
|
||||
public class RichPresence
|
||||
{
|
||||
/// <summary>
|
||||
/// The user's current <see cref="Party"/> status. For example, "Playing Solo" or "With Friends".
|
||||
/// <para>Max 128 bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string State
|
||||
{
|
||||
get { return _state; }
|
||||
set
|
||||
{
|
||||
if (!ValidateString(value, out _state, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException("State", 0, 128);
|
||||
}
|
||||
}
|
||||
private string _state;
|
||||
|
||||
/// <summary>
|
||||
/// What the user is currently doing. For example, "Competitive - Total Mayhem".
|
||||
/// <para>Max 128 bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("details", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Details
|
||||
{
|
||||
get { return _details; }
|
||||
set
|
||||
{
|
||||
if (!ValidateString(value, out _details, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _details;
|
||||
|
||||
/// <summary>
|
||||
/// The time elapsed / remaining time data.
|
||||
/// </summary>
|
||||
[JsonProperty("timestamps", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Timestamps Timestamps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The names of the images to use and the tooltips to give those images.
|
||||
/// </summary>
|
||||
[JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Assets Assets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The party the player is currently in. The <see cref="Party.ID"/> must be set for this to be included in the RichPresence update.
|
||||
/// </summary>
|
||||
[JsonProperty("party", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Party Party { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The secrets used for Join / Spectate. Secrets are obfuscated data of your choosing. They could be match ids, player ids, lobby ids, etc. Make this object null if you do not wish too / unable too implement the Join / Request feature.
|
||||
/// <para>To keep security on the up and up, Discord requires that you properly hash/encode/encrypt/put-a-padlock-on-and-swallow-the-key-but-wait-then-how-would-you-open-it your secrets.</para>
|
||||
/// <para>Visit the <see href="https://discordapp.com/developers/docs/rich-presence/how-to#secrets">Rich Presence How-To</see> for more information.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("secrets", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Secrets Secrets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Marks the <see cref="Secrets.MatchSecret"/> as a game session with a specific beginning and end. It was going to be used as a form of notification, but was replaced with the join feature. It may potentially have use in the future, but it currently has no use.
|
||||
/// <para>
|
||||
/// "TLDR it marks the matchSecret field as an instance, that is to say a context in game that’s not like a lobby state/not in game state. It was gonna he used for notify me, but we scrapped that for ask to join. We may put it to another use in the future. For now, don’t worry about it" - Mason (Discord API Server 14 / 03 / 2018)
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonProperty("instance", NullValueHandling = NullValueHandling.Ignore)]
|
||||
[Obsolete("This was going to be used, but was replaced by JoinSecret instead")]
|
||||
private bool Instance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Clones the presence into a new instance. Used for thread safe writing and reading. This function will ignore properties if they are in a invalid state.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public RichPresence Clone()
|
||||
{
|
||||
return new RichPresence
|
||||
{
|
||||
State = this._state != null ? _state.Clone() as string : null,
|
||||
Details = this._details != null ? _details.Clone() as string : null,
|
||||
|
||||
Secrets = !HasSecrets() ? null : new Secrets
|
||||
{
|
||||
//MatchSecret = this.Secrets.MatchSecret?.Clone() as string,
|
||||
JoinSecret = this.Secrets.JoinSecret != null ? this.Secrets.JoinSecret.Clone() as string : null,
|
||||
SpectateSecret = this.Secrets.SpectateSecret != null ? this.Secrets.SpectateSecret.Clone() as string : null
|
||||
},
|
||||
|
||||
Timestamps = !HasTimestamps() ? null : new Timestamps
|
||||
{
|
||||
Start = this.Timestamps.Start,
|
||||
End = this.Timestamps.End
|
||||
},
|
||||
|
||||
Assets = !HasAssets() ? null : new Assets
|
||||
{
|
||||
LargeImageKey = this.Assets.LargeImageKey != null ? this.Assets.LargeImageKey.Clone() as string : null,
|
||||
LargeImageText = this.Assets.LargeImageText != null ? this.Assets.LargeImageText.Clone() as string : null,
|
||||
SmallImageKey = this.Assets.SmallImageKey != null ? this.Assets.SmallImageKey.Clone() as string : null,
|
||||
SmallImageText = this.Assets.SmallImageText != null ? this.Assets.SmallImageText.Clone() as string : null
|
||||
},
|
||||
|
||||
Party = !HasParty() ? null : new Party
|
||||
{
|
||||
ID = this.Party.ID,
|
||||
Size = this.Party.Size,
|
||||
Max = this.Party.Max
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the passed presence with this one, taking into account the image key to image id annoyance.
|
||||
/// </summary>
|
||||
/// <param name="presence"></param>
|
||||
internal void Merge(RichPresence presence)
|
||||
{
|
||||
this._state = presence._state;
|
||||
this._details = presence._details;
|
||||
this.Party = presence.Party;
|
||||
this.Timestamps = presence.Timestamps;
|
||||
this.Secrets = presence.Secrets;
|
||||
|
||||
//If they have assets, we should merge them
|
||||
if (presence.HasAssets())
|
||||
{
|
||||
//Make sure we actually have assets too
|
||||
if (!this.HasAssets())
|
||||
{
|
||||
//We dont, so we will just use theirs
|
||||
this.Assets = presence.Assets;
|
||||
}
|
||||
else
|
||||
{
|
||||
//We do, so we better merge them!
|
||||
this.Assets.Merge(presence.Assets);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//They dont have assets, so we will just set ours to null
|
||||
this.Assets = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates this presence with any values from the previous one
|
||||
/// </summary>
|
||||
/// <param name="presence"></param>
|
||||
[System.Obsolete("No longer used and probably can be removed.")]
|
||||
internal void Update(RichPresence presence)
|
||||
{
|
||||
if (presence == null) return;
|
||||
|
||||
this._state = presence._state ?? this._state;
|
||||
this._details = presence._details ?? this._details;
|
||||
|
||||
if (presence.Party != null)
|
||||
{
|
||||
if (this.Party != null)
|
||||
{
|
||||
this.Party.ID = presence.Party.ID ?? this.Party.ID;
|
||||
this.Party.Size = presence.Party.Size;
|
||||
this.Party.Max = presence.Party.Max;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Party = presence.Party;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Has Checks
|
||||
/// <summary>
|
||||
/// Does the Rich Presence have valid timestamps?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasTimestamps()
|
||||
{
|
||||
return this.Timestamps != null && (Timestamps.Start != null || Timestamps.End != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the Rich Presence have valid assets?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasAssets()
|
||||
{
|
||||
return this.Assets != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the Rich Presence have a valid party?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasParty()
|
||||
{
|
||||
return this.Party != null && this.Party.ID != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the Rich Presence have valid secrets?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasSecrets()
|
||||
{
|
||||
return Secrets != null && (Secrets.JoinSecret != null || Secrets.SpectateSecret != null);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Builder
|
||||
/// <summary>
|
||||
/// Sets the state of the Rich Presence. See also <seealso cref="State"/>.
|
||||
/// </summary>
|
||||
/// <param name="state">The user's current <see cref="Party"/> status.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithState(string state)
|
||||
{
|
||||
State = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the details of the Rich Presence. See also <seealso cref="Details"/>.
|
||||
/// </summary>
|
||||
/// <param name="details">What the user is currently doing.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithDetails(string details)
|
||||
{
|
||||
Details = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the timestamp of the Rich Presence. See also <seealso cref="Timestamps"/>.
|
||||
/// </summary>
|
||||
/// <param name="timestamps">The time elapsed / remaining time data.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithTimestamps(Timestamps timestamps)
|
||||
{
|
||||
Timestamps = timestamps;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the assets of the Rich Presence. See also <seealso cref="Assets"/>.
|
||||
/// </summary>
|
||||
/// <param name="assets">The names of the images to use and the tooltips to give those images.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithAssets(Assets assets)
|
||||
{
|
||||
Assets = assets;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Rich Presence's party. See also <seealso cref="Party"/>.
|
||||
/// </summary>
|
||||
/// <param name="party">The party the player is currently in.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithParty(Party party)
|
||||
{
|
||||
Party = party;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Rich Presence's secrets. See also <seealso cref="Secrets"/>.
|
||||
/// </summary>
|
||||
/// <param name="secrets">The secrets used for Join / Spectate.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithSecrets(Secrets secrets)
|
||||
{
|
||||
Secrets = secrets;
|
||||
return this;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to call <see cref="StringTools.GetNullOrString(string)"/> on the string and return the result, if its within a valid length.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to check</param>
|
||||
/// <param name="result">The formatted string result</param>
|
||||
/// <param name="bytes">The maximum number of bytes the string can take up</param>
|
||||
/// <param name="encoding">The encoding to count the bytes with</param>
|
||||
/// <returns>True if the string fits within the number of bytes</returns>
|
||||
internal static bool ValidateString(string str, out string result, int bytes, Encoding encoding)
|
||||
{
|
||||
result = str;
|
||||
if (str == null)
|
||||
return true;
|
||||
|
||||
//Trim the string, for the best chance of fitting
|
||||
var s = str.Trim();
|
||||
|
||||
//Make sure it fits
|
||||
if (!s.WithinLength(bytes, encoding))
|
||||
return false;
|
||||
|
||||
//Make sure its not empty
|
||||
result = s.GetNullOrString();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Operator that converts a presence into a boolean for null checks.
|
||||
/// </summary>
|
||||
/// <param name="presesnce"></param>
|
||||
public static implicit operator bool(RichPresence presesnce)
|
||||
{
|
||||
return presesnce != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the other rich presence differs from the current one
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
/// <returns></returns>
|
||||
internal bool Matches(RichPresence other)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (other == null)
|
||||
return false;
|
||||
|
||||
if (State != other.State || Details != other.Details)
|
||||
return false;
|
||||
|
||||
//Checks if the timestamps are different
|
||||
if (Timestamps != null)
|
||||
{
|
||||
if (other.Timestamps == null ||
|
||||
other.Timestamps.StartUnixMilliseconds != Timestamps.StartUnixMilliseconds ||
|
||||
other.Timestamps.EndUnixMilliseconds != Timestamps.EndUnixMilliseconds)
|
||||
return false;
|
||||
}
|
||||
else if (other.Timestamps != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Checks if the secrets are different
|
||||
if (Secrets != null)
|
||||
{
|
||||
if (other.Secrets == null ||
|
||||
other.Secrets.JoinSecret != Secrets.JoinSecret ||
|
||||
other.Secrets.MatchSecret != Secrets.MatchSecret ||
|
||||
other.Secrets.SpectateSecret != Secrets.SpectateSecret)
|
||||
return false;
|
||||
}
|
||||
else if (other.Secrets != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Checks if the timestamps are different
|
||||
if (Party != null)
|
||||
{
|
||||
if (other.Party == null ||
|
||||
other.Party.ID != Party.ID ||
|
||||
other.Party.Max != Party.Max ||
|
||||
other.Party.Size != Party.Size)
|
||||
return false;
|
||||
}
|
||||
else if (other.Party != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Checks if the assets are different
|
||||
if (Assets != null)
|
||||
{
|
||||
if (other.Assets == null ||
|
||||
other.Assets.LargeImageKey != Assets.LargeImageKey ||
|
||||
other.Assets.LargeImageText != Assets.LargeImageText ||
|
||||
other.Assets.SmallImageKey != Assets.SmallImageKey ||
|
||||
other.Assets.SmallImageText != Assets.SmallImageText)
|
||||
return false;
|
||||
}
|
||||
else if (other.Assets != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Instance == other.Instance;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The secrets used for Join / Spectate. Secrets are obfuscated data of your choosing. They could be match ids, player ids, lobby ids, etc.
|
||||
/// <para>To keep security on the up and up, Discord requires that you properly hash/encode/encrypt/put-a-padlock-on-and-swallow-the-key-but-wait-then-how-would-you-open-it your secrets.</para>
|
||||
/// <para>You should send discord data that someone else's game client would need to join or spectate their friend. If you can't or don't want to support those actions, you don't need to send secrets.</para>
|
||||
/// <para>Visit the <see href="https://discordapp.com/developers/docs/rich-presence/how-to#secrets">Rich Presence How-To</see> for more information.</para>
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class Secrets
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique match code to distinguish different games/lobbies. Use <see cref="Secrets.CreateSecret(Random)"/> to get an appropriately sized secret.
|
||||
/// <para>This cannot be null and must be supplied for the Join / Spectate feature to work.</para>
|
||||
/// <para>Max Length of 128 Bytes</para>
|
||||
/// </summary>
|
||||
[Obsolete("This feature has been deprecated my Mason in issue #152 on the offical library. Was originally used as a Notify Me feature, it has been replaced with Join / Spectate.")]
|
||||
[JsonProperty("match", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string MatchSecret
|
||||
{
|
||||
get { return _matchSecret; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _matchSecret, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _matchSecret;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>It is recommended to encrypt this information so its hard for people to replicate it.
|
||||
/// Do <b>NOT</b> just use the IP address in this. That is a bad practice and can leave your players vulnerable!
|
||||
/// </para>
|
||||
/// <para>Max Length of 128 Bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("join", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string JoinSecret
|
||||
{
|
||||
get { return _joinSecret; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _joinSecret, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _joinSecret;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>It is recommended to encrypt this information so its hard for people to replicate it.
|
||||
/// Do <b>NOT</b> just use the IP address in this. That is a bad practice and can leave your players vulnerable!
|
||||
/// </para>
|
||||
/// <para>Max Length of 128 Bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("spectate", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string SpectateSecret
|
||||
{
|
||||
get { return _spectateSecret; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _spectateSecret, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _spectateSecret;
|
||||
|
||||
|
||||
#region Statics
|
||||
|
||||
/// <summary>
|
||||
/// The encoding the secret generator is using
|
||||
/// </summary>
|
||||
public static Encoding Encoding { get { return Encoding.UTF8; } }
|
||||
|
||||
/// <summary>
|
||||
/// The length of a secret in bytes.
|
||||
/// </summary>
|
||||
public static int SecretLength { get { return 128; } }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="random">The random to use</param>
|
||||
/// <returns>Returns a <see cref="SecretLength"/> sized string with random characters from <see cref="Encoding"/></returns>
|
||||
public static string CreateSecret(Random random)
|
||||
{
|
||||
//Prepare an array and fill it with random bytes
|
||||
// THIS IS NOT SECURE! DO NOT USE THIS FOR PASSWORDS!
|
||||
byte[] bytes = new byte[SecretLength];
|
||||
random.NextBytes(bytes);
|
||||
|
||||
//Return the encoding. Probably should remove invalid characters but cannot be fucked.
|
||||
return Encoding.GetString(bytes);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="random">The random used to generate the characters</param>
|
||||
/// <returns></returns>
|
||||
public static string CreateFriendlySecret(Random random)
|
||||
{
|
||||
string charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
string secret = "";
|
||||
|
||||
for (int i = 0; i < SecretLength; i++)
|
||||
secret += charset[random.Next(charset.Length)];
|
||||
|
||||
return secret;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about the pictures used in the Rich Presence.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the uploaded image for the large profile artwork.
|
||||
/// <para>Max 32 Bytes.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("large_image", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string LargeImageKey
|
||||
{
|
||||
get { return _largeimagekey; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _largeimagekey, 32, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(32);
|
||||
|
||||
//Reset the large image ID
|
||||
_largeimageID = null;
|
||||
}
|
||||
}
|
||||
private string _largeimagekey;
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for the large square image. For example, "Summoners Rift" or "Horizon Lunar Colony".
|
||||
/// <para>Max 128 Bytes.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("large_text", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string LargeImageText
|
||||
{
|
||||
get { return _largeimagetext; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _largeimagetext, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _largeimagetext;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Name of the uploaded image for the small profile artwork.
|
||||
/// <para>Max 32 Bytes.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("small_image", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string SmallImageKey
|
||||
{
|
||||
get { return _smallimagekey; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _smallimagekey, 32, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(32);
|
||||
|
||||
//Reset the small image id
|
||||
_smallimageID = null;
|
||||
}
|
||||
}
|
||||
private string _smallimagekey;
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for the small circle image. For example, "LvL 6" or "Ultimate 85%".
|
||||
/// <para>Max 128 Bytes.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("small_text", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string SmallImageText
|
||||
{
|
||||
get { return _smallimagetext; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _smallimagetext, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _smallimagetext;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the large image. This is only set after Update Presence and will automatically become null when <see cref="LargeImageKey"/> is changed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ulong? LargeImageID { get { return _largeimageID; } }
|
||||
private ulong? _largeimageID;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the small image. This is only set after Update Presence and will automatically become null when <see cref="SmallImageKey"/> is changed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ulong? SmallImageID { get { return _smallimageID; } }
|
||||
private ulong? _smallimageID;
|
||||
|
||||
/// <summary>
|
||||
/// Merges this asset with the other, taking into account for ID's instead of keys.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
internal void Merge(Assets other)
|
||||
{
|
||||
//Copy over the names
|
||||
_smallimagetext = other._smallimagetext;
|
||||
_largeimagetext = other._largeimagetext;
|
||||
|
||||
//Convert large ID
|
||||
ulong largeID;
|
||||
if (ulong.TryParse(other._largeimagekey, out largeID))
|
||||
{
|
||||
_largeimageID = largeID;
|
||||
}
|
||||
else
|
||||
{
|
||||
_largeimagekey = other._largeimagekey;
|
||||
_largeimageID = null;
|
||||
}
|
||||
|
||||
//Convert the small ID
|
||||
ulong smallID;
|
||||
if (ulong.TryParse(other._smallimagekey, out smallID))
|
||||
{
|
||||
_smallimageID = smallID;
|
||||
}
|
||||
else
|
||||
{
|
||||
_smallimagekey = other._smallimagekey;
|
||||
_smallimageID = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structure representing the start and endtimes of a match.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class Timestamps
|
||||
{
|
||||
/// <summary>A new timestamp that starts from the current time.</summary>
|
||||
public static Timestamps Now { get { return new Timestamps(DateTime.UtcNow, end: null); } }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new timestamp starting at the current time and ending in the supplied timespan
|
||||
/// </summary>
|
||||
/// <param name="seconds">How long the Timestamp will last for in seconds.</param>
|
||||
/// <returns>Returns a new timestamp with given duration.</returns>
|
||||
public static Timestamps FromTimeSpan(double seconds) { return FromTimeSpan(TimeSpan.FromSeconds(seconds)); }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new timestamp starting at current time and ending in the supplied timespan
|
||||
/// </summary>
|
||||
/// <param name="timespan">How long the Timestamp will last for.</param>
|
||||
/// <returns>Returns a new timestamp with given duration.</returns>
|
||||
public static Timestamps FromTimeSpan(TimeSpan timespan)
|
||||
{
|
||||
return new Timestamps()
|
||||
{
|
||||
Start = DateTime.UtcNow,
|
||||
End = DateTime.UtcNow + timespan
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The time that match started. When included (not-null), the time in the rich presence will be shown as "00:01 elapsed".
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTime? Start { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the match will end. When included (not-null), the time in the rich presence will be shown as "00:01 remaining". This will override the "elapsed" to "remaining".
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTime? End { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a empty timestamp object
|
||||
/// </summary>
|
||||
public Timestamps()
|
||||
{
|
||||
Start = null;
|
||||
End = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a timestamp with the set start or end time.
|
||||
/// </summary>
|
||||
/// <param name="start">The start time</param>
|
||||
/// <param name="end">The end time</param>
|
||||
public Timestamps(DateTime start, DateTime? end = null)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts between DateTime and Milliseconds to give the Unix Epoch Time for the <see cref="Timestamps.Start"/>.
|
||||
/// </summary>
|
||||
[JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public ulong? StartUnixMilliseconds
|
||||
{
|
||||
get
|
||||
{
|
||||
return Start.HasValue ? ToUnixMilliseconds(Start.Value) : (ulong?)null;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
Start = value.HasValue ? FromUnixMilliseconds(value.Value) : (DateTime?)null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts between DateTime and Milliseconds to give the Unix Epoch Time for the <see cref="Timestamps.End"/>.
|
||||
/// <seealso cref="End"/>
|
||||
/// </summary>
|
||||
[JsonProperty("end", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public ulong? EndUnixMilliseconds
|
||||
{
|
||||
get
|
||||
{
|
||||
return End.HasValue ? ToUnixMilliseconds(End.Value) : (ulong?)null;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
End = value.HasValue ? FromUnixMilliseconds(value.Value) : (DateTime?)null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Unix Epoch time into a <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="unixTime">The time in milliseconds since 1970 / 01 / 01</param>
|
||||
/// <returns></returns>
|
||||
public static DateTime FromUnixMilliseconds(ulong unixTime)
|
||||
{
|
||||
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
return epoch.AddMilliseconds(Convert.ToDouble(unixTime));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="DateTime"/> into a Unix Epoch time (in milliseconds).
|
||||
/// </summary>
|
||||
/// <param name="date">The datetime to convert</param>
|
||||
/// <returns></returns>
|
||||
public static ulong ToUnixMilliseconds(DateTime date)
|
||||
{
|
||||
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
return Convert.ToUInt64((date - epoch).TotalMilliseconds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structure representing the part the player is in.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class Party
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>Max 128 Bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string ID { get { return _partyid; } set { _partyid = value.GetNullOrString(); } }
|
||||
private string _partyid;
|
||||
|
||||
/// <summary>
|
||||
/// The current size of the players party / lobby / group.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maxium size of the party / lobby / group. This is required to be larger than <see cref="Size"/>. If it is smaller than the current party size, it will automatically be set too <see cref="Size"/> when the presence is sent.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int Max { get; set; }
|
||||
|
||||
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
|
||||
private int[] _size
|
||||
{
|
||||
get
|
||||
{
|
||||
//see issue https://github.com/discordapp/discord-rpc/issues/111
|
||||
int size = Math.Max(1, Size);
|
||||
return new int[] { size, Math.Max(size, Max) };
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value.Length != 2)
|
||||
{
|
||||
Size = 0;
|
||||
Max = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Size = value[0];
|
||||
Max = value[1];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A rich presence that has been parsed from the pipe as a response.
|
||||
/// </summary>
|
||||
internal class RichPresenceResponse : RichPresence
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the client
|
||||
/// </summary>
|
||||
[JsonProperty("application_id")]
|
||||
public string ClientID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the bot
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; private set; }
|
||||
|
||||
}
|
||||
}
|
229
DiscordAPI/User.cs
Normal file
229
DiscordAPI/User.cs
Normal file
@ -0,0 +1,229 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// Object representing a Discord user. This is used for join requests.
|
||||
/// </summary>
|
||||
public class User
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible formats for avatars
|
||||
/// </summary>
|
||||
public enum AvatarFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Portable Network Graphics format (.png)
|
||||
/// <para>Losses format that supports transparent avatars. Most recommended for stationary formats with wide support from many libraries.</para>
|
||||
/// </summary>
|
||||
PNG,
|
||||
|
||||
/// <summary>
|
||||
/// Joint Photographic Experts Group format (.jpeg)
|
||||
/// <para>The format most cameras use. Lossy and does not support transparent avatars.</para>
|
||||
/// </summary>
|
||||
JPEG,
|
||||
|
||||
/// <summary>
|
||||
/// WebP format (.webp)
|
||||
/// <para>Picture only version of WebM. Pronounced "weeb p".</para>
|
||||
/// </summary>
|
||||
WebP,
|
||||
|
||||
/// <summary>
|
||||
/// Graphics Interchange Format (.gif)
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
GIF //Gif, as in gift.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible square sizes of avatars.
|
||||
/// </summary>
|
||||
public enum AvatarSize
|
||||
{
|
||||
/// <summary> 16 x 16 pixels.</summary>
|
||||
x16 = 16,
|
||||
/// <summary> 32 x 32 pixels.</summary>
|
||||
x32 = 32,
|
||||
/// <summary> 64 x 64 pixels.</summary>
|
||||
x64 = 64,
|
||||
/// <summary> 128 x 128 pixels.</summary>
|
||||
x128 = 128,
|
||||
/// <summary> 256 x 256 pixels.</summary>
|
||||
x256 = 256,
|
||||
/// <summary> 512 x 512 pixels.</summary>
|
||||
x512 = 512,
|
||||
/// <summary> 1024 x 1024 pixels.</summary>
|
||||
x1024 = 1024,
|
||||
/// <summary> 2048 x 2048 pixels.</summary>
|
||||
x2048 = 2048
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The snowflake ID of the user.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public ulong ID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The username of the player.
|
||||
/// </summary>
|
||||
[JsonProperty("username")]
|
||||
public string Username { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The discriminator of the user.
|
||||
/// </summary>
|
||||
[JsonProperty("discriminator")]
|
||||
public int Discriminator { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The avatar hash of the user. Too get a URL for the avatar, use the <see cref="GetAvatarURL(AvatarFormat, AvatarSize)"/>. This can be null if the user has no avatar. The <see cref="GetAvatarURL(AvatarFormat, AvatarSize)"/> will account for this and return the discord default.
|
||||
/// </summary>
|
||||
[JsonProperty("avatar")]
|
||||
public string Avatar { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flags on a users account, often represented as a badge.
|
||||
/// </summary>
|
||||
[JsonProperty("flags")]
|
||||
public Flag Flags { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A flag on the user account
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum Flag
|
||||
{
|
||||
/// <summary>No flag</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>Staff of Discord.</summary>
|
||||
Employee = 1 << 0,
|
||||
|
||||
/// <summary>Partners of Discord.</summary>
|
||||
Partner = 1 << 1,
|
||||
|
||||
/// <summary>Original HypeSquad which organise events.</summary>
|
||||
HypeSquad = 1 << 2,
|
||||
|
||||
/// <summary>Bug Hunters that found and reported bugs in Discord.</summary>
|
||||
BugHunter = 1 << 3,
|
||||
|
||||
//These 2 are mistery types
|
||||
//A = 1 << 4,
|
||||
//B = 1 << 5,
|
||||
|
||||
/// <summary>The HypeSquad House of Bravery.</summary>
|
||||
HouseBravery = 1 << 6,
|
||||
|
||||
/// <summary>The HypeSquad House of Brilliance.</summary>
|
||||
HouseBrilliance = 1 << 7,
|
||||
|
||||
/// <summary>The HypeSquad House of Balance (the best one).</summary>
|
||||
HouseBalance = 1 << 8,
|
||||
|
||||
/// <summary>Early Supporter of Discord and had Nitro before the store was released.</summary>
|
||||
EarlySupporter = 1 << 9,
|
||||
|
||||
/// <summary>Apart of a team.
|
||||
/// <para>Unclear if it is reserved for members that share a team with the current application.</para>
|
||||
/// </summary>
|
||||
TeamUser = 1 << 10
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The premium type of the user.
|
||||
/// </summary>
|
||||
[JsonProperty("premium_type")]
|
||||
public PremiumType Premium { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of premium
|
||||
/// </summary>
|
||||
public enum PremiumType
|
||||
{
|
||||
/// <summary>No subscription to any forms of Nitro.</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>Nitro Classic subscription. Has chat perks and animated avatars.</summary>
|
||||
NitroClassic = 1,
|
||||
|
||||
/// <summary>Nitro subscription. Has chat perks, animated avatars, server boosting, and access to free Nitro Games.</summary>
|
||||
Nitro = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the CDN. Normally cdn.discordapp.com.
|
||||
/// </summary>
|
||||
public string CdnEndpoint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new User instance.
|
||||
/// </summary>
|
||||
internal User()
|
||||
{
|
||||
CdnEndpoint = "cdn.discordapp.com";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the URL paths to the appropriate configuration
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration received by the OnReady event.</param>
|
||||
internal void SetConfiguration(Configuration configuration)
|
||||
{
|
||||
this.CdnEndpoint = configuration.CdnHost;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="AvatarFormat.PNG"/> format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format of the target avatar</param>
|
||||
/// <param name="size">The optional size of the avatar you wish for. Defaults to x128.</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file extension of the specified format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format to get the extention off</param>
|
||||
/// <returns>Returns a period prefixed file extension.</returns>
|
||||
public string GetAvatarExtension(AvatarFormat format)
|
||||
{
|
||||
return "." + format.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the user into username#discriminator
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Username + "#" + Discriminator.ToString("D4");
|
||||
}
|
||||
}
|
||||
}
|
191
DiscordAPI/Web/WebRPC.cs
Normal file
191
DiscordAPI/Web/WebRPC.cs
Normal file
@ -0,0 +1,191 @@
|
||||
using DiscordRPC.Exceptions;
|
||||
using DiscordRPC.RPC;
|
||||
using DiscordRPC.RPC.Commands;
|
||||
using DiscordRPC.RPC.Payload;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
#if INCLUDE_WEB_RPC
|
||||
namespace DiscordRPC.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles HTTP Rich Presence Requests
|
||||
/// </summary>
|
||||
[System.Obsolete("Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")]
|
||||
public static class WebRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the Rich Presence over the HTTP protocol. Does not support Join / Spectate and by default is blocking.
|
||||
/// </summary>
|
||||
/// <param name="presence">The presence to send to discord</param>
|
||||
/// <param name="applicationID">The ID of the application</param>
|
||||
/// <param name="port">The port the discord client is currently on. Specify this for testing. Will start scanning from supplied port.</param>
|
||||
/// <returns>Returns the rich presence result from the server. This can be null if presence was set to be null, or if there was no valid response from the client.</returns>
|
||||
[System.Obsolete("Setting Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")]
|
||||
public static RichPresence SetRichPresence(RichPresence presence, string applicationID, int port = 6463)
|
||||
{
|
||||
try
|
||||
{
|
||||
RichPresence response;
|
||||
if (TrySetRichPresence(presence, out response, applicationID, port))
|
||||
return response;
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set the Rich Presence over the HTTP protocol. Does not support Join / Specate and by default is blocking.
|
||||
/// </summary>
|
||||
/// <param name="presence">The presence to send to discord</param>
|
||||
/// <param name="response">The response object from the client</param>
|
||||
/// <param name="applicationID">The ID of the application</param>
|
||||
/// <param name="port">The port the discord client is currently on. Specify this for testing. Will start scanning from supplied port.</param>
|
||||
/// <returns>True if the response was valid from the server, otherwise false.</returns>
|
||||
[System.Obsolete("Setting Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")]
|
||||
public static bool TrySetRichPresence(RichPresence presence, out RichPresence response, string applicationID, int port = 6463)
|
||||
{
|
||||
//Validate the presence
|
||||
if (presence != null)
|
||||
{
|
||||
//Send valid presence
|
||||
//Validate the presence with our settings
|
||||
if (presence.HasSecrets())
|
||||
throw new BadPresenceException("Cannot send a presence with secrets as HTTP endpoint does not suppport events.");
|
||||
|
||||
if (presence.HasParty() && presence.Party.Max < presence.Party.Size)
|
||||
throw new BadPresenceException("Presence maximum party size cannot be smaller than the current size.");
|
||||
}
|
||||
|
||||
//Iterate over the ports until the first succesfull one
|
||||
for (int p = port; p < 6472; p++)
|
||||
{
|
||||
//Prepare the url and json
|
||||
using (WebClient client = new WebClient())
|
||||
{
|
||||
try
|
||||
{
|
||||
WebRequest request = PrepareRequest(presence, applicationID, p);
|
||||
client.Headers.Add("content-type", "application/json");
|
||||
|
||||
var result = client.UploadString(request.URL, request.Data);
|
||||
if (TryParseResponse(result, out response))
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//Something went wrong, but we are just going to ignore it and try the next port.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//we failed, return null
|
||||
response = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the response of a Web Request to a rich presence
|
||||
/// </summary>
|
||||
/// <param name="json">The json data received by the client</param>
|
||||
/// <param name="response">The parsed rich presence</param>
|
||||
/// <returns>True if the parse was succesfull</returns>
|
||||
public static bool TryParseResponse(string json, out RichPresence response)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Try to parse the JSON into a event
|
||||
EventPayload ev = JsonConvert.DeserializeObject<EventPayload>(json);
|
||||
|
||||
//We have a result, so parse the rich presence response and return it.
|
||||
if (ev != null)
|
||||
{
|
||||
//Parse the response into a rich presence response
|
||||
response = ev.GetObject<RichPresenceResponse>();
|
||||
return true;
|
||||
}
|
||||
|
||||
}catch(Exception) { }
|
||||
|
||||
//We failed.
|
||||
response = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a struct containing data requried to make a succesful web client request to set the rich presence.
|
||||
/// </summary>
|
||||
/// <param name="presence">The rich presence to set.</param>
|
||||
/// <param name="applicationID">The ID of the application the presence belongs too.</param>
|
||||
/// <param name="port">The port the client is located on. The default port for the discord client is 6463, but it may move iteratively upto 6473 if the ports are unavailable.</param>
|
||||
/// <returns>Returns a web request containing nessary data to make a POST request</returns>
|
||||
[System.Obsolete("WebRequests are no longer supported because of the removed HTTP functionality by Discord. See offical Rich Presence github for more information.")]
|
||||
public static WebRequest PrepareRequest(RichPresence presence, string applicationID, int port = 6463)
|
||||
{
|
||||
//Validate the presence
|
||||
if (presence != null)
|
||||
{
|
||||
//Send valid presence
|
||||
//Validate the presence with our settings
|
||||
if (presence.HasSecrets())
|
||||
throw new BadPresenceException("Cannot send a presence with secrets as HTTP endpoint does not suppport events.");
|
||||
|
||||
if (presence.HasParty() && presence.Party.Max < presence.Party.Size)
|
||||
throw new BadPresenceException("Presence maximum party size cannot be smaller than the current size.");
|
||||
}
|
||||
|
||||
//Prepare some params
|
||||
int pid = System.Diagnostics.Process.GetCurrentProcess().Id;
|
||||
|
||||
//Prepare the payload
|
||||
PresenceCommand command = new PresenceCommand() { PID = pid, Presence = presence };
|
||||
var payload = command.PreparePayload(DateTime.UtcNow.ToFileTime());
|
||||
|
||||
string json = JsonConvert.SerializeObject(payload);
|
||||
|
||||
string url = "http://127.0.0.1:" + port + "/rpc?v=" + RpcConnection.VERSION + "&client_id=" + applicationID;
|
||||
return new WebRequest(url, json);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details of a HTTP Post request that will set the rich presence.
|
||||
/// </summary>
|
||||
[System.Obsolete("Web Requests is no longer supported as Discord removed HTTP Rich Presence support. See offical Rich Presence github for more information.")]
|
||||
public struct WebRequest
|
||||
{
|
||||
private string _url;
|
||||
private string _json;
|
||||
private Dictionary<string, string> _headers;
|
||||
|
||||
/// <summary>
|
||||
/// The URL to send the POST request too
|
||||
/// </summary>
|
||||
public string URL { get { return _url; } }
|
||||
|
||||
/// <summary>
|
||||
/// The JSON formatted body to send with the POST request
|
||||
/// </summary>
|
||||
public string Data { get { return _json; } }
|
||||
|
||||
/// <summary>
|
||||
/// The headers to send with the body
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Headers { get { return _headers; } }
|
||||
|
||||
internal WebRequest(string url, string json)
|
||||
{
|
||||
_url = url;
|
||||
_json = json;
|
||||
_headers = new Dictionary<string, string>();
|
||||
_headers.Add("content-type", "application/json");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
38
LogWriter.cs
Normal file
38
LogWriter.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
public static class LogWriter
|
||||
{
|
||||
public static void WriteToFile(string Message)
|
||||
{
|
||||
if (!SDRSharp.Radio.Utils.GetBooleanSetting("LogRPC", false))
|
||||
return;
|
||||
string path = AppDomain.CurrentDomain.BaseDirectory + "\\RPCLogs\\";
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\RPCLogs\\DiscordRPCLog_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".log";
|
||||
if (!File.Exists(filepath))
|
||||
{
|
||||
using (StreamWriter sw = File.CreateText(filepath))
|
||||
{
|
||||
sw.WriteLine($"[{DateTime.Now}] {Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (StreamWriter sw = File.AppendText(filepath))
|
||||
{
|
||||
sw.WriteLine($"[{DateTime.Now}] {Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
298
MainPlugin.cs
Normal file
298
MainPlugin.cs
Normal file
@ -0,0 +1,298 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using System.ComponentModel;
|
||||
using SDRSharp.Common;
|
||||
using SDRSharp.Radio;
|
||||
using SDRSharp.PanView;
|
||||
using DiscordRPC.Logging;
|
||||
using DiscordRPC;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordRPC.Message;
|
||||
using DiscordRPC.IO;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
public class MainPlugin : ISharpPlugin
|
||||
{
|
||||
private const string _displayName = "Discord RPC";
|
||||
private SettingsPanel _controlPanel;
|
||||
private static LogLevel logLevel = LogLevel.Trace;
|
||||
private static int discordPipe = -1;
|
||||
bool RPCalreadyLoaded = false;
|
||||
private ISharpControl _control;
|
||||
bool playedBefore = false;
|
||||
private IConfigurationPanelProvider configurationPanelProvider;
|
||||
private SDRSharp.FrontEnds.SpyServer.ControllerPanel controllerPanel;
|
||||
public TopWindowMessages windowMessages;
|
||||
private RichPresence presence = new RichPresence()
|
||||
{
|
||||
Details = "Loading...",
|
||||
State = "Loading...",
|
||||
Assets = new Assets()
|
||||
{
|
||||
LargeImageKey = "image_large",
|
||||
LargeImageText = "SDRSharp",
|
||||
SmallImageKey = "image_small",
|
||||
SmallImageText = $"SDR# RPC plugin v{Assembly.GetEntryAssembly().GetName().Version} by EnderIce2"
|
||||
},
|
||||
Secrets = new Secrets()
|
||||
{
|
||||
JoinSecret = "invalid_secret"
|
||||
},
|
||||
Party = new Party()
|
||||
{
|
||||
ID = Secrets.CreateFriendlySecret(new Random()),
|
||||
Size = 1,
|
||||
Max = 100
|
||||
}
|
||||
};
|
||||
private static DiscordRpcClient client;
|
||||
private static bool isRunning = true;
|
||||
public string DisplayName
|
||||
{
|
||||
get { return _displayName; }
|
||||
}
|
||||
public bool HasGui
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
public UserControl Gui
|
||||
{
|
||||
get { return _controlPanel; }
|
||||
}
|
||||
public void Initialize(ISharpControl control)
|
||||
{
|
||||
if (Utils.GetBooleanSetting("ShowWelcomePage", true))
|
||||
new WelcomeForm().ShowDialog();
|
||||
_controlPanel = new SettingsPanel();
|
||||
windowMessages = new TopWindowMessages();
|
||||
_control = control;
|
||||
try
|
||||
{
|
||||
_control.RegisterFrontControl(windowMessages, PluginPosition.Top);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(ex.ToString());
|
||||
}
|
||||
windowMessages.Show();
|
||||
if (Utils.GetBooleanSetting("EnableRPC", true))
|
||||
{
|
||||
if (RPCalreadyLoaded)
|
||||
{
|
||||
_controlPanel.ChangeStatus = "Restart required";
|
||||
return;
|
||||
}
|
||||
if (Utils.GetStringSetting("ClientID").Replace(" ", "").Length != 18)
|
||||
client = new DiscordRpcClient("765213507321856078", pipe: discordPipe)
|
||||
{
|
||||
Logger = new ConsoleLogger(logLevel, true)
|
||||
};
|
||||
else
|
||||
client = new DiscordRpcClient(Utils.GetStringSetting("ClientID"), pipe: discordPipe)
|
||||
{
|
||||
Logger = new ConsoleLogger(logLevel, true)
|
||||
};
|
||||
client.RegisterUriScheme();
|
||||
client.OnRpcMessage += Client_OnRpcMessage;
|
||||
client.OnPresenceUpdate += Client_OnPresenceUpdate;
|
||||
client.OnReady += OnReady;
|
||||
client.OnClose += OnClose;
|
||||
client.OnError += OnError;
|
||||
client.OnConnectionEstablished += OnConnectionEstablished;
|
||||
client.OnConnectionFailed += OnConnectionFailed;
|
||||
client.OnSubscribe += OnSubscribe;
|
||||
client.OnUnsubscribe += OnUnsubscribe;
|
||||
client.OnJoin += OnJoin;
|
||||
client.OnJoinRequested += OnJoinRequested;
|
||||
//client.OnSpectate += OnSpectate;
|
||||
presence.Timestamps = new Timestamps()
|
||||
{
|
||||
Start = DateTime.UtcNow
|
||||
};
|
||||
client.SetSubscription(EventType.Join | EventType.JoinRequest);
|
||||
client.SetPresence(presence);
|
||||
client.Initialize();
|
||||
try
|
||||
{
|
||||
configurationPanelProvider = (IConfigurationPanelProvider)_control.Source;
|
||||
controllerPanel = (SDRSharp.FrontEnds.SpyServer.ControllerPanel)configurationPanelProvider.Gui;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWriter.WriteToFile("----> " + ex.ToString());
|
||||
MessageBox.Show($"Cannot get Spy Server Network address\n\nError:\n{ex}", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
_ = MainLoop();
|
||||
}
|
||||
else
|
||||
_controlPanel.ChangeStatus = "RPC is disabled";
|
||||
LogWriter.WriteToFile("EOM Initialize");
|
||||
}
|
||||
private void Client_OnPresenceUpdate(object sender, PresenceMessage args)
|
||||
{
|
||||
LogWriter.WriteToFile($"[RpcMessage] | Presence state: {args.Presence.State}");
|
||||
}
|
||||
private void Client_OnRpcMessage(object sender, IMessage msg)
|
||||
{
|
||||
LogWriter.WriteToFile($"[RpcMessage] | {msg.Type} | {msg}");
|
||||
}
|
||||
private void OnConnectionFailed(object sender, ConnectionFailedMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = $"RPC Connection Failed!\n{args.Type} | {args.FailedPipe}";
|
||||
}
|
||||
private void OnConnectionEstablished(object sender, ConnectionEstablishedMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = "RPC Connection Established!";
|
||||
}
|
||||
private void OnError(object sender, ErrorMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = $"RPC Error:\n{args.Message}";
|
||||
windowMessages.ChangeLabel = "SDR# RPC | Internal error";
|
||||
}
|
||||
private void OnClose(object sender, CloseMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = "RPC Closed";
|
||||
windowMessages.ChangeLabel = "SDR# RPC | Closed";
|
||||
Close();
|
||||
}
|
||||
private void OnReady(object sender, ReadyMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = "RPC Ready";
|
||||
windowMessages.ChangeLabel = "SDR# RPC | Ready";
|
||||
}
|
||||
private void OnSubscribe(object sender, SubscribeMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = $"Subscribed: {args.Event}";
|
||||
}
|
||||
private void OnUnsubscribe(object sender, UnsubscribeMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = $"Unsubscribed: {args.Event}";
|
||||
}
|
||||
private void OnJoin(object sender, JoinMessage args)
|
||||
{
|
||||
presence.Party.Size++;
|
||||
presence.Secrets.JoinSecret = args.Secret;
|
||||
MessageBox.Show("OnJoin: " + args.Secret);
|
||||
_control.StopRadio();
|
||||
_control.RefreshSource(true);
|
||||
Utils.SaveSetting("spyserver.uri", args.Secret);
|
||||
_control.StartRadio();
|
||||
}
|
||||
private async void OnJoinRequested(object sender, JoinRequestMessage args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await windowMessages.RequestAnswer(client, args))
|
||||
{
|
||||
MessageBox.Show("Accepted RequestAnswer");
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("Declined RequestAnswer");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(ex.ToString());
|
||||
}
|
||||
}
|
||||
async Task MainLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
isRunning = true;
|
||||
LogWriter.WriteToFile($"MainLoop called {isRunning} {client.IsInitialized}");
|
||||
while (client != null && isRunning)
|
||||
{
|
||||
LogWriter.WriteToFile("Setting secret...");
|
||||
try
|
||||
{
|
||||
string sdr_url = "sdr://" + controllerPanel.Host + ":" + controllerPanel.Port + "/";
|
||||
LogWriter.WriteToFile(sdr_url);
|
||||
presence.Secrets.JoinSecret = sdr_url;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWriter.WriteToFile(ex.ToString());
|
||||
}
|
||||
LogWriter.WriteToFile("Waiting 500ms in loop...");
|
||||
await Task.Delay(500);
|
||||
if (_control.RdsRadioText != null)
|
||||
{
|
||||
if (_control.IsPlaying)
|
||||
{
|
||||
presence.Assets.SmallImageKey = "play";
|
||||
playedBefore = true;
|
||||
}
|
||||
else if (!_control.IsPlaying && playedBefore)
|
||||
presence.Assets.SmallImageKey = "pause";
|
||||
if (!playedBefore)
|
||||
{
|
||||
presence.Details = "Frequency: Not playing";
|
||||
presence.State = "RDS: Not playing";
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
LogWriter.WriteToFile($"Frequency: {_control.Frequency}");
|
||||
LogWriter.WriteToFile($"RdsRadioText: {_control.RdsRadioText}");
|
||||
LogWriter.WriteToFile($"RdsProgramService: {_control.RdsProgramService}");
|
||||
LogWriter.WriteToFile("Setting presence...");
|
||||
presence.Details = $"Frequency: {string.Format("{0:#,0,,0 Hz}", _control.Frequency)}";
|
||||
if (string.IsNullOrWhiteSpace(_control.RdsRadioText + _control.RdsProgramService))
|
||||
presence.State = $"RDS: unknown | ";
|
||||
else
|
||||
presence.State = $"RDS: {_control.RdsProgramService} - {_control.RdsRadioText} | ";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWriter.WriteToFile(ex.ToString());
|
||||
}
|
||||
/*presence.Secrets.JoinSecret = */
|
||||
//_control.RegisterFrontControl(Gui, PluginPosition.Top);
|
||||
}
|
||||
try
|
||||
{
|
||||
client.SetPresence(presence);
|
||||
}
|
||||
catch (ObjectDisposedException) {; }
|
||||
LogWriter.WriteToFile("SetPresence");
|
||||
_controlPanel.ChangeStatus = $"Presence Updated {DateTime.UtcNow}";
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWriter.WriteToFile("Frequency or Radio Text are null!");
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
if (client == null)
|
||||
_controlPanel.ChangeStatus = "Client was null";
|
||||
else
|
||||
_controlPanel.ChangeStatus = "Presence stopped";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message.Contains("The process cannot access the file"))
|
||||
{
|
||||
_ = MainLoop();
|
||||
return;
|
||||
}
|
||||
_controlPanel.ChangeStatus = $"RPC Update Error\n{ex.Message}";
|
||||
LogWriter.WriteToFile(ex.ToString());
|
||||
}
|
||||
}
|
||||
public void Close()
|
||||
{
|
||||
LogWriter.WriteToFile("Close called");
|
||||
isRunning = false;
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
36
Properties/AssemblyInfo.cs
Normal file
36
Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,36 @@
|
||||
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 2020")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("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.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
1
Resgister.txt
Normal file
1
Resgister.txt
Normal file
@ -0,0 +1 @@
|
||||
<add key="Discord RPC" value="EnderIce2.SDRSharpPlugin.MainPlugin,DiscordRPC" />
|
173
SDRSharpPlugin.DiscordRPC.csproj
Normal file
173
SDRSharpPlugin.DiscordRPC.csproj
Normal file
@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{72E8628F-BA39-4915-BF3C-DD48BF477D30}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>DiscordRPC</RootNamespace>
|
||||
<AssemblyName>DiscordRPC</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<CodeAnalysisRuleSet />
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<CodeAnalysisRuleSet />
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="SDRSharp, Version=1.0.0.1765, Culture=neutral, processorArchitecture=x86">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\Downloads\sdrsharp-x86\SDRSharp.exe</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SDRSharp.Common, Version=0.0.0.0, Culture=neutral, processorArchitecture=x86">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\sdrsharp-x86\SDRSharp.Common.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SDRSharp.PanView, Version=0.0.0.0, Culture=neutral, processorArchitecture=x86">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\sdrsharp-x86\SDRSharp.PanView.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SDRSharp.Radio, Version=0.0.0.0, Culture=neutral, processorArchitecture=x86">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\sdrsharp-x86\SDRSharp.Radio.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="DiscordAPI\Configuration.cs" />
|
||||
<Compile Include="DiscordAPI\Converters\EnumSnakeCaseConverter.cs" />
|
||||
<Compile Include="DiscordAPI\Converters\EnumValueAttribute.cs" />
|
||||
<Compile Include="DiscordAPI\DiscordRpcClient.cs" />
|
||||
<Compile Include="DiscordAPI\Events.cs" />
|
||||
<Compile Include="DiscordAPI\EventType.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\BadPresenceException.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\InvalidConfigurationException.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\InvalidPipeException.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\StringOutOfRangeException.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\UninitializedException.cs" />
|
||||
<Compile Include="DiscordAPI\Helper\BackoffDelay.cs" />
|
||||
<Compile Include="DiscordAPI\Helper\StringTools.cs" />
|
||||
<Compile Include="DiscordAPI\IO\Handshake.cs" />
|
||||
<Compile Include="DiscordAPI\IO\INamedPipeClient.cs" />
|
||||
<Compile Include="DiscordAPI\IO\ManagedNamedPipeClient.cs" />
|
||||
<Compile Include="DiscordAPI\IO\Opcode.cs" />
|
||||
<Compile Include="DiscordAPI\IO\PipeFrame.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\ConsoleLogger.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\FileLogger.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\ILogger.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\LogLevel.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\NullLogger.cs" />
|
||||
<Compile Include="DiscordAPI\Message\CloseMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\ConnectionEstablishedMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\ConnectionFailedMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\ErrorMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\IMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\JoinMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\JoinRequestMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\MessageType.cs" />
|
||||
<Compile Include="DiscordAPI\Message\PresenceMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\ReadyMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\SpectateMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\SubscribeMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\UnsubscribeMsesage.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\IUriSchemeCreator.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\MacUriSchemeCreator.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\UnixUriSchemeCreator.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\UriScheme.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\WindowsUriSchemeCreator.cs" />
|
||||
<Compile Include="DiscordAPI\RichPresence.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\CloseCommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\ICommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\PresenceCommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\RespondCommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\SubscribeCommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\ClosePayload.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\Command.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\IPayload.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\PayloadArgument.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\PayloadEvent.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\ServerEvent.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\RpcConnection.cs" />
|
||||
<Compile Include="DiscordAPI\User.cs" />
|
||||
<Compile Include="DiscordAPI\Web\WebRPC.cs" />
|
||||
<Compile Include="LogWriter.cs" />
|
||||
<Compile Include="MainPlugin.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="SettingsPanel.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SettingsPanel.Designer.cs">
|
||||
<DependentUpon>SettingsPanel.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="TopWindowMessages.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="TopWindowMessages.Designer.cs">
|
||||
<DependentUpon>TopWindowMessages.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="WelcomeForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="WelcomeForm.Designer.cs">
|
||||
<DependentUpon>WelcomeForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="SettingsPanel.resx">
|
||||
<DependentUpon>SettingsPanel.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="TopWindowMessages.resx">
|
||||
<DependentUpon>TopWindowMessages.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="WelcomeForm.resx">
|
||||
<DependentUpon>WelcomeForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="DiscordAPI\LICENSE.txt" />
|
||||
<Content Include="Resgister.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
25
SDRSharpPlugin.DiscordRPC.sln
Normal file
25
SDRSharpPlugin.DiscordRPC.sln
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
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
|
||||
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
|
135
SettingsPanel.Designer.cs
generated
Normal file
135
SettingsPanel.Designer.cs
generated
Normal file
@ -0,0 +1,135 @@
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
partial class SettingsPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.checkBox1 = new System.Windows.Forms.CheckBox();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.textBox1 = new System.Windows.Forms.TextBox();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
this.button1 = new System.Windows.Forms.Button();
|
||||
this.checkBox2 = new System.Windows.Forms.CheckBox();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// checkBox1
|
||||
//
|
||||
this.checkBox1.Checked = true;
|
||||
this.checkBox1.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.checkBox1.Location = new System.Drawing.Point(3, 3);
|
||||
this.checkBox1.Name = "checkBox1";
|
||||
this.checkBox1.Size = new System.Drawing.Size(146, 21);
|
||||
this.checkBox1.TabIndex = 0;
|
||||
this.checkBox1.Text = "Enable Discord RPC";
|
||||
this.checkBox1.UseVisualStyleBackColor = true;
|
||||
this.checkBox1.CheckedChanged += new System.EventHandler(this.CheckBox1_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.AutoEllipsis = true;
|
||||
this.label1.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.label1.Location = new System.Drawing.Point(0, 49);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(196, 88);
|
||||
this.label1.TabIndex = 1;
|
||||
this.label1.Text = "Loading status...";
|
||||
this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// textBox1
|
||||
//
|
||||
this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.textBox1.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
this.textBox1.Location = new System.Drawing.Point(56, 143);
|
||||
this.textBox1.Name = "textBox1";
|
||||
this.textBox1.Size = new System.Drawing.Size(134, 13);
|
||||
this.textBox1.TabIndex = 2;
|
||||
this.textBox1.KeyDown += new System.Windows.Forms.KeyEventHandler(this.textBox1_KeyDown);
|
||||
this.textBox1.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.textBox1_KeyPress);
|
||||
//
|
||||
// label2
|
||||
//
|
||||
this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.label2.AutoSize = true;
|
||||
this.label2.Location = new System.Drawing.Point(0, 143);
|
||||
this.label2.Name = "label2";
|
||||
this.label2.Size = new System.Drawing.Size(50, 13);
|
||||
this.label2.TabIndex = 3;
|
||||
this.label2.Text = "Client ID:";
|
||||
//
|
||||
// button1
|
||||
//
|
||||
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.button1.Location = new System.Drawing.Point(147, 3);
|
||||
this.button1.Name = "button1";
|
||||
this.button1.Size = new System.Drawing.Size(46, 23);
|
||||
this.button1.TabIndex = 4;
|
||||
this.button1.Text = "Help";
|
||||
this.button1.UseVisualStyleBackColor = true;
|
||||
this.button1.Click += new System.EventHandler(this.button1_Click);
|
||||
//
|
||||
// checkBox2
|
||||
//
|
||||
this.checkBox2.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.25F);
|
||||
this.checkBox2.Location = new System.Drawing.Point(3, 25);
|
||||
this.checkBox2.Name = "checkBox2";
|
||||
this.checkBox2.Size = new System.Drawing.Size(158, 21);
|
||||
this.checkBox2.TabIndex = 5;
|
||||
this.checkBox2.Text = "Log RPC (for debugging)";
|
||||
this.checkBox2.UseVisualStyleBackColor = true;
|
||||
this.checkBox2.CheckedChanged += new System.EventHandler(this.checkBox2_CheckedChanged);
|
||||
//
|
||||
// SettingsPanel
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.Controls.Add(this.button1);
|
||||
this.Controls.Add(this.checkBox2);
|
||||
this.Controls.Add(this.label2);
|
||||
this.Controls.Add(this.textBox1);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.checkBox1);
|
||||
this.Name = "SettingsPanel";
|
||||
this.Size = new System.Drawing.Size(196, 163);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.CheckBox checkBox1;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.TextBox textBox1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Button button1;
|
||||
private System.Windows.Forms.CheckBox checkBox2;
|
||||
}
|
||||
}
|
86
SettingsPanel.cs
Normal file
86
SettingsPanel.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using SDRSharp.Radio;
|
||||
using System.Security.Policy;
|
||||
using System.Media;
|
||||
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
public partial class SettingsPanel : UserControl
|
||||
{
|
||||
private string _ChangeStatus;
|
||||
public string ChangeStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
return _ChangeStatus;
|
||||
}
|
||||
set
|
||||
{
|
||||
_ChangeStatus = value;
|
||||
label1.Text = value;
|
||||
LogWriter.WriteToFile(value);
|
||||
}
|
||||
}
|
||||
public SettingsPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
textBox1.Text = Utils.GetStringSetting("ClientID");
|
||||
if (Utils.GetBooleanSetting("EnableRPC", true))
|
||||
checkBox1.Checked = true;
|
||||
else
|
||||
checkBox1.Checked = false;
|
||||
if (Utils.GetBooleanSetting("LogRPC", false))
|
||||
checkBox2.Checked = true;
|
||||
else
|
||||
checkBox2.Checked = false;
|
||||
LogWriter.WriteToFile("SettingsPanel loaded");
|
||||
}
|
||||
|
||||
private void CheckBox1_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
Utils.SaveSetting("EnableRPC", checkBox1.Checked);
|
||||
label1.Text = "Restart required";
|
||||
LogWriter.WriteToFile($"checkbox on SettingsPanel clicked {checkBox1.Checked}");
|
||||
//Utils.GetBooleanSetting("EnableRPC");
|
||||
}
|
||||
|
||||
private void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
System.Diagnostics.Process.Start("https://github.com/EnderIce2/SDRSharpRPC");
|
||||
}
|
||||
|
||||
private void checkBox2_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
Utils.SaveSetting("LogRPC", checkBox2.Checked);
|
||||
}
|
||||
|
||||
private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
|
||||
{
|
||||
if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar) && (e.KeyChar != '.'))
|
||||
e.Handled = true;
|
||||
if ((e.KeyChar == '.') && ((sender as TextBox).Text.IndexOf('.') > -1))
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private async void textBox1_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Enter && textBox1.Text.Replace(" ", "").Length == 18)
|
||||
{
|
||||
Utils.SaveSetting("ClientID", textBox1.Text);
|
||||
e.Handled = true;
|
||||
e.SuppressKeyPress = true;
|
||||
await Task.Delay(200);
|
||||
textBox1.Text = Utils.GetStringSetting("ClientID");
|
||||
label1.Text = "Saved.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
SettingsPanel.resx
Normal file
120
SettingsPanel.resx
Normal file
@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
108
TopWindowMessages.Designer.cs
generated
Normal file
108
TopWindowMessages.Designer.cs
generated
Normal file
@ -0,0 +1,108 @@
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
partial class TopWindowMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.button1 = new System.Windows.Forms.Button();
|
||||
this.button2 = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// button1
|
||||
//
|
||||
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.button1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(50)))), ((int)(((byte)(50)))));
|
||||
this.button1.FlatAppearance.BorderSize = 0;
|
||||
this.button1.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Maroon;
|
||||
this.button1.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))));
|
||||
this.button1.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.button1.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.25F);
|
||||
this.button1.Location = new System.Drawing.Point(629, 4);
|
||||
this.button1.Name = "button1";
|
||||
this.button1.Size = new System.Drawing.Size(75, 23);
|
||||
this.button1.TabIndex = 0;
|
||||
this.button1.Text = "Decline";
|
||||
this.button1.UseVisualStyleBackColor = false;
|
||||
this.button1.Visible = false;
|
||||
//
|
||||
// button2
|
||||
//
|
||||
this.button2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.button2.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(50)))), ((int)(((byte)(50)))));
|
||||
this.button2.FlatAppearance.BorderSize = 0;
|
||||
this.button2.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
|
||||
this.button2.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(64)))), ((int)(((byte)(0)))));
|
||||
this.button2.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.button2.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.25F);
|
||||
this.button2.Location = new System.Drawing.Point(548, 4);
|
||||
this.button2.Name = "button2";
|
||||
this.button2.Size = new System.Drawing.Size(75, 23);
|
||||
this.button2.TabIndex = 1;
|
||||
this.button2.Text = "Accept";
|
||||
this.button2.UseVisualStyleBackColor = false;
|
||||
this.button2.Visible = false;
|
||||
//
|
||||
// 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.AutoEllipsis = true;
|
||||
this.label1.Location = new System.Drawing.Point(3, 4);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(304, 23);
|
||||
this.label1.TabIndex = 2;
|
||||
this.label1.Text = "Loading";
|
||||
this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// TopWindowMessages
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(60)))), ((int)(((byte)(60)))), ((int)(((byte)(60)))));
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.button2);
|
||||
this.Controls.Add(this.button1);
|
||||
this.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.25F);
|
||||
this.MaximumSize = new System.Drawing.Size(9999999, 30);
|
||||
this.MinimumSize = new System.Drawing.Size(0, 30);
|
||||
this.Name = "TopWindowMessages";
|
||||
this.Size = new System.Drawing.Size(707, 30);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Button button1;
|
||||
private System.Windows.Forms.Button button2;
|
||||
private System.Windows.Forms.Label label1;
|
||||
}
|
||||
}
|
72
TopWindowMessages.cs
Normal file
72
TopWindowMessages.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using DiscordRPC.Message;
|
||||
using DiscordRPC;
|
||||
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
public partial class TopWindowMessages : UserControl
|
||||
{
|
||||
public TopWindowMessages()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
private string _ChangeLabel;
|
||||
public string ChangeLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
return _ChangeLabel;
|
||||
}
|
||||
set
|
||||
{
|
||||
_ChangeLabel = value;
|
||||
label1.Text = value;
|
||||
LogWriter.WriteToFile(value);
|
||||
}
|
||||
}
|
||||
private bool AnswerA = false;
|
||||
private bool AnswerD = false;
|
||||
public async Task<bool> RequestAnswer(DiscordRpcClient client, JoinRequestMessage args)
|
||||
{
|
||||
bool tmpansw = false;
|
||||
LogWriter.WriteToFile("Incoming RPC request from " + args.User.Username);
|
||||
button1.Visible = true;
|
||||
button2.Visible = true;
|
||||
ChangeLabel = $"SDR# RPC | {args.User.Username} has requested to get Spy Server Network address.";
|
||||
while (!AnswerA || !AnswerD) // TODO: Rework
|
||||
{
|
||||
LogWriter.WriteToFile("waiting...");
|
||||
Application.DoEvents();
|
||||
await Task.Delay(200);
|
||||
}
|
||||
tmpansw = AnswerA;
|
||||
LogWriter.WriteToFile($"Client sent an answer. {tmpansw}");
|
||||
client.Respond(args, tmpansw);
|
||||
AnswerA = false;
|
||||
AnswerD = false;
|
||||
button1.Visible = false;
|
||||
button2.Visible = false;
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
SetDefaultTextInLabel(tmpansw);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
return tmpansw;
|
||||
}
|
||||
private async Task SetDefaultTextInLabel(bool accepted)
|
||||
{
|
||||
if (accepted)
|
||||
ChangeLabel = $"SDR# RPC | Request accepted";
|
||||
else
|
||||
ChangeLabel = $"SDR# RPC | Request declined";
|
||||
await Task.Delay(5000);
|
||||
ChangeLabel = $"SDR# RPC | Ready";
|
||||
}
|
||||
}
|
||||
}
|
120
TopWindowMessages.resx
Normal file
120
TopWindowMessages.resx
Normal file
@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
135
WelcomeForm.Designer.cs
generated
Normal file
135
WelcomeForm.Designer.cs
generated
Normal file
@ -0,0 +1,135 @@
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
partial class WelcomeForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(WelcomeForm));
|
||||
this.richTextBox1 = new System.Windows.Forms.RichTextBox();
|
||||
this.button1 = new System.Windows.Forms.Button();
|
||||
this.checkBox1 = new System.Windows.Forms.CheckBox();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
this.button2 = new System.Windows.Forms.Button();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// richTextBox1
|
||||
//
|
||||
this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.richTextBox1.Cursor = System.Windows.Forms.Cursors.Default;
|
||||
this.richTextBox1.Location = new System.Drawing.Point(12, 58);
|
||||
this.richTextBox1.Name = "richTextBox1";
|
||||
this.richTextBox1.Size = new System.Drawing.Size(776, 327);
|
||||
this.richTextBox1.TabIndex = 0;
|
||||
this.richTextBox1.Text = resources.GetString("richTextBox1.Text");
|
||||
//
|
||||
// button1
|
||||
//
|
||||
this.button1.Location = new System.Drawing.Point(713, 415);
|
||||
this.button1.Name = "button1";
|
||||
this.button1.Size = new System.Drawing.Size(75, 23);
|
||||
this.button1.TabIndex = 1;
|
||||
this.button1.Text = "&OK";
|
||||
this.button1.UseVisualStyleBackColor = true;
|
||||
this.button1.Click += new System.EventHandler(this.button1_Click);
|
||||
//
|
||||
// checkBox1
|
||||
//
|
||||
this.checkBox1.AutoSize = true;
|
||||
this.checkBox1.Location = new System.Drawing.Point(93, 419);
|
||||
this.checkBox1.Name = "checkBox1";
|
||||
this.checkBox1.Size = new System.Drawing.Size(160, 17);
|
||||
this.checkBox1.TabIndex = 2;
|
||||
this.checkBox1.Text = "Don\'t show me this next time";
|
||||
this.checkBox1.UseVisualStyleBackColor = true;
|
||||
this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged);
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F);
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(170, 24);
|
||||
this.label1.TabIndex = 3;
|
||||
this.label1.Text = "DiscordRPC Plugin";
|
||||
//
|
||||
// label2
|
||||
//
|
||||
this.label2.AutoSize = true;
|
||||
this.label2.Location = new System.Drawing.Point(9, 42);
|
||||
this.label2.Name = "label2";
|
||||
this.label2.Size = new System.Drawing.Size(52, 13);
|
||||
this.label2.TabIndex = 4;
|
||||
this.label2.Text = "Licenses:";
|
||||
//
|
||||
// button2
|
||||
//
|
||||
this.button2.Location = new System.Drawing.Point(12, 415);
|
||||
this.button2.Name = "button2";
|
||||
this.button2.Size = new System.Drawing.Size(75, 23);
|
||||
this.button2.TabIndex = 5;
|
||||
this.button2.Text = "Support Me";
|
||||
this.button2.UseVisualStyleBackColor = true;
|
||||
this.button2.Click += new System.EventHandler(this.button2_Click);
|
||||
//
|
||||
// WelcomeForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(800, 450);
|
||||
this.Controls.Add(this.button2);
|
||||
this.Controls.Add(this.label2);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.checkBox1);
|
||||
this.Controls.Add(this.button1);
|
||||
this.Controls.Add(this.richTextBox1);
|
||||
this.MaximizeBox = false;
|
||||
this.MaximumSize = new System.Drawing.Size(816, 489);
|
||||
this.MinimizeBox = false;
|
||||
this.MinimumSize = new System.Drawing.Size(816, 489);
|
||||
this.Name = "WelcomeForm";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "Thank you for installing DiscordRPC by EnderIce2";
|
||||
this.TopMost = true;
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.RichTextBox richTextBox1;
|
||||
private System.Windows.Forms.Button button1;
|
||||
private System.Windows.Forms.CheckBox checkBox1;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Button button2;
|
||||
}
|
||||
}
|
36
WelcomeForm.cs
Normal file
36
WelcomeForm.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using SDRSharp.Radio;
|
||||
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 WelcomeForm : Form
|
||||
{
|
||||
public WelcomeForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void button2_Click(object sender, EventArgs e)
|
||||
{
|
||||
System.Diagnostics.Process.Start("https://ko-fi.com/enderice2");
|
||||
}
|
||||
|
||||
private void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void checkBox1_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
Utils.SaveSetting("ShowWelcomePage", !checkBox1.Checked);
|
||||
}
|
||||
}
|
||||
}
|
160
WelcomeForm.resx
Normal file
160
WelcomeForm.resx
Normal file
@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="richTextBox1.Text" xml:space="preserve">
|
||||
<value>-----------------------------------------------------------------------------------
|
||||
URL for discord-rpc-csharp: https://github.com/Lachee/discord-rpc-csharp
|
||||
License for discord-rpc-csharp:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Lachee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-----------------------------------------------------------------------------------
|
||||
URL for Newtonsoft.Json: https://github.com/JamesNK/Newtonsoft.Json
|
||||
License for Newtonsoft.Json:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2007 James Newton-King
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DE</value>
|
||||
</data>
|
||||
</root>
|
4
packages.config
Normal file
4
packages.config
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net46" />
|
||||
</packages>
|
Loading…
x
Reference in New Issue
Block a user