@@ -1,16 +1,19 @@ | |||
using Discord.API.Rest; | |||
using Discord.API.Gateway; | |||
using Discord.API.Rest; | |||
using Discord.Net; | |||
using Discord.Net.Converters; | |||
using Discord.Net.Queue; | |||
using Discord.Net.Rest; | |||
using Discord.Net.WebSockets; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Diagnostics; | |||
using System.Globalization; | |||
using System.IO; | |||
using System.IO.Compression; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Text; | |||
@@ -21,14 +24,17 @@ namespace Discord.API | |||
{ | |||
public class DiscordApiClient : IDisposable | |||
{ | |||
internal event Func<SentRequestEventArgs, Task> SentRequest; | |||
public event Func<string, string, double, Task> SentRequest; | |||
public event Func<int, Task> SentGatewayMessage; | |||
public event Func<GatewayOpCodes, string, JToken, Task> ReceivedGatewayEvent; | |||
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 bool _isDisposed; | |||
public LoginState LoginState { get; private set; } | |||
@@ -48,6 +54,26 @@ namespace Discord.API | |||
{ | |||
_gatewayClient = webSocketProvider(); | |||
_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); | |||
_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.Raise((GatewayOpCodes)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); | |||
} | |||
} | |||
}; | |||
_gatewayClient.TextMessage += async text => | |||
{ | |||
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text); | |||
await ReceivedGatewayEvent.Raise((GatewayOpCodes)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); | |||
}; | |||
} | |||
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
@@ -95,6 +121,7 @@ namespace Discord.API | |||
_loginCancelToken = new CancellationTokenSource(); | |||
AuthTokenType = TokenType.User; | |||
_authToken = null; | |||
_restClient.SetHeader("authorization", null); | |||
await _requestQueue.SetCancelToken(_loginCancelToken.Token).ConfigureAwait(false); | |||
_restClient.SetCancelToken(_loginCancelToken.Token); | |||
@@ -106,6 +133,7 @@ namespace Discord.API | |||
} | |||
AuthTokenType = tokenType; | |||
_authToken = token; | |||
switch (tokenType) | |||
{ | |||
case TokenType.Bot: | |||
@@ -181,7 +209,10 @@ namespace Discord.API | |||
_gatewayClient.SetCancelToken(_connectCancelToken.Token); | |||
var gatewayResponse = await GetGateway().ConfigureAwait(false); | |||
await _gatewayClient.Connect(gatewayResponse.Url).ConfigureAwait(false); | |||
var url = $"{gatewayResponse.Url}?v={DiscordConfig.GatewayAPIVersion}&encoding={DiscordConfig.GatewayEncoding}"; | |||
await _gatewayClient.Connect(url).ConfigureAwait(false); | |||
await SendIdentify().ConfigureAwait(false); | |||
ConnectionState = ConnectionState.Connected; | |||
} | |||
@@ -226,13 +257,13 @@ namespace Discord.API | |||
=> SendInternal(method, endpoint, multipartArgs, true, bucket); | |||
public async Task<TResponse> Send<TResponse>(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) | |||
where TResponse : class | |||
=> Deserialize<TResponse>(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false)); | |||
=> DeserializeJson<TResponse>(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false)); | |||
public async Task<TResponse> Send<TResponse>(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) | |||
where TResponse : class | |||
=> Deserialize<TResponse>(await SendInternal(method, endpoint, payload, false, bucket).ConfigureAwait(false)); | |||
=> DeserializeJson<TResponse>(await SendInternal(method, endpoint, payload, false, bucket).ConfigureAwait(false)); | |||
public async Task<TResponse> Send<TResponse>(string method, string endpoint, Stream file, IReadOnlyDictionary<string, string> multipartArgs, GlobalBucket bucket = GlobalBucket.General) | |||
where TResponse : class | |||
=> Deserialize<TResponse>(await SendInternal(method, endpoint, multipartArgs, false, bucket).ConfigureAwait(false)); | |||
=> DeserializeJson<TResponse>(await SendInternal(method, endpoint, multipartArgs, false, bucket).ConfigureAwait(false)); | |||
public Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId) | |||
=> SendInternal(method, endpoint, null, true, bucket, guildId); | |||
@@ -242,14 +273,14 @@ namespace Discord.API | |||
=> SendInternal(method, endpoint, multipartArgs, true, bucket, guildId); | |||
public async Task<TResponse> Send<TResponse>(string method, string endpoint, GuildBucket bucket, ulong guildId) | |||
where TResponse : class | |||
=> Deserialize<TResponse>(await SendInternal(method, endpoint, null, false, bucket, guildId).ConfigureAwait(false)); | |||
=> DeserializeJson<TResponse>(await SendInternal(method, endpoint, null, false, bucket, guildId).ConfigureAwait(false)); | |||
public async Task<TResponse> Send<TResponse>(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) | |||
where TResponse : class | |||
=> Deserialize<TResponse>(await SendInternal(method, endpoint, payload, false, bucket, guildId).ConfigureAwait(false)); | |||
=> DeserializeJson<TResponse>(await SendInternal(method, endpoint, payload, false, bucket, guildId).ConfigureAwait(false)); | |||
public async Task<TResponse> Send<TResponse>(string method, string endpoint, Stream file, IReadOnlyDictionary<string, string> multipartArgs, GuildBucket bucket, ulong guildId) | |||
where TResponse : class | |||
=> Deserialize<TResponse>(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).ConfigureAwait(false)); | |||
=> DeserializeJson<TResponse>(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).ConfigureAwait(false)); | |||
private Task<Stream> SendInternal(string method, string endpoint, object payload, bool headerOnly, GlobalBucket bucket) | |||
=> SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Global, (int)bucket, 0); | |||
private Task<Stream> SendInternal(string method, string endpoint, object payload, bool headerOnly, GuildBucket bucket, ulong guildId) | |||
@@ -264,13 +295,12 @@ namespace Discord.API | |||
var stopwatch = Stopwatch.StartNew(); | |||
string json = null; | |||
if (payload != null) | |||
json = Serialize(payload); | |||
json = SerializeJson(payload); | |||
var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, json, headerOnly), group, bucketId, guildId).ConfigureAwait(false); | |||
int bytes = headerOnly ? 0 : (int)responseStream.Length; | |||
stopwatch.Stop(); | |||
double milliseconds = ToMilliseconds(stopwatch); | |||
await SentRequest.Raise(new SentRequestEventArgs(method, endpoint, bytes, milliseconds)).ConfigureAwait(false); | |||
await SentRequest.Raise(method, endpoint, milliseconds).ConfigureAwait(false); | |||
return responseStream; | |||
} | |||
@@ -282,11 +312,28 @@ namespace Discord.API | |||
stopwatch.Stop(); | |||
double milliseconds = ToMilliseconds(stopwatch); | |||
await SentRequest.Raise(new SentRequestEventArgs(method, endpoint, bytes, milliseconds)).ConfigureAwait(false); | |||
await SentRequest.Raise(method, endpoint, milliseconds).ConfigureAwait(false); | |||
return responseStream; | |||
} | |||
public Task SendGateway(GatewayOpCodes opCode, object payload, GlobalBucket bucket = GlobalBucket.Gateway) | |||
=> SendGateway((int)opCode, payload, BucketGroup.Global, (int)bucket, 0); | |||
public Task SendGateway(VoiceOpCodes opCode, object payload, GlobalBucket bucket = GlobalBucket.Gateway) | |||
=> SendGateway((int)opCode, payload, BucketGroup.Global, (int)bucket, 0); | |||
public Task SendGateway(GatewayOpCodes opCode, object payload, GuildBucket bucket, ulong guildId) | |||
=> SendGateway((int)opCode, payload, BucketGroup.Guild, (int)bucket, guildId); | |||
public Task SendGateway(VoiceOpCodes opCode, object payload, GuildBucket bucket, ulong guildId) | |||
=> SendGateway((int)opCode, payload, BucketGroup.Guild, (int)bucket, guildId); | |||
private async Task SendGateway(int opCode, object payload, BucketGroup group, int bucketId, ulong guildId) | |||
{ | |||
//TODO: Add ETF | |||
byte[] bytes = null; | |||
payload = new WebSocketMessage { Operation = opCode, Payload = payload }; | |||
if (payload != null) | |||
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | |||
await _requestQueue.Send(new WebSocketRequest(_gatewayClient, bytes, true), group, bucketId, guildId).ConfigureAwait(false); | |||
} | |||
//Auth | |||
public async Task ValidateToken() | |||
@@ -299,6 +346,21 @@ namespace Discord.API | |||
{ | |||
return await Send<GetGatewayResponse>("GET", "gateway").ConfigureAwait(false); | |||
} | |||
public async Task SendIdentify(int largeThreshold = 100, bool useCompression = true) | |||
{ | |||
var props = new Dictionary<string, string> | |||
{ | |||
["$device"] = "Discord.Net" | |||
}; | |||
var msg = new IdentifyParams() | |||
{ | |||
Token = _authToken, | |||
Properties = props, | |||
LargeThreshold = largeThreshold, | |||
UseCompression = useCompression | |||
}; | |||
await SendGateway(GatewayOpCodes.Identify, msg).ConfigureAwait(false); | |||
} | |||
//Channels | |||
public async Task<Channel> GetChannel(ulong channelId) | |||
@@ -986,7 +1048,7 @@ namespace Discord.API | |||
//Helpers | |||
private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); | |||
private string Serialize(object value) | |||
private string SerializeJson(object value) | |||
{ | |||
var sb = new StringBuilder(256); | |||
using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) | |||
@@ -994,7 +1056,7 @@ namespace Discord.API | |||
_serializer.Serialize(writer, value); | |||
return sb.ToString(); | |||
} | |||
private T Deserialize<T>(Stream jsonStream) | |||
private T DeserializeJson<T>(Stream jsonStream) | |||
{ | |||
using (TextReader text = new StreamReader(jsonStream)) | |||
using (JsonReader reader = new JsonTextReader(text)) | |||
@@ -1,11 +0,0 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Discord.API | |||
{ | |||
public class DiscordAPISocketClient | |||
{ | |||
} | |||
} |
@@ -1,6 +1,6 @@ | |||
namespace Discord.API.Gateway | |||
{ | |||
public enum OpCodes : byte | |||
public enum GatewayOpCodes : byte | |||
{ | |||
/// <summary> C←S - Used to send most events. </summary> | |||
Dispatch = 0, |
@@ -3,7 +3,7 @@ using System.Collections.Generic; | |||
namespace Discord.API.Gateway | |||
{ | |||
public class IdentifyCommand | |||
public class IdentifyParams | |||
{ | |||
[JsonProperty("token")] | |||
public string Token { get; set; } | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Gateway | |||
{ | |||
public class RequestMembersCommand | |||
public class RequestMembersParams | |||
{ | |||
[JsonProperty("guild_id")] | |||
public ulong[] GuildId { get; set; } | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Gateway | |||
{ | |||
public class ResumeCommand | |||
public class ResumeParams | |||
{ | |||
[JsonProperty("session_id")] | |||
public string SessionId { get; set; } | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Gateway | |||
{ | |||
public class UpdateStatusCommand | |||
public class UpdateStatusParams | |||
{ | |||
[JsonProperty("idle_since")] | |||
public long? IdleSince { get; set; } | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Gateway | |||
{ | |||
public class UpdateVoiceCommand | |||
public class UpdateVoiceParams | |||
{ | |||
[JsonProperty("guild_id")] | |||
public ulong? GuildId { get; set; } | |||
@@ -0,0 +1,18 @@ | |||
namespace Discord.API.Gateway | |||
{ | |||
public enum VoiceOpCodes : byte | |||
{ | |||
/// <summary> C→S - Used to associate a connection with a token. </summary> | |||
Identify = 0, | |||
/// <summary> C→S - Used to specify configuration. </summary> | |||
SelectProtocol = 1, | |||
/// <summary> C←S - Used to notify that the voice connection was successful and informs the client of available protocols. </summary> | |||
Ready = 2, | |||
/// <summary> C↔S - Used to keep the connection alive and measure latency. </summary> | |||
Heartbeat = 3, | |||
/// <summary> C←S - Used to provide an encryption key to the client. </summary> | |||
SessionDescription = 4, | |||
/// <summary> C↔S - Used to inform that a certain user is speaking. </summary> | |||
Speaking = 5 | |||
} | |||
} |
@@ -1,17 +1,16 @@ | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
namespace Discord.API | |||
{ | |||
public class WebSocketMessage | |||
{ | |||
[JsonProperty("op")] | |||
public int? Operation { get; set; } | |||
public int Operation { get; set; } | |||
[JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Type { get; set; } | |||
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] | |||
public uint? Sequence { get; set; } | |||
[JsonProperty("d")] | |||
public JToken Payload { get; set; } | |||
public object Payload { get; set; } | |||
} | |||
} |
@@ -10,7 +10,8 @@ namespace Discord | |||
public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; | |||
public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; | |||
public const int GatewayAPIVersion = 3; | |||
public const int GatewayAPIVersion = 3; //TODO: Upgrade to 4 | |||
public const string GatewayEncoding = "json"; | |||
public const string ClientAPIUrl = "https://discordapp.com/api/"; | |||
public const string CDNUrl = "https://cdn.discordapp.com/"; | |||
@@ -5,6 +5,7 @@ namespace Discord | |||
{ | |||
internal static class EventExtensions | |||
{ | |||
//TODO: Optimize these for if there is only 1 subscriber (can we do this?) | |||
public static async Task Raise(this Func<Task> eventHandler) | |||
{ | |||
var subscriptions = eventHandler?.GetInvocationList(); | |||
@@ -32,7 +33,7 @@ namespace Discord | |||
await (subscriptions[i] as Func<T1, T2, Task>).Invoke(arg1, arg2).ConfigureAwait(false); | |||
} | |||
} | |||
public static async Task Raise<T1, T2, T3>(this Func<T1, T2, Task> eventHandler, T1 arg1, T2 arg2, T3 arg3) | |||
public static async Task Raise<T1, T2, T3>(this Func<T1, T2, T3, Task> eventHandler, T1 arg1, T2 arg2, T3 arg3) | |||
{ | |||
var subscriptions = eventHandler?.GetInvocationList(); | |||
if (subscriptions != null) | |||
@@ -1,20 +0,0 @@ | |||
using System; | |||
namespace Discord.Net.Rest | |||
{ | |||
public class SentRequestEventArgs : EventArgs | |||
{ | |||
public string Method { get; } | |||
public string Endpoint { get; } | |||
public int ResponseLength { get; } | |||
public double Milliseconds { get; } | |||
public SentRequestEventArgs(string method, string endpoint, int responseLength, double milliseconds) | |||
{ | |||
Method = method; | |||
Endpoint = endpoint; | |||
ResponseLength = responseLength; | |||
Milliseconds = milliseconds; | |||
} | |||
} | |||
} |
@@ -7,7 +7,7 @@ namespace Discord.Logging | |||
{ | |||
public LogSeverity Level { get; } | |||
public event Func<LogMessageEventArgs, Task> Message; | |||
public event Func<LogMessage, Task> Message; | |||
internal LogManager(LogSeverity minSeverity) | |||
{ | |||
@@ -17,32 +17,32 @@ namespace Discord.Logging | |||
public async Task Log(LogSeverity severity, string source, string message, Exception ex = null) | |||
{ | |||
if (severity <= Level) | |||
await Message.Raise(new LogMessageEventArgs(severity, source, message, ex)).ConfigureAwait(false); | |||
await Message.Raise(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); | |||
} | |||
public async Task Log(LogSeverity severity, string source, FormattableString message, Exception ex = null) | |||
{ | |||
if (severity <= Level) | |||
await Message.Raise(new LogMessageEventArgs(severity, source, message.ToString(), ex)).ConfigureAwait(false); | |||
await Message.Raise(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); | |||
} | |||
public async Task Log(LogSeverity severity, string source, Exception ex) | |||
{ | |||
if (severity <= Level) | |||
await Message.Raise(new LogMessageEventArgs(severity, source, null, ex)).ConfigureAwait(false); | |||
await Message.Raise(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); | |||
} | |||
async Task ILogger.Log(LogSeverity severity, string message, Exception ex) | |||
{ | |||
if (severity <= Level) | |||
await Message.Raise(new LogMessageEventArgs(severity, "Discord", message, ex)).ConfigureAwait(false); | |||
await Message.Raise(new LogMessage(severity, "Discord", message, ex)).ConfigureAwait(false); | |||
} | |||
async Task ILogger.Log(LogSeverity severity, FormattableString message, Exception ex) | |||
{ | |||
if (severity <= Level) | |||
await Message.Raise(new LogMessageEventArgs(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); | |||
await Message.Raise(new LogMessage(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); | |||
} | |||
async Task ILogger.Log(LogSeverity severity, Exception ex) | |||
{ | |||
if (severity <= Level) | |||
await Message.Raise(new LogMessageEventArgs(severity, "Discord", null, ex)).ConfigureAwait(false); | |||
await Message.Raise(new LogMessage(severity, "Discord", null, ex)).ConfigureAwait(false); | |||
} | |||
public Task Error(string source, string message, Exception ex = null) | |||
@@ -3,14 +3,14 @@ using System.Text; | |||
namespace Discord | |||
{ | |||
public class LogMessageEventArgs : EventArgs | |||
public struct LogMessage | |||
{ | |||
public LogSeverity Severity { get; } | |||
public string Source { get; } | |||
public string Message { get; } | |||
public Exception Exception { get; } | |||
public LogMessageEventArgs(LogSeverity severity, string source, string message, Exception exception = null) | |||
public LogMessage(LogSeverity severity, string source, string message, Exception exception = null) | |||
{ | |||
Severity = severity; | |||
Source = source; |
@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Threading.Tasks; | |||
namespace Discord.Logging | |||
{ | |||
@@ -15,44 +16,44 @@ namespace Discord.Logging | |||
Name = name; | |||
} | |||
public void Log(LogSeverity severity, string message, Exception exception = null) | |||
public Task Log(LogSeverity severity, string message, Exception exception = null) | |||
=> _manager.Log(severity, Name, message, exception); | |||
public void Log(LogSeverity severity, FormattableString message, Exception exception = null) | |||
public Task Log(LogSeverity severity, FormattableString message, Exception exception = null) | |||
=> _manager.Log(severity, Name, message, exception); | |||
public void Error(string message, Exception exception = null) | |||
public Task Error(string message, Exception exception = null) | |||
=> _manager.Error(Name, message, exception); | |||
public void Error(FormattableString message, Exception exception = null) | |||
public Task Error(FormattableString message, Exception exception = null) | |||
=> _manager.Error(Name, message, exception); | |||
public void Error(Exception exception) | |||
public Task Error(Exception exception) | |||
=> _manager.Error(Name, exception); | |||
public void Warning(string message, Exception exception = null) | |||
public Task Warning(string message, Exception exception = null) | |||
=> _manager.Warning(Name, message, exception); | |||
public void Warning(FormattableString message, Exception exception = null) | |||
public Task Warning(FormattableString message, Exception exception = null) | |||
=> _manager.Warning(Name, message, exception); | |||
public void Warning(Exception exception) | |||
public Task Warning(Exception exception) | |||
=> _manager.Warning(Name, exception); | |||
public void Info(string message, Exception exception = null) | |||
public Task Info(string message, Exception exception = null) | |||
=> _manager.Info(Name, message, exception); | |||
public void Info(FormattableString message, Exception exception = null) | |||
public Task Info(FormattableString message, Exception exception = null) | |||
=> _manager.Info(Name, message, exception); | |||
public void Info(Exception exception) | |||
public Task Info(Exception exception) | |||
=> _manager.Info(Name, exception); | |||
public void Verbose(string message, Exception exception = null) | |||
public Task Verbose(string message, Exception exception = null) | |||
=> _manager.Verbose(Name, message, exception); | |||
public void Verbose(FormattableString message, Exception exception = null) | |||
public Task Verbose(FormattableString message, Exception exception = null) | |||
=> _manager.Verbose(Name, message, exception); | |||
public void Verbose(Exception exception) | |||
public Task Verbose(Exception exception) | |||
=> _manager.Verbose(Name, exception); | |||
public void Debug(string message, Exception exception = null) | |||
public Task Debug(string message, Exception exception = null) | |||
=> _manager.Debug(Name, message, exception); | |||
public void Debug(FormattableString message, Exception exception = null) | |||
public Task Debug(FormattableString message, Exception exception = null) | |||
=> _manager.Debug(Name, message, exception); | |||
public void Debug(Exception exception) | |||
public Task Debug(Exception exception) | |||
=> _manager.Debug(Name, exception); | |||
} | |||
} |
@@ -12,7 +12,7 @@ namespace Discord.Net.Queue | |||
private readonly RequestQueueBucket[] _globalBuckets; | |||
private readonly Dictionary<ulong, RequestQueueBucket>[] _guildBuckets; | |||
private CancellationTokenSource _clearToken; | |||
private CancellationToken? _parentToken; | |||
private CancellationToken _parentToken; | |||
private CancellationToken _cancelToken; | |||
public RequestQueue() | |||
@@ -20,10 +20,12 @@ namespace Discord.Net.Queue | |||
_lock = new SemaphoreSlim(1, 1); | |||
_globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length]; | |||
_guildBuckets = new Dictionary<ulong, RequestQueueBucket>[Enum.GetValues(typeof(GuildBucket)).Length]; | |||
_clearToken = new CancellationTokenSource(); | |||
_cancelToken = _clearToken.Token; | |||
_cancelToken = CancellationToken.None; | |||
_parentToken = CancellationToken.None; | |||
} | |||
internal async Task SetCancelToken(CancellationToken cancelToken) | |||
public async Task SetCancelToken(CancellationToken cancelToken) | |||
{ | |||
await Lock().ConfigureAwait(false); | |||
try | |||
@@ -33,8 +35,18 @@ namespace Discord.Net.Queue | |||
} | |||
finally { Unlock(); } | |||
} | |||
internal async Task<Stream> Send(IQueuedRequest request, BucketGroup group, int bucketId, ulong guildId) | |||
internal Task<Stream> Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) | |||
{ | |||
request.CancelToken = _cancelToken; | |||
return Send(request as IQueuedRequest, group, bucketId, guildId); | |||
} | |||
internal Task<Stream> Send(WebSocketRequest request, BucketGroup group, int bucketId, ulong guildId) | |||
{ | |||
request.CancelToken = _cancelToken; | |||
return Send(request as IQueuedRequest, group, bucketId, guildId); | |||
} | |||
private async Task<Stream> Send(IQueuedRequest request, BucketGroup group, int bucketId, ulong guildId) | |||
{ | |||
RequestQueueBucket bucket; | |||
@@ -121,13 +133,13 @@ namespace Discord.Net.Queue | |||
return bucket; | |||
} | |||
internal void DestroyGlobalBucket(GlobalBucket type) | |||
public void DestroyGlobalBucket(GlobalBucket type) | |||
{ | |||
//Assume this object is locked | |||
_globalBuckets[(int)type] = null; | |||
} | |||
internal void DestroyGuildBucket(GuildBucket type, ulong guildId) | |||
public void DestroyGuildBucket(GuildBucket type, ulong guildId) | |||
{ | |||
//Assume this object is locked | |||
@@ -153,7 +165,7 @@ namespace Discord.Net.Queue | |||
_clearToken?.Cancel(); | |||
_clearToken = new CancellationTokenSource(); | |||
if (_parentToken != null) | |||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken.Value).Token; | |||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token; | |||
else | |||
_cancelToken = _clearToken.Token; | |||
} | |||
@@ -15,7 +15,7 @@ namespace Discord.Net.Queue | |||
public bool HeaderOnly { get; } | |||
public IReadOnlyDictionary<string, object> MultipartParams { get; } | |||
public TaskCompletionSource<Stream> Promise { get; } | |||
public CancellationToken CancelToken { get; internal set; } | |||
public CancellationToken CancelToken { get; set; } | |||
public bool IsMultipart => MultipartParams != null; | |||
@@ -9,25 +9,26 @@ namespace Discord.Net.Queue | |||
{ | |||
public IWebSocketClient Client { get; } | |||
public byte[] Data { get; } | |||
public int Offset { get; } | |||
public int Bytes { get; } | |||
public int DataIndex { get; } | |||
public int DataCount { get; } | |||
public bool IsText { get; } | |||
public CancellationToken CancelToken { get; } | |||
public TaskCompletionSource<Stream> Promise { get; } | |||
public CancellationToken CancelToken { get; set; } | |||
public WebSocketRequest(byte[] data, bool isText, CancellationToken cancelToken) : this(data, 0, data.Length, isText, cancelToken) { } | |||
public WebSocketRequest(byte[] data, int offset, int length, bool isText, CancellationToken cancelToken) | |||
public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText) : this(client, data, 0, data.Length, isText) { } | |||
public WebSocketRequest(IWebSocketClient client, byte[] data, int index, int count, bool isText) | |||
{ | |||
Client = client; | |||
Data = data; | |||
Offset = offset; | |||
Bytes = length; | |||
DataIndex = index; | |||
DataCount = count; | |||
IsText = isText; | |||
Promise = new TaskCompletionSource<Stream>(); | |||
} | |||
public async Task<Stream> Send() | |||
{ | |||
await Client.Send(Data, Offset, Bytes, IsText).ConfigureAwait(false); | |||
await Client.Send(Data, DataIndex, DataCount, IsText).ConfigureAwait(false); | |||
return null; | |||
} | |||
} | |||
@@ -1,11 +0,0 @@ | |||
using System; | |||
namespace Discord.Net.WebSockets | |||
{ | |||
public class BinaryMessageEventArgs : EventArgs | |||
{ | |||
public byte[] Data { get; } | |||
public BinaryMessageEventArgs(byte[] data) { } | |||
} | |||
} |
@@ -14,8 +14,8 @@ namespace Discord.Net.WebSockets | |||
public const int SendChunkSize = 4 * 1024; //4KB | |||
private const int HR_TIMEOUT = -2147012894; | |||
public event Func<BinaryMessageEventArgs, Task> BinaryMessage; | |||
public event Func<TextMessageEventArgs, Task> TextMessage; | |||
public event Func<byte[], int, int, Task> BinaryMessage; | |||
public event Func<string, Task> TextMessage; | |||
private readonly ClientWebSocket _client; | |||
private Task _task; | |||
@@ -79,12 +79,12 @@ namespace Discord.Net.WebSockets | |||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | |||
} | |||
public async Task Send(byte[] data, int offset, int count, bool isText) | |||
public async Task Send(byte[] data, int index, int count, bool isText) | |||
{ | |||
//TODO: If connection is temporarily down, retry? | |||
int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); | |||
for (int i = 0; i < frameCount; i++, offset += SendChunkSize) | |||
for (int i = 0; i < frameCount; i++, index += SendChunkSize) | |||
{ | |||
bool isLast = i == (frameCount - 1); | |||
@@ -96,7 +96,7 @@ namespace Discord.Net.WebSockets | |||
try | |||
{ | |||
await _client.SendAsync(new ArraySegment<byte>(data, offset, count), isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, isLast, _cancelToken).ConfigureAwait(false); | |||
await _client.SendAsync(new ArraySegment<byte>(data, index, count), isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, isLast, _cancelToken).ConfigureAwait(false); | |||
} | |||
catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) | |||
{ | |||
@@ -139,11 +139,11 @@ namespace Discord.Net.WebSockets | |||
var array = stream.ToArray(); | |||
if (result.MessageType == WebSocketMessageType.Binary) | |||
await BinaryMessage.Raise(new BinaryMessageEventArgs(array)).ConfigureAwait(false); | |||
await BinaryMessage.Raise(array, 0, array.Length).ConfigureAwait(false); | |||
else if (result.MessageType == WebSocketMessageType.Text) | |||
{ | |||
string text = Encoding.UTF8.GetString(array, 0, array.Length); | |||
await TextMessage.Raise(new TextMessageEventArgs(text)).ConfigureAwait(false); | |||
await TextMessage.Raise(text).ConfigureAwait(false); | |||
} | |||
stream.Position = 0; | |||
@@ -4,11 +4,10 @@ using System.Threading.Tasks; | |||
namespace Discord.Net.WebSockets | |||
{ | |||
//TODO: Add ETF | |||
public interface IWebSocketClient | |||
{ | |||
event Func<BinaryMessageEventArgs, Task> BinaryMessage; | |||
event Func<TextMessageEventArgs, Task> TextMessage; | |||
event Func<byte[], int, int, Task> BinaryMessage; | |||
event Func<string, Task> TextMessage; | |||
void SetHeader(string key, string value); | |||
void SetCancelToken(CancellationToken cancelToken); | |||
@@ -16,6 +15,6 @@ namespace Discord.Net.WebSockets | |||
Task Connect(string host); | |||
Task Disconnect(); | |||
Task Send(byte[] data, int offset, int length, bool isText); | |||
Task Send(byte[] data, int index, int count, bool isText); | |||
} | |||
} |
@@ -1,11 +0,0 @@ | |||
using System; | |||
namespace Discord.Net.WebSockets | |||
{ | |||
public class TextMessageEventArgs : EventArgs | |||
{ | |||
public string Message { get; } | |||
public TextMessageEventArgs(string msg) { Message = msg; } | |||
} | |||
} |
@@ -17,7 +17,7 @@ namespace Discord.Rest | |||
//TODO: Log Logins/Logouts | |||
public sealed class DiscordClient : IDiscordClient, IDisposable | |||
{ | |||
public event Func<LogMessageEventArgs, Task> Log; | |||
public event Func<LogMessage, Task> Log; | |||
public event Func<Task> LoggedIn, LoggedOut; | |||
private readonly Logger _discordLogger, _restLogger; | |||
@@ -39,7 +39,7 @@ namespace Discord.Rest | |||
config = new DiscordConfig(); | |||
_log = new LogManager(config.LogLevel); | |||
_log.Message += async e => await Log.Raise(e).ConfigureAwait(false); | |||
_log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); | |||
_discordLogger = _log.CreateLogger("Discord"); | |||
_restLogger = _log.CreateLogger("Rest"); | |||
@@ -47,7 +47,7 @@ namespace Discord.Rest | |||
_requestQueue = new RequestQueue(); | |||
ApiClient = new API.DiscordApiClient(config.RestClientProvider, requestQueue: _requestQueue); | |||
ApiClient.SentRequest += async e => await _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms").ConfigureAwait(false); | |||
ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); | |||
} | |||
public async Task Login(string email, string password) | |||
@@ -24,6 +24,10 @@ namespace Discord.WebSocket | |||
public string Username { get; private set; } | |||
/// <inheritdoc /> | |||
public DMChannel DMChannel { get; internal set; } | |||
/// <inheritdoc /> | |||
public Game? CurrentGame { get; internal set; } | |||
/// <inheritdoc /> | |||
public UserStatus Status { get; internal set; } | |||
/// <inheritdoc /> | |||
public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); | |||
@@ -65,11 +69,6 @@ namespace Discord.WebSocket | |||
public override string ToString() => $"{Username}#{Discriminator}"; | |||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; | |||
/// <inheritdoc /> | |||
Game? IUser.CurrentGame => null; | |||
/// <inheritdoc /> | |||
UserStatus IUser.Status => UserStatus.Unknown; | |||
/// <inheritdoc /> | |||
async Task<IDMChannel> IUser.CreateDMChannel() | |||
=> await CreateDMChannel().ConfigureAwait(false); | |||