Browse Source

Merge branch 'dev' of https://github.com/RogueException/Discord.Net into dev

tags/1.0-rc
Christopher F 8 years ago
parent
commit
dc58f92333
100 changed files with 1757 additions and 435 deletions
  1. +2
    -7
      src/Discord.Net.Commands/Command.cs
  2. +5
    -5
      src/Discord.Net/API/CDN.cs
  3. +86
    -278
      src/Discord.Net/API/DiscordRestApiClient.cs
  4. +360
    -0
      src/Discord.Net/API/DiscordRpcAPIClient.cs
  5. +243
    -0
      src/Discord.Net/API/DiscordSocketApiClient.cs
  6. +2
    -0
      src/Discord.Net/API/Gateway/IdentifyParams.cs
  7. +1
    -1
      src/Discord.Net/API/Gateway/RequestMembersParams.cs
  8. +4
    -2
      src/Discord.Net/API/Rest/GetChannelMessagesParams.cs
  9. +18
    -0
      src/Discord.Net/API/Rpc/Application.cs
  10. +10
    -0
      src/Discord.Net/API/Rpc/AuthenticateParams.cs
  11. +17
    -0
      src/Discord.Net/API/Rpc/AuthenticateResponse.cs
  12. +14
    -0
      src/Discord.Net/API/Rpc/AuthorizeParams.cs
  13. +11
    -0
      src/Discord.Net/API/Rpc/AuthorizeResponse.cs
  14. +10
    -0
      src/Discord.Net/API/Rpc/ChannelSubscriptionParams.cs
  15. +12
    -0
      src/Discord.Net/API/Rpc/ErrorEvent.cs
  16. +10
    -0
      src/Discord.Net/API/Rpc/GetChannelParams.cs
  17. +10
    -0
      src/Discord.Net/API/Rpc/GetChannelsParams.cs
  18. +10
    -0
      src/Discord.Net/API/Rpc/GetChannelsResponse.cs
  19. +10
    -0
      src/Discord.Net/API/Rpc/GetGuildParams.cs
  20. +11
    -0
      src/Discord.Net/API/Rpc/GetGuildsParams.cs
  21. +10
    -0
      src/Discord.Net/API/Rpc/GetGuildsResponse.cs
  22. +12
    -0
      src/Discord.Net/API/Rpc/GuildStatusEvent.cs
  23. +10
    -0
      src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs
  24. +11
    -0
      src/Discord.Net/API/Rpc/MessageEvent.cs
  25. +12
    -0
      src/Discord.Net/API/Rpc/ReadyEvent.cs
  26. +10
    -0
      src/Discord.Net/API/Rpc/RpcChannel.cs
  27. +14
    -0
      src/Discord.Net/API/Rpc/RpcConfig.cs
  28. +12
    -0
      src/Discord.Net/API/Rpc/RpcGuild.cs
  29. +19
    -0
      src/Discord.Net/API/Rpc/RpcMessage.cs
  30. +12
    -0
      src/Discord.Net/API/Rpc/RpcUserGuild.cs
  31. +10
    -0
      src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs
  32. +10
    -0
      src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs
  33. +12
    -0
      src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs
  34. +11
    -0
      src/Discord.Net/API/Rpc/SpeakingEvent.cs
  35. +10
    -0
      src/Discord.Net/API/Rpc/SubscriptionResponse.cs
  36. +11
    -0
      src/Discord.Net/API/Rpc/VoiceStateEvent.cs
  37. +1
    -0
      src/Discord.Net/Audio/AudioClient.cs
  38. +1
    -2
      src/Discord.Net/DataStore.cs
  39. +8
    -1
      src/Discord.Net/DiscordConfig.cs
  40. +2
    -2
      src/Discord.Net/Entities/Channels/IMessageChannel.cs
  41. +0
    -21
      src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs
  42. +1
    -1
      src/Discord.Net/Extensions/CollectionExtensions.cs
  43. +3
    -2
      src/Discord.Net/Extensions/DiscordClientExtensions.cs
  44. +1
    -1
      src/Discord.Net/Extensions/GuildExtensions.cs
  45. +1
    -1
      src/Discord.Net/Extensions/GuildUserExtensions.cs
  46. +1
    -1
      src/Discord.Net/IDiscordClient.cs
  47. +4
    -1
      src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs
  48. +4
    -1
      src/Discord.Net/Net/Queue/RequestQueue.cs
  49. +17
    -0
      src/Discord.Net/Net/RpcException.cs
  50. +42
    -30
      src/Discord.Net/Rest/DiscordRestClient.cs
  51. +2
    -6
      src/Discord.Net/Rest/DiscordRestConfig.cs
  52. +3
    -2
      src/Discord.Net/Rest/Entities/Application.cs
  53. +1
    -0
      src/Discord.Net/Rest/Entities/Channels/DMChannel.cs
  54. +2
    -2
      src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs
  55. +1
    -0
      src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs
  56. +0
    -0
      src/Discord.Net/Rest/Entities/Channels/TextChannel.cs
  57. +0
    -0
      src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs
  58. +3
    -1
      src/Discord.Net/Rest/Entities/Entity.cs
  59. +1
    -1
      src/Discord.Net/Rest/Entities/Guilds/Guild.cs
  60. +1
    -0
      src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs
  61. +2
    -1
      src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs
  62. +0
    -0
      src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs
  63. +4
    -3
      src/Discord.Net/Rest/Entities/Invites/Invite.cs
  64. +2
    -1
      src/Discord.Net/Rest/Entities/Invites/InviteMetadata.cs
  65. +0
    -0
      src/Discord.Net/Rest/Entities/Messages/Attachment.cs
  66. +0
    -0
      src/Discord.Net/Rest/Entities/Messages/Embed.cs
  67. +5
    -8
      src/Discord.Net/Rest/Entities/Messages/Message.cs
  68. +1
    -3
      src/Discord.Net/Rest/Entities/Roles/Role.cs
  69. +0
    -0
      src/Discord.Net/Rest/Entities/SnowflakeEntity.cs
  70. +0
    -0
      src/Discord.Net/Rest/Entities/Users/Connection.cs
  71. +1
    -0
      src/Discord.Net/Rest/Entities/Users/GroupUser.cs
  72. +1
    -0
      src/Discord.Net/Rest/Entities/Users/GuildUser.cs
  73. +6
    -25
      src/Discord.Net/Rest/Entities/Users/SelfUser.cs
  74. +2
    -1
      src/Discord.Net/Rest/Entities/Users/User.cs
  75. +64
    -0
      src/Discord.Net/Rpc/DiscordRpcClient.Events.cs
  76. +400
    -0
      src/Discord.Net/Rpc/DiscordRpcClient.cs
  77. +27
    -0
      src/Discord.Net/Rpc/DiscordRpcConfig.cs
  78. +8
    -0
      src/Discord.Net/Rpc/Entities/IRemoteUserGuild.cs
  79. +30
    -0
      src/Discord.Net/Rpc/Entities/RemoteUserGuild.cs
  80. +15
    -0
      src/Discord.Net/Rpc/Entities/RpcMessage.cs
  81. +14
    -0
      src/Discord.Net/Rpc/RpcChannelEvent.cs
  82. +7
    -0
      src/Discord.Net/Rpc/RpcGuildEvent.cs
  83. +1
    -1
      src/Discord.Net/WebSocket/DiscordSocketClient.Events.cs
  84. +13
    -6
      src/Discord.Net/WebSocket/DiscordSocketClient.cs
  85. +1
    -5
      src/Discord.Net/WebSocket/DiscordSocketConfig.cs
  86. +0
    -0
      src/Discord.Net/WebSocket/Entities/Channels/ISocketChannel.cs
  87. +0
    -0
      src/Discord.Net/WebSocket/Entities/Channels/ISocketGuildChannel.cs
  88. +0
    -0
      src/Discord.Net/WebSocket/Entities/Channels/ISocketMessageChannel.cs
  89. +0
    -0
      src/Discord.Net/WebSocket/Entities/Channels/ISocketPrivateChannel.cs
  90. +3
    -2
      src/Discord.Net/WebSocket/Entities/Channels/MessageCache.cs
  91. +4
    -2
      src/Discord.Net/WebSocket/Entities/Channels/MessageManager.cs
  92. +2
    -1
      src/Discord.Net/WebSocket/Entities/Channels/SocketDMChannel.cs
  93. +1
    -1
      src/Discord.Net/WebSocket/Entities/Channels/SocketGroupChannel.cs
  94. +5
    -3
      src/Discord.Net/WebSocket/Entities/Channels/SocketTextChannel.cs
  95. +1
    -0
      src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs
  96. +1
    -1
      src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs
  97. +2
    -1
      src/Discord.Net/WebSocket/Entities/Messages/SocketMessage.cs
  98. +0
    -0
      src/Discord.Net/WebSocket/Entities/Users/ISocketUser.cs
  99. +0
    -0
      src/Discord.Net/WebSocket/Entities/Users/Presence.cs
  100. +2
    -1
      src/Discord.Net/WebSocket/Entities/Users/SocketDMUser.cs

+ 2
- 7
src/Discord.Net.Commands/Command.cs View File

@@ -88,13 +88,8 @@ namespace Discord.Commands
throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?");

bool isUnparsed = parameter.GetCustomAttribute<UnparsedAttribute>() != null;
if (isUnparsed)
{
if (type != typeof(string))
throw new InvalidOperationException("Unparsed parameters only support the string type.");
else if (i != parameters.Length - 1)
throw new InvalidOperationException("Unparsed parameters must be the last parameter in a command.");
}
if (isUnparsed && i != parameters.Length - 1)
throw new InvalidOperationException("Unparsed parameters must be the last parameter in a command.");

string name = parameter.Name;
string description = typeInfo.GetCustomAttribute<DescriptionAttribute>()?.Text;


+ 5
- 5
src/Discord.Net/API/CDN.cs View File

@@ -3,14 +3,14 @@
internal static class CDN
{
public static string GetApplicationIconUrl(ulong appId, string iconId)
=> iconId != null ? $"{DiscordRestConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null;
=> iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null;
public static string GetUserAvatarUrl(ulong userId, string avatarId)
=> avatarId != null ? $"{DiscordRestConfig.CDNUrl}avatars/{userId}/{avatarId}.jpg" : null;
=> avatarId != null ? $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.jpg" : null;
public static string GetGuildIconUrl(ulong guildId, string iconId)
=> iconId != null ? $"{DiscordRestConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;
public static string GetGuildSplashUrl(ulong guildId, string splashId)
=> splashId != null ? $"{DiscordRestConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null;
=> splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null;
public static string GetChannelIconUrl(ulong channelId, string iconId)
=> iconId != null ? $"{DiscordRestConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null;
=> iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null;
}
}

