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