@@ -71,16 +71,21 @@ namespace Discord.API | |||||
zlib.CopyTo(decompressed); | zlib.CopyTo(decompressed); | ||||
decompressed.Position = 0; | decompressed.Position = 0; | ||||
using (var reader = new StreamReader(decompressed)) | using (var reader = new StreamReader(decompressed)) | ||||
using (var jsonReader = new JsonTextReader(reader)) | |||||
{ | { | ||||
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(reader.ReadToEnd()); | |||||
var msg = _serializer.Deserialize<WebSocketMessage>(jsonReader); | |||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); | await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); | ||||
} | } | ||||
} | } | ||||
}; | }; | ||||
_gatewayClient.TextMessage += async text => | _gatewayClient.TextMessage += async text => | ||||
{ | { | ||||
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text); | |||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); | |||||
using (var reader = new StringReader(text)) | |||||
using (var jsonReader = new JsonTextReader(reader)) | |||||
{ | |||||
var msg = _serializer.Deserialize<WebSocketMessage>(jsonReader); | |||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); | |||||
} | |||||
}; | }; | ||||
_gatewayClient.Closed += async ex => | _gatewayClient.Closed += async ex => | ||||
{ | { | ||||
@@ -1,21 +1,16 @@ | |||||
using Discord.API.Gateway; | |||||
using Discord.API.Rest; | |||||
using Discord.API.Rpc; | |||||
using Discord.Net; | |||||
using Discord.API.Rpc; | |||||
using Discord.Logging; | |||||
using Discord.Net.Converters; | using Discord.Net.Converters; | ||||
using Discord.Net.Queue; | using Discord.Net.Queue; | ||||
using Discord.Net.Rest; | |||||
using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using Newtonsoft.Json.Linq; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using System.Collections.Concurrent; | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.Globalization; | using System.Globalization; | ||||
using System.IO; | using System.IO; | ||||
using System.IO.Compression; | using System.IO.Compression; | ||||
using System.Linq; | |||||
using System.Net; | |||||
using System.Text; | using System.Text; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -24,16 +19,46 @@ namespace Discord.API | |||||
{ | { | ||||
public class DiscordRpcApiClient : IDisposable | public class DiscordRpcApiClient : IDisposable | ||||
{ | { | ||||
private abstract class RpcRequest | |||||
{ | |||||
public abstract Task SetResultAsync(JToken data, JsonSerializer serializer); | |||||
public abstract Task SetExceptionAsync(JToken data, JsonSerializer serializer); | |||||
} | |||||
private class RpcRequest<T> : RpcRequest | |||||
{ | |||||
public TaskCompletionSource<T> Promise { get; set; } | |||||
public RpcRequest(RequestOptions options) | |||||
{ | |||||
Promise = new TaskCompletionSource<T>(); | |||||
Task.Run(async () => | |||||
{ | |||||
await Task.Delay(options?.Timeout ?? 15000).ConfigureAwait(false); | |||||
Promise.TrySetCanceled(); //Doesn't need to be async, we're already in a separate task | |||||
}); | |||||
} | |||||
public override Task SetResultAsync(JToken data, JsonSerializer serializer) | |||||
{ | |||||
return Promise.TrySetResultAsync(data.ToObject<T>(serializer)); | |||||
} | |||||
public override Task SetExceptionAsync(JToken data, JsonSerializer serializer) | |||||
{ | |||||
var error = data.ToObject<ErrorEvent>(serializer); | |||||
return Promise.TrySetExceptionAsync(new RpcException(error.Code, error.Message)); | |||||
} | |||||
} | |||||
private object _eventLock = new object(); | private object _eventLock = new object(); | ||||
public event Func<string, Task> SentRpcMessage { add { _sentRpcMessageEvent.Add(value); } remove { _sentRpcMessageEvent.Remove(value); } } | public event Func<string, Task> SentRpcMessage { add { _sentRpcMessageEvent.Add(value); } remove { _sentRpcMessageEvent.Remove(value); } } | ||||
private readonly AsyncEvent<Func<string, Task>> _sentRpcMessageEvent = new AsyncEvent<Func<string, Task>>(); | private readonly AsyncEvent<Func<string, Task>> _sentRpcMessageEvent = new AsyncEvent<Func<string, Task>>(); | ||||
public event Func<string, string, object, string, Task> ReceivedRpcEvent { add { _receivedRpcEvent.Add(value); } remove { _receivedRpcEvent.Remove(value); } } | |||||
private readonly AsyncEvent<Func<string, string, object, string, Task>> _receivedRpcEvent = new AsyncEvent<Func<string, string, object, string, Task>>(); | |||||
public event Func<string, Optional<string>, Optional<object>, Task> ReceivedRpcEvent { add { _receivedRpcEvent.Add(value); } remove { _receivedRpcEvent.Remove(value); } } | |||||
private readonly AsyncEvent<Func<string, Optional<string>, Optional<object>, Task>> _receivedRpcEvent = new AsyncEvent<Func<string, Optional<string>, Optional<object>, Task>>(); | |||||
public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | ||||
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | ||||
private readonly ConcurrentDictionary<Guid, RpcRequest> _requests; | |||||
private readonly RequestQueue _requestQueue; | private readonly RequestQueue _requestQueue; | ||||
private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
private readonly IWebSocketClient _webSocketClient; | private readonly IWebSocketClient _webSocketClient; | ||||
@@ -41,22 +66,26 @@ namespace Discord.API | |||||
private readonly string _clientId; | private readonly string _clientId; | ||||
private CancellationTokenSource _loginCancelToken, _connectCancelToken; | private CancellationTokenSource _loginCancelToken, _connectCancelToken; | ||||
private string _authToken; | private string _authToken; | ||||
private string _origin; | |||||
private bool _isDisposed; | private bool _isDisposed; | ||||
public LoginState LoginState { get; private set; } | public LoginState LoginState { get; private set; } | ||||
public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
public DiscordRpcApiClient(string clientId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||||
public DiscordRpcApiClient(string clientId, string origin, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||||
{ | { | ||||
_connectionLock = new SemaphoreSlim(1, 1); | _connectionLock = new SemaphoreSlim(1, 1); | ||||
_clientId = clientId; | _clientId = clientId; | ||||
_origin = origin; | |||||
_requestQueue = requestQueue ?? new RequestQueue(); | _requestQueue = requestQueue ?? new RequestQueue(); | ||||
_requests = new ConcurrentDictionary<Guid, RpcRequest>(); | |||||
if (webSocketProvider != null) | if (webSocketProvider != null) | ||||
{ | { | ||||
_webSocketClient = webSocketProvider(); | _webSocketClient = webSocketProvider(); | ||||
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) | |||||
//_webSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) | |||||
_webSocketClient.SetHeader("origin", _origin); | |||||
_webSocketClient.BinaryMessage += async (data, index, count) => | _webSocketClient.BinaryMessage += async (data, index, count) => | ||||
{ | { | ||||
using (var compressed = new MemoryStream(data, index + 2, count - 2)) | using (var compressed = new MemoryStream(data, index + 2, count - 2)) | ||||
@@ -66,16 +95,25 @@ namespace Discord.API | |||||
zlib.CopyTo(decompressed); | zlib.CopyTo(decompressed); | ||||
decompressed.Position = 0; | decompressed.Position = 0; | ||||
using (var reader = new StreamReader(decompressed)) | using (var reader = new StreamReader(decompressed)) | ||||
using (var jsonReader = new JsonTextReader(reader)) | |||||
{ | { | ||||
var msg = JsonConvert.DeserializeObject<RpcMessage>(reader.ReadToEnd()); | |||||
await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data, msg.Nonce).ConfigureAwait(false); | |||||
var msg = _serializer.Deserialize<RpcMessage>(jsonReader); | |||||
await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); | |||||
if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) | |||||
ProcessMessage(msg); | |||||
} | } | ||||
} | } | ||||
}; | }; | ||||
_webSocketClient.TextMessage += async text => | _webSocketClient.TextMessage += async text => | ||||
{ | { | ||||
var msg = JsonConvert.DeserializeObject<RpcMessage>(text); | |||||
await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data, msg.Nonce).ConfigureAwait(false); | |||||
using (var reader = new StringReader(text)) | |||||
using (var jsonReader = new JsonTextReader(reader)) | |||||
{ | |||||
var msg = _serializer.Deserialize<RpcMessage>(jsonReader); | |||||
await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); | |||||
if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) | |||||
ProcessMessage(msg); | |||||
} | |||||
}; | }; | ||||
_webSocketClient.Closed += async ex => | _webSocketClient.Closed += async ex => | ||||
{ | { | ||||
@@ -99,19 +137,19 @@ namespace Discord.API | |||||
} | } | ||||
} | } | ||||
public void Dispose() => Dispose(true); | public void Dispose() => Dispose(true); | ||||
public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) | |||||
public async Task LoginAsync(TokenType tokenType, string token, bool upgrade = false, RequestOptions options = null) | |||||
{ | { | ||||
await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
try | try | ||||
{ | { | ||||
await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); | |||||
await LoginInternalAsync(tokenType, token, upgrade, options).ConfigureAwait(false); | |||||
} | } | ||||
finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
} | } | ||||
private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) | |||||
private async Task LoginInternalAsync(TokenType tokenType, string token, bool upgrade = false, RequestOptions options = null) | |||||
{ | { | ||||
if (LoginState != LoginState.LoggedOut) | |||||
if (!upgrade && LoginState != LoginState.LoggedOut) | |||||
await LogoutInternalAsync().ConfigureAwait(false); | await LogoutInternalAsync().ConfigureAwait(false); | ||||
if (tokenType != TokenType.Bearer) | if (tokenType != TokenType.Bearer) | ||||
@@ -233,39 +271,155 @@ namespace Discord.API | |||||
} | } | ||||
//Core | //Core | ||||
public Task SendRpcAsync(string cmd, object payload, GlobalBucket bucket = GlobalBucket.GeneralRpc, RequestOptions options = null) | |||||
=> SendRpcAsyncInternal(cmd, payload, BucketGroup.Global, (int)bucket, 0, options); | |||||
public Task SendRpcAsync(string cmd, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) | |||||
=> SendRpcAsyncInternal(cmd, payload, BucketGroup.Guild, (int)bucket, guildId, options); | |||||
private async Task SendRpcAsyncInternal(string cmd, object payload, | |||||
BucketGroup group, int bucketId, ulong guildId, RequestOptions options) | |||||
public Task<TResponse> SendRpcAsync<TResponse>(string cmd, object payload, GlobalBucket bucket = GlobalBucket.GeneralRpc, | |||||
Optional<string> evt = default(Optional<string>), RequestOptions options = null) | |||||
where TResponse : class | |||||
=> SendRpcAsyncInternal<TResponse>(cmd, payload, BucketGroup.Global, (int)bucket, 0, evt, options); | |||||
public Task<TResponse> SendRpcAsync<TResponse>(string cmd, object payload, GuildBucket bucket, ulong guildId, | |||||
Optional<string> evt = default(Optional<string>), RequestOptions options = null) | |||||
where TResponse : class | |||||
=> SendRpcAsyncInternal<TResponse>(cmd, payload, BucketGroup.Guild, (int)bucket, guildId, evt, options); | |||||
private async Task<TResponse> SendRpcAsyncInternal<TResponse>(string cmd, object payload, BucketGroup group, int bucketId, ulong guildId, | |||||
Optional<string> evt, RequestOptions options) | |||||
where TResponse : class | |||||
{ | { | ||||
//TODO: Add Nonce to pair sent requests with responses | |||||
byte[] bytes = null; | byte[] bytes = null; | ||||
payload = new RpcMessage { Cmd = cmd, Args = payload, Nonce = Guid.NewGuid().ToString() }; | |||||
var guid = Guid.NewGuid(); | |||||
payload = new RpcMessage { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; | |||||
if (payload != null) | if (payload != null) | ||||
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | |||||
{ | |||||
var json = SerializeJson(payload); | |||||
bytes = Encoding.UTF8.GetBytes(json); | |||||
} | |||||
var requestTracker = new RpcRequest<TResponse>(options); | |||||
_requests[guid] = requestTracker; | |||||
await _requestQueue.SendAsync(new WebSocketRequest(_webSocketClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); | await _requestQueue.SendAsync(new WebSocketRequest(_webSocketClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); | ||||
await _sentRpcMessageEvent.InvokeAsync(cmd).ConfigureAwait(false); | await _sentRpcMessageEvent.InvokeAsync(cmd).ConfigureAwait(false); | ||||
return await requestTracker.Promise.Task.ConfigureAwait(false); | |||||
} | } | ||||
//Rpc | //Rpc | ||||
public async Task SendAuthenticateAsync(RequestOptions options = null) | |||||
public async Task<AuthenticateResponse> SendAuthenticateAsync(RequestOptions options = null) | |||||
{ | { | ||||
var msg = new AuthenticateParams() | var msg = new AuthenticateParams() | ||||
{ | { | ||||
AccessToken = _authToken | AccessToken = _authToken | ||||
}; | }; | ||||
await SendRpcAsync("AUTHENTICATE", msg, options: options).ConfigureAwait(false); | |||||
return await SendRpcAsync<AuthenticateResponse>("AUTHENTICATE", msg, options: options).ConfigureAwait(false); | |||||
} | } | ||||
public async Task SendAuthorizeAsync(string[] scopes, RequestOptions options = null) | |||||
public async Task<AuthorizeResponse> SendAuthorizeAsync(string[] scopes, RequestOptions options = null) | |||||
{ | { | ||||
var msg = new AuthorizeParams() | var msg = new AuthorizeParams() | ||||
{ | { | ||||
ClientId = _clientId, | ClientId = _clientId, | ||||
Scopes = scopes | Scopes = scopes | ||||
}; | }; | ||||
await SendRpcAsync("AUTHORIZE", msg, options: options).ConfigureAwait(false); | |||||
if (options == null) | |||||
options = new RequestOptions(); | |||||
if (options.Timeout == null) | |||||
options.Timeout = 60000; //This requires manual input on the user's end, lets give them more time | |||||
return await SendRpcAsync<AuthorizeResponse>("AUTHORIZE", msg, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<GetGuildsResponse> SendGetGuildsAsync(RequestOptions options = null) | |||||
{ | |||||
return await SendRpcAsync<GetGuildsResponse>("GET_GUILDS", null, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<RpcGuild> SendGetGuildAsync(ulong guildId, RequestOptions options = null) | |||||
{ | |||||
var msg = new GetGuildParams | |||||
{ | |||||
GuildId = guildId | |||||
}; | |||||
return await SendRpcAsync<RpcGuild>("GET_GUILD", msg, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<GetChannelsResponse> SendGetChannelsAsync(ulong guildId, RequestOptions options = null) | |||||
{ | |||||
var msg = new GetChannelsParams | |||||
{ | |||||
GuildId = guildId | |||||
}; | |||||
return await SendRpcAsync<GetChannelsResponse>("GET_CHANNELS", msg, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<RpcChannel> SendGetChannelAsync(ulong channelId, RequestOptions options = null) | |||||
{ | |||||
var msg = new GetChannelParams | |||||
{ | |||||
ChannelId = channelId | |||||
}; | |||||
return await SendRpcAsync<RpcChannel>("GET_CHANNEL", msg, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<SetLocalVolumeResponse> SendSetLocalVolumeAsync(int volume, RequestOptions options = null) | |||||
{ | |||||
var msg = new SetLocalVolumeParams | |||||
{ | |||||
Volume = volume | |||||
}; | |||||
return await SendRpcAsync<SetLocalVolumeResponse>("SET_LOCAL_VOLUME", msg, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<RpcChannel> SendSelectVoiceChannelAsync(ulong channelId, RequestOptions options = null) | |||||
{ | |||||
var msg = new SelectVoiceChannelParams | |||||
{ | |||||
ChannelId = channelId | |||||
}; | |||||
return await SendRpcAsync<RpcChannel>("SELECT_VOICE_CHANNEL", msg, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<SubscriptionResponse> SendChannelSubscribeAsync(string evt, ulong channelId, RequestOptions options = null) | |||||
{ | |||||
var msg = new ChannelSubscriptionParams | |||||
{ | |||||
ChannelId = channelId | |||||
}; | |||||
return await SendRpcAsync<SubscriptionResponse>("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<SubscriptionResponse> SendChannelUnsubscribeAsync(string evt, ulong channelId, RequestOptions options = null) | |||||
{ | |||||
var msg = new ChannelSubscriptionParams | |||||
{ | |||||
ChannelId = channelId | |||||
}; | |||||
return await SendRpcAsync<SubscriptionResponse>("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<SubscriptionResponse> SendGuildSubscribeAsync(string evt, ulong guildId, RequestOptions options = null) | |||||
{ | |||||
var msg = new GuildSubscriptionParams | |||||
{ | |||||
GuildId = guildId | |||||
}; | |||||
return await SendRpcAsync<SubscriptionResponse>("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<SubscriptionResponse> SendGuildUnsubscribeAsync(string evt, ulong guildId, RequestOptions options = null) | |||||
{ | |||||
var msg = new GuildSubscriptionParams | |||||
{ | |||||
GuildId = guildId | |||||
}; | |||||
return await SendRpcAsync<SubscriptionResponse>("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); | |||||
} | |||||
private bool ProcessMessage(RpcMessage msg) | |||||
{ | |||||
RpcRequest requestTracker; | |||||
if (_requests.TryGetValue(msg.Nonce.Value.Value, out requestTracker)) | |||||
{ | |||||
if (msg.Event.GetValueOrDefault("") == "ERROR") | |||||
{ | |||||
var _ = requestTracker.SetExceptionAsync(msg.Data.GetValueOrDefault() as JToken, _serializer); | |||||
} | |||||
else | |||||
{ | |||||
var _ = requestTracker.SetResultAsync(msg.Data.GetValueOrDefault() as JToken, _serializer); | |||||
} | |||||
return true; | |||||
} | |||||
else | |||||
return false; | |||||
} | } | ||||
//Helpers | //Helpers | ||||
@@ -11,7 +11,7 @@ namespace Discord.API.Rpc | |||||
[JsonProperty("id")] | [JsonProperty("id")] | ||||
public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
[JsonProperty("rpc_origins")] | [JsonProperty("rpc_origins")] | ||||
public string RpcOrigins { get; set; } | |||||
public string[] RpcOrigins { get; set; } | |||||
[JsonProperty("name")] | [JsonProperty("name")] | ||||
public string Name { get; set; } | public string Name { get; set; } | ||||
} | } | ||||
@@ -3,7 +3,7 @@ using System; | |||||
namespace Discord.API.Rpc | namespace Discord.API.Rpc | ||||
{ | { | ||||
public class AuthenticateEvent | |||||
public class AuthenticateResponse | |||||
{ | { | ||||
[JsonProperty("application")] | [JsonProperty("application")] | ||||
public Application Application { get; set; } | public Application Application { get; set; } |
@@ -3,7 +3,7 @@ using System; | |||||
namespace Discord.API.Rpc | namespace Discord.API.Rpc | ||||
{ | { | ||||
public class AuthorizeEvent | |||||
public class AuthorizeResponse | |||||
{ | { | ||||
[JsonProperty("code")] | [JsonProperty("code")] | ||||
public string Code { get; set; } | public string Code { get; set; } |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class ChannelSubscriptionParams | |||||
{ | |||||
[JsonProperty("channel_id")] | |||||
public ulong ChannelId { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class GetChannelParams | |||||
{ | |||||
[JsonProperty("channel_id")] | |||||
public ulong ChannelId { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class GetChannelsParams | |||||
{ | |||||
[JsonProperty("guild_id")] | |||||
public ulong GuildId { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class GetChannelsResponse | |||||
{ | |||||
[JsonProperty("channels")] | |||||
public RpcChannel[] Channels { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class GetGuildParams | |||||
{ | |||||
[JsonProperty("guild_id")] | |||||
public ulong GuildId { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class GetGuildsParams | |||||
{ | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class GetGuildsResponse | |||||
{ | |||||
[JsonProperty("guilds")] | |||||
public RpcUserGuild[] Guilds { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class GuildSubscriptionParams | |||||
{ | |||||
[JsonProperty("guild_id")] | |||||
public ulong GuildId { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class MessageEvent | |||||
{ | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class RpcChannel : Channel | |||||
{ | |||||
[JsonProperty("voice_states")] | |||||
public VoiceState[] VoiceStates { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,12 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class RpcGuild : Guild | |||||
{ | |||||
[JsonProperty("online")] | |||||
public int Online { get; set; } | |||||
[JsonProperty("members")] | |||||
public GuildMember[] Members { get; set; } | |||||
} | |||||
} |
@@ -1,4 +1,5 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System; | |||||
namespace Discord.API.Rpc | namespace Discord.API.Rpc | ||||
{ | { | ||||
@@ -7,11 +8,11 @@ namespace Discord.API.Rpc | |||||
[JsonProperty("cmd")] | [JsonProperty("cmd")] | ||||
public string Cmd { get; set; } | public string Cmd { get; set; } | ||||
[JsonProperty("nonce")] | [JsonProperty("nonce")] | ||||
public string Nonce { get; set; } | |||||
public Optional<Guid?> Nonce { get; set; } | |||||
[JsonProperty("evt")] | [JsonProperty("evt")] | ||||
public string Event { get; set; } | |||||
public Optional<string> Event { get; set; } | |||||
[JsonProperty("data")] | [JsonProperty("data")] | ||||
public object Data { get; set; } | |||||
public Optional<object> Data { get; set; } | |||||
[JsonProperty("args")] | [JsonProperty("args")] | ||||
public object Args { get; set; } | public object Args { get; set; } | ||||
} | } | ||||
@@ -0,0 +1,12 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class RpcUserGuild | |||||
{ | |||||
[JsonProperty("id")] | |||||
public ulong Id { get; set; } | |||||
[JsonProperty("name")] | |||||
public string Name { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class SelectVoiceChannelParams | |||||
{ | |||||
[JsonProperty("channel_id")] | |||||
public ulong? ChannelId { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class SetLocalVolumeParams | |||||
{ | |||||
[JsonProperty("volume")] | |||||
public int Volume { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,12 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class SetLocalVolumeResponse | |||||
{ | |||||
[JsonProperty("user_id")] | |||||
public ulong UserId { get; set; } | |||||
[JsonProperty("volume")] | |||||
public int Volume { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class SpeakingEvent | |||||
{ | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class SubscriptionResponse | |||||
{ | |||||
[JsonProperty("evt")] | |||||
public string Event { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.API.Rpc | |||||
{ | |||||
public class VoiceStateEvent | |||||
{ | |||||
} | |||||
} |
@@ -314,7 +314,7 @@ namespace Discord | |||||
private async Task WriteInitialLog() | private async Task WriteInitialLog() | ||||
{ | { | ||||
if (this is DiscordSocketClient) | if (this is DiscordSocketClient) | ||||
await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordConfig.GatewayEncoding})").ConfigureAwait(false); | |||||
await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordSocketConfig.GatewayEncoding})").ConfigureAwait(false); | |||||
else if (this is DiscordRpcClient) | else if (this is DiscordRpcClient) | ||||
await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, RPC API v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false); | await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, RPC API v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false); | ||||
else | else | ||||
@@ -4,7 +4,6 @@ using Discord.Net.Converters; | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Runtime.InteropServices; | using System.Runtime.InteropServices; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -48,7 +47,7 @@ namespace Discord | |||||
public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
/// <summary> Creates a new RPC discord client. </summary> | /// <summary> Creates a new RPC discord client. </summary> | ||||
public DiscordRpcClient(string clientId) : this(new DiscordRpcConfig(clientId)) { } | |||||
public DiscordRpcClient(string clientId, string origin) : this(new DiscordRpcConfig(clientId, origin)) { } | |||||
/// <summary> Creates a new RPC discord client. </summary> | /// <summary> Creates a new RPC discord client. </summary> | ||||
public DiscordRpcClient(DiscordRpcConfig config) | public DiscordRpcClient(DiscordRpcConfig config) | ||||
{ | { | ||||
@@ -59,7 +58,7 @@ namespace Discord | |||||
_isFirstLogSub = true; | _isFirstLogSub = true; | ||||
_connectionLock = new SemaphoreSlim(1, 1); | _connectionLock = new SemaphoreSlim(1, 1); | ||||
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | ||||
_serializer.Error += (s, e) => | _serializer.Error += (s, e) => | ||||
{ | { | ||||
@@ -67,7 +66,7 @@ namespace Discord | |||||
e.ErrorContext.Handled = true; | e.ErrorContext.Handled = true; | ||||
}; | }; | ||||
ApiClient = new API.DiscordRpcApiClient(config.ClientId, config.WebSocketProvider); | |||||
ApiClient = new API.DiscordRpcApiClient(config.ClientId, config.Origin, config.WebSocketProvider); | |||||
ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); | ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); | ||||
ApiClient.ReceivedRpcEvent += ProcessMessageAsync; | ApiClient.ReceivedRpcEvent += ProcessMessageAsync; | ||||
ApiClient.Disconnected += async ex => | ApiClient.Disconnected += async ex => | ||||
@@ -198,11 +197,6 @@ namespace Discord | |||||
await ApiClient.ConnectAsync().ConfigureAwait(false); | await ApiClient.ConnectAsync().ConfigureAwait(false); | ||||
await _connectedEvent.InvokeAsync().ConfigureAwait(false); | await _connectedEvent.InvokeAsync().ConfigureAwait(false); | ||||
/*if (_sessionId != null) | |||||
await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); | |||||
else | |||||
await ApiClient.SendIdentifyAsync().ConfigureAwait(false);*/ | |||||
await _connectTask.Task.ConfigureAwait(false); | await _connectTask.Task.ConfigureAwait(false); | ||||
ConnectionState = ConnectionState.Connected; | ConnectionState = ConnectionState.Connected; | ||||
@@ -301,25 +295,40 @@ namespace Discord | |||||
} | } | ||||
} | } | ||||
private async Task ProcessMessageAsync(string cmd, string evnt, object payload, string nonce) | |||||
private async Task ProcessMessageAsync(string cmd, Optional<string> evnt, Optional<object> payload) | |||||
{ | { | ||||
try | try | ||||
{ | { | ||||
switch (cmd) | switch (cmd) | ||||
{ | { | ||||
case "DISPATCH": | case "DISPATCH": | ||||
switch (evnt) | |||||
switch (evnt.Value) | |||||
{ | { | ||||
//Connection | //Connection | ||||
case "READY": | case "READY": | ||||
{ | { | ||||
await _rpcLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); | await _rpcLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); | ||||
var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | |||||
if (_scopes != null) | |||||
await ApiClient.SendAuthorizeAsync(_scopes).ConfigureAwait(false); //No bearer | |||||
else | |||||
await ApiClient.SendAuthenticateAsync().ConfigureAwait(false); //Has bearer | |||||
var data = (payload.Value as JToken).ToObject<ReadyEvent>(_serializer); | |||||
var cancelToken = _cancelToken; | |||||
var _ = Task.Run(async () => | |||||
{ | |||||
RequestOptions options = new RequestOptions | |||||
{ | |||||
//CancellationToken = cancelToken //TODO: Implement | |||||
}; | |||||
if (_scopes != null) //No bearer | |||||
{ | |||||
var authorizeData = await ApiClient.SendAuthorizeAsync(_scopes, options).ConfigureAwait(false); | |||||
await ApiClient.LoginAsync(TokenType.Bearer, authorizeData.Code, options).ConfigureAwait(false); | |||||
} | |||||
var authenticateData = await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer | |||||
var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete | |||||
await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); | |||||
}); | |||||
} | } | ||||
break; | break; | ||||
@@ -329,32 +338,15 @@ namespace Discord | |||||
return; | return; | ||||
} | } | ||||
break; | break; | ||||
case "AUTHORIZE": | |||||
{ | |||||
await _rpcLogger.DebugAsync("Received AUTHORIZE").ConfigureAwait(false); | |||||
var data = (payload as JToken).ToObject<AuthorizeEvent>(_serializer); | |||||
await ApiClient.LoginAsync(TokenType.Bearer, data.Code).ConfigureAwait(false); | |||||
await ApiClient.SendAuthenticateAsync().ConfigureAwait(false); | |||||
} | |||||
break; | |||||
case "AUTHENTICATE": | |||||
{ | |||||
await _rpcLogger.DebugAsync("Received AUTHENTICATE").ConfigureAwait(false); | |||||
var data = (payload as JToken).ToObject<AuthenticateEvent>(_serializer); | |||||
var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete | |||||
await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); | |||||
} | |||||
break; | |||||
default: | |||||
/*default: | |||||
await _rpcLogger.WarningAsync($"Unknown OpCode ({cmd})").ConfigureAwait(false); | await _rpcLogger.WarningAsync($"Unknown OpCode ({cmd})").ConfigureAwait(false); | ||||
return; | |||||
return;*/ | |||||
} | } | ||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt != null ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); | |||||
await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); | |||||
return; | return; | ||||
} | } | ||||
} | } | ||||
@@ -9,13 +9,16 @@ namespace Discord | |||||
public const int PortRangeStart = 6463; | public const int PortRangeStart = 6463; | ||||
public const int PortRangeEnd = 6472; | public const int PortRangeEnd = 6472; | ||||
public DiscordRpcConfig(string clientId) | |||||
public DiscordRpcConfig(string clientId, string origin) | |||||
{ | { | ||||
ClientId = clientId; | ClientId = clientId; | ||||
Origin = origin; | |||||
} | } | ||||
/// <summary> Gets or sets the Discord client/application id used for this RPC connection. </summary> | /// <summary> Gets or sets the Discord client/application id used for this RPC connection. </summary> | ||||
public string ClientId { get; set; } | public string ClientId { get; set; } | ||||
/// <summary> Gets or sets the origin used for this RPC connection. </summary> | |||||
public string Origin { get; set; } | |||||
/// <summary> Gets or sets the provider used to generate new websocket connections. </summary> | /// <summary> Gets or sets the provider used to generate new websocket connections. </summary> | ||||
public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); | public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); | ||||
@@ -0,0 +1,8 @@ | |||||
namespace Discord.Entities.Rpc | |||||
{ | |||||
public interface IRemoteUserGuild : ISnowflakeEntity | |||||
{ | |||||
/// <summary> Gets the name of this guild. </summary> | |||||
string Name { get; } | |||||
} | |||||
} |
@@ -0,0 +1,29 @@ | |||||
using System; | |||||
using Model = Discord.API.Rpc.RpcUserGuild; | |||||
namespace Discord.Entities.Rpc | |||||
{ | |||||
internal class RemoteUserGuild : IRemoteUserGuild, ISnowflakeEntity | |||||
{ | |||||
public ulong Id { get; } | |||||
public DiscordRestClient Discord { get; } | |||||
public string Name { get; private set; } | |||||
public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||||
public RemoteUserGuild(DiscordRestClient discord, Model model) | |||||
{ | |||||
Id = model.Id; | |||||
Discord = discord; | |||||
Update(model, UpdateSource.Creation); | |||||
} | |||||
public void Update(Model model, UpdateSource source) | |||||
{ | |||||
if (source == UpdateSource.Rest) return; | |||||
Name = model.Name; | |||||
} | |||||
bool IEntity<ulong>.IsAttached => false; | |||||
} | |||||
} |
@@ -0,0 +1,17 @@ | |||||
using System; | |||||
namespace Discord | |||||
{ | |||||
public class RpcException : Exception | |||||
{ | |||||
public int ErrorCode { get; } | |||||
public string Reason { get; } | |||||
public RpcException(int errorCode, string reason = null) | |||||
: base($"The server sent error {errorCode}{(reason != null ? $": \"{reason}\"" : "")}") | |||||
{ | |||||
ErrorCode = errorCode; | |||||
Reason = reason; | |||||
} | |||||
} | |||||
} |