src/Discord.Net/API/DiscordAPIClient.cs → src/Discord.Net/API/DiscordRestApiClient.cs View File

@@ -5,6 +5,7 @@ using Discord.Net.Converters;
using Discord.Net.Queue;
using Discord.Net.Rest;
using Discord.Net.WebSockets;
using Discord.Rest;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
@@ -21,133 +22,96 @@ using System.Threading.Tasks;

namespace Discord.API
{
public class DiscordApiClient : IDisposable
public class DiscordRestApiClient : IDisposable
{
private object _eventLock = new object();

public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } }
private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>();
public event Func<GatewayOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } }
private readonly AsyncEvent<Func<GatewayOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<GatewayOpCode, Task>>();

public event Func<GatewayOpCode, int?, string, object, Task> ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } }
private readonly AsyncEvent<Func<GatewayOpCode, int?, string, object, Task>> _receivedGatewayEvent = new AsyncEvent<Func<GatewayOpCode, int?, string, 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 RequestQueue _requestQueue;
private readonly JsonSerializer _serializer;
private readonly IRestClient _restClient;
private readonly IWebSocketClient _gatewayClient;
private readonly SemaphoreSlim _connectionLock;
private CancellationTokenSource _loginCancelToken, _connectCancelToken;
private string _authToken;
private string _gatewayUrl;
private bool _isDisposed;
protected readonly JsonSerializer _serializer;
protected readonly SemaphoreSlim _stateLock;
private readonly RestClientProvider _restClientProvider;

protected string _authToken;
protected bool _isDisposed;
private CancellationTokenSource _loginCancelToken;
private IRestClient _restClient;

public LoginState LoginState { get; private set; }
public ConnectionState ConnectionState { get; private set; }
public TokenType AuthTokenType { get; private set; }
internal RequestQueue RequestQueue { get; private set; }

public DiscordApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider = null, JsonSerializer serializer = null, RequestQueue requestQueue = null)
public DiscordRestApiClient(RestClientProvider restClientProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null)
{
_connectionLock = new SemaphoreSlim(1, 1);
_restClientProvider = restClientProvider;
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
RequestQueue = requestQueue;

_requestQueue = requestQueue ?? new RequestQueue();
_stateLock = new SemaphoreSlim(1, 1);

_restClient = restClientProvider(DiscordRestConfig.ClientAPIUrl);
SetBaseUrl(DiscordConfig.ClientAPIUrl);
}
internal void SetBaseUrl(string baseUrl)
{
_restClient = _restClientProvider(baseUrl);
_restClient.SetHeader("accept", "*/*");
_restClient.SetHeader("user-agent", DiscordRestConfig.UserAgent);
if (webSocketProvider != null)
_restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken));
}
internal static string GetPrefixedToken(TokenType tokenType, string token)
{
switch (tokenType)
{
_gatewayClient = webSocketProvider();
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+)
_gatewayClient.BinaryMessage += async (data, index, count) =>
{
using (var compressed = new MemoryStream(data, index + 2, count - 2))
using (var decompressed = new MemoryStream())
{
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress))
zlib.CopyTo(decompressed);
decompressed.Position = 0;
using (var reader = new StreamReader(decompressed))
{
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(reader.ReadToEnd());
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);
};
_gatewayClient.Closed += async ex =>
{
await DisconnectAsync().ConfigureAwait(false);
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
};
case TokenType.Bot:
return $"Bot {token}";
case TokenType.Bearer:
return $"Bearer {token}";
case TokenType.User:
return token;
default:
throw new ArgumentException("Unknown OAuth token type", nameof(tokenType));
}

_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
}
private void Dispose(bool disposing)
internal virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_loginCancelToken?.Dispose();
_connectCancelToken?.Dispose();
(_restClient as IDisposable)?.Dispose();
(_gatewayClient as IDisposable)?.Dispose();
}
_isDisposed = true;
}
}
public void Dispose() => Dispose(true);

public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
finally { _stateLock.Release(); }
}
private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null)
{
if (LoginState != LoginState.LoggedOut)
await LogoutInternalAsync().ConfigureAwait(false);
LoginState = LoginState.LoggingIn;
try
{
_loginCancelToken = new CancellationTokenSource();

AuthTokenType = TokenType.User;
_authToken = null;
_restClient.SetHeader("authorization", null);
await _requestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false);
await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false);
_restClient.SetCancelToken(_loginCancelToken.Token);

AuthTokenType = tokenType;
_authToken = token;
switch (tokenType)
{
case TokenType.Bot:
token = $"Bot {token}";
break;
case TokenType.Bearer:
token = $"Bearer {token}";
break;
case TokenType.User:
break;
default:
throw new ArgumentException("Unknown oauth token type", nameof(tokenType));
}
_restClient.SetHeader("authorization", token);
_restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken));

LoginState = LoginState.LoggedIn;
}
@@ -160,129 +124,58 @@ namespace Discord.API

public async Task LogoutAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await LogoutInternalAsync().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
finally { _stateLock.Release(); }
}
private async Task LogoutInternalAsync()
{
//An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too.
if (LoginState == LoginState.LoggedOut) return;
LoginState = LoginState.LoggingOut;
try { _loginCancelToken?.Cancel(false); }
catch { }

await DisconnectInternalAsync().ConfigureAwait(false);
await _requestQueue.ClearAsync().ConfigureAwait(false);
await RequestQueue.ClearAsync().ConfigureAwait(false);

await _requestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false);
await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false);
_restClient.SetCancelToken(CancellationToken.None);

LoginState = LoginState.LoggedOut;
}

public async Task ConnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task ConnectInternalAsync()
{
if (LoginState != LoginState.LoggedIn)
throw new InvalidOperationException("You must log in before connecting.");
if (_gatewayClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");

ConnectionState = ConnectionState.Connecting;
try
{
_connectCancelToken = new CancellationTokenSource();
if (_gatewayClient != null)
_gatewayClient.SetCancelToken(_connectCancelToken.Token);

if (_gatewayUrl == null)
{
var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false);
_gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}";
}
await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false);

ConnectionState = ConnectionState.Connected;
}
catch (Exception)
{
_gatewayUrl = null; //Uncache in case the gateway url changed
await DisconnectInternalAsync().ConfigureAwait(false);
throw;
}
}

public async Task DisconnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
public async Task DisconnectAsync(Exception ex)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync()
{
if (_gatewayClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");

if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;
try { _connectCancelToken?.Cancel(false); }
catch { }

await _gatewayClient.DisconnectAsync().ConfigureAwait(false);

ConnectionState = ConnectionState.Disconnected;
}
internal virtual Task ConnectInternalAsync() => Task.CompletedTask;
internal virtual Task DisconnectInternalAsync() => Task.CompletedTask;

//REST
public Task SendAsync(string method, string endpoint,
public Task SendAsync(string method, string endpoint,
GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null)
=> SendInternalAsync(method, endpoint, null, true, BucketGroup.Global, (int)bucket, 0, options);
public Task SendAsync(string method, string endpoint, object payload,
public Task SendAsync(string method, string endpoint, object payload,
GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null)
=> SendInternalAsync(method, endpoint, payload, true, BucketGroup.Global, (int)bucket, 0, options);
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint,
GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class
=> DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, null, false, BucketGroup.Global, (int)bucket, 0, options).ConfigureAwait(false));
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, object payload, GlobalBucket bucket =
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, object payload, GlobalBucket bucket =
GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class
=> DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, payload, false, BucketGroup.Global, (int)bucket, 0, options).ConfigureAwait(false));
public Task SendAsync(string method, string endpoint,

public Task SendAsync(string method, string endpoint,
GuildBucket bucket, ulong guildId, RequestOptions options = null)
=> SendInternalAsync(method, endpoint, null, true, BucketGroup.Guild, (int)bucket, guildId, options);
public Task SendAsync(string method, string endpoint, object payload,
public Task SendAsync(string method, string endpoint, object payload,
GuildBucket bucket, ulong guildId, RequestOptions options = null)
=> SendInternalAsync(method, endpoint, payload, true, BucketGroup.Guild, (int)bucket, guildId, options);
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint,
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint,
GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class
=> DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, null, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false));
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, object payload,
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, object payload,
GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class
=> DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, payload, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false));

@@ -301,24 +194,15 @@ namespace Discord.API
GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class
=> DeserializeJson<TResponse>(await SendMultipartInternalAsync(method, endpoint, multipartArgs, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false));

//Gateway
public Task SendGatewayAsync(GatewayOpCode opCode, object payload,
GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null)
=> SendGatewayInternalAsync(opCode, payload, BucketGroup.Global, (int)bucket, 0, options);

public Task SendGatewayAsync(GatewayOpCode opCode, object payload,
GuildBucket bucket, ulong guildId, RequestOptions options = null)
=> SendGatewayInternalAsync(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options);

//Core
private async Task<Stream> SendInternalAsync(string method, string endpoint, object payload, bool headerOnly,
private async Task<Stream> SendInternalAsync(string method, string endpoint, object payload, bool headerOnly,
BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null)
{
var stopwatch = Stopwatch.StartNew();
string json = null;
if (payload != null)
json = SerializeJson(payload);
var responseStream = await _requestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, json, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false);
var responseStream = await RequestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, json, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false);
stopwatch.Stop();

double milliseconds = ToMilliseconds(stopwatch);
@@ -326,11 +210,11 @@ namespace Discord.API

return responseStream;
}
private async Task<Stream> SendMultipartInternalAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, bool headerOnly,
private async Task<Stream> SendMultipartInternalAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, bool headerOnly,
BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null)
{
var stopwatch = Stopwatch.StartNew();
var responseStream = await _requestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false);
var responseStream = await RequestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false);
int bytes = headerOnly ? 0 : (int)responseStream.Length;
stopwatch.Stop();

@@ -339,23 +223,6 @@ namespace Discord.API

return responseStream;
}
private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload,
BucketGroup group, int bucketId, ulong guildId, RequestOptions options)
{
//TODO: Add ETF
byte[] bytes = null;
payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload };
if (payload != null)
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload));
await _requestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
}

//Application
public async Task<Application> GetMyApplicationInfoAsync(RequestOptions options = null)
{
return await SendAsync<Application>("GET", "oauth2/applications/@me", options: options).ConfigureAwait(false);
}

