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