diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 2f285030d..dee9fddf0 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -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 SentRequest; - + public event Func SentRequest; + public event Func SentGatewayMessage; + public event Func 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(reader.ReadToEnd()); + await ReceivedGatewayEvent.Raise((GatewayOpCodes)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + } + } + }; + _gatewayClient.TextMessage += async text => + { + var msg = JsonConvert.DeserializeObject(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 Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false)); + => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false)); public async Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, payload, false, bucket).ConfigureAwait(false)); + => DeserializeJson(await SendInternal(method, endpoint, payload, false, bucket).ConfigureAwait(false)); public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, multipartArgs, false, bucket).ConfigureAwait(false)); + => DeserializeJson(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 Send(string method, string endpoint, GuildBucket bucket, ulong guildId) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, null, false, bucket, guildId).ConfigureAwait(false)); + => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket, guildId).ConfigureAwait(false)); public async Task Send(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, payload, false, bucket, guildId).ConfigureAwait(false)); + => DeserializeJson(await SendInternal(method, endpoint, payload, false, bucket, guildId).ConfigureAwait(false)); public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).ConfigureAwait(false)); - + => DeserializeJson(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).ConfigureAwait(false)); + private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, GlobalBucket bucket) => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Global, (int)bucket, 0); private Task 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("GET", "gateway").ConfigureAwait(false); } + public async Task SendIdentify(int largeThreshold = 100, bool useCompression = true) + { + var props = new Dictionary + { + ["$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 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(Stream jsonStream) + private T DeserializeJson(Stream jsonStream) { using (TextReader text = new StreamReader(jsonStream)) using (JsonReader reader = new JsonTextReader(text)) diff --git a/src/Discord.Net/API/DiscordAPISocketClient.cs b/src/Discord.Net/API/DiscordAPISocketClient.cs deleted file mode 100644 index fcff89923..000000000 --- a/src/Discord.Net/API/DiscordAPISocketClient.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.API -{ - public class DiscordAPISocketClient - { - } -} diff --git a/src/Discord.Net/API/Gateway/OpCodes.cs b/src/Discord.Net/API/Gateway/GatewayOpCodes.cs similarity index 96% rename from src/Discord.Net/API/Gateway/OpCodes.cs rename to src/Discord.Net/API/Gateway/GatewayOpCodes.cs index cf237bd03..82fbf51f3 100644 --- a/src/Discord.Net/API/Gateway/OpCodes.cs +++ b/src/Discord.Net/API/Gateway/GatewayOpCodes.cs @@ -1,6 +1,6 @@ namespace Discord.API.Gateway { - public enum OpCodes : byte + public enum GatewayOpCodes : byte { /// C←S - Used to send most events. Dispatch = 0, diff --git a/src/Discord.Net/API/Gateway/IdentifyParams.cs b/src/Discord.Net/API/Gateway/IdentifyParams.cs index 96c45927a..8338e6e14 100644 --- a/src/Discord.Net/API/Gateway/IdentifyParams.cs +++ b/src/Discord.Net/API/Gateway/IdentifyParams.cs @@ -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; } diff --git a/src/Discord.Net/API/Gateway/RequestMembersParams.cs b/src/Discord.Net/API/Gateway/RequestMembersParams.cs index 16077939d..ed6edc6ef 100644 --- a/src/Discord.Net/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net/API/Gateway/RequestMembersParams.cs @@ -2,7 +2,7 @@ namespace Discord.API.Gateway { - public class RequestMembersCommand + public class RequestMembersParams { [JsonProperty("guild_id")] public ulong[] GuildId { get; set; } diff --git a/src/Discord.Net/API/Gateway/ResumeParams.cs b/src/Discord.Net/API/Gateway/ResumeParams.cs index c0fb296f4..ba4489336 100644 --- a/src/Discord.Net/API/Gateway/ResumeParams.cs +++ b/src/Discord.Net/API/Gateway/ResumeParams.cs @@ -2,7 +2,7 @@ namespace Discord.API.Gateway { - public class ResumeCommand + public class ResumeParams { [JsonProperty("session_id")] public string SessionId { get; set; } diff --git a/src/Discord.Net/API/Gateway/UpdateStatusParams.cs b/src/Discord.Net/API/Gateway/UpdateStatusParams.cs index e673cb51e..99e5ed7b8 100644 --- a/src/Discord.Net/API/Gateway/UpdateStatusParams.cs +++ b/src/Discord.Net/API/Gateway/UpdateStatusParams.cs @@ -2,7 +2,7 @@ namespace Discord.API.Gateway { - public class UpdateStatusCommand + public class UpdateStatusParams { [JsonProperty("idle_since")] public long? IdleSince { get; set; } diff --git a/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs b/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs index 689394e5d..d72d63548 100644 --- a/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs +++ b/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs @@ -2,7 +2,7 @@ namespace Discord.API.Gateway { - public class UpdateVoiceCommand + public class UpdateVoiceParams { [JsonProperty("guild_id")] public ulong? GuildId { get; set; } diff --git a/src/Discord.Net/API/Voice/VoiceOpCodes.cs b/src/Discord.Net/API/Voice/VoiceOpCodes.cs new file mode 100644 index 000000000..73c7eda0c --- /dev/null +++ b/src/Discord.Net/API/Voice/VoiceOpCodes.cs @@ -0,0 +1,18 @@ +namespace Discord.API.Gateway +{ + public enum VoiceOpCodes : byte + { + /// C→S - Used to associate a connection with a token. + Identify = 0, + /// C→S - Used to specify configuration. + SelectProtocol = 1, + /// C←S - Used to notify that the voice connection was successful and informs the client of available protocols. + Ready = 2, + /// C↔S - Used to keep the connection alive and measure latency. + Heartbeat = 3, + /// C←S - Used to provide an encryption key to the client. + SessionDescription = 4, + /// C↔S - Used to inform that a certain user is speaking. + Speaking = 5 + } +} diff --git a/src/Discord.Net/API/WebSocketMessage.cs b/src/Discord.Net/API/WebSocketMessage.cs index 285f5e13f..19ec2ac41 100644 --- a/src/Discord.Net/API/WebSocketMessage.cs +++ b/src/Discord.Net/API/WebSocketMessage.cs @@ -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; } } } diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index 50847dbb3..35e6f010b 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -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/"; diff --git a/src/Discord.Net/EventExtensions.cs b/src/Discord.Net/EventExtensions.cs index f961866d1..b46cb9056 100644 --- a/src/Discord.Net/EventExtensions.cs +++ b/src/Discord.Net/EventExtensions.cs @@ -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 eventHandler) { var subscriptions = eventHandler?.GetInvocationList(); @@ -32,7 +33,7 @@ namespace Discord await (subscriptions[i] as Func).Invoke(arg1, arg2).ConfigureAwait(false); } } - public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3) + public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3) { var subscriptions = eventHandler?.GetInvocationList(); if (subscriptions != null) diff --git a/src/Discord.Net/Events/SentRequestEventArgs.cs b/src/Discord.Net/Events/SentRequestEventArgs.cs deleted file mode 100644 index c62c4d917..000000000 --- a/src/Discord.Net/Events/SentRequestEventArgs.cs +++ /dev/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; - } - } -} diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index bcbfbd20a..5e5d819b7 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -7,7 +7,7 @@ namespace Discord.Logging { public LogSeverity Level { get; } - public event Func Message; + public event Func 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) diff --git a/src/Discord.Net/Events/LogMessageEventArgs.cs b/src/Discord.Net/Logging/LogMessage.cs similarity index 91% rename from src/Discord.Net/Events/LogMessageEventArgs.cs rename to src/Discord.Net/Logging/LogMessage.cs index dd8c885ee..14bc4e263 100644 --- a/src/Discord.Net/Events/LogMessageEventArgs.cs +++ b/src/Discord.Net/Logging/LogMessage.cs @@ -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; diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs index 274655948..74435e012 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net/Logging/Logger.cs @@ -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); } } diff --git a/src/Discord.Net/Net/Queue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs index 91ad91711..365ebfb68 100644 --- a/src/Discord.Net/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -12,7 +12,7 @@ namespace Discord.Net.Queue private readonly RequestQueueBucket[] _globalBuckets; private readonly Dictionary[] _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[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 Send(IQueuedRequest request, BucketGroup group, int bucketId, ulong guildId) + + internal Task Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) + { + request.CancelToken = _cancelToken; + return Send(request as IQueuedRequest, group, bucketId, guildId); + } + internal Task Send(WebSocketRequest request, BucketGroup group, int bucketId, ulong guildId) + { + request.CancelToken = _cancelToken; + return Send(request as IQueuedRequest, group, bucketId, guildId); + } + private async Task 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; } diff --git a/src/Discord.Net/Net/Queue/RestRequest.cs b/src/Discord.Net/Net/Queue/RestRequest.cs index 179ce6423..7c71d114a 100644 --- a/src/Discord.Net/Net/Queue/RestRequest.cs +++ b/src/Discord.Net/Net/Queue/RestRequest.cs @@ -15,7 +15,7 @@ namespace Discord.Net.Queue public bool HeaderOnly { get; } public IReadOnlyDictionary MultipartParams { get; } public TaskCompletionSource Promise { get; } - public CancellationToken CancelToken { get; internal set; } + public CancellationToken CancelToken { get; set; } public bool IsMultipart => MultipartParams != null; diff --git a/src/Discord.Net/Net/Queue/WebSocketRequest.cs b/src/Discord.Net/Net/Queue/WebSocketRequest.cs index 47a00ad68..bd8f492c3 100644 --- a/src/Discord.Net/Net/Queue/WebSocketRequest.cs +++ b/src/Discord.Net/Net/Queue/WebSocketRequest.cs @@ -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 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(); } public async Task Send() { - await Client.Send(Data, Offset, Bytes, IsText).ConfigureAwait(false); + await Client.Send(Data, DataIndex, DataCount, IsText).ConfigureAwait(false); return null; } } diff --git a/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs b/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs deleted file mode 100644 index 3fd4425fa..000000000 --- a/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord.Net.WebSockets -{ - public class BinaryMessageEventArgs : EventArgs - { - public byte[] Data { get; } - - public BinaryMessageEventArgs(byte[] data) { } - } -} diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index e9ae38f55..d9559a2cf 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -14,8 +14,8 @@ namespace Discord.Net.WebSockets public const int SendChunkSize = 4 * 1024; //4KB private const int HR_TIMEOUT = -2147012894; - public event Func BinaryMessage; - public event Func TextMessage; + public event Func BinaryMessage; + public event Func 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(data, offset, count), isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, isLast, _cancelToken).ConfigureAwait(false); + await _client.SendAsync(new ArraySegment(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; diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs index 20cfd0da8..2925c1350 100644 --- a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs @@ -4,11 +4,10 @@ using System.Threading.Tasks; namespace Discord.Net.WebSockets { - //TODO: Add ETF public interface IWebSocketClient { - event Func BinaryMessage; - event Func TextMessage; + event Func BinaryMessage; + event Func 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); } } diff --git a/src/Discord.Net/Net/WebSockets/TextMessageEventArgs.cs b/src/Discord.Net/Net/WebSockets/TextMessageEventArgs.cs deleted file mode 100644 index e4e186044..000000000 --- a/src/Discord.Net/Net/WebSockets/TextMessageEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord.Net.WebSockets -{ - public class TextMessageEventArgs : EventArgs - { - public string Message { get; } - - public TextMessageEventArgs(string msg) { Message = msg; } - } -} diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index 992b52c92..89475ca93 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/Rest/DiscordClient.cs @@ -17,7 +17,7 @@ namespace Discord.Rest //TODO: Log Logins/Logouts public sealed class DiscordClient : IDiscordClient, IDisposable { - public event Func Log; + public event Func Log; public event Func 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) diff --git a/src/Discord.Net/WebSocket/DiscordClient.cs b/src/Discord.Net/WebSocket/DiscordClient.cs index 2639c9389..271248def 100644 --- a/src/Discord.Net/WebSocket/DiscordClient.cs +++ b/src/Discord.Net/WebSocket/DiscordClient.cs @@ -8,6 +8,7 @@ using Discord.Net.Queue; using Discord.Net.WebSockets; using Discord.WebSocket.Data; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -25,7 +26,7 @@ namespace Discord.WebSocket //TODO: Do a final namespace and file structure review public sealed class DiscordClient : IDiscordClient, IDisposable { - public event Func Log; + public event Func Log; public event Func LoggedIn, LoggedOut; public event Func Connected, Disconnected; //public event Func VoiceConnected, VoiceDisconnected; @@ -42,7 +43,7 @@ namespace Discord.WebSocket public event Func UserIsTyping; private readonly ConcurrentQueue _largeGuilds; - private readonly Logger _discordLogger, _gatewayLogger; + private readonly Logger _discordLogger, _restLogger, _gatewayLogger; private readonly SemaphoreSlim _connectionLock; private readonly DataStoreProvider _dataStoreProvider; private readonly LogManager _log; @@ -90,8 +91,9 @@ namespace Discord.WebSocket _largeThreshold = config.LargeThreshold; _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"); _gatewayLogger = _log.CreateLogger("Gateway"); _connectionLock = new SemaphoreSlim(1, 1); @@ -99,21 +101,10 @@ namespace Discord.WebSocket _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; ApiClient = new API.DiscordApiClient(config.RestClientProvider, config.WebSocketProvider, _serializer, _requestQueue); - ApiClient.SentRequest += async e => await _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); + ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.Verbose($"{method} {endpoint}: {millis} ms"); + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {opCode}"); + ApiClient.ReceivedGatewayEvent += ProcessMessage; GatewaySocket = config.WebSocketProvider(); - GatewaySocket.BinaryMessage += async e => - { - using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 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)) - await ProcessMessage(reader.ReadToEnd()).ConfigureAwait(false); - } - }; - GatewaySocket.TextMessage += async e => await ProcessMessage(e.Message).ConfigureAwait(false); _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); @@ -169,11 +160,8 @@ namespace Discord.WebSocket try { await ApiClient.ValidateToken().ConfigureAwait(false); - var gateway = await ApiClient.GetGateway(); var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false); _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); - - await GatewaySocket.Connect(gateway.Url).ConfigureAwait(false); } catch (HttpException ex) { @@ -325,543 +313,548 @@ namespace Discord.WebSocket return null; } - private async Task ProcessMessage(string json) + private async Task ProcessMessage(GatewayOpCodes opCode, string type, JToken payload) { - var msg = JsonConvert.DeserializeObject(json); try { - switch (msg.Type) + switch (opCode) { - //Global - case "READY": + case GatewayOpCodes.Dispatch: + switch (type) { - //TODO: Store guilds even if they're unavailable - //TODO: Make downloading large guilds optional - - var data = msg.Payload.ToObject(_serializer); - var store = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - - _sessionId = data.SessionId; - var currentUser = new SelfUser(this, data.User); - store.AddUser(currentUser); + //Global + case "READY": + { + //TODO: Store guilds even if they're unavailable + //TODO: Make downloading large guilds optional + //TODO: Add support for unavailable guilds - for (int i = 0; i < data.Guilds.Length; i++) - { - var model = data.Guilds[i]; - var guild = new Guild(this, model); - store.AddGuild(guild); + var data = payload.ToObject(_serializer); + var store = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - foreach (var channel in guild.Channels) - store.AddChannel(channel); + _sessionId = data.SessionId; + var currentUser = new SelfUser(this, data.User); + store.AddUser(currentUser); - /*if (model.IsLarge) - _largeGuilds.Enqueue(model.Id);*/ - } + for (int i = 0; i < data.Guilds.Length; i++) + { + var model = data.Guilds[i]; + var guild = new Guild(this, model); + store.AddGuild(guild); - for (int i = 0; i < data.PrivateChannels.Length; i++) - { - var model = data.PrivateChannels[i]; - var recipient = new User(this, model.Recipient); - var channel = new DMChannel(this, recipient, model); + foreach (var channel in guild.Channels) + store.AddChannel(channel); - recipient.DMChannel = channel; - store.AddChannel(channel); - } + /*if (model.IsLarge) + _largeGuilds.Enqueue(model.Id);*/ + } - CurrentUser = currentUser; - DataStore = store; - } - break; + for (int i = 0; i < data.PrivateChannels.Length; i++) + { + var model = data.PrivateChannels[i]; + var recipient = new User(this, model.Recipient); + var channel = new DMChannel(this, recipient, model); - //Servers - case "GUILD_CREATE": - { - /*var data = msg.Payload.ToObject(Serializer); - if (data.Unavailable != true) - { - var server = AddServer(data.Id); - server.Update(data); + recipient.DMChannel = channel; + store.AddChannel(channel); + } - if (data.Unavailable != false) - { - _gatewayLogger.Info($"GUILD_CREATE: {server.Path}"); - JoinedServer.Raise(server); + CurrentUser = currentUser; + DataStore = store; } - else - _gatewayLogger.Info($"GUILD_AVAILABLE: {server.Path}"); - - if (!data.IsLarge) - await GuildAvailable.Raise(server); - else - _largeServers.Enqueue(data.Id); - }*/ - } - break; - case "GUILD_UPDATE": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.Id); - if (server != null) - { - var before = Config.EnablePreUpdateEvents ? server.Clone() : null; - server.Update(data); - _gatewayLogger.Info($"GUILD_UPDATE: {server.Path}"); - await GuildUpdated.Raise(before, server); - } - else - _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild.");*/ - } - break; - case "GUILD_DELETE": - { - /*var data = msg.Payload.ToObject(Serializer); - Server server = RemoveServer(data.Id); - if (server != null) - { - if (data.Unavailable != true) - _gatewayLogger.Info($"GUILD_DELETE: {server.Path}"); - else - _gatewayLogger.Info($"GUILD_UNAVAILABLE: {server.Path}"); - - OnServerUnavailable(server); - if (data.Unavailable != true) - OnLeftServer(server); - } - else - _gatewayLogger.Warning("GUILD_DELETE referenced an unknown guild.");*/ - } - break; - - //Channels - case "CHANNEL_CREATE": - { - /*var data = msg.Payload.ToObject(Serializer); - - Channel channel = null; - if (data.GuildId != null) - { - var server = GetServer(data.GuildId.Value); - if (server != null) - channel = server.AddChannel(data.Id, true); - else - _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); - } - else - channel = AddPrivateChannel(data.Id, data.Recipient.Id); - if (channel != null) - { - channel.Update(data); - _gatewayLogger.Info($"CHANNEL_CREATE: {channel.Path}"); - ChannelCreated.Raise(new ChannelEventArgs(channel)); - }*/ - } - break; - case "CHANNEL_UPDATE": - { - /*var data = msg.Payload.ToObject(Serializer); - var channel = GetChannel(data.Id); - if (channel != null) - { - var before = Config.EnablePreUpdateEvents ? channel.Clone() : null; - channel.Update(data); - _gateway_gatewayLogger.Info($"CHANNEL_UPDATE: {channel.Path}"); - OnChannelUpdated(before, channel); - } - else - _gateway_gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel.");*/ - } - break; - case "CHANNEL_DELETE": - { - /*var data = msg.Payload.ToObject(Serializer); - var channel = RemoveChannel(data.Id); - if (channel != null) - { - _gateway_gatewayLogger.Info($"CHANNEL_DELETE: {channel.Path}"); - OnChannelDestroyed(channel); - } - else - _gateway_gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel.");*/ - } - break; + break; - //Members - case "GUILD_MEMBER_ADD": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.AddUser(data.User.Id, true, true); - user.Update(data); - user.UpdateActivity(); - _gatewayLogger.Info($"GUILD_MEMBER_ADD: {user.Path}"); - OnUserJoined(user); - } - else - _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild.");*/ - } - break; - case "GUILD_MEMBER_UPDATE": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.GetUser(data.User.Id); - if (user != null) + //Servers + case "GUILD_CREATE": { - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - _gatewayLogger.Info($"GUILD_MEMBER_UPDATE: {user.Path}"); - OnUserUpdated(before, user); + /*var data = msg.Payload.ToObject(Serializer); + if (data.Unavailable != true) + { + var server = AddServer(data.Id); + server.Update(data); + + if (data.Unavailable != false) + { + _gatewayLogger.Info($"GUILD_CREATE: {server.Path}"); + JoinedServer.Raise(server); + } + else + _gatewayLogger.Info($"GUILD_AVAILABLE: {server.Path}"); + + if (!data.IsLarge) + await GuildAvailable.Raise(server); + else + _largeServers.Enqueue(data.Id); + }*/ } - else - _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); - } - else - _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild.");*/ - } - break; - case "GUILD_MEMBER_REMOVE": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.RemoveUser(data.User.Id); - if (user != null) + break; + case "GUILD_UPDATE": { - _gatewayLogger.Info($"GUILD_MEMBER_REMOVE: {user.Path}"); - OnUserLeft(user); + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.Id); + if (server != null) + { + var before = Config.EnablePreUpdateEvents ? server.Clone() : null; + server.Update(data); + _gatewayLogger.Info($"GUILD_UPDATE: {server.Path}"); + await GuildUpdated.Raise(before, server); + } + else + _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild.");*/ } - else - _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); - } - else - _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild.");*/ - } - break; - case "GUILD_MEMBERS_CHUNK": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - foreach (var memberData in data.Members) + break; + case "GUILD_DELETE": { - var user = server.AddUser(memberData.User.Id, true, false); - user.Update(memberData); + /*var data = msg.Payload.ToObject(Serializer); + Server server = RemoveServer(data.Id); + if (server != null) + { + if (data.Unavailable != true) + _gatewayLogger.Info($"GUILD_DELETE: {server.Path}"); + else + _gatewayLogger.Info($"GUILD_UNAVAILABLE: {server.Path}"); + + OnServerUnavailable(server); + if (data.Unavailable != true) + OnLeftServer(server); + } + else + _gatewayLogger.Warning("GUILD_DELETE referenced an unknown guild.");*/ } - _gateway_gatewayLogger.Verbose($"GUILD_MEMBERS_CHUNK: {data.Members.Length} users"); + break; - if (server.CurrentUserCount >= server.UserCount) //Finished downloading for there - OnServerAvailable(server); - } - else - _gateway_gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild.");*/ - } - break; + //Channels + case "CHANNEL_CREATE": + { + /*var data = msg.Payload.ToObject(Serializer); - //Roles - case "GUILD_ROLE_CREATE": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.AddRole(data.Data.Id); - role.Update(data.Data, false); - _gateway_gatewayLogger.Info($"GUILD_ROLE_CREATE: {role.Path}"); - OnRoleCreated(role); - } - else - _gateway_gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild.");*/ - } - break; - case "GUILD_ROLE_UPDATE": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.GetRole(data.Data.Id); - if (role != null) + Channel channel = null; + if (data.GuildId != null) + { + var server = GetServer(data.GuildId.Value); + if (server != null) + channel = server.AddChannel(data.Id, true); + else + _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); + } + else + channel = AddPrivateChannel(data.Id, data.Recipient.Id); + if (channel != null) + { + channel.Update(data); + _gatewayLogger.Info($"CHANNEL_CREATE: {channel.Path}"); + ChannelCreated.Raise(channel); + }*/ + } + break; + case "CHANNEL_UPDATE": { - var before = Config.EnablePreUpdateEvents ? role.Clone() : null; - role.Update(data.Data, true); - _gateway_gatewayLogger.Info($"GUILD_ROLE_UPDATE: {role.Path}"); - OnRoleUpdated(before, role); + /*var data = msg.Payload.ToObject(Serializer); + var channel = GetChannel(data.Id); + if (channel != null) + { + var before = Config.EnablePreUpdateEvents ? channel.Clone() : null; + channel.Update(data); + _gateway_gatewayLogger.Info($"CHANNEL_UPDATE: {channel.Path}"); + OnChannelUpdated(before, channel); + } + else + _gateway_gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel.");*/ } - else - _gateway_gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); - } - else - _gateway_gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild.");*/ - } - break; - case "GUILD_ROLE_DELETE": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.RemoveRole(data.RoleId); - if (role != null) + break; + case "CHANNEL_DELETE": { - _gateway_gatewayLogger.Info($"GUILD_ROLE_DELETE: {role.Path}"); - OnRoleDeleted(role); + /*var data = msg.Payload.ToObject(Serializer); + var channel = RemoveChannel(data.Id); + if (channel != null) + { + _gateway_gatewayLogger.Info($"CHANNEL_DELETE: {channel.Path}"); + OnChannelDestroyed(channel); + } + else + _gateway_gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel.");*/ } - else - _gateway_gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); - } - else - _gateway_gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild.");*/ - } - break; + break; - //Bans - case "GUILD_BAN_ADD": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.GetUser(data.User.Id); - if (user != null) + //Members + case "GUILD_MEMBER_ADD": { - _gateway_gatewayLogger.Info($"GUILD_BAN_ADD: {user.Path}"); - OnUserBanned(user); + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.AddUser(data.User.Id, true, true); + user.Update(data); + user.UpdateActivity(); + _gatewayLogger.Info($"GUILD_MEMBER_ADD: {user.Path}"); + OnUserJoined(user); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild.");*/ } - else - _gateway_gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown user."); - } - else - _gateway_gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild.");*/ - } - break; - case "GUILD_BAN_REMOVE": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = new User(this, data.User.Id, server); - user.Update(data.User); - _gateway_gatewayLogger.Info($"GUILD_BAN_REMOVE: {user.Path}"); - OnUserUnbanned(user); - } - else - _gateway_gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild.");*/ - } - break; - - //Messages - case "MESSAGE_CREATE": - { - /*var data = msg.Payload.ToObject(Serializer); - - Channel channel = GetChannel(data.ChannelId); - if (channel != null) - { - var user = channel.GetUserFast(data.Author.Id); + break; + case "GUILD_MEMBER_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.GetUser(data.User.Id); + if (user != null) + { + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + _gatewayLogger.Info($"GUILD_MEMBER_UPDATE: {user.Path}"); + OnUserUpdated(before, user); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild.");*/ + } + break; + case "GUILD_MEMBER_REMOVE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.RemoveUser(data.User.Id); + if (user != null) + { + _gatewayLogger.Info($"GUILD_MEMBER_REMOVE: {user.Path}"); + OnUserLeft(user); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild.");*/ + } + break; + case "GUILD_MEMBERS_CHUNK": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + foreach (var memberData in data.Members) + { + var user = server.AddUser(memberData.User.Id, true, false); + user.Update(memberData); + } + _gateway_gatewayLogger.Verbose($"GUILD_MEMBERS_CHUNK: {data.Members.Length} users"); + + if (server.CurrentUserCount >= server.UserCount) //Finished downloading for there + OnServerAvailable(server); + } + else + _gateway_gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild.");*/ + } + break; - if (user != null) + //Roles + case "GUILD_ROLE_CREATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var role = server.AddRole(data.Data.Id); + role.Update(data.Data, false); + _gateway_gatewayLogger.Info($"GUILD_ROLE_CREATE: {role.Path}"); + OnRoleCreated(role); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild.");*/ + } + break; + case "GUILD_ROLE_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var role = server.GetRole(data.Data.Id); + if (role != null) + { + var before = Config.EnablePreUpdateEvents ? role.Clone() : null; + role.Update(data.Data, true); + _gateway_gatewayLogger.Info($"GUILD_ROLE_UPDATE: {role.Path}"); + OnRoleUpdated(before, role); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild.");*/ + } + break; + case "GUILD_ROLE_DELETE": { - Message msg = null; - bool isAuthor = data.Author.Id == CurrentUser.Id; - //ulong nonce = 0; - - //if (data.Author.Id == _privateUser.Id && Config.UseMessageQueue) - //{ - // if (data.Nonce != null && ulong.TryParse(data.Nonce, out nonce)) - // msg = _messages[nonce]; - //} - if (msg == null) + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) { - msg = channel.AddMessage(data.Id, user, data.Timestamp.Value); - //nonce = 0; + var role = server.RemoveRole(data.RoleId); + if (role != null) + { + _gateway_gatewayLogger.Info($"GUILD_ROLE_DELETE: {role.Path}"); + OnRoleDeleted(role); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild.");*/ + } + break; - //Remapped queued message - //if (nonce != 0) - //{ - // msg = _messages.Remap(nonce, data.Id); - // msg.Id = data.Id; - // RaiseMessageSent(msg); - //} + //Bans + case "GUILD_BAN_ADD": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.GetUser(data.User.Id); + if (user != null) + { + _gateway_gatewayLogger.Info($"GUILD_BAN_ADD: {user.Path}"); + OnUserBanned(user); + } + else + _gateway_gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown user."); + } + else + _gateway_gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild.");*/ + } + break; + case "GUILD_BAN_REMOVE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = new User(this, data.User.Id, server); + user.Update(data.User); + _gateway_gatewayLogger.Info($"GUILD_BAN_REMOVE: {user.Path}"); + OnUserUnbanned(user); + } + else + _gateway_gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild.");*/ + } + break; - msg.Update(data); - user.UpdateActivity(); + //Messages + case "MESSAGE_CREATE": + { + /*var data = msg.Payload.ToObject(Serializer); - _gateway_gatewayLogger.Verbose($"MESSAGE_CREATE: {channel.Path} ({user.Name ?? "Unknown"})"); - OnMessageReceived(msg); + Channel channel = GetChannel(data.ChannelId); + if (channel != null) + { + var user = channel.GetUserFast(data.Author.Id); + + if (user != null) + { + Message msg = null; + bool isAuthor = data.Author.Id == CurrentUser.Id; + //ulong nonce = 0; + + //if (data.Author.Id == _privateUser.Id && Config.UseMessageQueue) + //{ + // if (data.Nonce != null && ulong.TryParse(data.Nonce, out nonce)) + // msg = _messages[nonce]; + //} + if (msg == null) + { + msg = channel.AddMessage(data.Id, user, data.Timestamp.Value); + //nonce = 0; + } + + //Remapped queued message + //if (nonce != 0) + //{ + // msg = _messages.Remap(nonce, data.Id); + // msg.Id = data.Id; + // RaiseMessageSent(msg); + //} + + msg.Update(data); + user.UpdateActivity(); + + _gateway_gatewayLogger.Verbose($"MESSAGE_CREATE: {channel.Path} ({user.Name ?? "Unknown"})"); + OnMessageReceived(msg); + } + else + _gateway_gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); + } + else + _gateway_gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel.");*/ } - else - _gateway_gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); - } - else - _gateway_gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel.");*/ - } - break; - case "MESSAGE_UPDATE": - { - /*var data = msg.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId); - if (channel != null) - { - var msg = channel.GetMessage(data.Id, data.Author?.Id); - var before = Config.EnablePreUpdateEvents ? msg.Clone() : null; - msg.Update(data); - _gatewayLogger.Verbose($"MESSAGE_UPDATE: {channel.Path} ({data.Author?.Username ?? "Unknown"})"); - OnMessageUpdated(before, msg); - } - else - _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel.");*/ - } - break; - case "MESSAGE_DELETE": - { - /*var data = msg.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId); - if (channel != null) - { - var msg = channel.RemoveMessage(data.Id); - _gatewayLogger.Verbose($"MESSAGE_DELETE: {channel.Path} ({msg.User?.Name ?? "Unknown"})"); - OnMessageDeleted(msg); - } - else - _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel.");*/ - } - break; - - //Statuses - case "PRESENCE_UPDATE": - { - /*var data = msg.Payload.ToObject(Serializer); - User user; - Server server; - if (data.GuildId == null) - { - server = null; - user = GetPrivateChannel(data.User.Id)?.Recipient; - } - else - { - server = GetServer(data.GuildId.Value); - if (server == null) + break; + case "MESSAGE_UPDATE": { - _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown server."); - break; + /*var data = msg.Payload.ToObject(Serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { + var msg = channel.GetMessage(data.Id, data.Author?.Id); + var before = Config.EnablePreUpdateEvents ? msg.Clone() : null; + msg.Update(data); + _gatewayLogger.Verbose($"MESSAGE_UPDATE: {channel.Path} ({data.Author?.Username ?? "Unknown"})"); + OnMessageUpdated(before, msg); + } + else + _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel.");*/ } - else - user = server.GetUser(data.User.Id); - } - - if (user != null) - { - if (Config.LogLevel == LogSeverity.Debug) - _gatewayLogger.Debug($"PRESENCE_UPDATE: {user.Path}"); - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - OnUserUpdated(before, user); - } - //else //Occurs when a user leaves a server - // _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown user.");*/ - } - break; - case "TYPING_START": - { - /*var data = msg.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId); - if (channel != null) - { - User user; - if (channel.IsPrivate) + break; + case "MESSAGE_DELETE": { - if (channel.Recipient.Id == data.UserId) - user = channel.Recipient; + /*var data = msg.Payload.ToObject(Serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { + var msg = channel.RemoveMessage(data.Id); + _gatewayLogger.Verbose($"MESSAGE_DELETE: {channel.Path} ({msg.User?.Name ?? "Unknown"})"); + OnMessageDeleted(msg); + } else - break; + _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel.");*/ } - else - user = channel.Server.GetUser(data.UserId); - if (user != null) + break; + + //Statuses + case "PRESENCE_UPDATE": { - if (Config.LogLevel == LogSeverity.Debug) - _gatewayLogger.Debug($"TYPING_START: {channel.Path} ({user.Name})"); - OnUserIsTypingUpdated(channel, user); - user.UpdateActivity(); + /*var data = msg.Payload.ToObject(Serializer); + User user; + Server server; + if (data.GuildId == null) + { + server = null; + user = GetPrivateChannel(data.User.Id)?.Recipient; + } + else + { + server = GetServer(data.GuildId.Value); + if (server == null) + { + _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown server."); + break; + } + else + user = server.GetUser(data.User.Id); + } + + if (user != null) + { + if (Config.LogLevel == LogSeverity.Debug) + _gatewayLogger.Debug($"PRESENCE_UPDATE: {user.Path}"); + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + OnUserUpdated(before, user); + } + //else //Occurs when a user leaves a server + // _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown user.");*/ } - } - else - _gatewayLogger.Warning("TYPING_START referenced an unknown channel.");*/ - } - break; + break; + case "TYPING_START": + { + /*var data = msg.Payload.ToObject(Serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { + User user; + if (channel.IsPrivate) + { + if (channel.Recipient.Id == data.UserId) + user = channel.Recipient; + else + break; + } + else + user = channel.Server.GetUser(data.UserId); + if (user != null) + { + if (Config.LogLevel == LogSeverity.Debug) + _gatewayLogger.Debug($"TYPING_START: {channel.Path} ({user.Name})"); + OnUserIsTypingUpdated(channel, user); + user.UpdateActivity(); + } + } + else + _gatewayLogger.Warning("TYPING_START referenced an unknown channel.");*/ + } + break; - //Voice - case "VOICE_STATE_UPDATE": - { - /*var data = msg.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var user = server.GetUser(data.UserId); - if (user != null) + //Voice + case "VOICE_STATE_UPDATE": { - if (Config.LogLevel == LogSeverity.Debug) - _gatewayLogger.Debug($"VOICE_STATE_UPDATE: {user.Path}"); - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - //_gatewayLogger.Verbose($"Voice Updated: {server.Name}/{user.Name}"); - OnUserUpdated(before, user); + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var user = server.GetUser(data.UserId); + if (user != null) + { + if (Config.LogLevel == LogSeverity.Debug) + _gatewayLogger.Debug($"VOICE_STATE_UPDATE: {user.Path}"); + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + //_gatewayLogger.Verbose($"Voice Updated: {server.Name}/{user.Name}"); + OnUserUpdated(before, user); + } + //else //Occurs when a user leaves a server + // _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown user."); + } + else + _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown server.");*/ } - //else //Occurs when a user leaves a server - // _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown user."); - } - else - _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown server.");*/ - } - break; + break; - //Settings - case "USER_UPDATE": - { - /*var data = msg.Payload.ToObject(Serializer); - if (data.Id == CurrentUser.Id) - { - var before = Config.EnablePreUpdateEvents ? CurrentUser.Clone() : null; - CurrentUser.Update(data); - foreach (var server in _servers) - server.Value.CurrentUser.Update(data); - _gatewayLogger.Info($"USER_UPDATE"); - OnProfileUpdated(before, CurrentUser); - }*/ + //Settings + case "USER_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + if (data.Id == CurrentUser.Id) + { + var before = Config.EnablePreUpdateEvents ? CurrentUser.Clone() : null; + CurrentUser.Update(data); + foreach (var server in _servers) + server.Value.CurrentUser.Update(data); + _gatewayLogger.Info($"USER_UPDATE"); + OnProfileUpdated(before, CurrentUser); + }*/ + } + break; + + //Handled in GatewaySocket + case "RESUMED": + break; + + //Ignored + case "USER_SETTINGS_UPDATE": + case "MESSAGE_ACK": //TODO: Add (User only) + case "GUILD_EMOJIS_UPDATE": //TODO: Add + case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add + case "VOICE_SERVER_UPDATE": //TODO: Add + _gatewayLogger.Debug($"Ignored message {opCode}{(type != null ? $" ({type})" : "")}"); + break; + + //Others + default: + _gatewayLogger.Warning($"Unknown message {opCode}{(type != null ? $" ({type})" : "")}"); + break; } break; - - //Handled in GatewaySocket - case "RESUMED": - break; - - //Ignored - case "USER_SETTINGS_UPDATE": - case "MESSAGE_ACK": //TODO: Add (User only) - case "GUILD_EMOJIS_UPDATE": //TODO: Add - case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add - case "VOICE_SERVER_UPDATE": //TODO: Add - _gatewayLogger.Debug($"{msg.Type} [Ignored]"); - break; - - //Others - default: - _gatewayLogger.Warning($"Unknown message type: {msg.Type}"); - break; } } catch (Exception ex) { - _gatewayLogger.Error($"Error handling {msg.Type} event", ex); + _gatewayLogger.Error($"Error handling msg {opCode}{(type != null ? $" ({type})" : "")}", ex); } } diff --git a/src/Discord.Net/WebSocket/Entities/Users/User.cs b/src/Discord.Net/WebSocket/Entities/Users/User.cs index a69f3e5f4..e507b4df8 100644 --- a/src/Discord.Net/WebSocket/Entities/Users/User.cs +++ b/src/Discord.Net/WebSocket/Entities/Users/User.cs @@ -24,6 +24,10 @@ namespace Discord.WebSocket public string Username { get; private set; } /// public DMChannel DMChannel { get; internal set; } + /// + public Game? CurrentGame { get; internal set; } + /// + public UserStatus Status { get; internal set; } /// 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})"; - /// - Game? IUser.CurrentGame => null; - /// - UserStatus IUser.Status => UserStatus.Unknown; - /// async Task IUser.CreateDMChannel() => await CreateDMChannel().ConfigureAwait(false);