//Auth
public async Task ValidateTokenAsync(RequestOptions options = null)
@@ -363,69 +230,6 @@ namespace Discord.API
await SendAsync("GET", "auth/login", options: options).ConfigureAwait(false);
}

//Gateway
public async Task<GetGatewayResponse> GetGatewayAsync(RequestOptions options = null)
{
return await SendAsync<GetGatewayResponse>("GET", "gateway", options: options).ConfigureAwait(false);
}
public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, RequestOptions options = null)
{
var props = new Dictionary<string, string>
{
["$device"] = "Discord.Net"
};
var msg = new IdentifyParams()
{
Token = _authToken,
Properties = props,
LargeThreshold = largeThreshold,
UseCompression = useCompression
};
await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false);
}
public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null)
{
var msg = new ResumeParams()
{
Token = _authToken,
SessionId = sessionId,
Sequence = lastSeq
};
await SendGatewayAsync(GatewayOpCode.Resume, msg, options: options).ConfigureAwait(false);
}
public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false);
}
public async Task SendStatusUpdateAsync(long? idleSince, Game game, RequestOptions options = null)
{
var args = new StatusUpdateParams
{
IdleSince = idleSince,
Game = game
};
await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false);
}
public async Task SendRequestMembersAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false);
}
public async Task SendVoiceStateUpdateAsync(ulong guildId, ulong? channelId, bool selfDeaf, bool selfMute, RequestOptions options = null)
{
var payload = new VoiceStateUpdateParams
{
GuildId = guildId,
ChannelId = channelId,
SelfDeaf = selfDeaf,
SelfMute = selfMute
};
await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false);
}
public async Task SendGuildSyncAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false);
}

//Channels
public async Task<Channel> GetChannelAsync(ulong channelId, RequestOptions options = null)
{
@@ -791,13 +595,13 @@ namespace Discord.API

List<GuildMember[]> result;
if (args._limit.IsSpecified)
result = new List<GuildMember[]>((limit + DiscordRestConfig.MaxUsersPerBatch - 1) / DiscordRestConfig.MaxUsersPerBatch);
result = new List<GuildMember[]>((limit + DiscordConfig.MaxUsersPerBatch - 1) / DiscordConfig.MaxUsersPerBatch);
else
result = new List<GuildMember[]>();

while (true)
{
int runLimit = (limit >= DiscordRestConfig.MaxUsersPerBatch) ? DiscordRestConfig.MaxUsersPerBatch : limit;
int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit;
string endpoint = $"guilds/{guildId}/members?limit={runLimit}&after={afterUserId}";
var models = await SendAsync<GuildMember[]>("GET", endpoint, options: options).ConfigureAwait(false);

@@ -806,11 +610,11 @@ namespace Discord.API

result.Add(models);

limit -= DiscordRestConfig.MaxUsersPerBatch;
limit -= DiscordConfig.MaxUsersPerBatch;
afterUserId = models[models.Length - 1].User.Id;

//Was this an incomplete (the last) batch?
if (models.Length != DiscordRestConfig.MaxUsersPerBatch) break;
if (models.Length != DiscordConfig.MaxUsersPerBatch) break;
}

if (result.Count > 1)
@@ -919,15 +723,15 @@ namespace Discord.API
relativeDir = "around";
break;
}
int runs = (limit + DiscordRestConfig.MaxMessagesPerBatch - 1) / DiscordRestConfig.MaxMessagesPerBatch;
int lastRunCount = limit - (runs - 1) * DiscordRestConfig.MaxMessagesPerBatch;
int runs = (limit + DiscordConfig.MaxMessagesPerBatch - 1) / DiscordConfig.MaxMessagesPerBatch;
int lastRunCount = limit - (runs - 1) * DiscordConfig.MaxMessagesPerBatch;
var result = new API.Message[runs][];

int i = 0;
for (; i < runs; i++)
{
int runCount = i == (runs - 1) ? lastRunCount : DiscordRestConfig.MaxMessagesPerBatch;
int runCount = i == (runs - 1) ? lastRunCount : DiscordConfig.MaxMessagesPerBatch;
string endpoint;
if (relativeId != null)
endpoint = $"channels/{channelId}/messages?limit={runCount}&{relativeDir}={relativeId}";
@@ -966,7 +770,7 @@ namespace Discord.API
}

//Was this an incomplete (the last) batch?
if (models.Length != DiscordRestConfig.MaxMessagesPerBatch) { i++; break; }
if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; }
}

if (i > 1)
@@ -1011,8 +815,8 @@ namespace Discord.API
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotNull(args, nameof(args));
Preconditions.NotNullOrEmpty(args._content, nameof(args.Content));
if (args._content.Length > DiscordRestConfig.MaxMessageSize)
throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordRestConfig.MaxMessageSize}.", nameof(args.Content));
if (args._content.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));

if (guildId != 0)
return await SendAsync<Message>("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false);
@@ -1033,15 +837,15 @@ namespace Discord.API
{
Preconditions.NotNull(args, nameof(args));
Preconditions.NotEqual(channelId, 0, nameof(channelId));
if (args._content.GetValueOrDefault(null) == null)
args._content = "";
else if (args._content.IsSpecified)
{
if (args._content.Value == null)
args._content = "";
if (args._content.Value?.Length > DiscordRestConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordRestConfig.MaxMessageSize}.", nameof(args.Content));
if (args._content.Value?.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
}

if (guildId != 0)
@@ -1121,8 +925,8 @@ namespace Discord.API
if (args._content.IsSpecified)
{
Preconditions.NotNullOrEmpty(args._content, nameof(args.Content));
if (args._content.Value.Length > DiscordRestConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordRestConfig.MaxMessageSize}.", nameof(args.Content));
if (args._content.Value.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
}

if (guildId != 0)
@@ -1159,7 +963,7 @@ namespace Discord.API
{
Preconditions.NotNullOrEmpty(username, nameof(username));
Preconditions.NotNullOrEmpty(discriminator, nameof(discriminator));
try
{
var models = await QueryUsersAsync($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false);
@@ -1176,7 +980,7 @@ namespace Discord.API
}

//Current User/DMs
public async Task<User> GetSelfAsync(RequestOptions options = null)
public async Task<User> GetMyUserAsync(RequestOptions options = null)
{
return await SendAsync<User>("GET", "users/@me", options: options).ConfigureAwait(false);
}
@@ -1192,6 +996,10 @@ namespace Discord.API
{
return await SendAsync<IReadOnlyCollection<UserGuild>>("GET", "users/@me/guilds", options: options).ConfigureAwait(false);
}
public async Task<Application> GetMyApplicationAsync(RequestOptions options = null)
{
return await SendAsync<Application>("GET", "oauth2/applications/@me", options: options).ConfigureAwait(false);
}
public async Task<User> ModifySelfAsync(ModifyCurrentUserParams args, RequestOptions options = null)
{
Preconditions.NotNull(args, nameof(args));
@@ -1227,8 +1035,8 @@ namespace Discord.API
}

//Helpers
private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2);
private string SerializeJson(object value)
protected static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2);
protected string SerializeJson(object value)
{
var sb = new StringBuilder(256);
using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture))
@@ -1236,7 +1044,7 @@ namespace Discord.API
_serializer.Serialize(writer, value);
return sb.ToString();
}
private T DeserializeJson<T>(Stream jsonStream)
protected T DeserializeJson<T>(Stream jsonStream)
{
using (TextReader text = new StreamReader(jsonStream))
using (JsonReader reader = new JsonTextReader(text))

+ 360
- 0
src/Discord.Net/API/DiscordRpcAPIClient.cs View File

@@ -0,0 +1,360 @@
using Discord.API.Rpc;
using Discord.Net.Queue;
using Discord.Net.Rest;
using Discord.Net.WebSockets;
using Discord.Rpc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.API
{
public class DiscordRpcApiClient : DiscordRestApiClient, 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, 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 IWebSocketClient _webSocketClient;
private readonly SemaphoreSlim _connectionLock;
private readonly string _clientId;
private CancellationTokenSource _loginCancelToken, _connectCancelToken;
private string _origin;

public ConnectionState ConnectionState { get; private set; }

public DiscordRpcApiClient(string clientId, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null)
: base(restClientProvider, serializer, requestQueue)
{
_connectionLock = new SemaphoreSlim(1, 1);
_clientId = clientId;
_origin = origin;

_requestQueue = requestQueue ?? new RequestQueue();
_requests = new ConcurrentDictionary<Guid, RpcRequest>();
_webSocketClient = webSocketProvider();
//_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))
using (var decompressed = new MemoryStream())
{
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress))
zlib.CopyTo(decompressed);
decompressed.Position = 0;
using (var reader = new StreamReader(decompressed))
using (var jsonReader = new JsonTextReader(reader))
{
var msg = _serializer.Deserialize<API.Rpc.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 =>
{
using (var reader = new StringReader(text))
using (var jsonReader = new JsonTextReader(reader))
{
var msg = _serializer.Deserialize<API.Rpc.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 =>
{
await DisconnectAsync().ConfigureAwait(false);
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
};
}
internal override void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_connectCancelToken?.Dispose();
(_webSocketClient as IDisposable)?.Dispose();
}
_isDisposed = true;
}
}

public async Task ConnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
internal override async Task ConnectInternalAsync()
{
/*if (LoginState != LoginState.LoggedIn)
throw new InvalidOperationException("You must log in before connecting.");*/

ConnectionState = ConnectionState.Connecting;
try
{
_connectCancelToken = new CancellationTokenSource();
if (_webSocketClient != null)
_webSocketClient.SetCancelToken(_connectCancelToken.Token);

bool success = false;
int port;
string uuid = Guid.NewGuid().ToString();

for ( port = DiscordRpcConfig.PortRangeStart; port <= DiscordRpcConfig.PortRangeEnd; port++)
{
try
{
string url = $"wss://{uuid}.discordapp.io:{port}/?v={DiscordRpcConfig.RpcAPIVersion}&client_id={_clientId}";
await _webSocketClient.ConnectAsync(url).ConfigureAwait(false);
success = true;
break;
}
catch (Exception)
{
}
}

if (!success)
throw new Exception("Unable to connect to the RPC server.");

SetBaseUrl($"https://{uuid}.discordapp.io:{port}/");
ConnectionState = ConnectionState.Connected;
}
catch (Exception)
{
await DisconnectInternalAsync().ConfigureAwait(false);
throw;
}
}

public async Task DisconnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
internal override async Task DisconnectInternalAsync()
{
if (_webSocketClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");

if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;
try { _connectCancelToken?.Cancel(false); }
catch { }

await _webSocketClient.DisconnectAsync().ConfigureAwait(false);

ConnectionState = ConnectionState.Disconnected;
}

//Core
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
{
byte[] bytes = null;
var guid = Guid.NewGuid();
payload = new API.Rpc.RpcMessage { Cmd = cmd, Event = evt, Args = payload, Nonce = guid };
if (payload != null)
{
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<AuthenticateResponse> SendAuthenticateAsync(RequestOptions options = null)
{
var msg = new AuthenticateParams
{
AccessToken = _authToken
};
return await SendRpcAsync<AuthenticateResponse>("AUTHENTICATE", msg, options: options).ConfigureAwait(false);
}
public async Task<AuthorizeResponse> SendAuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null)
{
var msg = new AuthorizeParams
{
ClientId = _clientId,
Scopes = scopes,
RpcToken = rpcToken != null ? rpcToken : Optional.Create<string>()
};
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(API.Rpc.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;
}
}
}

+ 243
- 0
src/Discord.Net/API/DiscordSocketApiClient.cs View File

@@ -0,0 +1,243 @@
using Discord.API.Gateway;
using Discord.API.Rest;
using Discord.Net.Queue;
using Discord.Net.Rest;
using Discord.Net.WebSockets;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.API
{
public class DiscordSocketApiClient : DiscordRestApiClient
{
public event Func<GatewayOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } }
private readonly AsyncEvent<Func<GatewayOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<GatewayOpCode, Task>>();
public event Func<GatewayOpCode, int?, string, object, Task> ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } }
private readonly AsyncEvent<Func<GatewayOpCode, int?, string, object, Task>> _receivedGatewayEvent = new AsyncEvent<Func<GatewayOpCode, int?, string, 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 IWebSocketClient _gatewayClient;
private CancellationTokenSource _connectCancelToken;
private string _gatewayUrl;

public ConnectionState ConnectionState { get; private set; }

public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null)
: base(restClientProvider, serializer, requestQueue)
{
_gatewayClient = webSocketProvider();
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+)
_gatewayClient.BinaryMessage += async (data, index, count) =>
{
using (var compressed = new MemoryStream(data, index + 2, count - 2))
using (var decompressed = new MemoryStream())
{
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress))
zlib.CopyTo(decompressed);
decompressed.Position = 0;
using (var reader = new StreamReader(decompressed))
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.TextMessage += async text =>
{
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 =>
{
await DisconnectAsync().ConfigureAwait(false);
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
};
}
internal override void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_connectCancelToken?.Dispose();
(_gatewayClient as IDisposable)?.Dispose();
}
_isDisposed = true;
}
}

