@@ -71,16 +71,21 @@ namespace Discord.API | |||
zlib.CopyTo(decompressed); | |||
decompressed.Position = 0; | |||
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); | |||
} | |||
} | |||
}; | |||
_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 => | |||
{ | |||
@@ -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.Queue; | |||
using Discord.Net.Rest; | |||
using Discord.Net.WebSockets; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Collections.Concurrent; | |||
using System.Diagnostics; | |||
using System.Globalization; | |||
using System.IO; | |||
using System.IO.Compression; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Text; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -24,16 +19,46 @@ namespace Discord.API | |||
{ | |||
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(); | |||
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>>(); | |||
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); } } | |||
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||
private readonly ConcurrentDictionary<Guid, RpcRequest> _requests; | |||
private readonly RequestQueue _requestQueue; | |||
private readonly JsonSerializer _serializer; | |||
private readonly IWebSocketClient _webSocketClient; | |||
@@ -41,22 +66,26 @@ namespace Discord.API | |||
private readonly string _clientId; | |||
private CancellationTokenSource _loginCancelToken, _connectCancelToken; | |||
private string _authToken; | |||
private string _origin; | |||
private bool _isDisposed; | |||
public LoginState LoginState { 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); | |||
_clientId = clientId; | |||
_origin = origin; | |||
_requestQueue = requestQueue ?? new RequestQueue(); | |||
_requests = new ConcurrentDictionary<Guid, RpcRequest>(); | |||
if (webSocketProvider != null) | |||
{ | |||
_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) => | |||
{ | |||
using (var compressed = new MemoryStream(data, index + 2, count - 2)) | |||
@@ -66,16 +95,25 @@ namespace Discord.API | |||
zlib.CopyTo(decompressed); | |||
decompressed.Position = 0; | |||
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 => | |||
{ | |||
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 => | |||
{ | |||
@@ -99,19 +137,19 @@ namespace Discord.API | |||
} | |||
} | |||
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); | |||
try | |||
{ | |||
await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); | |||
await LoginInternalAsync(tokenType, token, upgrade, options).ConfigureAwait(false); | |||
} | |||
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); | |||
if (tokenType != TokenType.Bearer) | |||
@@ -233,39 +271,155 @@ namespace Discord.API | |||
} | |||
//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; | |||
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) | |||
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 _sentRpcMessageEvent.InvokeAsync(cmd).ConfigureAwait(false); | |||
return await requestTracker.Promise.Task.ConfigureAwait(false); | |||
} | |||
//Rpc | |||
public async Task SendAuthenticateAsync(RequestOptions options = null) | |||
public async Task<AuthenticateResponse> SendAuthenticateAsync(RequestOptions options = null) | |||
{ | |||
var msg = new AuthenticateParams() | |||
{ | |||
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() | |||
{ | |||
ClientId = _clientId, | |||
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 | |||
@@ -11,7 +11,7 @@ namespace Discord.API.Rpc | |||
[JsonProperty("id")] | |||
public ulong Id { get; set; } | |||
[JsonProperty("rpc_origins")] | |||
public string RpcOrigins { get; set; } | |||
public string[] RpcOrigins { get; set; } | |||
[JsonProperty("name")] | |||
public string Name { get; set; } | |||
} | |||
@@ -3,7 +3,7 @@ using System; | |||
namespace Discord.API.Rpc | |||
{ | |||
public class AuthenticateEvent | |||
public class AuthenticateResponse | |||
{ | |||
[JsonProperty("application")] | |||
public Application Application { get; set; } |
@@ -3,7 +3,7 @@ using System; | |||
namespace Discord.API.Rpc | |||
{ | |||
public class AuthorizeEvent | |||
public class AuthorizeResponse | |||
{ | |||
[JsonProperty("code")] | |||
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 System; | |||
namespace Discord.API.Rpc | |||
{ | |||
@@ -7,11 +8,11 @@ namespace Discord.API.Rpc | |||
[JsonProperty("cmd")] | |||
public string Cmd { get; set; } | |||
[JsonProperty("nonce")] | |||
public string Nonce { get; set; } | |||
public Optional<Guid?> Nonce { get; set; } | |||
[JsonProperty("evt")] | |||
public string Event { get; set; } | |||
public Optional<string> Event { get; set; } | |||
[JsonProperty("data")] | |||
public object Data { get; set; } | |||
public Optional<object> Data { get; set; } | |||
[JsonProperty("args")] | |||
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() | |||
{ | |||
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) | |||
await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, RPC API v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false); | |||
else | |||
@@ -4,7 +4,6 @@ using Discord.Net.Converters; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Runtime.InteropServices; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -48,7 +47,7 @@ namespace Discord | |||
public ConnectionState ConnectionState { get; private set; } | |||
/// <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> | |||
public DiscordRpcClient(DiscordRpcConfig config) | |||
{ | |||
@@ -59,7 +58,7 @@ namespace Discord | |||
_isFirstLogSub = true; | |||
_connectionLock = new SemaphoreSlim(1, 1); | |||
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
_serializer.Error += (s, e) => | |||
{ | |||
@@ -67,7 +66,7 @@ namespace Discord | |||
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.ReceivedRpcEvent += ProcessMessageAsync; | |||
ApiClient.Disconnected += async ex => | |||
@@ -198,11 +197,6 @@ namespace Discord | |||
await ApiClient.ConnectAsync().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); | |||
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 | |||
{ | |||
switch (cmd) | |||
{ | |||
case "DISPATCH": | |||
switch (evnt) | |||
switch (evnt.Value) | |||
{ | |||
//Connection | |||
case "READY": | |||
{ | |||
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; | |||
@@ -329,32 +338,15 @@ namespace Discord | |||
return; | |||
} | |||
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); | |||
return; | |||
return;*/ | |||
} | |||
} | |||
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; | |||
} | |||
} | |||
@@ -9,13 +9,16 @@ namespace Discord | |||
public const int PortRangeStart = 6463; | |||
public const int PortRangeEnd = 6472; | |||
public DiscordRpcConfig(string clientId) | |||
public DiscordRpcConfig(string clientId, string origin) | |||
{ | |||
ClientId = clientId; | |||
Origin = origin; | |||
} | |||
/// <summary> Gets or sets the Discord client/application id used for this RPC connection. </summary> | |||
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> | |||
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; | |||
} | |||
} | |||
} |