@@ -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.") | |||
{ | |||
} | |||
} | |||
} |