public async Task ConnectAsync()
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync().ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
internal override async Task ConnectInternalAsync()
{
if (LoginState != LoginState.LoggedIn)
throw new InvalidOperationException("You must log in before connecting.");
if (_gatewayClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");

ConnectionState = ConnectionState.Connecting;
try
{
_connectCancelToken = new CancellationTokenSource();
if (_gatewayClient != null)
_gatewayClient.SetCancelToken(_connectCancelToken.Token);

if (_gatewayUrl == null)
{
var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false);
_gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}";
}
await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false);

ConnectionState = ConnectionState.Connected;
}
catch (Exception)
{
_gatewayUrl = null; //Uncache in case the gateway url changed
await DisconnectInternalAsync().ConfigureAwait(false);
throw;
}
}

public async Task DisconnectAsync()
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
public async Task DisconnectAsync(Exception ex)
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
internal override async Task DisconnectInternalAsync()
{
if (_gatewayClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");

if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;

try { _connectCancelToken?.Cancel(false); }
catch { }

await _gatewayClient.DisconnectAsync().ConfigureAwait(false);

ConnectionState = ConnectionState.Disconnected;
}

//Core
private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload,
BucketGroup group, int bucketId, ulong guildId, RequestOptions options)
{
//TODO: Add ETF
byte[] bytes = null;
payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload };
if (payload != null)
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload));
await RequestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
}

//Gateway
public Task SendGatewayAsync(GatewayOpCode opCode, object payload,
GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null)
=> SendGatewayInternalAsync(opCode, payload, BucketGroup.Global, (int)bucket, 0, options);

public Task SendGatewayAsync(GatewayOpCode opCode, object payload,
GuildBucket bucket, ulong guildId, RequestOptions options = null)
=> SendGatewayInternalAsync(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options);

public async Task<GetGatewayResponse> GetGatewayAsync(RequestOptions options = null)
{
return await SendAsync<GetGatewayResponse>("GET", "gateway", options: options).ConfigureAwait(false);
}
public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, int shardID = 0, int totalShards = 1, RequestOptions options = null)
{
var props = new Dictionary<string, string>
{
["$device"] = "Discord.Net"
};
var msg = new IdentifyParams()
{
Token = _authToken,
Properties = props,
LargeThreshold = largeThreshold,
UseCompression = useCompression,
};
if (totalShards > 1)
msg.ShardingParams = new int[] { shardID, totalShards };

await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false);
}
public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null)
{
var msg = new ResumeParams()
{
Token = _authToken,
SessionId = sessionId,
Sequence = lastSeq
};
await SendGatewayAsync(GatewayOpCode.Resume, msg, options: options).ConfigureAwait(false);
}
public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false);
}
public async Task SendStatusUpdateAsync(long? idleSince, Game game, RequestOptions options = null)
{
var args = new StatusUpdateParams
{
IdleSince = idleSince,
Game = game
};
await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false);
}
public async Task SendRequestMembersAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false);
}
public async Task SendVoiceStateUpdateAsync(ulong guildId, ulong? channelId, bool selfDeaf, bool selfMute, RequestOptions options = null)
{
var payload = new VoiceStateUpdateParams
{
GuildId = guildId,
ChannelId = channelId,
SelfDeaf = selfDeaf,
SelfMute = selfMute
};
await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false);
}
public async Task SendGuildSyncAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false);
}
}
}

+ 2
- 0
src/Discord.Net/API/Gateway/IdentifyParams.cs View File

@@ -14,5 +14,7 @@ namespace Discord.API.Gateway
public int LargeThreshold { get; set; }
[JsonProperty("compress")]
public bool UseCompression { get; set; }
[JsonProperty("shard")]
public Optional<int[]> ShardingParams { get; set; }
}
}

+ 1
- 1
src/Discord.Net/API/Gateway/RequestMembersParams.cs View File

@@ -13,7 +13,7 @@ namespace Discord.API.Gateway
public int Limit { get; set; }

[JsonProperty("guild_id")]
private ulong[] _guildIds;
private ulong[] _guildIds { get; set; }
public IEnumerable<ulong> GuildIds { set { _guildIds = value.ToArray(); } }
public IEnumerable<IGuild> Guilds { set { _guildIds = value.Select(x => x.Id).ToArray(); } }
}


+ 4
- 2
src/Discord.Net/API/Rest/GetChannelMessagesParams.cs View File

@@ -1,8 +1,10 @@
namespace Discord.API.Rest
using Discord.Rest;

namespace Discord.API.Rest
{
public class GetChannelMessagesParams
{
public int Limit { internal get; set; } = DiscordRestConfig.MaxMessagesPerBatch;
public int Limit { internal get; set; } = DiscordConfig.MaxMessagesPerBatch;

public Direction RelativeDirection { internal get; set; } = Direction.Before;



+ 18
- 0
src/Discord.Net/API/Rpc/Application.cs View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class Application
{
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("icon")]
public string Icon { get; set; }
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("rpc_origins")]
public string[] RpcOrigins { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/AuthenticateParams.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class AuthenticateParams
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
}
}

+ 17
- 0
src/Discord.Net/API/Rpc/AuthenticateResponse.cs View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
using System;

namespace Discord.API.Rpc
{
public class AuthenticateResponse
{
[JsonProperty("application")]
public Application Application { get; set; }
[JsonProperty("expires")]
public DateTimeOffset Expires { get; set; }
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("scopes")]
public string[] Scopes { get; set; }
}
}

+ 14
- 0
src/Discord.Net/API/Rpc/AuthorizeParams.cs View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class AuthorizeParams
{
[JsonProperty("client_id")]
public string ClientId { get; set; }
[JsonProperty("scopes")]
public string[] Scopes { get; set; }
[JsonProperty("rpc_token")]
public Optional<string> RpcToken { get; set; }
}
}

+ 11
- 0
src/Discord.Net/API/Rpc/AuthorizeResponse.cs View File

@@ -0,0 +1,11 @@
using Newtonsoft.Json;
using System;

