| @@ -7,7 +7,7 @@ | |||
| <ProjectGuid>{1B5603B4-6F8F-4289-B945-7BAAE523D740}</ProjectGuid> | |||
| <OutputType>Library</OutputType> | |||
| <AppDesignerFolder>Properties</AppDesignerFolder> | |||
| <RootNamespace>Discord.Net</RootNamespace> | |||
| <RootNamespace>Discord</RootNamespace> | |||
| <AssemblyName>Discord.Net.Commands</AssemblyName> | |||
| <FileAlignment>512</FileAlignment> | |||
| <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | |||
| @@ -7,7 +7,7 @@ | |||
| <ProjectGuid>{8D71A857-879A-4A10-859E-5FF824ED6688}</ProjectGuid> | |||
| <OutputType>Library</OutputType> | |||
| <AppDesignerFolder>Properties</AppDesignerFolder> | |||
| <RootNamespace>Discord.Net</RootNamespace> | |||
| <RootNamespace>Discord</RootNamespace> | |||
| <AssemblyName>Discord.Net</AssemblyName> | |||
| <FileAlignment>512</FileAlignment> | |||
| <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | |||
| @@ -159,6 +159,9 @@ | |||
| <Compile Include="..\Discord.Net\Net\API\Endpoints.cs"> | |||
| <Link>Net\API\Endpoints.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\Net\API\HttpException.cs"> | |||
| <Link>Net\API\HttpException.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\Net\API\Requests.cs"> | |||
| <Link>Net\API\Requests.cs</Link> | |||
| </Compile> | |||
| @@ -177,9 +180,6 @@ | |||
| <Compile Include="..\Discord.Net\Net\API\RestClient.SharpRest.cs"> | |||
| <Link>Net\API\RestClient.SharpRest.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\Net\HttpException.cs"> | |||
| <Link>Net\HttpException.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\Net\WebSockets\Commands.cs"> | |||
| <Link>Net\WebSockets\Commands.cs</Link> | |||
| </Compile> | |||
| @@ -187,7 +187,7 @@ | |||
| <Link>Net\WebSockets\DataWebSocket.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\Net\WebSockets\DataWebSockets.Events.cs"> | |||
| <Link>Net\DataWebSockets.Events.cs</Link> | |||
| <Link>Net\WebSockets\DataWebSockets.Events.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\Net\WebSockets\Events.cs"> | |||
| <Link>Net\WebSockets\Events.cs</Link> | |||
| @@ -216,6 +216,9 @@ | |||
| <Compile Include="..\Discord.Net\Net\WebSockets\WebSocketMessage.cs"> | |||
| <Link>Net\WebSockets\WebSocketMessage.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\TimeoutException.cs"> | |||
| <Link>TimeoutException.cs</Link> | |||
| </Compile> | |||
| <Compile Include="Properties\AssemblyInfo.cs" /> | |||
| </ItemGroup> | |||
| <ItemGroup /> | |||
| @@ -95,7 +95,7 @@ namespace Discord | |||
| _api = new DiscordAPIClient(_config.LogLevel); | |||
| _dataSocket = new DataWebSocket(this); | |||
| _dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); }; | |||
| _dataSocket.Disconnected += async (s, e) => { RaiseDisconnected(e); if (e.WasUnexpected) await Reconnect(_token); }; | |||
| _dataSocket.Disconnected += async (s, e) => { RaiseDisconnected(e); if (e.WasUnexpected) await _dataSocket.Login(_token); }; | |||
| if (_config.EnableVoice) | |||
| { | |||
| _voiceSocket = new VoiceWebSocket(this); | |||
| @@ -112,7 +112,7 @@ namespace Discord | |||
| } | |||
| RaiseVoiceDisconnected(e); | |||
| if (e.WasUnexpected) | |||
| await _voiceSocket.Reconnect(_cancelToken); | |||
| await _voiceSocket.Reconnect(); | |||
| }; | |||
| _voiceSocket.IsSpeaking += (s, e) => | |||
| { | |||
| @@ -292,6 +292,8 @@ namespace Discord | |||
| } | |||
| } | |||
| break; | |||
| case "RESUMED": | |||
| break; | |||
| //Servers | |||
| case "GUILD_CREATE": | |||
| @@ -358,7 +360,7 @@ namespace Discord | |||
| var user = _users.GetOrAdd(data.User.Id); | |||
| user.Update(data.User); | |||
| if (_config.TrackActivity) | |||
| user.UpdateActivity(DateTime.UtcNow); | |||
| user.UpdateActivity(); | |||
| var member = _members.GetOrAdd(data.User.Id, data.GuildId); | |||
| member.Update(data); | |||
| RaiseUserAdded(member); | |||
| @@ -536,7 +538,7 @@ namespace Discord | |||
| if (user != null) | |||
| { | |||
| if (_config.TrackActivity) | |||
| user.UpdateActivity(DateTime.UtcNow); | |||
| user.UpdateActivity(); | |||
| if (channel != null) | |||
| RaiseUserIsTyping(user, channel); | |||
| } | |||
| @@ -550,8 +552,8 @@ namespace Discord | |||
| var server = _servers[data.GuildId]; | |||
| if (_config.EnableVoice) | |||
| { | |||
| string host = "wss://" + data.Endpoint.Split(':')[0]; | |||
| await _voiceSocket.Login(host, data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); | |||
| _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; | |||
| await _voiceSocket.Login(data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); | |||
| } | |||
| } | |||
| break; | |||
| @@ -582,11 +584,6 @@ namespace Discord | |||
| }; | |||
| } | |||
| private void _dataSocket_Connected(object sender, EventArgs e) | |||
| { | |||
| throw new NotImplementedException(); | |||
| } | |||
| //Connection | |||
| /// <summary> Connects to the Discord server with the provided token. </summary> | |||
| public async Task Connect(string token) | |||
| @@ -594,46 +591,54 @@ namespace Discord | |||
| if (_state != (int)DiscordClientState.Disconnected) | |||
| await Disconnect().ConfigureAwait(false); | |||
| if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
| RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Using cached token."); | |||
| await ConnectInternal(token).ConfigureAwait(false); | |||
| } | |||
| await ConnectInternal(token) | |||
| .Timeout(_config.ConnectionTimeout) | |||
| .ConfigureAwait(false); | |||
| } | |||
| /// <summary> Connects to the Discord server with the provided email and password. </summary> | |||
| /// <returns> Returns a token for future connections. </returns> | |||
| public async Task<string> Connect(string email, string password) | |||
| { | |||
| if (_state != (int)DiscordClientState.Disconnected) | |||
| await Disconnect().ConfigureAwait(false); | |||
| var response = await _api.Login(email, password).ConfigureAwait(false); | |||
| if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
| RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, "Login successful, got token."); | |||
| return await ConnectInternal(response.Token).ConfigureAwait(false); | |||
| } | |||
| private Task Reconnect(string token) | |||
| { | |||
| if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
| RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Using cached token."); | |||
| return ConnectInternal(token); | |||
| string token; | |||
| try | |||
| { | |||
| var cancelToken = new CancellationTokenSource(); | |||
| cancelToken.CancelAfter(5000); | |||
| _api.CancelToken = cancelToken.Token; | |||
| var response = await _api.Login(email, password).ConfigureAwait(false); | |||
| token = response.Token; | |||
| if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
| RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, "Login successful, got token."); | |||
| } | |||
| catch (TaskCanceledException) { throw new TimeoutException(); } | |||
| return await ConnectInternal(token) | |||
| .Timeout(_config.ConnectionTimeout) | |||
| .ConfigureAwait(false); | |||
| } | |||
| private async Task<string> ConnectInternal(string token) | |||
| { | |||
| try | |||
| try | |||
| { | |||
| _disconnectedEvent.Reset(); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| _state = (int)DiscordClientState.Connecting; | |||
| _api.Token = token; | |||
| _api.CancelToken = _cancelToken; | |||
| _token = token; | |||
| _state = (int)DiscordClientState.Connecting; | |||
| string url = (await _api.GetWebSocketEndpoint().ConfigureAwait(false)).Url; | |||
| url = "wss://gateway-besaid.discord.gg/"; | |||
| if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
| RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Websocket endpoint: {url}"); | |||
| await _dataSocket.Login(url, token, _cancelToken).ConfigureAwait(false); | |||
| _dataSocket.Host = url; | |||
| _dataSocket.ParentCancelToken = _cancelToken; | |||
| await _dataSocket.Login(token).ConfigureAwait(false); | |||
| _runTask = RunTasks(); | |||
| @@ -641,8 +646,7 @@ namespace Discord | |||
| { | |||
| //Cancel if either Disconnect is called, data socket errors or timeout is reached | |||
| var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _dataSocket.CancelToken).Token; | |||
| if (!_connectedEvent.Wait(_config.ConnectionTimeout, cancelToken)) | |||
| throw new Exception("Operation timed out."); | |||
| _connectedEvent.Wait(cancelToken); | |||
| } | |||
| catch (OperationCanceledException) | |||
| { | |||
| @@ -656,6 +660,7 @@ namespace Discord | |||
| } | |||
| catch | |||
| { | |||
| await Disconnect().ConfigureAwait(false); | |||
| throw; | |||
| } | |||
| @@ -13,5 +13,26 @@ namespace Discord.Helpers | |||
| CompletedTask = Task.Delay(0); | |||
| #endif | |||
| } | |||
| public static async Task Timeout(this Task self, int milliseconds) | |||
| { | |||
| Task timeoutTask = Task.Delay(milliseconds); | |||
| Task finishedTask = await Task.WhenAny(self, timeoutTask); | |||
| if (finishedTask == timeoutTask) | |||
| { | |||
| throw new TimeoutException(); | |||
| } | |||
| else | |||
| await self; | |||
| } | |||
| public static async Task<T> Timeout<T>(this Task<T> self, int milliseconds) | |||
| { | |||
| Task timeoutTask = Task.Delay(milliseconds); | |||
| Task finishedTask = await Task.WhenAny(self, timeoutTask).ConfigureAwait(false); | |||
| if (finishedTask == timeoutTask) | |||
| throw new TimeoutException(); | |||
| else | |||
| return await self.ConfigureAwait(false); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using System; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Net.API | |||
| @@ -21,6 +22,12 @@ namespace Discord.Net.API | |||
| get { return _token; } | |||
| set { _token = value; _rest.SetToken(value); } | |||
| } | |||
| private CancellationToken _cancelToken; | |||
| public CancellationToken CancelToken | |||
| { | |||
| get { return _cancelToken; } | |||
| set { _cancelToken = value; _rest.SetCancelToken(value); } | |||
| } | |||
| //Auth | |||
| public Task<Responses.Gateway> GetWebSocketEndpoint() | |||
| @@ -191,5 +198,15 @@ namespace Discord.Net.API | |||
| var request = new Requests.ChangeAvatar { Avatar = $"data:{type},/9j/{base64}", CurrentEmail = currentEmail, CurrentPassword = currentPassword }; | |||
| return _rest.Patch<Responses.ChangeProfile>(Endpoints.UserMe, request); | |||
| } | |||
| //Other | |||
| /*public Task<Responses.Status> GetUnresolvedIncidents() | |||
| { | |||
| return _rest.Get<Responses.Status>(Endpoints.StatusUnresolvedMaintenance); | |||
| } | |||
| public Task<Responses.Status> GetActiveIncidents() | |||
| { | |||
| return _rest.Get<Responses.Status>(Endpoints.StatusActiveMaintenance); | |||
| }*/ | |||
| } | |||
| } | |||
| @@ -1,7 +1,8 @@ | |||
| namespace Discord.Net.API | |||
| { | |||
| internal static class Endpoints | |||
| { | |||
| { | |||
| public const string BaseStatusApi = "https://status.discordapp.com/api/v2/"; | |||
| public const string BaseApi = "https://discordapp.com/api/"; | |||
| //public const string Track = "track"; | |||
| public const string Gateway = "gateway"; | |||
| @@ -41,5 +42,8 @@ | |||
| public const string Voice = "voice"; | |||
| public const string VoiceRegions = "voice/regions"; | |||
| public const string VoiceIce = "voice/ice"; | |||
| } | |||
| public const string StatusActiveMaintenance = "scheduled-maintenances/active.json"; | |||
| public const string StatusUnresolvedMaintenance = "scheduled-maintenances/unresolved.json"; | |||
| } | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| using System; | |||
| using System.Net; | |||
| namespace Discord.Net | |||
| namespace Discord.Net.API | |||
| { | |||
| public class HttpException : Exception | |||
| { | |||
| @@ -7,8 +7,7 @@ using System.Threading.Tasks; | |||
| namespace Discord.Net.WebSockets | |||
| { | |||
| internal partial class DataWebSocket : WebSocket | |||
| { | |||
| private string _redirectServer; | |||
| { | |||
| private int _lastSeq; | |||
| public string SessionId => _sessionId; | |||
| @@ -18,29 +17,25 @@ namespace Discord.Net.WebSockets | |||
| : base(client) | |||
| { | |||
| } | |||
| public async Task Login(string host, string token, CancellationToken cancelToken) | |||
| public async Task Login(string token) | |||
| { | |||
| await base.Connect(host, cancelToken); | |||
| await Connect(); | |||
| Commands.Login msg = new Commands.Login(); | |||
| msg.Payload.Token = token; | |||
| msg.Payload.Properties["$device"] = "Discord.Net"; | |||
| QueueMessage(msg); | |||
| } | |||
| protected override Task[] Run() | |||
| private async Task Redirect(string server) | |||
| { | |||
| //Send resume session if we were transferred | |||
| if (_redirectServer != null) | |||
| { | |||
| var resumeMsg = new Commands.Resume(); | |||
| resumeMsg.Payload.SessionId = _sessionId; | |||
| resumeMsg.Payload.Sequence = _lastSeq; | |||
| QueueMessage(resumeMsg); | |||
| _redirectServer = null; | |||
| } | |||
| return base.Run(); | |||
| await DisconnectInternal(isUnexpected: false); | |||
| await Connect(); | |||
| var resumeMsg = new Commands.Resume(); | |||
| resumeMsg.Payload.SessionId = _sessionId; | |||
| resumeMsg.Payload.Sequence = _lastSeq; | |||
| QueueMessage(resumeMsg); | |||
| } | |||
| protected override async Task ProcessMessage(string json) | |||
| @@ -54,27 +49,31 @@ namespace Discord.Net.WebSockets | |||
| case 0: | |||
| { | |||
| JToken token = msg.Payload as JToken; | |||
| if (msg.Type == "READY") | |||
| if (msg.Type == "READY") | |||
| { | |||
| var payload = token.ToObject<Events.Ready>(); | |||
| _sessionId = payload.SessionId; | |||
| _heartbeatInterval = payload.HeartbeatInterval; | |||
| QueueMessage(new Commands.UpdateStatus()); | |||
| } | |||
| else if (msg.Type == "RESUMED") | |||
| { | |||
| var payload = token.ToObject<Events.Resumed>(); | |||
| _heartbeatInterval = payload.HeartbeatInterval; | |||
| QueueMessage(new Commands.UpdateStatus()); | |||
| } | |||
| RaiseReceivedEvent(msg.Type, token); | |||
| if (msg.Type == "READY") | |||
| if (msg.Type == "READY" || msg.Type == "RESUMED") | |||
| CompleteConnect(); | |||
| /*if (_logLevel >= LogMessageSeverity.Info) | |||
| RaiseOnLog(LogMessageSeverity.Info, "Got Event: " + msg.Type);*/ | |||
| } | |||
| break; | |||
| case 7: //Redirect | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<Events.Redirect>(); | |||
| _host = payload.Url; | |||
| Host = payload.Url; | |||
| if (_logLevel >= LogMessageSeverity.Info) | |||
| RaiseOnLog(LogMessageSeverity.Info, "Redirected to " + payload.Url); | |||
| await DisconnectInternal(new Exception("Server is redirecting."), true); | |||
| await Redirect(payload.Url); | |||
| } | |||
| break; | |||
| default: | |||
| @@ -36,6 +36,11 @@ namespace Discord.Net.WebSockets | |||
| [JsonProperty(PropertyName = "heartbeat_interval")] | |||
| public int HeartbeatInterval; | |||
| } | |||
| public sealed class Resumed | |||
| { | |||
| [JsonProperty(PropertyName = "heartbeat_interval")] | |||
| public int HeartbeatInterval; | |||
| } | |||
| public sealed class Redirect | |||
| { | |||
| @@ -53,13 +53,12 @@ namespace Discord.Net.WebSockets | |||
| _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | |||
| } | |||
| public async Task Login(string host, string serverId, string userId, string sessionId, string token, CancellationToken cancelToken) | |||
| public async Task Login(string serverId, string userId, string sessionId, string token, CancellationToken cancelToken) | |||
| { | |||
| if (_serverId == serverId && _userId == userId && _sessionId == sessionId && _token == token) | |||
| { | |||
| //Adjust the host and tell the system to reconnect | |||
| _host = host; | |||
| await DisconnectInternal(new Exception("Server transfer occurred.")); | |||
| await DisconnectInternal(new Exception("Server transfer occurred."), isUnexpected: false); | |||
| return; | |||
| } | |||
| @@ -68,18 +67,19 @@ namespace Discord.Net.WebSockets | |||
| _sessionId = sessionId; | |||
| _token = token; | |||
| await Connect(host, cancelToken); | |||
| await Connect(); | |||
| } | |||
| public async Task Reconnect(CancellationToken cancelToken) | |||
| public async Task Reconnect() | |||
| { | |||
| try | |||
| { | |||
| await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||
| var cancelToken = ParentCancelToken; | |||
| await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| try | |||
| { | |||
| await Connect(_host, cancelToken).ConfigureAwait(false); | |||
| await Connect().ConfigureAwait(false); | |||
| break; | |||
| } | |||
| catch (OperationCanceledException) { throw; } | |||
| @@ -295,7 +295,7 @@ namespace Discord.Net.WebSockets | |||
| var payload = (msg.Payload as JToken).ToObject<VoiceEvents.Ready>(); | |||
| _heartbeatInterval = payload.HeartbeatInterval; | |||
| _ssrc = payload.SSRC; | |||
| _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(_host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port); | |||
| _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port); | |||
| //_mode = payload.Modes.LastOrDefault(); | |||
| _isEncrypted = !payload.Modes.Contains("plain"); | |||
| _udp.Connect(_endpoint); | |||
| @@ -38,7 +38,8 @@ namespace Discord.Net.WebSockets | |||
| protected readonly DiscordClient _client; | |||
| protected readonly LogMessageSeverity _logLevel; | |||
| protected string _host; | |||
| public string Host { get; set; } | |||
| protected int _loginTimeout, _heartbeatInterval; | |||
| private DateTime _lastHeartbeat; | |||
| private Task _runTask; | |||
| @@ -49,6 +50,7 @@ namespace Discord.Net.WebSockets | |||
| protected ExceptionDispatchInfo _disconnectReason; | |||
| private bool _wasDisconnectUnexpected; | |||
| public CancellationToken ParentCancelToken { get; set; } | |||
| public CancellationToken CancelToken => _cancelToken; | |||
| private CancellationTokenSource _cancelTokenSource; | |||
| protected CancellationToken _cancelToken; | |||
| @@ -69,22 +71,24 @@ namespace Discord.Net.WebSockets | |||
| }; | |||
| } | |||
| protected virtual async Task Connect(string host, CancellationToken cancelToken) | |||
| protected virtual async Task Connect() | |||
| { | |||
| if (_state != (int)WebSocketState.Disconnected) | |||
| throw new InvalidOperationException("Client is already connected or connecting to the server."); | |||
| try | |||
| try | |||
| { | |||
| await Disconnect().ConfigureAwait(false); | |||
| _state = (int)WebSocketState.Connecting; | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token; | |||
| if (ParentCancelToken != null) | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken).Token; | |||
| else | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| await _engine.Connect(host, _cancelToken).ConfigureAwait(false); | |||
| _host = host; | |||
| await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||
| _lastHeartbeat = DateTime.UtcNow; | |||
| _runTask = RunTasks(); | |||
| @@ -0,0 +1,16 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| public sealed class TimeoutException : Exception | |||
| { | |||
| internal TimeoutException() | |||
| : base("An operation has timed out.") | |||
| { | |||
| } | |||
| } | |||
| } | |||