| @@ -3,6 +3,6 @@ | |||||
| "sdk": { | "sdk": { | ||||
| "version": "1.0.0-beta6", | "version": "1.0.0-beta6", | ||||
| "architecture": "x64", | "architecture": "x64", | ||||
| "runtime": "clr" | |||||
| "runtime": "coreclr" | |||||
| } | } | ||||
| } | } | ||||
| @@ -8,30 +8,6 @@ using System; | |||||
| namespace Discord.API.Models | namespace Discord.API.Models | ||||
| { | { | ||||
| internal class WebSocketMessage | |||||
| { | |||||
| [JsonProperty(PropertyName = "op")] | |||||
| public int Operation; | |||||
| [JsonProperty(PropertyName = "t")] | |||||
| public string Type; | |||||
| [JsonProperty(PropertyName = "d")] | |||||
| public object Payload; | |||||
| } | |||||
| internal abstract class WebSocketMessage<T> : WebSocketMessage | |||||
| where T : new() | |||||
| { | |||||
| public WebSocketMessage() { Payload = new T(); } | |||||
| public WebSocketMessage(int op) { Operation = op; Payload = new T(); } | |||||
| public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; } | |||||
| [JsonIgnore] | |||||
| public new T Payload | |||||
| { | |||||
| get { if (base.Payload is JToken) { base.Payload = (base.Payload as JToken).ToObject<T>(); } return (T)base.Payload; } | |||||
| set { base.Payload = value; } | |||||
| } | |||||
| } | |||||
| //Users | //Users | ||||
| internal class UserReference | internal class UserReference | ||||
| { | { | ||||
| @@ -83,9 +59,9 @@ namespace Discord.API.Models | |||||
| [JsonProperty(PropertyName = "session_id")] | [JsonProperty(PropertyName = "session_id")] | ||||
| public string SessionId; | public string SessionId; | ||||
| [JsonProperty(PropertyName = "self_mute")] | [JsonProperty(PropertyName = "self_mute")] | ||||
| public bool IsSelfMuted; | |||||
| public bool? IsSelfMuted; | |||||
| [JsonProperty(PropertyName = "self_deaf")] | [JsonProperty(PropertyName = "self_deaf")] | ||||
| public bool IsSelfDeafened; | |||||
| public bool? IsSelfDeafened; | |||||
| [JsonProperty(PropertyName = "mute")] | [JsonProperty(PropertyName = "mute")] | ||||
| public bool IsMuted; | public bool IsMuted; | ||||
| [JsonProperty(PropertyName = "deaf")] | [JsonProperty(PropertyName = "deaf")] | ||||
| @@ -3,6 +3,7 @@ | |||||
| #pragma warning disable CS0169 | #pragma warning disable CS0169 | ||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| @@ -10,6 +11,29 @@ namespace Discord.API.Models | |||||
| { | { | ||||
| internal static class TextWebSocketCommands | internal static class TextWebSocketCommands | ||||
| { | { | ||||
| public class WebSocketMessage | |||||
| { | |||||
| [JsonProperty(PropertyName = "op")] | |||||
| public int Operation; | |||||
| [JsonProperty(PropertyName = "t")] | |||||
| public string Type; | |||||
| [JsonProperty(PropertyName = "d")] | |||||
| public object Payload; | |||||
| } | |||||
| internal abstract class WebSocketMessage<T> : WebSocketMessage | |||||
| where T : new() | |||||
| { | |||||
| public WebSocketMessage() { Payload = new T(); } | |||||
| public WebSocketMessage(int op) { Operation = op; Payload = new T(); } | |||||
| public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; } | |||||
| [JsonIgnore] | |||||
| public new T Payload | |||||
| { | |||||
| get { if (base.Payload is JToken) { base.Payload = (base.Payload as JToken).ToObject<T>(); } return (T)base.Payload; } | |||||
| set { base.Payload = value; } | |||||
| } | |||||
| } | |||||
| public sealed class KeepAlive : WebSocketMessage<int> | public sealed class KeepAlive : WebSocketMessage<int> | ||||
| { | { | ||||
| private static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | private static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||||
| @@ -3,13 +3,34 @@ | |||||
| #pragma warning disable CS0169 | #pragma warning disable CS0169 | ||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using Newtonsoft.Json.Linq; | |||||
| namespace Discord.API.Models | namespace Discord.API.Models | ||||
| { | { | ||||
| internal static class VoiceWebSocketCommands | internal static class VoiceWebSocketCommands | ||||
| { | { | ||||
| public class WebSocketMessage | |||||
| { | |||||
| [JsonProperty(PropertyName = "op")] | |||||
| public int Operation; | |||||
| [JsonProperty(PropertyName = "d")] | |||||
| public object Payload; | |||||
| } | |||||
| internal abstract class WebSocketMessage<T> : WebSocketMessage | |||||
| where T : new() | |||||
| { | |||||
| public WebSocketMessage() { Payload = new T(); } | |||||
| public WebSocketMessage(int op) { Operation = op; Payload = new T(); } | |||||
| public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; } | |||||
| [JsonIgnore] | |||||
| public new T Payload | |||||
| { | |||||
| get { if (base.Payload is JToken) { base.Payload = (base.Payload as JToken).ToObject<T>(); } return (T)base.Payload; } | |||||
| set { base.Payload = value; } | |||||
| } | |||||
| } | |||||
| public sealed class KeepAlive : WebSocketMessage<object> | public sealed class KeepAlive : WebSocketMessage<object> | ||||
| { | { | ||||
| public KeepAlive() : base(3, null) { } | public KeepAlive() : base(3, null) { } | ||||
| @@ -29,12 +50,12 @@ namespace Discord.API.Models | |||||
| public string Token; | public string Token; | ||||
| } | } | ||||
| } | } | ||||
| public sealed class Login2 : WebSocketMessage<Login.Data> | |||||
| public sealed class Login2 : WebSocketMessage<Login2.Data> | |||||
| { | { | ||||
| public Login2() : base(1) { } | public Login2() : base(1) { } | ||||
| public class Data | public class Data | ||||
| { | { | ||||
| public class PCData | |||||
| public class SocketInfo | |||||
| { | { | ||||
| [JsonProperty(PropertyName = "address")] | [JsonProperty(PropertyName = "address")] | ||||
| public string Address; | public string Address; | ||||
| @@ -45,8 +66,8 @@ namespace Discord.API.Models | |||||
| } | } | ||||
| [JsonProperty(PropertyName = "protocol")] | [JsonProperty(PropertyName = "protocol")] | ||||
| public string Protocol = "udp"; | public string Protocol = "udp"; | ||||
| [JsonProperty(PropertyName = "token")] | |||||
| public string Token; | |||||
| [JsonProperty(PropertyName = "data")] | |||||
| public SocketInfo SocketData = new SocketInfo(); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -8,5 +8,24 @@ namespace Discord.API.Models | |||||
| { | { | ||||
| internal static class VoiceWebSocketEvents | internal static class VoiceWebSocketEvents | ||||
| { | { | ||||
| public sealed class Ready | |||||
| { | |||||
| [JsonProperty(PropertyName = "ssrc")] | |||||
| public int SSRC; | |||||
| [JsonProperty(PropertyName = "port")] | |||||
| public int Port; | |||||
| [JsonProperty(PropertyName = "modes")] | |||||
| public string[] Modes; | |||||
| [JsonProperty(PropertyName = "heartbeat_interval")] | |||||
| public int HeartbeatInterval; | |||||
| } | |||||
| public sealed class JoinServer | |||||
| { | |||||
| [JsonProperty(PropertyName = "secret_key")] | |||||
| public byte[] SecretKey; | |||||
| [JsonProperty(PropertyName = "mode")] | |||||
| public string Mode; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -249,6 +249,18 @@ namespace Discord | |||||
| } | } | ||||
| //Voice | //Voice | ||||
| public event EventHandler VoiceConnected; | |||||
| private void RaiseVoiceConnected() | |||||
| { | |||||
| if (VoiceConnected != null) | |||||
| VoiceConnected(this, EventArgs.Empty); | |||||
| } | |||||
| public event EventHandler VoiceDisconnected; | |||||
| private void RaiseVoiceDisconnected() | |||||
| { | |||||
| if (VoiceDisconnected != null) | |||||
| VoiceDisconnected(this, EventArgs.Empty); | |||||
| } | |||||
| public sealed class VoiceServerUpdatedEventArgs : EventArgs | public sealed class VoiceServerUpdatedEventArgs : EventArgs | ||||
| { | { | ||||
| public readonly Server Server; | public readonly Server Server; | ||||
| @@ -19,6 +19,7 @@ namespace Discord | |||||
| { | { | ||||
| private readonly DiscordClientConfig _config; | private readonly DiscordClientConfig _config; | ||||
| private readonly DiscordTextWebSocket _webSocket; | private readonly DiscordTextWebSocket _webSocket; | ||||
| private readonly DiscordVoiceWebSocket _voiceWebSocket; | |||||
| private readonly ManualResetEventSlim _blockEvent; | private readonly ManualResetEventSlim _blockEvent; | ||||
| private readonly Regex _userRegex, _channelRegex; | private readonly Regex _userRegex, _channelRegex; | ||||
| private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator; | private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator; | ||||
| @@ -26,12 +27,8 @@ namespace Discord | |||||
| private readonly Random _rand; | private readonly Random _rand; | ||||
| private volatile CancellationTokenSource _disconnectToken; | private volatile CancellationTokenSource _disconnectToken; | ||||
| private volatile Task _tasks; | |||||
| #if !DNXCORE50 | |||||
| private readonly DiscordVoiceWebSocket _voiceWebSocket; | |||||
| private string _currentVoiceServer, _currentVoiceToken; | |||||
| #endif | |||||
| private volatile Task _tasks; | |||||
| private string _currentVoiceServerId, _currentVoiceEndpoint, _currentVoiceToken; | |||||
| /// <summary> Returns the User object for the current logged in user. </summary> | /// <summary> Returns the User object for the current logged in user. </summary> | ||||
| public User User { get; private set; } | public User User { get; private set; } | ||||
| @@ -64,6 +61,9 @@ namespace Discord | |||||
| /// <remarks> This collection does not guarantee any ordering. </remarks> | /// <remarks> This collection does not guarantee any ordering. </remarks> | ||||
| public IEnumerable<Role> Roles => _roles; | public IEnumerable<Role> Roles => _roles; | ||||
| private readonly AsyncCache<Role, API.Models.Role> _roles; | private readonly AsyncCache<Role, API.Models.Role> _roles; | ||||
| public string CurrentVoiceServerId { get { return _currentVoiceEndpoint != null ? _currentVoiceToken : null; } } | |||||
| public Server CurrentVoiceServer => _servers[CurrentVoiceServerId]; | |||||
| /// <summary> Returns true if the user has successfully logged in and the websocket connection has been established. </summary> | /// <summary> Returns true if the user has successfully logged in and the websocket connection has been established. </summary> | ||||
| public bool IsConnected => _isConnected; | public bool IsConnected => _isConnected; | ||||
| @@ -306,34 +306,15 @@ namespace Discord | |||||
| } | } | ||||
| }; | }; | ||||
| _webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | _webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | ||||
| #if !DNXCORE50 | |||||
| _voiceWebSocket = new DiscordVoiceWebSocket(_config.WebSocketInterval); | _voiceWebSocket = new DiscordVoiceWebSocket(_config.WebSocketInterval); | ||||
| _voiceWebSocket.Connected += (s, e) => RaiseConnected(); | |||||
| _voiceWebSocket.Disconnected += async (s, e) => | |||||
| _voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||||
| _voiceWebSocket.Disconnected += (s, e) => | |||||
| { | { | ||||
| //Reconnect if we didn't cause the disconnect | |||||
| RaiseDisconnected(); | |||||
| while (!_disconnectToken.IsCancellationRequested) | |||||
| { | |||||
| try | |||||
| { | |||||
| await Task.Delay(_config.ReconnectDelay); | |||||
| await _voiceWebSocket.ConnectAsync(Endpoints.WebSocket_Hub); | |||||
| if (_currentVoiceServer != null) | |||||
| await _voiceWebSocket.Login(_currentVoiceServer, UserId, SessionId, _currentVoiceToken); | |||||
| break; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| RaiseOnDebugMessage($"Reconnect Failed: {ex.Message}"); | |||||
| //Net is down? We can keep trying to reconnect until the user runs Disconnect() | |||||
| await Task.Delay(_config.FailedReconnectDelay); | |||||
| } | |||||
| } | |||||
| //TODO: Reconnect if we didn't cause the disconnect | |||||
| RaiseVoiceDisconnected(); | |||||
| }; | }; | ||||
| _voiceWebSocket.OnDebugMessage += (s, e) => RaiseOnVoiceDebugMessage(e.Message); | _voiceWebSocket.OnDebugMessage += (s, e) => RaiseOnVoiceDebugMessage(e.Message); | ||||
| #endif | |||||
| #pragma warning disable CS1998 //Disable unused async keyword warning | #pragma warning disable CS1998 //Disable unused async keyword warning | ||||
| _webSocket.GotEvent += async (s, e) => | _webSocket.GotEvent += async (s, e) => | ||||
| @@ -587,16 +568,14 @@ namespace Discord | |||||
| var server = _servers[data.ServerId]; | var server = _servers[data.ServerId]; | ||||
| server.VoiceServer = data.Endpoint; | server.VoiceServer = data.Endpoint; | ||||
| try { RaiseVoiceServerUpdated(server, data.Endpoint); } catch { } | try { RaiseVoiceServerUpdated(server, data.Endpoint); } catch { } | ||||
| #if !DNXCORE50 | |||||
| if (data.ServerId == _currentVoiceServer) | |||||
| if (data.ServerId == _currentVoiceServerId) | |||||
| { | { | ||||
| _currentVoiceServer = data.Endpoint; | |||||
| _currentVoiceEndpoint = data.Endpoint.Split(':')[0]; | |||||
| _currentVoiceToken = data.Token; | _currentVoiceToken = data.Token; | ||||
| await _voiceWebSocket.ConnectAsync(_currentVoiceServer); | |||||
| await _voiceWebSocket.Login(_currentVoiceServer, UserId, SessionId, _currentVoiceToken); | |||||
| await _voiceWebSocket.ConnectAsync("wss://" + _currentVoiceEndpoint); | |||||
| await _voiceWebSocket.Login(_currentVoiceServerId, UserId, SessionId, _currentVoiceToken); | |||||
| } | } | ||||
| #endif | |||||
| } | } | ||||
| break; | break; | ||||
| @@ -667,7 +646,6 @@ namespace Discord | |||||
| catch { } | catch { } | ||||
| } | } | ||||
| /// <summary> Returns the user with the specified id, or null if none was found. </summary> | /// <summary> Returns the user with the specified id, or null if none was found. </summary> | ||||
| public User GetUser(string id) => _users[id]; | public User GetUser(string id) => _users[id]; | ||||
| /// <summary> Returns the user with the specified name and discriminator, or null if none was found. </summary> | /// <summary> Returns the user with the specified name and discriminator, or null if none was found. </summary> | ||||
| @@ -918,9 +896,7 @@ namespace Discord | |||||
| _tasks = _tasks.ContinueWith(async x => | _tasks = _tasks.ContinueWith(async x => | ||||
| { | { | ||||
| await _webSocket.DisconnectAsync(); | await _webSocket.DisconnectAsync(); | ||||
| #if !DNXCORE50 | |||||
| await _voiceWebSocket.DisconnectAsync(); | await _voiceWebSocket.DisconnectAsync(); | ||||
| #endif | |||||
| //Clear send queue | //Clear send queue | ||||
| Message ignored; | Message ignored; | ||||
| @@ -1252,6 +1228,29 @@ namespace Discord | |||||
| //Voice | //Voice | ||||
| public Task JoinVoice(Server server, Channel channel) | |||||
| => JoinVoice(server.Id, channel.Id); | |||||
| public Task JoinVoice(Server server, string channelId) | |||||
| => JoinVoice(server.Id, channelId); | |||||
| public Task JoinVoice(string serverId, Channel channel) | |||||
| => JoinVoice(serverId, channel.Id); | |||||
| public async Task JoinVoice(string serverId, string channelId) | |||||
| { | |||||
| await LeaveVoice(); | |||||
| _currentVoiceServerId = serverId; | |||||
| _webSocket.JoinVoice(serverId, channelId); | |||||
| } | |||||
| public async Task LeaveVoice() | |||||
| { | |||||
| await _voiceWebSocket.DisconnectAsync(); | |||||
| if (_currentVoiceEndpoint != null) | |||||
| _webSocket.LeaveVoice(); | |||||
| _currentVoiceEndpoint = null; | |||||
| _currentVoiceServerId = null; | |||||
| _currentVoiceToken = null; | |||||
| } | |||||
| /// <summary> Mutes a user on the provided server. </summary> | /// <summary> Mutes a user on the provided server. </summary> | ||||
| public Task Mute(Server server, User user) | public Task Mute(Server server, User user) | ||||
| => Mute(server.Id, user.Id); | => Mute(server.Id, user.Id); | ||||
| @@ -1,9 +1,11 @@ | |||||
| using Discord.API.Models; | using Discord.API.Models; | ||||
| using Discord.Helpers; | using Discord.Helpers; | ||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using WebSocketMessage = Discord.API.Models.TextWebSocketCommands.WebSocketMessage; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -51,22 +53,25 @@ namespace Discord | |||||
| SetConnected(); | SetConnected(); | ||||
| } | } | ||||
| protected override void ProcessMessage(WebSocketMessage msg) | |||||
| protected override void ProcessMessage(string json) | |||||
| { | { | ||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||||
| switch (msg.Operation) | switch (msg.Operation) | ||||
| { | { | ||||
| case 0: | case 0: | ||||
| if (msg.Type == "READY") | |||||
| { | { | ||||
| var payload = (msg.Payload as JToken).ToObject<TextWebSocketEvents.Ready>(); | |||||
| _heartbeatInterval = payload.HeartbeatInterval; | |||||
| QueueMessage(new TextWebSocketCommands.UpdateStatus()); | |||||
| QueueMessage(new TextWebSocketCommands.KeepAlive()); | |||||
| _connectWaitOnLogin.Set(); //Pre-Event | |||||
| if (msg.Type == "READY") | |||||
| { | |||||
| var payload = (msg.Payload as JToken).ToObject<TextWebSocketEvents.Ready>(); | |||||
| _heartbeatInterval = payload.HeartbeatInterval; | |||||
| QueueMessage(new TextWebSocketCommands.UpdateStatus()); | |||||
| QueueMessage(GetKeepAlive()); | |||||
| _connectWaitOnLogin.Set(); //Pre-Event | |||||
| } | |||||
| RaiseGotEvent(msg.Type, msg.Payload as JToken); | |||||
| if (msg.Type == "READY") | |||||
| _connectWaitOnLogin2.Set(); //Post-Event | |||||
| } | } | ||||
| RaiseGotEvent(msg.Type, msg.Payload as JToken); | |||||
| if (msg.Type == "READY") | |||||
| _connectWaitOnLogin2.Set(); //Post-Event | |||||
| break; | break; | ||||
| default: | default: | ||||
| RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | ||||
| @@ -74,7 +79,7 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| protected override WebSocketMessage GetKeepAlive() | |||||
| protected override object GetKeepAlive() | |||||
| { | { | ||||
| return new TextWebSocketCommands.KeepAlive(); | return new TextWebSocketCommands.KeepAlive(); | ||||
| } | } | ||||
| @@ -1,12 +1,16 @@ | |||||
| #if !DNXCORE50 | |||||
| //#if !DNXCORE50 | |||||
| using Discord.API.Models; | using Discord.API.Models; | ||||
| using Discord.Helpers; | using Discord.Helpers; | ||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json.Linq; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Net; | |||||
| using System.Net.Sockets; | using System.Net.Sockets; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -14,20 +18,28 @@ namespace Discord | |||||
| { | { | ||||
| private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event | private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event | ||||
| private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | |||||
| private ManualResetEventSlim _connectWaitOnLogin; | |||||
| private UdpClient _udp; | private UdpClient _udp; | ||||
| private ConcurrentQueue<byte[]> _sendQueue; | private ConcurrentQueue<byte[]> _sendQueue; | ||||
| private string _ip; | |||||
| public DiscordVoiceWebSocket(int interval) | public DiscordVoiceWebSocket(int interval) | ||||
| : base(interval) | : base(interval) | ||||
| { | { | ||||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | _connectWaitOnLogin = new ManualResetEventSlim(false); | ||||
| _connectWaitOnLogin2 = new ManualResetEventSlim(false); | |||||
| _udp = new UdpClient(); | |||||
| _sendQueue = new ConcurrentQueue<byte[]>(); | _sendQueue = new ConcurrentQueue<byte[]>(); | ||||
| } | } | ||||
| protected override void OnConnect() | |||||
| { | |||||
| _udp = new UdpClient(0); | |||||
| } | |||||
| protected override void OnDisconnect() | |||||
| { | |||||
| _udp = null; | |||||
| } | |||||
| protected override Task[] CreateTasks(CancellationToken cancelToken) | protected override Task[] CreateTasks(CancellationToken cancelToken) | ||||
| { | { | ||||
| return new Task[] | return new Task[] | ||||
| @@ -37,14 +49,14 @@ namespace Discord | |||||
| Task.Factory.StartNew(WatcherAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result | Task.Factory.StartNew(WatcherAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result | ||||
| }.Concat(base.CreateTasks(cancelToken)).ToArray(); | }.Concat(base.CreateTasks(cancelToken)).ToArray(); | ||||
| } | } | ||||
| public async Task Login(string serverId, string userId, string sessionId, string token) | public async Task Login(string serverId, string userId, string sessionId, string token) | ||||
| { | { | ||||
| var cancelToken = _disconnectToken.Token; | var cancelToken = _disconnectToken.Token; | ||||
| _connectWaitOnLogin.Reset(); | _connectWaitOnLogin.Reset(); | ||||
| _connectWaitOnLogin2.Reset(); | |||||
| string ip = await Http.Get("http://ipinfo.io/ip"); | |||||
| _ip = (await Http.Get("http://ipinfo.io/ip")).Trim(); | |||||
| VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); | VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); | ||||
| msg.Payload.ServerId = serverId; | msg.Payload.ServerId = serverId; | ||||
| @@ -52,18 +64,17 @@ namespace Discord | |||||
| msg.Payload.Token = token; | msg.Payload.Token = token; | ||||
| msg.Payload.UserId = userId; | msg.Payload.UserId = userId; | ||||
| await SendMessage(msg, cancelToken); | await SendMessage(msg, cancelToken); | ||||
| System.Diagnostics.Debug.WriteLine("<<< " + JsonConvert.SerializeObject(msg)); | |||||
| try | try | ||||
| { | { | ||||
| if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message | |||||
| if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on JoinServer message | |||||
| throw new Exception("No reply from Discord server"); | throw new Exception("No reply from Discord server"); | ||||
| } | } | ||||
| catch (OperationCanceledException) | catch (OperationCanceledException) | ||||
| { | { | ||||
| throw new InvalidOperationException("Bad Token"); | throw new InvalidOperationException("Bad Token"); | ||||
| } | } | ||||
| try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler | |||||
| catch (OperationCanceledException) { return; } | |||||
| SetConnected(); | SetConnected(); | ||||
| } | } | ||||
| @@ -105,21 +116,57 @@ namespace Discord | |||||
| { | { | ||||
| await Task.Delay(-1, _disconnectToken.Token); | await Task.Delay(-1, _disconnectToken.Token); | ||||
| } | } | ||||
| catch (OperationCanceledException) { } | |||||
| _udp.Close(); | |||||
| catch (TaskCanceledException) { } | |||||
| #if DNXCORE50 | |||||
| finally { _udp.Dispose(); } | |||||
| #else | |||||
| finally { _udp.Close(); } | |||||
| #endif | |||||
| } | } | ||||
| protected override void ProcessMessage(WebSocketMessage msg) | |||||
| protected override void ProcessMessage(string json) | |||||
| { | { | ||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||||
| System.Diagnostics.Debug.WriteLine(">>> " + JsonConvert.SerializeObject(msg)); | |||||
| switch (msg.Operation) | |||||
| { | |||||
| case 2: | |||||
| { | |||||
| var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.Ready>(); | |||||
| _heartbeatInterval = payload.HeartbeatInterval; | |||||
| var login2 = new VoiceWebSocketCommands.Login2(); | |||||
| login2.Payload.Protocol = "udp"; | |||||
| login2.Payload.SocketData.Address = _ip; | |||||
| login2.Payload.SocketData.Mode = payload.Modes.Last(); | |||||
| login2.Payload.SocketData.Port = (_udp.Client.LocalEndPoint as IPEndPoint).Port; | |||||
| QueueMessage(login2); | |||||
| System.Diagnostics.Debug.WriteLine("<<< " + JsonConvert.SerializeObject(login2)); | |||||
| } | |||||
| break; | |||||
| case 4: | |||||
| { | |||||
| var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.JoinServer>(); | |||||
| QueueMessage(GetKeepAlive()); | |||||
| System.Diagnostics.Debug.WriteLine("<<< " + JsonConvert.SerializeObject(GetKeepAlive())); | |||||
| _connectWaitOnLogin.Set(); | |||||
| } | |||||
| break; | |||||
| default: | |||||
| RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | |||||
| break; | |||||
| } | |||||
| } | } | ||||
| private void ProcessUdpMessage(UdpReceiveResult msg) | private void ProcessUdpMessage(UdpReceiveResult msg) | ||||
| { | { | ||||
| } | } | ||||
| protected override WebSocketMessage GetKeepAlive() | |||||
| protected override object GetKeepAlive() | |||||
| { | { | ||||
| return new VoiceWebSocketCommands.KeepAlive(); | return new VoiceWebSocketCommands.KeepAlive(); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| #endif | |||||
| //#endif | |||||
| @@ -43,6 +43,8 @@ namespace Discord | |||||
| _webSocket.Options.KeepAliveInterval = TimeSpan.Zero; | _webSocket.Options.KeepAliveInterval = TimeSpan.Zero; | ||||
| await _webSocket.ConnectAsync(new Uri(url), cancelToken); | await _webSocket.ConnectAsync(new Uri(url), cancelToken); | ||||
| OnConnect(); | |||||
| _tasks = Task.WhenAll(CreateTasks(cancelToken)) | _tasks = Task.WhenAll(CreateTasks(cancelToken)) | ||||
| .ContinueWith(x => | .ContinueWith(x => | ||||
| { | { | ||||
| @@ -58,6 +60,8 @@ namespace Discord | |||||
| byte[] ignored; | byte[] ignored; | ||||
| while (_sendQueue.TryDequeue(out ignored)) { } | while (_sendQueue.TryDequeue(out ignored)) { } | ||||
| OnDisconnect(); | |||||
| if (_isConnected) | if (_isConnected) | ||||
| { | { | ||||
| _isConnected = false; | _isConnected = false; | ||||
| @@ -75,6 +79,8 @@ namespace Discord | |||||
| try { await _tasks; } catch (NullReferenceException) { } | try { await _tasks; } catch (NullReferenceException) { } | ||||
| } | } | ||||
| } | } | ||||
| protected virtual void OnConnect() { } | |||||
| protected virtual void OnDisconnect() { } | |||||
| protected void SetConnected() | protected void SetConnected() | ||||
| { | { | ||||
| @@ -117,8 +123,7 @@ namespace Discord | |||||
| } | } | ||||
| while (!result.EndOfMessage); | while (!result.EndOfMessage); | ||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(builder.ToString()); | |||||
| ProcessMessage(msg); | |||||
| ProcessMessage(builder.ToString()); | |||||
| builder.Clear(); | builder.Clear(); | ||||
| } | } | ||||
| @@ -152,8 +157,8 @@ namespace Discord | |||||
| finally { _disconnectToken.Cancel(); } | finally { _disconnectToken.Cancel(); } | ||||
| } | } | ||||
| protected abstract void ProcessMessage(WebSocketMessage msg); | |||||
| protected abstract WebSocketMessage GetKeepAlive(); | |||||
| protected abstract void ProcessMessage(string json); | |||||
| protected abstract object GetKeepAlive(); | |||||
| protected void QueueMessage(object message) | protected void QueueMessage(object message) | ||||
| { | { | ||||
| @@ -7,6 +7,7 @@ using System.Reflection; | |||||
| using System.Net; | using System.Net; | ||||
| using System.IO; | using System.IO; | ||||
| using System.Globalization; | using System.Globalization; | ||||
| using Discord.API; | |||||
| #if TEST_RESPONSES | #if TEST_RESPONSES | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| @@ -23,7 +24,9 @@ namespace Discord.Helpers | |||||
| #endif | #endif | ||||
| private static readonly HttpClient _client; | private static readonly HttpClient _client; | ||||
| private static readonly HttpMethod _patch; | private static readonly HttpMethod _patch; | ||||
| #if TEST_RESPONSES | |||||
| private static readonly JsonSerializerSettings _settings; | private static readonly JsonSerializerSettings _settings; | ||||
| #endif | |||||
| static Http() | static Http() | ||||
| { | { | ||||
| @@ -41,12 +44,12 @@ namespace Discord.Helpers | |||||
| string version = typeof(Http).GetTypeInfo().Assembly.GetName().Version.ToString(2); | string version = typeof(Http).GetTypeInfo().Assembly.GetName().Version.ToString(2); | ||||
| _client.DefaultRequestHeaders.Add("user-agent", $"Discord.Net/{version} (https://github.com/RogueException/Discord.Net)"); | _client.DefaultRequestHeaders.Add("user-agent", $"Discord.Net/{version} (https://github.com/RogueException/Discord.Net)"); | ||||
| _settings = new JsonSerializerSettings(); | |||||
| #if TEST_RESPONSES | #if TEST_RESPONSES | ||||
| _settings = new JsonSerializerSettings(); | |||||
| _settings.CheckAdditionalContent = true; | _settings.CheckAdditionalContent = true; | ||||
| _settings.MissingMemberHandling = MissingMemberHandling.Error; | _settings.MissingMemberHandling = MissingMemberHandling.Error; | ||||
| #endif | #endif | ||||
| } | |||||
| } | |||||
| private static string _token; | private static string _token; | ||||
| public static string Token | public static string Token | ||||
| @@ -121,14 +124,18 @@ namespace Discord.Helpers | |||||
| where ResponseT : class | where ResponseT : class | ||||
| { | { | ||||
| string responseJson = await SendRequest(method, path, content, true); | string responseJson = await SendRequest(method, path, content, true); | ||||
| var response = JsonConvert.DeserializeObject<ResponseT>(responseJson, _settings); | |||||
| return response; | |||||
| #if TEST_RESPONSES | |||||
| if (path.StartsWith(Endpoints.BaseApi)) | |||||
| return JsonConvert.DeserializeObject<ResponseT>(responseJson, _settings); | |||||
| #endif | |||||
| return JsonConvert.DeserializeObject<ResponseT>(responseJson); | |||||
| } | } | ||||
| private static async Task<string> Send(HttpMethod method, string path, HttpContent content) | private static async Task<string> Send(HttpMethod method, string path, HttpContent content) | ||||
| { | { | ||||
| string responseJson = await SendRequest(method, path, content, _isDebug); | string responseJson = await SendRequest(method, path, content, _isDebug); | ||||
| #if TEST_RESPONSES | #if TEST_RESPONSES | ||||
| CheckEmptyResponse(responseJson); | |||||
| if (path.StartsWith(Endpoints.BaseApi)) | |||||
| CheckEmptyResponse(responseJson); | |||||
| #endif | #endif | ||||
| return responseJson; | return responseJson; | ||||
| } | } | ||||
| @@ -54,6 +54,10 @@ namespace Discord | |||||
| /// <summary> Returns a collection of all channels within this server. </summary> | /// <summary> Returns a collection of all channels within this server. </summary> | ||||
| public IEnumerable<Channel> Channels => _client.Channels.Where(x => x.ServerId == Id); | public IEnumerable<Channel> Channels => _client.Channels.Where(x => x.ServerId == Id); | ||||
| /// <summary> Returns a collection of all channels within this server. </summary> | |||||
| public IEnumerable<Channel> TextChannels => _client.Channels.Where(x => x.ServerId == Id && x.Type == ChannelTypes.Text); | |||||
| /// <summary> Returns a collection of all channels within this server. </summary> | |||||
| public IEnumerable<Channel> VoiceChannels => _client.Channels.Where(x => x.ServerId == Id && x.Type == ChannelTypes.Voice); | |||||
| /// <summary> Returns a collection of all roles within this server. </summary> | /// <summary> Returns a collection of all roles within this server. </summary> | ||||
| public IEnumerable<Role> Roles => _client.Roles.Where(x => x.ServerId == Id); | public IEnumerable<Role> Roles => _client.Roles.Where(x => x.ServerId == Id); | ||||
| @@ -78,8 +82,10 @@ namespace Discord | |||||
| member.VoiceChannelId = extendedModel.ChannelId; | member.VoiceChannelId = extendedModel.ChannelId; | ||||
| member.IsDeafened = extendedModel.IsDeafened; | member.IsDeafened = extendedModel.IsDeafened; | ||||
| member.IsMuted = extendedModel.IsMuted; | member.IsMuted = extendedModel.IsMuted; | ||||
| member.IsSelfDeafened = extendedModel.IsSelfDeafened; | |||||
| member.IsSelfMuted = extendedModel.IsSelfMuted; | |||||
| if (extendedModel.IsSelfDeafened.HasValue) | |||||
| member.IsSelfDeafened = extendedModel.IsSelfDeafened.Value; | |||||
| if (extendedModel.IsSelfMuted.HasValue) | |||||
| member.IsSelfMuted = extendedModel.IsSelfMuted.Value; | |||||
| member.IsSuppressed = extendedModel.IsSuppressed; | member.IsSuppressed = extendedModel.IsSuppressed; | ||||
| member.SessionId = extendedModel.SessionId; | member.SessionId = extendedModel.SessionId; | ||||
| member.Token = extendedModel.Token; | member.Token = extendedModel.Token; | ||||
| @@ -39,6 +39,7 @@ | |||||
| "System.IO.Compression": "4.0.0", | "System.IO.Compression": "4.0.0", | ||||
| "System.Linq": "4.0.0", | "System.Linq": "4.0.0", | ||||
| "System.Net.Requests": "4.0.10", | "System.Net.Requests": "4.0.10", | ||||
| "System.Net.Sockets": "4.0.10-beta-23019", | |||||
| "System.Net.WebSockets.Client": "4.0.0-beta-23123", | "System.Net.WebSockets.Client": "4.0.0-beta-23123", | ||||
| "System.Runtime": "4.0.20", | "System.Runtime": "4.0.20", | ||||
| "System.Text.RegularExpressions": "4.0.10" | "System.Text.RegularExpressions": "4.0.10" | ||||