namespace Discord.API.Rpc
{
public class AuthorizeResponse
{
[JsonProperty("code")]
public string Code { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/ChannelSubscriptionParams.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class ChannelSubscriptionParams
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
}
}

+ 12
- 0
src/Discord.Net/API/Rpc/ErrorEvent.cs View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class ErrorEvent
{
[JsonProperty("code")]
public int Code { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/GetChannelParams.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class GetChannelParams
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/GetChannelsParams.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class GetChannelsParams
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/GetChannelsResponse.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class GetChannelsResponse
{
[JsonProperty("channels")]
public RpcChannel[] Channels { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/GetGuildParams.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class GetGuildParams
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

+ 11
- 0
src/Discord.Net/API/Rpc/GetGuildsParams.cs View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.API.Rpc
{
public class GetGuildsParams
{
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/GetGuildsResponse.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class GetGuildsResponse
{
[JsonProperty("guilds")]
public RpcUserGuild[] Guilds { get; set; }
}
}

+ 12
- 0
src/Discord.Net/API/Rpc/GuildStatusEvent.cs View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class GuildStatusEvent
{
[JsonProperty("guild")]
public Guild Guild { get; set; }
[JsonProperty("online")]
public int Online { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class GuildSubscriptionParams
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

+ 11
- 0
src/Discord.Net/API/Rpc/MessageEvent.cs View File

@@ -0,0 +1,11 @@
using Newtonsoft.Json;
namespace Discord.API.Rpc
{
public class MessageEvent
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("message")]
public Message Message { get; set; }
}
}

+ 12
- 0
src/Discord.Net/API/Rpc/ReadyEvent.cs View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class ReadyEvent
{
[JsonProperty("v")]
public int Version { get; set; }
[JsonProperty("config")]
public RpcConfig Config { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/RpcChannel.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class RpcChannel : Channel
{
[JsonProperty("voice_states")]
public VoiceState[] VoiceStates { get; set; }
}
}

+ 14
- 0
src/Discord.Net/API/Rpc/RpcConfig.cs View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class RpcConfig
{
[JsonProperty("cdn_host")]
public string CdnHost { get; set; }
[JsonProperty("api_endpoint")]
public string ApiEndpoint { get; set; }
[JsonProperty("environment")]
public string Environment { get; set; }
}
}

+ 12
- 0
src/Discord.Net/API/Rpc/RpcGuild.cs View File

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

+ 19
- 0
src/Discord.Net/API/Rpc/RpcMessage.cs View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System;

namespace Discord.API.Rpc
{
public class RpcMessage
{
[JsonProperty("cmd")]
public string Cmd { get; set; }
[JsonProperty("nonce")]
public Optional<Guid?> Nonce { get; set; }
[JsonProperty("evt")]
public Optional<string> Event { get; set; }
[JsonProperty("data")]
public Optional<object> Data { get; set; }
[JsonProperty("args")]
public object Args { get; set; }
}
}

+ 12
- 0
src/Discord.Net/API/Rpc/RpcUserGuild.cs View File

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

+ 10
- 0
src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class SelectVoiceChannelParams
{
[JsonProperty("channel_id")]
public ulong? ChannelId { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class SetLocalVolumeParams
{
[JsonProperty("volume")]
public int Volume { get; set; }
}
}

+ 12
- 0
src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs View File

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

+ 11
- 0
src/Discord.Net/API/Rpc/SpeakingEvent.cs View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.API.Rpc
{
public class SpeakingEvent
{
}
}

+ 10
- 0
src/Discord.Net/API/Rpc/SubscriptionResponse.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Rpc
{
public class SubscriptionResponse
{
[JsonProperty("evt")]
public string Event { get; set; }
}
}

+ 11
- 0
src/Discord.Net/API/Rpc/VoiceStateEvent.cs View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.API.Rpc
{
public class VoiceStateEvent
{
}
}

+ 1
- 0
src/Discord.Net/Audio/AudioClient.cs View File

@@ -1,6 +1,7 @@
using Discord.API.Voice;
using Discord.Logging;
using Discord.Net.Converters;
using Discord.WebSocket;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;


src/Discord.Net/Data/DataStore.cs → src/Discord.Net/DataStore.cs View File

@@ -1,5 +1,4 @@
using Discord.Extensions;
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

+ 8
- 1
src/Discord.Net/DiscordConfig.cs View File

@@ -5,12 +5,19 @@ namespace Discord
public class DiscordConfig
{
public const int APIVersion = 6;
public static string Version { get; } = typeof(DiscordRestConfig).GetTypeInfo().Assembly?.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion ?? "Unknown";
public static string Version { get; } =
typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ??
typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ??
"Unknown";

public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/";
public const string CDNUrl = "https://cdn.discordapp.com/";
public const string InviteUrl = "https://discord.gg/";

public const int MaxMessageSize = 2000;
public const int MaxMessagesPerBatch = 100;
public const int MaxUsersPerBatch = 1000;

/// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary>
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;
}


+ 2
- 2
src/Discord.Net/Entities/Channels/IMessageChannel.cs View File

@@ -20,9 +20,9 @@ namespace Discord
/// <summary> Gets the message from this channel's cache with the given id, or null if not found. </summary>
IMessage GetCachedMessage(ulong id);
/// <summary> Gets the last N messages from this message channel. </summary>
Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordRestConfig.MaxMessagesPerBatch);
Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Gets a collection of messages in this channel. </summary>
Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch);
Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Bulk deletes multiple messages. </summary>
Task DeleteMessagesAsync(IEnumerable<IMessage> messages);



+ 0
- 21
src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs View File

@@ -1,21 +0,0 @@
using System;
using Model = Discord.API.User;

namespace Discord
{
internal class SocketSelfUser : SelfUser, ISocketUser
{
internal override bool IsAttached => true;

public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
SocketGlobalUser ISocketUser.User { get { throw new NotSupportedException(); } }

public SocketSelfUser(DiscordSocketClient discord, Model model)
: base(discord, model)
{
}

public SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser;
ISocketUser ISocketUser.Clone() => Clone();
}
}

+ 1
- 1
src/Discord.Net/Extensions/CollectionExtensions.cs View File

@@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Discord.Extensions
namespace Discord
{
internal static class CollectionExtensions
{


+ 3
- 2
src/Discord.Net/Extensions/DiscordClientExtensions.cs View File

@@ -1,7 +1,8 @@
using System.Linq;
using Discord.Rest;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Extensions
namespace Discord
{
public static class DiscordClientExtensions
{


+ 1
- 1
src/Discord.Net/Extensions/GuildExtensions.cs View File

@@ -1,6 +1,6 @@
using System.Threading.Tasks;

namespace Discord.Extensions
namespace Discord
{
public static class GuildExtensions
{


+ 1
- 1
src/Discord.Net/Extensions/GuildUserExtensions.cs View File

@@ -2,7 +2,7 @@
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Extensions
namespace Discord
{
public static class GuildUserExtensions
{


+ 1
- 1
src/Discord.Net/IDiscordClient.cs View File

@@ -13,7 +13,7 @@ namespace Discord
{
ConnectionState ConnectionState { get; }

DiscordApiClient ApiClient { get; }
DiscordRestApiClient ApiClient { get; }
ILogManager LogManager { get; }

Task ConnectAsync();


+ 4
- 1
src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs View File

@@ -5,7 +5,10 @@
GeneralRest,
DirectMessage,
SendEditMessage,

GeneralGateway,
UpdateStatus
UpdateStatus,

GeneralRpc
}
}

+ 4
- 1
src/Discord.Net/Net/Queue/RequestQueue.cs View File

@@ -36,7 +36,10 @@ namespace Discord.Net.Queue

//Gateway
[GlobalBucket.GeneralGateway] = new Bucket(null, "gateway", 120, 60, BucketTarget.Both),
[GlobalBucket.UpdateStatus] = new Bucket(null, "status", 5, 1, BucketTarget.Both, GlobalBucket.GeneralGateway)
[GlobalBucket.UpdateStatus] = new Bucket(null, "status", 5, 1, BucketTarget.Both, GlobalBucket.GeneralGateway),

//Rpc
[GlobalBucket.GeneralRpc] = new Bucket(null, "rpc", 120, 60, BucketTarget.Both)
}.ToImmutableDictionary();

_guildLimits = new Dictionary<GuildBucket, Bucket>


+ 17
- 0
src/Discord.Net/Net/RpcException.cs View File

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

src/Discord.Net/DiscordRestClient.cs → src/Discord.Net/Rest/DiscordRestClient.cs View File

@@ -10,8 +10,10 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Discord.Rpc;
using Discord.WebSocket;

namespace Discord
namespace Discord.Rest
{
public class DiscordRestClient : IDiscordClient
{
@@ -27,20 +29,21 @@ namespace Discord

internal readonly ILogger _clientLogger, _restLogger, _queueLogger;
internal readonly SemaphoreSlim _connectionLock;
internal readonly RequestQueue _requestQueue;
internal bool _isDisposed;
internal SelfUser _currentUser;
private bool _isFirstLogSub;
internal bool _isDisposed;

public API.DiscordApiClient ApiClient { get; }
public API.DiscordRestApiClient ApiClient { get; }
internal LogManager LogManager { get; }
public LoginState LoginState { get; private set; }

/// <summary> Creates a new REST-only discord client. </summary>
public DiscordRestClient() : this(new DiscordRestConfig()) { }
public DiscordRestClient(DiscordRestConfig config) : this(config, CreateApiClient(config)) { }
/// <summary> Creates a new REST-only discord client. </summary>
public DiscordRestClient(DiscordRestConfig config)
internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient client)
{
ApiClient = client;
LogManager = new LogManager(config.LogLevel);
LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false);
_clientLogger = LogManager.CreateLogger("Client");
@@ -50,19 +53,16 @@ namespace Discord

_connectionLock = new SemaphoreSlim(1, 1);

_requestQueue = new RequestQueue();
_requestQueue.RateLimitTriggered += async (id, bucket, millis) =>
ApiClient.RequestQueue.RateLimitTriggered += async (id, bucket, millis) =>
{
await _queueLogger.WarningAsync($"Rate limit triggered (id = \"{id ?? "null"}\")").ConfigureAwait(false);
if (bucket == null && id != null)
await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false);
};

var restProvider = config.RestClientProvider;
var webSocketProvider = (this is DiscordSocketClient) ? (config as DiscordSocketConfig)?.WebSocketProvider : null; //TODO: Clean this check
ApiClient = new API.DiscordApiClient(restProvider, webSocketProvider, requestQueue: _requestQueue);
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
}
private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
=> new API.DiscordRestApiClient(config.RestClientProvider, requestQueue: new RequestQueue());

/// <inheritdoc />
public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true)
@@ -89,20 +89,9 @@ namespace Discord
try
{
await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false);

if (validateToken)
{
try
{
await ApiClient.ValidateTokenAsync().ConfigureAwait(false);
}
catch (HttpException ex)
{
throw new ArgumentException("Token validation failed", nameof(token), ex);
}
}

await OnLoginAsync().ConfigureAwait(false);
await ValidateTokenAsync(tokenType, token).ConfigureAwait(false);
await OnLoginAsync(tokenType, token).ConfigureAwait(false);

LoginState = LoginState.LoggedIn;
}
@@ -114,7 +103,26 @@ namespace Discord

await _loggedInEvent.InvokeAsync().ConfigureAwait(false);
}
protected virtual Task OnLoginAsync() => Task.CompletedTask;
protected virtual async Task ValidateTokenAsync(TokenType tokenType, string token)
{
try
{
var user = await GetCurrentUserAsync().ConfigureAwait(false);
if (user == null) //Is using a cached DiscordClient
user = new SelfUser(this, await ApiClient.GetMyUserAsync().ConfigureAwait(false));

if (user.IsBot && tokenType == TokenType.User)
throw new InvalidOperationException($"A bot token used provided with {nameof(TokenType)}.{nameof(TokenType.User)}");
else if (!user.IsBot && tokenType == TokenType.Bot) //Discord currently sends a 401 in this case
throw new InvalidOperationException($"A user token used provided with {nameof(TokenType)}.{nameof(TokenType.Bot)}");
}
catch (HttpException ex)
{
throw new ArgumentException("Token validation failed", nameof(token), ex);
}
}
protected virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.CompletedTask;


/// <inheritdoc />
public async Task LogoutAsync()
@@ -146,7 +154,7 @@ namespace Discord
/// <inheritdoc />
public async Task<IApplication> GetApplicationInfoAsync()
{
var model = await ApiClient.GetMyApplicationInfoAsync().ConfigureAwait(false);
var model = await ApiClient.GetMyApplicationAsync().ConfigureAwait(false);
return new Application(this, model);
}
@@ -266,12 +274,13 @@ namespace Discord
var user = _currentUser;
if (user == null)
{
var model = await ApiClient.GetSelfAsync().ConfigureAwait(false);
var model = await ApiClient.GetMyUserAsync().ConfigureAwait(false);
user = new SelfUser(this, model);
_currentUser = user;
}
return user;
}

/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IUser>> QueryUsersAsync(string query, int limit)
{
@@ -295,8 +304,10 @@ namespace Discord
internal virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
ApiClient.Dispose();
_isDisposed = true;
ApiClient.Dispose();
}
}
/// <inheritdoc />
public void Dispose() => Dispose(true);
@@ -305,9 +316,10 @@ namespace Discord
{
if (this is DiscordSocketClient)
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
await _clientLogger.InfoAsync($"DiscordRestClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false);

await _clientLogger.InfoAsync($"DiscordClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false);
await _clientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false);
await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false);
await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false);

src/Discord.Net/DiscordRestConfig.cs → src/Discord.Net/Rest/DiscordRestConfig.cs View File

@@ -1,15 +1,11 @@
using Discord.Net.Rest;

namespace Discord
namespace Discord.Rest
{
public class DiscordRestConfig : DiscordConfig
{
public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})";

public const int MaxMessageSize = 2000;
public const int MaxMessagesPerBatch = 100;
public const int MaxUsersPerBatch = 1000;

internal const int RestTimeout = 10000;
internal const int MessageQueueInterval = 100;
internal const int WebSocketQueueInterval = 100;

src/Discord.Net/Entities/Application.cs → src/Discord.Net/Rest/Entities/Application.cs View File

@@ -1,4 +1,5 @@
using System;
using Discord.Rest;
using System;
using System.Threading.Tasks;
using Model = Discord.API.Application;

@@ -42,7 +43,7 @@ namespace Discord
{
if (IsAttached) throw new NotSupportedException();

var response = await Discord.ApiClient.GetMyApplicationInfoAsync().ConfigureAwait(false);
var response = await Discord.ApiClient.GetMyApplicationAsync().ConfigureAwait(false);
if (response.Id != Id)
throw new InvalidOperationException("Unable to update this object from a different application token.");
Update(response, UpdateSource.Rest);

src/Discord.Net/Entities/Channels/DMChannel.cs → src/Discord.Net/Rest/Entities/Channels/DMChannel.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

src/Discord.Net/Entities/Channels/GroupChannel.cs → src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs View File

@@ -1,5 +1,5 @@
using Discord.API.Rest;
using Discord.Extensions;
using Discord.Rest;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -17,7 +17,7 @@ namespace Discord
{
protected ConcurrentDictionary<ulong, GroupUser> _users;
private string _iconId;
public override DiscordRestClient Discord { get; }
public string Name { get; private set; }


src/Discord.Net/Entities/Channels/GuildChannel.cs → src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

src/Discord.Net/Entities/Channels/TextChannel.cs → src/Discord.Net/Rest/Entities/Channels/TextChannel.cs View File


src/Discord.Net/Entities/Channels/VoiceChannel.cs → src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs View File


src/Discord.Net/Entities/Entity.cs → src/Discord.Net/Rest/Entities/Entity.cs View File

@@ -1,4 +1,6 @@
namespace Discord
using Discord.Rest;

namespace Discord
{
internal abstract class Entity<T> : IEntity<T>
{

src/Discord.Net/Entities/Guilds/Guild.cs → src/Discord.Net/Rest/Entities/Guilds/Guild.cs View File

@@ -1,6 +1,6 @@
using Discord.API.Rest;
using Discord.Audio;
using Discord.Extensions;
using Discord.Rest;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

src/Discord.Net/Entities/Guilds/GuildIntegration.cs → src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;

src/Discord.Net/Entities/Guilds/UserGuild.cs → src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using Discord.Rest;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.UserGuild;


src/Discord.Net/Entities/Guilds/VoiceRegion.cs → src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs View File


src/Discord.Net/Entities/Invites/Invite.cs → src/Discord.Net/Rest/Entities/Invites/Invite.cs View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using Discord.Rest;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Invite;

@@ -16,8 +17,8 @@ namespace Discord
public override DiscordRestClient Discord { get; }

public string Code => Id;
public string Url => $"{DiscordRestConfig.InviteUrl}/{XkcdCode ?? Code}";
public string XkcdUrl => XkcdCode != null ? $"{DiscordRestConfig.InviteUrl}/{XkcdCode}" : null;
public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}";
public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null;

public Invite(DiscordRestClient discord, Model model)
: base(model.Code)

src/Discord.Net/Entities/Invites/InviteMetadata.cs → src/Discord.Net/Rest/Entities/Invites/InviteMetadata.cs View File

@@ -1,4 +1,5 @@
using System;
using Discord.Rest;
using System;
using Model = Discord.API.InviteMetadata;

namespace Discord

src/Discord.Net/Entities/Messages/Attachment.cs → src/Discord.Net/Rest/Entities/Messages/Attachment.cs View File


src/Discord.Net/Entities/Messages/Embed.cs → src/Discord.Net/Rest/Entities/Messages/Embed.cs View File


src/Discord.Net/Entities/Messages/Message.cs → src/Discord.Net/Rest/Entities/Messages/Message.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -39,13 +40,10 @@ namespace Discord
Channel = channel;
Author = author;
Type = model.Type;

if (channel is IGuildChannel)
{
MentionedUsers = ImmutableArray.Create<IUser>();
MentionedChannelIds = ImmutableArray.Create<ulong>();
MentionedRoles = ImmutableArray.Create<IRole>();
}
MentionedUsers = ImmutableArray.Create<IUser>();
MentionedChannelIds = ImmutableArray.Create<ulong>();
MentionedRoles = ImmutableArray.Create<IRole>();

Update(model, UpdateSource.Creation);
}
@@ -55,7 +53,6 @@ namespace Discord

var guildChannel = Channel as GuildChannel;
var guild = guildChannel?.Guild;
var discord = Discord;

if (model.IsTextToSpeech.IsSpecified)
IsTTS = model.IsTextToSpeech.Value;

src/Discord.Net/Entities/Roles/Role.cs → src/Discord.Net/Rest/Entities/Roles/Role.cs View File

@@ -1,9 +1,7 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Role;


src/Discord.Net/Entities/SnowflakeEntity.cs → src/Discord.Net/Rest/Entities/SnowflakeEntity.cs View File


src/Discord.Net/Entities/Users/Connection.cs → src/Discord.Net/Rest/Entities/Users/Connection.cs View File


src/Discord.Net/Entities/Users/GroupUser.cs → src/Discord.Net/Rest/Entities/Users/GroupUser.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Threading.Tasks;


src/Discord.Net/Entities/Users/GuildUser.cs → src/Discord.Net/Rest/Entities/Users/GuildUser.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

src/Discord.Net/Entities/Users/SelfUser.cs → src/Discord.Net/Rest/Entities/Users/SelfUser.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Threading.Tasks;
using Model = Discord.API.User;
@@ -7,9 +8,9 @@ namespace Discord
{
internal class SelfUser : User, ISelfUser
{
private long _idleSince;
private UserStatus _status;
private Game _game;
protected long _idleSince;
protected UserStatus _status;
protected Game _game;

public string Email { get; private set; }
public bool IsVerified { get; private set; }
@@ -43,7 +44,7 @@ namespace Discord
{
if (IsAttached) throw new NotSupportedException();

var model = await Discord.ApiClient.GetSelfAsync().ConfigureAwait(false);
var model = await Discord.ApiClient.GetMyUserAsync().ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task ModifyAsync(Action<ModifyCurrentUserParams> func)
@@ -61,27 +62,7 @@ namespace Discord
var model = await Discord.ApiClient.ModifySelfAsync(args).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task ModifyStatusAsync(Action<ModifyPresenceParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
var args = new ModifyPresenceParams();
func(args);

var game = args._game.GetValueOrDefault(_game);
var status = args._status.GetValueOrDefault(_status);

long idleSince = _idleSince;
if (status == UserStatus.Idle && _status != UserStatus.Idle)
idleSince = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var apiGame = game != null ? new API.Game { Name = game.Name, StreamType = game.StreamType, StreamUrl = game.StreamUrl } : null;

await Discord.ApiClient.SendStatusUpdateAsync(status == UserStatus.Idle ? _idleSince : (long?)null, apiGame).ConfigureAwait(false);
//Save values
_idleSince = idleSince;
_game = game;
_status = status;
}
Task ISelfUser.ModifyStatusAsync(Action<ModifyPresenceParams> func) { throw new NotSupportedException(); }
}
}

src/Discord.Net/Entities/Users/User.cs → src/Discord.Net/Rest/Entities/Users/User.cs View File

@@ -1,4 +1,5 @@
using System;
using Discord.Rest;
using System;
using System.Diagnostics;
using Model = Discord.API.User;


+ 64
- 0
src/Discord.Net/Rpc/DiscordRpcClient.Events.cs View File

@@ -0,0 +1,64 @@
using System;
using System.Threading.Tasks;

namespace Discord.Rpc
{
public partial class DiscordRpcClient
{
//General
public event Func<Task> Connected
{
add { _connectedEvent.Add(value); }
remove { _connectedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<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>>();
public event Func<Task> Ready
{
add { _readyEvent.Add(value); }
remove { _readyEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Task>> _readyEvent = new AsyncEvent<Func<Task>>();

//Guild
public event Func<Task> GuildUpdated
{
add { _guildUpdatedEvent.Add(value); }
remove { _guildUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Task>> _guildUpdatedEvent = new AsyncEvent<Func<Task>>();

//Voice
public event Func<Task> VoiceStateUpdated
{
add { _voiceStateUpdatedEvent.Add(value); }
remove { _voiceStateUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Task>> _voiceStateUpdatedEvent = new AsyncEvent<Func<Task>>();

//Messages
public event Func<ulong, IMessage, Task> MessageReceived
{
add { _messageReceivedEvent.Add(value); }
remove { _messageReceivedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<ulong, IMessage, Task>> _messageReceivedEvent = new AsyncEvent<Func<ulong, IMessage, Task>>();
public event Func<ulong, IMessage, Task> MessageUpdated
{
add { _messageUpdatedEvent.Add(value); }
remove { _messageUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<ulong, IMessage, Task>> _messageUpdatedEvent = new AsyncEvent<Func<ulong, IMessage, Task>>();
public event Func<ulong, ulong, Task> MessageDeleted
{
add { _messageDeletedEvent.Add(value); }
remove { _messageDeletedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<ulong, ulong, Task>> _messageDeletedEvent = new AsyncEvent<Func<ulong, ulong, Task>>();
}
}

+ 400
- 0
src/Discord.Net/Rpc/DiscordRpcClient.cs View File

@@ -0,0 +1,400 @@
using Discord.API.Rpc;
using Discord.Logging;
using Discord.Net.Converters;
using Discord.Net.Queue;
using Discord.Rest;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Rpc
{
public partial class DiscordRpcClient : DiscordRestClient
{
private readonly ILogger _rpcLogger;
private readonly JsonSerializer _serializer;

private TaskCompletionSource<bool> _connectTask;
private CancellationTokenSource _cancelToken, _reconnectCancelToken;
private Task _reconnectTask;
private bool _isReconnecting;
private bool _canReconnect;

public ConnectionState ConnectionState { get; private set; }

public new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient;

/// <summary> Creates a new RPC discord client. </summary>
public DiscordRpcClient(string clientId, string origin) : this(new DiscordRpcConfig(clientId, origin)) { }
/// <summary> Creates a new RPC discord client. </summary>
public DiscordRpcClient(DiscordRpcConfig config)
: base(config, CreateApiClient(config))
{
_rpcLogger = LogManager.CreateLogger("RPC");

_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
_serializer.Error += (s, e) =>
{
_rpcLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult();
e.ErrorContext.Handled = true;
};
ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
ApiClient.ReceivedRpcEvent += ProcessMessageAsync;
ApiClient.Disconnected += async ex =>
{
if (ex != null)
{
await _rpcLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false);
await StartReconnectAsync(ex).ConfigureAwait(false);
}
else
await _rpcLogger.WarningAsync($"Connection Closed").ConfigureAwait(false);
};
}
private static API.DiscordRpcApiClient CreateApiClient(DiscordRpcConfig config)
=> new API.DiscordRpcApiClient(config.ClientId, config.Origin, config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue());

internal override void Dispose(bool disposing)
{
if (!_isDisposed)
ApiClient.Dispose();
}

protected override Task ValidateTokenAsync(TokenType tokenType, string token)
{
return Task.CompletedTask; //Validation is done in DiscordRpcAPIClient
}

/// <inheritdoc />
public Task ConnectAsync() => ConnectAsync(false);
internal async Task ConnectAsync(bool ignoreLoginCheck)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
_isReconnecting = false;
await ConnectInternalAsync(ignoreLoginCheck, false).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task ConnectInternalAsync(bool ignoreLoginCheck, bool isReconnecting)
{
if (!ignoreLoginCheck && LoginState != LoginState.LoggedIn)
throw new InvalidOperationException("You must log in before connecting.");
if (!isReconnecting && _reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested)
_reconnectCancelToken.Cancel();

var state = ConnectionState;
if (state == ConnectionState.Connecting || state == ConnectionState.Connected)
await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false);

ConnectionState = ConnectionState.Connecting;
await _rpcLogger.InfoAsync("Connecting").ConfigureAwait(false);
try
{
_connectTask = new TaskCompletionSource<bool>();
_cancelToken = new CancellationTokenSource();
await ApiClient.ConnectAsync().ConfigureAwait(false);
await _connectedEvent.InvokeAsync().ConfigureAwait(false);

await _connectTask.Task.ConfigureAwait(false);
_canReconnect = true;
ConnectionState = ConnectionState.Connected;
await _rpcLogger.InfoAsync("Connected").ConfigureAwait(false);
}
catch (Exception)
{
await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false);
throw;
}
}
/// <inheritdoc />
public async Task DisconnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
_isReconnecting = false;
await DisconnectInternalAsync(null, false).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync(Exception ex, bool isReconnecting)
{
if (!isReconnecting)
{
_canReconnect = false;

if (_reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested)
_reconnectCancelToken.Cancel();
}

if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;
await _rpcLogger.InfoAsync("Disconnecting").ConfigureAwait(false);

await _rpcLogger.DebugAsync("Disconnecting - CancelToken").ConfigureAwait(false);
//Signal tasks to complete
try { _cancelToken.Cancel(); } catch { }

await _rpcLogger.DebugAsync("Disconnecting - ApiClient").ConfigureAwait(false);
//Disconnect from server
await ApiClient.DisconnectAsync().ConfigureAwait(false);
ConnectionState = ConnectionState.Disconnected;
await _rpcLogger.InfoAsync("Disconnected").ConfigureAwait(false);

await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
}

private async Task StartReconnectAsync(Exception ex)
{
_connectTask?.TrySetException(ex);
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
if (!_canReconnect || _reconnectTask != null) return;
await DisconnectInternalAsync(null, true).ConfigureAwait(false);
_reconnectCancelToken = new CancellationTokenSource();
_reconnectTask = ReconnectInternalAsync(_reconnectCancelToken.Token);
}
finally { _connectionLock.Release(); }
}
private async Task ReconnectInternalAsync(CancellationToken cancelToken)
{
try
{
Random jitter = new Random();
int nextReconnectDelay = 1000;
while (true)
{
await Task.Delay(nextReconnectDelay, cancelToken).ConfigureAwait(false);
nextReconnectDelay = nextReconnectDelay * 2 + jitter.Next(-250, 250);
if (nextReconnectDelay > 60000)
nextReconnectDelay = 60000;

await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
if (cancelToken.IsCancellationRequested) return;
await ConnectInternalAsync(false, true).ConfigureAwait(false);
_reconnectTask = null;
return;
}
catch (Exception ex)
{
await _rpcLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
}
catch (OperationCanceledException)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await _rpcLogger.DebugAsync("Reconnect cancelled").ConfigureAwait(false);
_reconnectTask = null;
}
finally { _connectionLock.Release(); }
}
}

public async Task<string> AuthorizeAsync(string[] scopes, string rpcToken = null)
{
await ConnectAsync(true).ConfigureAwait(false);
var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken).ConfigureAwait(false);
await DisconnectAsync().ConfigureAwait(false);
return result.Code;
}

public async Task SubscribeGuild(ulong guildId, params RpcChannelEvent[] events)
{
Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events));
for (int i = 0; i < events.Length; i++)
await ApiClient.SendGuildSubscribeAsync(GetEventName(events[i]), guildId);
}
public async Task UnsubscribeGuild(ulong guildId, params RpcChannelEvent[] events)
{
Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events));
for (int i = 0; i < events.Length; i++)
await ApiClient.SendGuildUnsubscribeAsync(GetEventName(events[i]), guildId);
}
public async Task SubscribeChannel(ulong channelId, params RpcChannelEvent[] events)
{
Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events));
for (int i = 0; i < events.Length; i++)
await ApiClient.SendChannelSubscribeAsync(GetEventName(events[i]), channelId);
}
public async Task UnsubscribeChannel(ulong channelId, params RpcChannelEvent[] events)
{
Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events));
for (int i = 0; i < events.Length; i++)
await ApiClient.SendChannelUnsubscribeAsync(GetEventName(events[i]), channelId);
}

private static string GetEventName(RpcGuildEvent rpcEvent)
{
switch (rpcEvent)
{
case RpcGuildEvent.GuildStatus: return "GUILD_STATUS";
default:
throw new InvalidOperationException($"Unknown RPC Guild Event: {rpcEvent}");
}
}
private static string GetEventName(RpcChannelEvent rpcEvent)
{
switch (rpcEvent)
{
case RpcChannelEvent.VoiceStateCreate: return "VOICE_STATE_CREATE";
case RpcChannelEvent.VoiceStateUpdate: return "VOICE_STATE_UPDATE";
case RpcChannelEvent.VoiceStateDelete: return "VOICE_STATE_DELETE";
case RpcChannelEvent.SpeakingStart: return "SPEAKING_START";
case RpcChannelEvent.SpeakingStop: return "SPEAKING_STOP";
case RpcChannelEvent.MessageCreate: return "MESSAGE_CREATE";
case RpcChannelEvent.MessageUpdate: return "MESSAGE_UPDATE";
case RpcChannelEvent.MessageDelete: return "MESSAGE_DELETE";
default:
throw new InvalidOperationException($"Unknown RPC Channel Event: {rpcEvent}");
}
}

private async Task ProcessMessageAsync(string cmd, Optional<string> evnt, Optional<object> payload)
{
try
{
switch (cmd)
{
case "DISPATCH":
switch (evnt.Value)
{
//Connection
case "READY":
{
await _rpcLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false);
var data = (payload.Value as JToken).ToObject<ReadyEvent>(_serializer);
var cancelToken = _cancelToken;

var _ = Task.Run(async () =>
{
try
{
RequestOptions options = new RequestOptions
{
//CancellationToken = cancelToken //TODO: Implement
};

if (LoginState != LoginState.LoggedOut)
await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer

var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete
await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false);
}
catch (Exception ex)
{
await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false);
return;
}
});
}
break;

//Guilds
case "GUILD_STATUS":
{
await _rpcLogger.DebugAsync("Received Dispatch (GUILD_STATUS)").ConfigureAwait(false);

await _guildUpdatedEvent.InvokeAsync().ConfigureAwait(false);
}
break;

//Voice
case "VOICE_STATE_CREATE":
{
await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_CREATE)").ConfigureAwait(false);

await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false);
}
break;
case "VOICE_STATE_UPDATE":
{
await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false);

await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false);
}
break;
case "VOICE_STATE_DELETE":
{
await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_DELETE)").ConfigureAwait(false);

await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false);
}
break;

case "SPEAKING_START":
{
await _rpcLogger.DebugAsync("Received Dispatch (SPEAKING_START)").ConfigureAwait(false);
await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false);
}
break;
case "SPEAKING_STOP":
{
await _rpcLogger.DebugAsync("Received Dispatch (SPEAKING_STOP)").ConfigureAwait(false);

await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false);
}
break;

//Messages
case "MESSAGE_CREATE":
{
await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false);
var data = (payload.Value as JToken).ToObject<MessageEvent>(_serializer);
var msg = new RpcMessage(this, data.Message);

await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false);
}
break;
case "MESSAGE_UPDATE":
{
await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false);
var data = (payload.Value as JToken).ToObject<MessageEvent>(_serializer);
var msg = new RpcMessage(this, data.Message);

await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false);
}
break;
case "MESSAGE_DELETE":
{
await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false);
var data = (payload.Value as JToken).ToObject<MessageEvent>(_serializer);

