@@ -3,6 +3,6 @@ | |||
"sdk": { | |||
"version": "1.0.0-beta6", | |||
"architecture": "x64", | |||
"runtime": "coreclr" | |||
"runtime": "clr" | |||
} | |||
} |
@@ -58,11 +58,17 @@ | |||
<Compile Include="..\Discord.Net\API\Models\Common.cs"> | |||
<Link>API\Models\Common.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Models\WebSocketCommands.cs"> | |||
<Link>API\Models\WebSocketCommands.cs</Link> | |||
<Compile Include="..\Discord.Net\API\Models\TextWebSocketCommands.cs"> | |||
<Link>API\Models\TextWebSocketCommands.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Models\WebSocketEvents.cs"> | |||
<Link>API\Models\WebSocketEvents.cs</Link> | |||
<Compile Include="..\Discord.Net\API\Models\TextWebSocketEvents.cs"> | |||
<Link>API\Models\TextWebSocketEvents.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Models\VoiceWebSocketCommands.cs"> | |||
<Link>API\Models\VoiceWebSocketCommands.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\API\Models\VoiceWebSocketEvents.cs"> | |||
<Link>API\Models\VoiceWebSocketEvents.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Channel.cs"> | |||
<Link>Channel.cs</Link> | |||
@@ -79,6 +85,15 @@ | |||
<Compile Include="..\Discord.Net\DiscordClientConfig.cs"> | |||
<Link>DiscordClientConfig.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\DiscordTextWebSocket.cs"> | |||
<Link>DiscordTextWebSocket.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\DiscordTextWebSocket.Events.cs"> | |||
<Link>DiscordTextWebSocket.Events.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\DiscordVoiceWebSocket.cs"> | |||
<Link>DiscordVoiceWebSocket.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\DiscordWebSocket.cs"> | |||
<Link>DiscordWebSocket.cs</Link> | |||
</Compile> | |||
@@ -91,6 +106,9 @@ | |||
<Compile Include="..\Discord.Net\Helpers\Http.cs"> | |||
<Link>Helpers\Http.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\HttpException.cs"> | |||
<Link>HttpException.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Invite.cs"> | |||
<Link>Invite.cs</Link> | |||
</Compile> | |||
@@ -115,7 +133,6 @@ | |||
<Compile Include="..\Discord.Net\User.cs"> | |||
<Link>User.cs</Link> | |||
</Compile> | |||
<Compile Include="HttpException.cs" /> | |||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||
</ItemGroup> | |||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||
@@ -1,16 +0,0 @@ | |||
using System; | |||
using System.Net; | |||
namespace Discord | |||
{ | |||
public class HttpException : Exception | |||
{ | |||
public HttpStatusCode StatusCode { get; } | |||
public HttpException(HttpStatusCode statusCode) | |||
: base($"The server responded with error {statusCode}") | |||
{ | |||
StatusCode = statusCode; | |||
} | |||
} | |||
} |
@@ -8,7 +8,7 @@ using System.Collections.Generic; | |||
namespace Discord.API.Models | |||
{ | |||
internal static class WebSocketCommands | |||
internal static class TextWebSocketCommands | |||
{ | |||
public sealed class KeepAlive : WebSocketMessage<int> | |||
{ |
@@ -3,11 +3,10 @@ | |||
#pragma warning disable CS0169 | |||
using Newtonsoft.Json; | |||
using System; | |||
namespace Discord.API.Models | |||
{ | |||
internal static class WebSocketEvents | |||
internal static class TextWebSocketEvents | |||
{ | |||
public sealed class Ready | |||
{ |
@@ -0,0 +1,53 @@ | |||
//Ignore unused/unassigned variable warnings | |||
#pragma warning disable CS0649 | |||
#pragma warning disable CS0169 | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Generic; | |||
namespace Discord.API.Models | |||
{ | |||
internal static class VoiceWebSocketCommands | |||
{ | |||
public sealed class KeepAlive : WebSocketMessage<object> | |||
{ | |||
public KeepAlive() : base(3, null) { } | |||
} | |||
public sealed class Login : WebSocketMessage<Login.Data> | |||
{ | |||
public Login() : base(0) { } | |||
public class Data | |||
{ | |||
[JsonProperty(PropertyName = "server_id")] | |||
public string ServerId; | |||
[JsonProperty(PropertyName = "user_id")] | |||
public string UserId; | |||
[JsonProperty(PropertyName = "session_id")] | |||
public string SessionId; | |||
[JsonProperty(PropertyName = "token")] | |||
public string Token; | |||
} | |||
} | |||
public sealed class Login2 : WebSocketMessage<Login.Data> | |||
{ | |||
public Login2() : base(1) { } | |||
public class Data | |||
{ | |||
public class PCData | |||
{ | |||
[JsonProperty(PropertyName = "address")] | |||
public string Address; | |||
[JsonProperty(PropertyName = "port")] | |||
public int Port; | |||
[JsonProperty(PropertyName = "mode")] | |||
public string Mode = "xsalsa20_poly1305"; | |||
} | |||
[JsonProperty(PropertyName = "protocol")] | |||
public string Protocol = "udp"; | |||
[JsonProperty(PropertyName = "token")] | |||
public string Token; | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,12 @@ | |||
//Ignore unused/unassigned variable warnings | |||
#pragma warning disable CS0649 | |||
#pragma warning disable CS0169 | |||
using Newtonsoft.Json; | |||
namespace Discord.API.Models | |||
{ | |||
internal static class VoiceWebSocketEvents | |||
{ | |||
} | |||
} |
@@ -16,6 +16,12 @@ namespace Discord | |||
if (DebugMessage != null) | |||
DebugMessage(this, new LogMessageEventArgs(message)); | |||
} | |||
public event EventHandler<LogMessageEventArgs> VoiceDebugMessage; | |||
private void RaiseOnVoiceDebugMessage(string message) | |||
{ | |||
if (VoiceDebugMessage != null) | |||
VoiceDebugMessage(this, new LogMessageEventArgs(message)); | |||
} | |||
//General | |||
public event EventHandler Connected; | |||
@@ -17,16 +17,22 @@ namespace Discord | |||
/// <summary> Provides a connection to the DiscordApp service. </summary> | |||
public partial class DiscordClient | |||
{ | |||
private DiscordClientConfig _config; | |||
private DiscordWebSocket _webSocket; | |||
private ManualResetEventSlim _blockEvent; | |||
private volatile CancellationTokenSource _disconnectToken; | |||
private volatile Task _tasks; | |||
private readonly DiscordClientConfig _config; | |||
private readonly DiscordTextWebSocket _webSocket; | |||
private readonly ManualResetEventSlim _blockEvent; | |||
private readonly Regex _userRegex, _channelRegex; | |||
private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator; | |||
private readonly JsonSerializer _serializer; | |||
private readonly Random _rand; | |||
private volatile CancellationTokenSource _disconnectToken; | |||
private volatile Task _tasks; | |||
#if !DNXCORE50 | |||
private readonly DiscordVoiceWebSocket _voiceWebSocket; | |||
private string _currentVoiceServer; | |||
#endif | |||
/// <summary> Returns the User object for the current logged in user. </summary> | |||
public User User { get; private set; } | |||
/// <summary> Returns the id of the current logged in user. </summary> | |||
@@ -276,7 +282,7 @@ namespace Discord | |||
user => { } | |||
); | |||
_webSocket = new DiscordWebSocket(_config.WebSocketInterval); | |||
_webSocket = new DiscordTextWebSocket(_config.WebSocketInterval); | |||
_webSocket.Connected += (s, e) => RaiseConnected(); | |||
_webSocket.Disconnected += async (s, e) => | |||
{ | |||
@@ -287,7 +293,8 @@ namespace Discord | |||
try | |||
{ | |||
await Task.Delay(_config.ReconnectDelay); | |||
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, true); | |||
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub); | |||
await _webSocket.Login(); | |||
break; | |||
} | |||
catch (Exception ex) | |||
@@ -298,6 +305,35 @@ namespace Discord | |||
} | |||
} | |||
}; | |||
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | |||
#if !DNXCORE50 | |||
_voiceWebSocket = new DiscordVoiceWebSocket(_config.WebSocketInterval); | |||
_voiceWebSocket.Connected += (s, e) => RaiseConnected(); | |||
_voiceWebSocket.Disconnected += async (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); | |||
await _voiceWebSocket.Login(_currentVoiceServer, UserId, SessionId); | |||
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); | |||
} | |||
} | |||
}; | |||
_voiceWebSocket.OnDebugMessage += (s, e) => RaiseOnVoiceDebugMessage(e.Message); | |||
#endif | |||
_webSocket.GotEvent += (s, e) => | |||
{ | |||
switch (e.Type) | |||
@@ -305,7 +341,7 @@ namespace Discord | |||
//Global | |||
case "READY": //Resync | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.Ready>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.Ready>(_serializer); | |||
_servers.Clear(); | |||
_channels.Clear(); | |||
@@ -324,21 +360,21 @@ namespace Discord | |||
//Servers | |||
case "GUILD_CREATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildCreate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildCreate>(_serializer); | |||
var server = _servers.Update(data.Id, data); | |||
try { RaiseServerCreated(server); } catch { } | |||
} | |||
break; | |||
case "GUILD_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildUpdate>(_serializer); | |||
var server = _servers.Update(data.Id, data); | |||
try { RaiseServerUpdated(server); } catch { } | |||
} | |||
break; | |||
case "GUILD_DELETE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildDelete>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildDelete>(_serializer); | |||
var server = _servers.Remove(data.Id); | |||
if (server != null) | |||
try { RaiseServerDestroyed(server); } catch { } | |||
@@ -348,21 +384,21 @@ namespace Discord | |||
//Channels | |||
case "CHANNEL_CREATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.ChannelCreate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.ChannelCreate>(_serializer); | |||
var channel = _channels.Update(data.Id, data.GuildId, data); | |||
try { RaiseChannelCreated(channel); } catch { } | |||
} | |||
break; | |||
case "CHANNEL_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.ChannelUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.ChannelUpdate>(_serializer); | |||
var channel = _channels.Update(data.Id, data.GuildId, data); | |||
try { RaiseChannelUpdated(channel); } catch { } | |||
} | |||
break; | |||
case "CHANNEL_DELETE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.ChannelDelete>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.ChannelDelete>(_serializer); | |||
var channel = _channels.Remove(data.Id); | |||
if (channel != null) | |||
try { RaiseChannelDestroyed(channel); } catch { } | |||
@@ -372,7 +408,7 @@ namespace Discord | |||
//Members | |||
case "GUILD_MEMBER_ADD": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildMemberAdd>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildMemberAdd>(_serializer); | |||
var user = _users.Update(data.User.Id, data.User); | |||
var server = _servers[data.ServerId]; | |||
if (server != null) | |||
@@ -384,7 +420,7 @@ namespace Discord | |||
break; | |||
case "GUILD_MEMBER_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildMemberUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildMemberUpdate>(_serializer); | |||
var user = _users.Update(data.User.Id, data.User); | |||
var server = _servers[data.ServerId]; | |||
if (server != null) | |||
@@ -396,7 +432,7 @@ namespace Discord | |||
break; | |||
case "GUILD_MEMBER_REMOVE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildMemberRemove>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildMemberRemove>(_serializer); | |||
var server = _servers[data.ServerId]; | |||
if (server != null) | |||
{ | |||
@@ -410,21 +446,21 @@ namespace Discord | |||
//Roles | |||
case "GUILD_ROLE_CREATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildRoleCreateUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildRoleCreateUpdate>(_serializer); | |||
var role = _roles.Update(data.Role.Id, data.ServerId, data.Role); | |||
try { RaiseRoleCreated(role); } catch { } | |||
} | |||
break; | |||
case "GUILD_ROLE_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildRoleCreateUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildRoleCreateUpdate>(_serializer); | |||
var role = _roles.Update(data.Role.Id, data.ServerId, data.Role); | |||
try { RaiseRoleUpdated(role); } catch { } | |||
} | |||
break; | |||
case "GUILD_ROLE_DELETE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildRoleDelete>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildRoleDelete>(_serializer); | |||
var role = _roles.Remove(data.RoleId); | |||
if (role != null) | |||
try { RaiseRoleDeleted(role); } catch { } | |||
@@ -434,7 +470,7 @@ namespace Discord | |||
//Bans | |||
case "GUILD_BAN_ADD": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildBanAddRemove>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildBanAddRemove>(_serializer); | |||
var user = _users.Update(data.User.Id, data.User); | |||
var server = _servers[data.ServerId]; | |||
try { RaiseBanAdded(user, server); } catch { } | |||
@@ -442,7 +478,7 @@ namespace Discord | |||
break; | |||
case "GUILD_BAN_REMOVE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildBanAddRemove>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.GuildBanAddRemove>(_serializer); | |||
var user = _users.Update(data.User.Id, data.User); | |||
var server = _servers[data.ServerId]; | |||
if (server != null && server.RemoveBan(user.Id)) | |||
@@ -455,7 +491,7 @@ namespace Discord | |||
//Messages | |||
case "MESSAGE_CREATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.MessageCreate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.MessageCreate>(_serializer); | |||
Message msg = null; | |||
bool wasLocal = _config.UseMessageQueue && data.Author.Id == UserId && data.Nonce != null; | |||
if (wasLocal) | |||
@@ -478,14 +514,14 @@ namespace Discord | |||
break; | |||
case "MESSAGE_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.MessageUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.MessageUpdate>(_serializer); | |||
var msg = _messages.Update(data.Id, data.ChannelId, data); | |||
try { RaiseMessageUpdated(msg); } catch { } | |||
} | |||
break; | |||
case "MESSAGE_DELETE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.MessageDelete>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.MessageDelete>(_serializer); | |||
var msg = GetMessage(data.MessageId); | |||
if (msg != null) | |||
{ | |||
@@ -496,7 +532,7 @@ namespace Discord | |||
break; | |||
case "MESSAGE_ACK": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.MessageAck>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.MessageAck>(_serializer); | |||
var msg = GetMessage(data.MessageId); | |||
if (msg != null) | |||
try { RaiseMessageRead(msg); } catch { } | |||
@@ -506,7 +542,7 @@ namespace Discord | |||
//Statuses | |||
case "PRESENCE_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.PresenceUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.PresenceUpdate>(_serializer); | |||
var user = _users.Update(data.User.Id, data.User); | |||
var server = _servers[data.ServerId]; | |||
if (server != null) | |||
@@ -518,7 +554,7 @@ namespace Discord | |||
break; | |||
case "VOICE_STATE_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.VoiceStateUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.VoiceStateUpdate>(_serializer); | |||
var server = _servers[data.ServerId]; | |||
if (server != null) | |||
{ | |||
@@ -530,7 +566,7 @@ namespace Discord | |||
break; | |||
case "TYPING_START": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.TypingStart>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.TypingStart>(_serializer); | |||
var channel = _channels[data.ChannelId]; | |||
var user = _users[data.UserId]; | |||
if (user != null) | |||
@@ -545,7 +581,7 @@ namespace Discord | |||
//Voice | |||
case "VOICE_SERVER_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.VoiceServerUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.VoiceServerUpdate>(_serializer); | |||
var server = _servers[data.ServerId]; | |||
try { RaiseVoiceServerUpdated(server, data.Endpoint); } catch { } | |||
} | |||
@@ -554,7 +590,7 @@ namespace Discord | |||
//Settings | |||
case "USER_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.UserUpdate>(_serializer); | |||
var data = e.Event.ToObject<TextWebSocketEvents.UserUpdate>(_serializer); | |||
var user = _users.Update(data.Id, data); | |||
try { RaiseUserUpdated(user); } catch { } | |||
} | |||
@@ -571,7 +607,6 @@ namespace Discord | |||
break; | |||
} | |||
}; | |||
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | |||
} | |||
private async Task SendAsync() | |||
@@ -822,8 +857,9 @@ namespace Discord | |||
{ | |||
try | |||
{ | |||
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub); | |||
Http.Token = token; | |||
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, true); | |||
await _webSocket.Login(); | |||
success = true; | |||
} | |||
catch (InvalidOperationException) //Bad Token | |||
@@ -835,27 +871,27 @@ namespace Discord | |||
if (!success && password != null) //Email/Password login | |||
{ | |||
//Open websocket while we wait for login response | |||
Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false); | |||
Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub); | |||
var response = await DiscordAPI.Login(emailOrUsername, password); | |||
await socketTask; | |||
//Wait for websocket to finish connecting, then send token | |||
token = response.Token; | |||
Http.Token = token; | |||
_webSocket.Login(); | |||
await _webSocket.Login(); | |||
success = true; | |||
} | |||
if (!success && password == null) //Anonymous login | |||
{ | |||
//Open websocket while we wait for login response | |||
Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false); | |||
Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub); | |||
var response = await DiscordAPI.LoginAnonymous(emailOrUsername); | |||
await socketTask; | |||
//Wait for websocket to finish connecting, then send token | |||
token = response.Token; | |||
Http.Token = token; | |||
_webSocket.Login(); | |||
await _webSocket.Login(); | |||
success = true; | |||
} | |||
if (success) | |||
@@ -868,6 +904,9 @@ namespace Discord | |||
_tasks = _tasks.ContinueWith(async x => | |||
{ | |||
await _webSocket.DisconnectAsync(); | |||
#if !DNXCORE50 | |||
await _voiceWebSocket.DisconnectAsync(); | |||
#endif | |||
//Clear send queue | |||
Message ignored; | |||
@@ -0,0 +1,25 @@ | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
namespace Discord | |||
{ | |||
internal partial class DiscordTextWebSocket | |||
{ | |||
public event EventHandler<MessageEventArgs> GotEvent; | |||
public sealed class MessageEventArgs : EventArgs | |||
{ | |||
public readonly string Type; | |||
public readonly JToken Event; | |||
internal MessageEventArgs(string type, JToken data) | |||
{ | |||
Type = type; | |||
Event = data; | |||
} | |||
} | |||
private void RaiseGotEvent(string type, JToken payload) | |||
{ | |||
if (GotEvent != null) | |||
GotEvent(this, new MessageEventArgs(type, payload)); | |||
} | |||
} | |||
} |
@@ -0,0 +1,82 @@ | |||
using Discord.API.Models; | |||
using Discord.Helpers; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
internal sealed partial class DiscordTextWebSocket : DiscordWebSocket | |||
{ | |||
private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event | |||
private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | |||
public DiscordTextWebSocket(int interval) | |||
: base(interval) | |||
{ | |||
_connectWaitOnLogin = new ManualResetEventSlim(false); | |||
_connectWaitOnLogin2 = new ManualResetEventSlim(false); | |||
} | |||
public async Task Login() | |||
{ | |||
var cancelToken = _disconnectToken.Token; | |||
_connectWaitOnLogin.Reset(); | |||
_connectWaitOnLogin2.Reset(); | |||
TextWebSocketCommands.Login msg = new TextWebSocketCommands.Login(); | |||
msg.Payload.Token = Http.Token; | |||
msg.Payload.Properties["$os"] = ""; | |||
msg.Payload.Properties["$browser"] = ""; | |||
msg.Payload.Properties["$device"] = "Discord.Net"; | |||
msg.Payload.Properties["$referrer"] = ""; | |||
msg.Payload.Properties["$referring_domain"] = ""; | |||
await SendMessage(msg, cancelToken); | |||
try | |||
{ | |||
if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message | |||
throw new Exception("No reply from Discord server"); | |||
} | |||
catch (OperationCanceledException) | |||
{ | |||
throw new InvalidOperationException("Bad Token"); | |||
} | |||
try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler | |||
catch (OperationCanceledException) { return; } | |||
SetConnected(); | |||
} | |||
protected override void ProcessMessage(WebSocketMessage msg) | |||
{ | |||
switch (msg.Operation) | |||
{ | |||
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 | |||
} | |||
RaiseGotEvent(msg.Type, msg.Payload as JToken); | |||
if (msg.Type == "READY") | |||
_connectWaitOnLogin2.Set(); //Post-Event | |||
break; | |||
default: | |||
RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | |||
break; | |||
} | |||
} | |||
protected override WebSocketMessage GetKeepAlive() | |||
{ | |||
return new TextWebSocketCommands.KeepAlive(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,125 @@ | |||
#if !DNXCORE50 | |||
using Discord.API.Models; | |||
using Discord.Helpers; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Linq; | |||
using System.Net.Sockets; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
internal sealed partial class DiscordVoiceWebSocket : DiscordWebSocket | |||
{ | |||
private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event | |||
private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | |||
private UdpClient _udp; | |||
private ConcurrentQueue<byte[]> _sendQueue; | |||
public DiscordVoiceWebSocket(int interval) | |||
: base(interval) | |||
{ | |||
_connectWaitOnLogin = new ManualResetEventSlim(false); | |||
_connectWaitOnLogin2 = new ManualResetEventSlim(false); | |||
_udp = new UdpClient(); | |||
_sendQueue = new ConcurrentQueue<byte[]>(); | |||
} | |||
protected override Task[] CreateTasks(CancellationToken cancelToken) | |||
{ | |||
return new Task[] | |||
{ | |||
Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | |||
Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | |||
Task.Factory.StartNew(WatcherAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result | |||
}.Concat(base.CreateTasks(cancelToken)).ToArray(); | |||
} | |||
public async Task Login(string serverId, string userId, string sessionId) | |||
{ | |||
var cancelToken = _disconnectToken.Token; | |||
_connectWaitOnLogin.Reset(); | |||
_connectWaitOnLogin2.Reset(); | |||
string ip = await Http.Get("http://ipinfo.io/ip"); | |||
VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); | |||
msg.Payload.Token = Http.Token; | |||
msg.Payload.ServerId = serverId; | |||
msg.Payload.UserId = userId; | |||
msg.Payload.SessionId = sessionId; | |||
await SendMessage(msg, cancelToken); | |||
try | |||
{ | |||
if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message | |||
throw new Exception("No reply from Discord server"); | |||
} | |||
catch (OperationCanceledException) | |||
{ | |||
throw new InvalidOperationException("Bad Token"); | |||
} | |||
try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler | |||
catch (OperationCanceledException) { return; } | |||
SetConnected(); | |||
} | |||
private async Task ReceiveAsync() | |||
{ | |||
var cancelToken = _disconnectToken.Token; | |||
try | |||
{ | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
var result = await _udp.ReceiveAsync(); | |||
ProcessUdpMessage(result); | |||
} | |||
} | |||
catch { } | |||
finally { _disconnectToken.Cancel(); } | |||
} | |||
private async Task SendAsync() | |||
{ | |||
var cancelToken = _disconnectToken.Token; | |||
try | |||
{ | |||
byte[] bytes; | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
while (_sendQueue.TryDequeue(out bytes)) | |||
await SendMessage(bytes, cancelToken); | |||
await Task.Delay(_sendInterval); | |||
} | |||
} | |||
catch { } | |||
finally { _disconnectToken.Cancel(); } | |||
} | |||
private async Task WatcherAsync() | |||
{ | |||
try | |||
{ | |||
await Task.Delay(-1, _disconnectToken.Token); | |||
} | |||
catch (OperationCanceledException) { } | |||
_udp.Close(); | |||
} | |||
protected override void ProcessMessage(WebSocketMessage msg) | |||
{ | |||
} | |||
private void ProcessUdpMessage(UdpReceiveResult msg) | |||
{ | |||
} | |||
protected override WebSocketMessage GetKeepAlive() | |||
{ | |||
return new VoiceWebSocketCommands.KeepAlive(); | |||
} | |||
} | |||
} | |||
#endif |
@@ -1,9 +1,8 @@ | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System; | |||
namespace Discord | |||
{ | |||
internal partial class DiscordWebSocket | |||
internal abstract partial class DiscordWebSocket | |||
{ | |||
public event EventHandler Connected; | |||
private void RaiseConnected() | |||
@@ -19,25 +18,8 @@ namespace Discord | |||
Disconnected(this, EventArgs.Empty); | |||
} | |||
public event EventHandler<MessageEventArgs> GotEvent; | |||
public sealed class MessageEventArgs : EventArgs | |||
{ | |||
public readonly string Type; | |||
public readonly JToken Event; | |||
internal MessageEventArgs(string type, JToken data) | |||
{ | |||
Type = type; | |||
Event = data; | |||
} | |||
} | |||
private void RaiseGotEvent(string type, JToken payload) | |||
{ | |||
if (GotEvent != null) | |||
GotEvent(this, new MessageEventArgs(type, payload)); | |||
} | |||
public event EventHandler<DiscordClient.LogMessageEventArgs> OnDebugMessage; | |||
private void RaiseOnDebugMessage(string message) | |||
protected void RaiseOnDebugMessage(string message) | |||
{ | |||
if (OnDebugMessage != null) | |||
OnDebugMessage(this, new DiscordClient.LogMessageEventArgs(message)); | |||
@@ -1,5 +1,4 @@ | |||
using Discord.API.Models; | |||
using Discord.Helpers; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
@@ -11,31 +10,29 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
internal sealed partial class DiscordWebSocket : IDisposable | |||
internal abstract partial class DiscordWebSocket : IDisposable | |||
{ | |||
private const int ReceiveChunkSize = 4096; | |||
private const int SendChunkSize = 4096; | |||
private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event | |||
protected volatile CancellationTokenSource _disconnectToken; | |||
protected int _heartbeatInterval; | |||
protected readonly int _sendInterval; | |||
private volatile ClientWebSocket _webSocket; | |||
private volatile CancellationTokenSource _disconnectToken; | |||
private volatile Task _tasks; | |||
private ConcurrentQueue<byte[]> _sendQueue; | |||
private int _heartbeatInterval, _sendInterval; | |||
private DateTime _lastHeartbeat; | |||
private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | |||
private bool _isConnected; | |||
public DiscordWebSocket(int interval) | |||
{ | |||
_sendInterval = interval; | |||
_connectWaitOnLogin = new ManualResetEventSlim(false); | |||
_connectWaitOnLogin2 = new ManualResetEventSlim(false); | |||
_sendQueue = new ConcurrentQueue<byte[]>(); | |||
} | |||
public async Task ConnectAsync(string url, bool autoLogin) | |||
public async Task ConnectAsync(string url) | |||
{ | |||
await DisconnectAsync(); | |||
@@ -46,9 +43,7 @@ namespace Discord | |||
_webSocket.Options.KeepAliveInterval = TimeSpan.Zero; | |||
await _webSocket.ConnectAsync(new Uri(url), cancelToken); | |||
_tasks = Task.WhenAll( | |||
await Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default), | |||
await Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default)) | |||
_tasks = Task.WhenAll(CreateTasks(cancelToken)) | |||
.ContinueWith(x => | |||
{ | |||
//Do not clean up until both tasks have ended | |||
@@ -71,48 +66,29 @@ namespace Discord | |||
_tasks = null; | |||
}); | |||
if (autoLogin) | |||
Login(); | |||
} | |||
public void Login() | |||
} | |||
public async Task DisconnectAsync() | |||
{ | |||
var cancelToken = _disconnectToken.Token; | |||
_connectWaitOnLogin.Reset(); | |||
_connectWaitOnLogin2.Reset(); | |||
WebSocketCommands.Login msg = new WebSocketCommands.Login(); | |||
msg.Payload.Token = Http.Token; | |||
msg.Payload.Properties["$os"] = ""; | |||
msg.Payload.Properties["$browser"] = ""; | |||
msg.Payload.Properties["$device"] = "Discord.Net"; | |||
msg.Payload.Properties["$referrer"] = ""; | |||
msg.Payload.Properties["$referring_domain"] = ""; | |||
SendMessage(msg, _disconnectToken.Token); | |||
try | |||
{ | |||
if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message | |||
throw new Exception("No reply from Discord server"); | |||
} | |||
catch (OperationCanceledException) | |||
if (_tasks != null) | |||
{ | |||
throw new InvalidOperationException("Bad Token"); | |||
try { _disconnectToken.Cancel(); } catch (NullReferenceException) { } | |||
try { await _tasks; } catch (NullReferenceException) { } | |||
} | |||
try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler | |||
catch (OperationCanceledException) { return; } | |||
} | |||
protected void SetConnected() | |||
{ | |||
_isConnected = true; | |||
RaiseConnected(); | |||
} | |||
public async Task DisconnectAsync() | |||
protected virtual Task[] CreateTasks(CancellationToken cancelToken) | |||
{ | |||
if (_tasks != null) | |||
return new Task[] | |||
{ | |||
try { _disconnectToken.Cancel(); } catch (NullReferenceException) { } | |||
try { await _tasks; } catch (NullReferenceException) { } | |||
} | |||
Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, | |||
Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result | |||
}; | |||
} | |||
private async Task ReceiveAsync() | |||
@@ -142,25 +118,7 @@ namespace Discord | |||
while (!result.EndOfMessage); | |||
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(builder.ToString()); | |||
switch (msg.Operation) | |||
{ | |||
case 0: | |||
if (msg.Type == "READY") | |||
{ | |||
var payload = (msg.Payload as JToken).ToObject<WebSocketEvents.Ready>(); | |||
_heartbeatInterval = payload.HeartbeatInterval; | |||
QueueMessage(new WebSocketCommands.UpdateStatus()); | |||
QueueMessage(new WebSocketCommands.KeepAlive()); | |||
_connectWaitOnLogin.Set(); //Pre-Event | |||
} | |||
RaiseGotEvent(msg.Type, msg.Payload as JToken); | |||
if (msg.Type == "READY") | |||
_connectWaitOnLogin2.Set(); //Post-Event | |||
break; | |||
default: | |||
RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | |||
break; | |||
} | |||
ProcessMessage(msg); | |||
builder.Clear(); | |||
} | |||
@@ -168,11 +126,10 @@ namespace Discord | |||
catch { } | |||
finally { _disconnectToken.Cancel(); } | |||
} | |||
private async Task SendAsync() | |||
{ | |||
var cancelToken = _disconnectToken.Token; | |||
try | |||
try | |||
{ | |||
byte[] bytes; | |||
while (_webSocket.State == WebSocketState.Open && !cancelToken.IsCancellationRequested) | |||
@@ -182,7 +139,7 @@ namespace Discord | |||
DateTime now = DateTime.UtcNow; | |||
if ((now - _lastHeartbeat).TotalMilliseconds > _heartbeatInterval) | |||
{ | |||
await SendMessage(new WebSocketCommands.KeepAlive(), cancelToken); | |||
await SendMessage(GetKeepAlive(), cancelToken); | |||
_lastHeartbeat = now; | |||
} | |||
} | |||
@@ -195,15 +152,17 @@ namespace Discord | |||
finally { _disconnectToken.Cancel(); } | |||
} | |||
private void QueueMessage(object message) | |||
protected abstract void ProcessMessage(WebSocketMessage msg); | |||
protected abstract WebSocketMessage GetKeepAlive(); | |||
protected void QueueMessage(object message) | |||
{ | |||
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)); | |||
_sendQueue.Enqueue(bytes); | |||
} | |||
private Task SendMessage(object message, CancellationToken cancelToken) | |||
protected Task SendMessage(object message, CancellationToken cancelToken) | |||
=> SendMessage(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), cancelToken); | |||
private async Task SendMessage(byte[] message, CancellationToken cancelToken) | |||
protected async Task SendMessage(byte[] message, CancellationToken cancelToken) | |||
{ | |||
var frameCount = (int)Math.Ceiling((double)message.Length / SendChunkSize); | |||