await _messageDeletedEvent.InvokeAsync(data.ChannelId, data.Message.Id).ConfigureAwait(false);
}
break;

//Others
default:
await _rpcLogger.WarningAsync($"Unknown Dispatch ({evnt})").ConfigureAwait(false);
return;
}
break;

/*default: //Other opcodes are used for command responses
await _rpcLogger.WarningAsync($"Unknown OpCode ({cmd})").ConfigureAwait(false);
return;*/
}
}
catch (Exception ex)
{
await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false);
return;
}
}
}
}

+ 27
- 0
src/Discord.Net/Rpc/DiscordRpcConfig.cs View File

@@ -0,0 +1,27 @@
using Discord.Net.WebSockets;
using Discord.Rest;

namespace Discord.Rpc
{
public class DiscordRpcConfig : DiscordRestConfig
{
public const int RpcAPIVersion = 1;

public const int PortRangeStart = 6463;
public const int PortRangeEnd = 6472;

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

+ 8
- 0
src/Discord.Net/Rpc/Entities/IRemoteUserGuild.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Rpc
{
/*public interface IRemoteUserGuild : ISnowflakeEntity
{
/// <summary> Gets the name of this guild. </summary>
string Name { get; }
}*/
}

+ 30
- 0
src/Discord.Net/Rpc/Entities/RemoteUserGuild.cs View File

@@ -0,0 +1,30 @@
using Discord.Rest;
using System;
using Model = Discord.API.Rpc.RpcUserGuild;

namespace Discord.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;
}*/
}

+ 15
- 0
src/Discord.Net/Rpc/Entities/RpcMessage.cs View File

@@ -0,0 +1,15 @@
using Discord.Rest;

namespace Discord.Rpc
{
internal class RpcMessage : Message
{
public override DiscordRestClient Discord { get; }

public RpcMessage(DiscordRpcClient discord, API.Message model)
: base(null, model.Author.IsSpecified ? new User(model.Author.Value) : null, model)
{
Discord = discord;
}
}
}

+ 14
- 0
src/Discord.Net/Rpc/RpcChannelEvent.cs View File

@@ -0,0 +1,14 @@
namespace Discord.Rpc
{
public enum RpcChannelEvent
{
VoiceStateCreate,
VoiceStateUpdate,
VoiceStateDelete,
SpeakingStart,
SpeakingStop,
MessageCreate,
MessageUpdate,
MessageDelete
}
}

+ 7
- 0
src/Discord.Net/Rpc/RpcGuildEvent.cs View File

@@ -0,0 +1,7 @@
namespace Discord.Rpc
{
public enum RpcGuildEvent
{
GuildStatus
}
}

src/Discord.Net/DiscordSocketClient.Events.cs → src/Discord.Net/WebSocket/DiscordSocketClient.Events.cs View File

@@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;

namespace Discord
namespace Discord.WebSocket
{
//TODO: Add event docstrings
public partial class DiscordSocketClient

src/Discord.Net/DiscordSocketClient.cs → src/Discord.Net/WebSocket/DiscordSocketClient.cs View File

@@ -1,9 +1,10 @@
using Discord.API.Gateway;
using Discord.Audio;
using Discord.Extensions;
using Discord.Logging;
using Discord.Net.Converters;
using Discord.Net.Queue;
using Discord.Net.WebSockets;
using Discord.Rest;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
@@ -14,7 +15,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Discord
namespace Discord.WebSocket
{
public partial class DiscordSocketClient : DiscordRestClient, IDiscordClient
{
@@ -52,6 +53,7 @@ namespace Discord
internal DataStore DataStore { get; private set; }
internal WebSocketProvider WebSocketProvider { get; private set; }

public new API.DiscordSocketApiClient ApiClient => base.ApiClient as API.DiscordSocketApiClient;
internal SocketSelfUser CurrentUser => _currentUser as SocketSelfUser;
internal IReadOnlyCollection<SocketGuild> Guilds => DataStore.Guilds;
internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection();
@@ -60,7 +62,7 @@ namespace Discord
public DiscordSocketClient() : this(new DiscordSocketConfig()) { }
/// <summary> Creates a new REST/WebSocket discord client. </summary>
public DiscordSocketClient(DiscordSocketConfig config)
: base(config)
: base(config, CreateApiClient(config))
{
ShardId = config.ShardId;
TotalShards = config.TotalShards;
@@ -106,8 +108,10 @@ namespace Discord
_voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>();
_largeGuilds = new ConcurrentQueue<ulong>();
}
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue());

protected override async Task OnLoginAsync()
protected override async Task OnLoginAsync(TokenType tokenType, string token)
{
var voiceRegions = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false);
_voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id);
@@ -162,7 +166,7 @@ namespace Discord
if (_sessionId != null)
await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false);
else
await ApiClient.SendIdentifyAsync().ConfigureAwait(false);
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards:TotalShards).ConfigureAwait(false);

await _connectTask.Task.ConfigureAwait(false);
_canReconnect = true;
@@ -530,7 +534,7 @@ namespace Discord

_sessionId = null;
_lastSeq = 0;
await ApiClient.SendIdentifyAsync().ConfigureAwait(false);
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false);
}
break;
case GatewayOpCode.Reconnect:
@@ -1467,6 +1471,9 @@ namespace Discord
return;

//Ignored (User only)
case "CHANNEL_PINS_UPDATE":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_UPDATE)");
break;
case "USER_SETTINGS_UPDATE":
await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false);
return;

src/Discord.Net/DiscordSocketConfig.cs → src/Discord.Net/WebSocket/DiscordSocketConfig.cs View File

@@ -1,5 +1,6 @@
using Discord.Audio;
using Discord.Net.WebSockets;
using Discord.Rest;

namespace Discord
{
@@ -14,11 +15,6 @@ namespace Discord

/// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary>
public int MessageCacheSize { get; set; } = 0;
/*/// <summary>
/// Gets or sets whether the permissions cache should be used.
/// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster at the expense of increased memory usage.
/// </summary>
public bool UsePermissionsCache { get; set; } = false;*/
/// <summary>
/// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250.
/// Decreasing this may reduce CPU usage while increasing login time and network usage.

src/Discord.Net/Entities/WebSocket/Channels/ISocketChannel.cs → src/Discord.Net/WebSocket/Entities/Channels/ISocketChannel.cs View File


src/Discord.Net/Entities/WebSocket/Channels/ISocketGuildChannel.cs → src/Discord.Net/WebSocket/Entities/Channels/ISocketGuildChannel.cs View File


src/Discord.Net/Entities/WebSocket/Channels/ISocketMessageChannel.cs → src/Discord.Net/WebSocket/Entities/Channels/ISocketMessageChannel.cs View File


src/Discord.Net/Entities/WebSocket/Channels/ISocketPrivateChannel.cs → src/Discord.Net/WebSocket/Entities/Channels/ISocketPrivateChannel.cs View File


src/Discord.Net/Entities/WebSocket/Channels/MessageCache.cs → src/Discord.Net/WebSocket/Entities/Channels/MessageCache.cs View File

@@ -1,4 +1,5 @@
using Discord.Extensions;
using Discord.Rest;
using Discord.WebSocket;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -51,7 +52,7 @@ namespace Discord
return result;
return null;
}
public override IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch)
public override IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
{
if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit));
if (limit == 0) return ImmutableArray<SocketMessage>.Empty;

src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs → src/Discord.Net/WebSocket/Entities/Channels/MessageManager.cs View File

@@ -1,4 +1,6 @@
using Discord.API.Rest;
using Discord.Rest;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -12,7 +14,7 @@ namespace Discord
private readonly DiscordSocketClient _discord;
private readonly ISocketMessageChannel _channel;

public virtual IReadOnlyCollection<SocketMessage> Messages
public virtual IReadOnlyCollection<SocketMessage> Messages
=> ImmutableArray.Create<SocketMessage>();

public MessageManager(DiscordSocketClient discord, ISocketMessageChannel channel)
@@ -25,7 +27,7 @@ namespace Discord
public virtual SocketMessage Remove(ulong id) => null;
public virtual SocketMessage Get(ulong id) => null;

public virtual IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch)
public virtual IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
=> ImmutableArray.Create<SocketMessage>();

public virtual async Task<SocketMessage> DownloadAsync(ulong id)

src/Discord.Net/Entities/WebSocket/Channels/SocketDMChannel.cs → src/Discord.Net/WebSocket/Entities/Channels/SocketDMChannel.cs View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Discord.WebSocket;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using MessageModel = Discord.API.Message;

src/Discord.Net/Entities/WebSocket/Channels/SocketGroupChannel.cs → src/Discord.Net/WebSocket/Entities/Channels/SocketGroupChannel.cs View File

@@ -1,4 +1,4 @@
using Discord.Extensions;
using Discord.WebSocket;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;

src/Discord.Net/Entities/WebSocket/Channels/SocketTextChannel.cs → src/Discord.Net/WebSocket/Entities/Channels/SocketTextChannel.cs View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Discord.Rest;
using Discord.WebSocket;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
@@ -48,11 +50,11 @@ namespace Discord
{
return await _messages.DownloadAsync(id).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordRestConfig.MaxMessagesPerBatch)
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch)
{
return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch)
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
{
return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false);
}

src/Discord.Net/Entities/WebSocket/Channels/SocketVoiceChannel.cs → src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs View File

@@ -1,4 +1,5 @@
using Discord.Audio;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

src/Discord.Net/Entities/WebSocket/Guilds/SocketGuild.cs → src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -1,5 +1,5 @@
using Discord.Audio;
using Discord.Extensions;
using Discord.WebSocket;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

src/Discord.Net/Entities/WebSocket/Messages/SocketMessage.cs → src/Discord.Net/WebSocket/Entities/Messages/SocketMessage.cs View File

@@ -1,4 +1,5 @@
using Model = Discord.API.Message;
using Discord.WebSocket;
using Model = Discord.API.Message;

namespace Discord
{

src/Discord.Net/Entities/WebSocket/Users/ISocketUser.cs → src/Discord.Net/WebSocket/Entities/Users/ISocketUser.cs View File


src/Discord.Net/Entities/WebSocket/Users/Presence.cs → src/Discord.Net/WebSocket/Entities/Users/Presence.cs View File


src/Discord.Net/Entities/WebSocket/Users/SocketDMUser.cs → src/Discord.Net/WebSocket/Entities/Users/SocketDMUser.cs View File

@@ -1,4 +1,5 @@
using System;
using Discord.WebSocket;
using System;
using System.Diagnostics;
using PresenceModel = Discord.API.Presence;


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save