@@ -1,4 +1,4 @@ | |||||
# Discord.Net v0.7.0-beta1 | |||||
# Discord.Net v0.7.0 | |||||
An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). | An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). | ||||
[Join the discussion](https://discord.gg/0SBTUU1wZTVjAMPx) on Discord. | [Join the discussion](https://discord.gg/0SBTUU1wZTVjAMPx) on Discord. | ||||
@@ -7,11 +7,13 @@ An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). | |||||
The Discord API is still in active development, meaning this library may break at any time without notice. | The Discord API is still in active development, meaning this library may break at any time without notice. | ||||
Discord.Net itself is also in alpha so several functions may be unstable or not work at all. | Discord.Net itself is also in alpha so several functions may be unstable or not work at all. | ||||
### Features | |||||
- Server Management (Servers, Channels, Messages, Invites) | |||||
- User Moderation (Kick/Ban/Unban/Mute/Unmute/Deafen/Undeafen) | |||||
- Alpha Voice Support (Outgoing only currently) | |||||
### Current Features | |||||
- Using Discord API version 3 | |||||
- Supports .Net 4.5 and DNX 4.5.1 | - Supports .Net 4.5 and DNX 4.5.1 | ||||
- Server Management (Servers, Channels, Messages, Invites, Roles, Users) | |||||
- Send/Receieve Messages (Including mentions and formatting) | |||||
- Basic Voice Support (Outgoing only, Unencrypted only) | |||||
- Command extension library (Supports permission levels) | |||||
### NuGet Packages | ### NuGet Packages | ||||
- [Discord.Net](https://www.nuget.org/packages/Discord.Net/) | - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) | ||||
@@ -26,7 +28,6 @@ client.MessageCreated += async (s, e) => | |||||
await client.SendMessage(e.Message.ChannelId, e.Message.Text); | await client.SendMessage(e.Message.ChannelId, e.Message.Text); | ||||
}; | }; | ||||
await client.Connect("discordtest@email.com", "Password123"); | await client.Connect("discordtest@email.com", "Password123"); | ||||
await client.AcceptInvite("channel-invite-code"); | |||||
``` | ``` | ||||
### Example (Command Client) | ### Example (Command Client) | ||||
@@ -48,11 +49,9 @@ client.CreateCommand("acceptinvite") | |||||
} | } | ||||
}); | }); | ||||
await client.Connect("discordtest@email.com", "Password123"); | await client.Connect("discordtest@email.com", "Password123"); | ||||
await client.AcceptInvite("channel-invite-code"); | |||||
``` | ``` | ||||
### Known Issues | ### Known Issues | ||||
- Due to current Discord restrictions, private messages are blocked unless both the sender and recipient are members of the same server. | - Due to current Discord restrictions, private messages are blocked unless both the sender and recipient are members of the same server. | ||||
- Caches do not currently clean up when their entries are no longer referenced, and there is no cap to the message cache. For now, disconencting and reconnecting will clear all caches. | |||||
- The Message caches does not currently clean up when their entries are no longer referenced, and there is currently no cap to it. For now, disconnecting and reconnecting will clear all caches. | |||||
- DNX Core 5.0 is experiencing several network-related issues and support has been temporarily dropped. | - DNX Core 5.0 is experiencing several network-related issues and support has been temporarily dropped. |
@@ -13,5 +13,5 @@ using System.Runtime.InteropServices; | |||||
[assembly: ComVisible(false)] | [assembly: ComVisible(false)] | ||||
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] | [assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] | ||||
[assembly: AssemblyVersion("0.6.1.2")] | |||||
[assembly: AssemblyFileVersion("0.6.1.2")] | |||||
[assembly: AssemblyVersion("0.7.0.0")] | |||||
[assembly: AssemblyFileVersion("0.7.0.0")] |
@@ -1,5 +1,5 @@ | |||||
{ | { | ||||
"version": "0.7.0-beta1", | |||||
"version": "0.7.0", | |||||
"description": "A small Discord.Net extension to make bot creation easier.", | "description": "A small Discord.Net extension to make bot creation easier.", | ||||
"authors": [ "RogueException" ], | "authors": [ "RogueException" ], | ||||
"tags": [ "discord", "discordapp" ], | "tags": [ "discord", "discordapp" ], | ||||
@@ -13,7 +13,7 @@ | |||||
"warningsAsErrors": true | "warningsAsErrors": true | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"Discord.Net": "0.7.0-beta1" | |||||
"Discord.Net": "0.7.0" | |||||
}, | }, | ||||
"frameworks": { | "frameworks": { | ||||
"net45": { }, | "net45": { }, | ||||
@@ -13,5 +13,5 @@ using System.Runtime.InteropServices; | |||||
[assembly: ComVisible(false)] | [assembly: ComVisible(false)] | ||||
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] | [assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] | ||||
[assembly: AssemblyVersion("0.6.1.2")] | |||||
[assembly: AssemblyFileVersion("0.6.1.2")] | |||||
[assembly: AssemblyVersion("0.7.0.0")] | |||||
[assembly: AssemblyFileVersion("0.7.0.0")] |
@@ -9,7 +9,7 @@ namespace Discord.Collections | |||||
public abstract class AsyncCollection<TValue> : IEnumerable<TValue> | public abstract class AsyncCollection<TValue> : IEnumerable<TValue> | ||||
where TValue : class | where TValue : class | ||||
{ | { | ||||
private static readonly object _writerLock = new object(); | |||||
private readonly object _writerLock; | |||||
internal class CollectionItemEventArgs : EventArgs | internal class CollectionItemEventArgs : EventArgs | ||||
{ | { | ||||
@@ -53,9 +53,10 @@ namespace Discord.Collections | |||||
protected readonly DiscordClient _client; | protected readonly DiscordClient _client; | ||||
protected readonly ConcurrentDictionary<string, TValue> _dictionary; | protected readonly ConcurrentDictionary<string, TValue> _dictionary; | ||||
protected AsyncCollection(DiscordClient client) | |||||
protected AsyncCollection(DiscordClient client, object writerLock) | |||||
{ | { | ||||
_client = client; | _client = client; | ||||
_writerLock = writerLock; | |||||
_dictionary = new ConcurrentDictionary<string, TValue>(); | _dictionary = new ConcurrentDictionary<string, TValue>(); | ||||
} | } | ||||
@@ -6,8 +6,8 @@ namespace Discord.Collections | |||||
{ | { | ||||
public sealed class Channels : AsyncCollection<Channel> | public sealed class Channels : AsyncCollection<Channel> | ||||
{ | { | ||||
internal Channels(DiscordClient client) | |||||
: base(client) { } | |||||
internal Channels(DiscordClient client, object writerLock) | |||||
: base(client, writerLock) { } | |||||
internal Channel GetOrAdd(string id, string serverId, string recipientId = null) => GetOrAdd(id, () => new Channel(_client, id, serverId, recipientId)); | internal Channel GetOrAdd(string id, string serverId, string recipientId = null) => GetOrAdd(id, () => new Channel(_client, id, serverId, recipientId)); | ||||
internal new Channel TryRemove(string id) => base.TryRemove(id); | internal new Channel TryRemove(string id) => base.TryRemove(id); | ||||
@@ -6,8 +6,8 @@ namespace Discord.Collections | |||||
{ | { | ||||
public sealed class Members : AsyncCollection<Member> | public sealed class Members : AsyncCollection<Member> | ||||
{ | { | ||||
internal Members(DiscordClient client) | |||||
: base(client) { } | |||||
internal Members(DiscordClient client, object writerLock) | |||||
: base(client, writerLock) { } | |||||
private string GetKey(string userId, string serverId) => serverId + '_' + userId; | private string GetKey(string userId, string serverId) => serverId + '_' + userId; | ||||
@@ -5,8 +5,8 @@ namespace Discord.Collections | |||||
public sealed class Messages : AsyncCollection<Message> | public sealed class Messages : AsyncCollection<Message> | ||||
{ | { | ||||
private readonly MessageCleaner _msgCleaner; | private readonly MessageCleaner _msgCleaner; | ||||
internal Messages(DiscordClient client) | |||||
: base(client) | |||||
internal Messages(DiscordClient client, object writerLock) | |||||
: base(client, writerLock) | |||||
{ | { | ||||
_msgCleaner = new MessageCleaner(client); | _msgCleaner = new MessageCleaner(client); | ||||
} | } | ||||
@@ -6,8 +6,8 @@ namespace Discord.Collections | |||||
{ | { | ||||
public sealed class Roles : AsyncCollection<Role> | public sealed class Roles : AsyncCollection<Role> | ||||
{ | { | ||||
internal Roles(DiscordClient client) | |||||
: base(client) { } | |||||
internal Roles(DiscordClient client, object writerLock) | |||||
: base(client, writerLock) { } | |||||
internal Role GetOrAdd(string id, string serverId) => GetOrAdd(id, () => new Role(_client, id, serverId)); | internal Role GetOrAdd(string id, string serverId) => GetOrAdd(id, () => new Role(_client, id, serverId)); | ||||
internal new Role TryRemove(string id) => base.TryRemove(id); | internal new Role TryRemove(string id) => base.TryRemove(id); | ||||
@@ -6,8 +6,8 @@ namespace Discord.Collections | |||||
{ | { | ||||
public sealed class Servers : AsyncCollection<Server> | public sealed class Servers : AsyncCollection<Server> | ||||
{ | { | ||||
internal Servers(DiscordClient client) | |||||
: base(client) { } | |||||
internal Servers(DiscordClient client, object writerLock) | |||||
: base(client, writerLock) { } | |||||
internal Server GetOrAdd(string id) => base.GetOrAdd(id, () => new Server(_client, id)); | internal Server GetOrAdd(string id) => base.GetOrAdd(id, () => new Server(_client, id)); | ||||
internal new Server TryRemove(string id) => base.TryRemove(id); | internal new Server TryRemove(string id) => base.TryRemove(id); | ||||
@@ -6,8 +6,8 @@ namespace Discord.Collections | |||||
{ | { | ||||
public sealed class Users : AsyncCollection<User> | public sealed class Users : AsyncCollection<User> | ||||
{ | { | ||||
internal Users(DiscordClient client) | |||||
: base(client) { } | |||||
internal Users(DiscordClient client, object writerLock) | |||||
: base(client, writerLock) { } | |||||
internal User GetOrAdd(string id) => GetOrAdd(id, () => new User(_client, id)); | internal User GetOrAdd(string id) => GetOrAdd(id, () => new User(_client, id)); | ||||
internal new User TryRemove(string id) => base.TryRemove(id); | internal new User TryRemove(string id) => base.TryRemove(id); | ||||
@@ -282,7 +282,8 @@ namespace Discord | |||||
var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); | var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); | ||||
msg.Update(model); | msg.Update(model); | ||||
RaiseMessageSent(msg); | RaiseMessageSent(msg); | ||||
} | |||||
result[i] = msg; | |||||
} | |||||
await Task.Delay(1000).ConfigureAwait(false); | await Task.Delay(1000).ConfigureAwait(false); | ||||
} | } | ||||
return result; | return result; | ||||
@@ -6,28 +6,44 @@ namespace Discord | |||||
{ | { | ||||
public partial class DiscordClient | public partial class DiscordClient | ||||
{ | { | ||||
public Task JoinVoiceServer(string channelId) | |||||
=> JoinVoiceServer(_channels[channelId]); | |||||
public async Task JoinVoiceServer(Channel channel) | |||||
public Task JoinVoiceServer(Channel channel) | |||||
=> JoinVoiceServer(channel.ServerId, channel.Id); | |||||
public async Task JoinVoiceServer(string serverId, string channelId) | |||||
{ | { | ||||
CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||||
if (serverId == null) throw new ArgumentNullException(nameof(serverId)); | |||||
if (channelId == null) throw new ArgumentNullException(nameof(channelId)); | |||||
await LeaveVoiceServer().ConfigureAwait(false); | await LeaveVoiceServer().ConfigureAwait(false); | ||||
_dataSocket.SendJoinVoice(channel); | |||||
//await _voiceSocket.WaitForConnection().ConfigureAwait(false); | |||||
//TODO: Add another ManualResetSlim to wait on here, base it off of DiscordClient's setup | |||||
} | |||||
try | |||||
{ | |||||
await Task.Run(() => | |||||
{ | |||||
_voiceSocket.SetServer(serverId); | |||||
_dataSocket.SendJoinVoice(serverId, channelId); | |||||
_voiceSocket.WaitForConnection(); | |||||
}) | |||||
.Timeout(_config.ConnectionTimeout) | |||||
.ConfigureAwait(false); | |||||
} | |||||
catch (TaskCanceledException) | |||||
{ | |||||
await LeaveVoiceServer().ConfigureAwait(false); | |||||
} | |||||
} | |||||
public async Task LeaveVoiceServer() | public async Task LeaveVoiceServer() | ||||
{ | { | ||||
CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
if (_voiceSocket.CurrentVoiceServerId != null) | |||||
if (_voiceSocket.State != Net.WebSockets.WebSocketState.Disconnected) | |||||
{ | { | ||||
await _voiceSocket.Disconnect().ConfigureAwait(false); | |||||
await TaskHelper.CompletedTask.ConfigureAwait(false); | |||||
_dataSocket.SendLeaveVoice(); | |||||
var serverId = _voiceSocket.CurrentVoiceServerId; | |||||
if (serverId != null) | |||||
{ | |||||
await _voiceSocket.Disconnect().ConfigureAwait(false); | |||||
_dataSocket.SendLeaveVoice(serverId); | |||||
} | |||||
} | } | ||||
} | } | ||||
@@ -43,7 +59,6 @@ namespace Discord | |||||
_voiceSocket.SendPCMFrames(data, count); | _voiceSocket.SendPCMFrames(data, count); | ||||
} | } | ||||
/// <summary> Clears the PCM buffer. </summary> | /// <summary> Clears the PCM buffer. </summary> | ||||
public void ClearVoicePCM() | public void ClearVoicePCM() | ||||
{ | { | ||||
@@ -57,7 +72,7 @@ namespace Discord | |||||
{ | { | ||||
CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
_voiceSocket.Wait(); | |||||
_voiceSocket.WaitForQueue(); | |||||
await TaskHelper.CompletedTask.ConfigureAwait(false); | await TaskHelper.CompletedTask.ConfigureAwait(false); | ||||
} | } | ||||
} | } | ||||
@@ -32,11 +32,12 @@ namespace Discord | |||||
private readonly ManualResetEvent _disconnectedEvent; | private readonly ManualResetEvent _disconnectedEvent; | ||||
private readonly ManualResetEventSlim _connectedEvent; | private readonly ManualResetEventSlim _connectedEvent; | ||||
private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
protected ExceptionDispatchInfo _disconnectReason; | |||||
private Task _runTask; | private Task _runTask; | ||||
private bool _wasDisconnectUnexpected; | |||||
private string _token; | private string _token; | ||||
protected ExceptionDispatchInfo _disconnectReason; | |||||
private bool _wasDisconnectUnexpected; | |||||
/// <summary> Returns the id of the current logged-in user. </summary> | /// <summary> Returns the id of the current logged-in user. </summary> | ||||
public string CurrentUserId => _currentUserId; | public string CurrentUserId => _currentUserId; | ||||
private string _currentUserId; | private string _currentUserId; | ||||
@@ -95,7 +96,12 @@ namespace Discord | |||||
_api = new DiscordAPIClient(_config.LogLevel, _config.APITimeout); | _api = new DiscordAPIClient(_config.LogLevel, _config.APITimeout); | ||||
_dataSocket = new DataWebSocket(this); | _dataSocket = new DataWebSocket(this); | ||||
_dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); }; | _dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); }; | ||||
_dataSocket.Disconnected += async (s, e) => { RaiseDisconnected(e); if (e.WasUnexpected) await _dataSocket.Login(_token); }; | |||||
_dataSocket.Disconnected += async (s, e) => | |||||
{ | |||||
RaiseDisconnected(e); | |||||
if (e.WasUnexpected) | |||||
await _dataSocket.Reconnect(_token); | |||||
}; | |||||
if (_config.EnableVoice) | if (_config.EnableVoice) | ||||
{ | { | ||||
_voiceSocket = new VoiceWebSocket(this); | _voiceSocket = new VoiceWebSocket(this); | ||||
@@ -116,7 +122,7 @@ namespace Discord | |||||
}; | }; | ||||
_voiceSocket.IsSpeaking += (s, e) => | _voiceSocket.IsSpeaking += (s, e) => | ||||
{ | { | ||||
if (_voiceSocket.CurrentVoiceServerId != null) | |||||
if (_voiceSocket.State == WebSocketState.Connected) | |||||
{ | { | ||||
var member = _members[e.UserId, _voiceSocket.CurrentVoiceServerId]; | var member = _members[e.UserId, _voiceSocket.CurrentVoiceServerId]; | ||||
bool value = e.IsSpeaking; | bool value = e.IsSpeaking; | ||||
@@ -131,12 +137,13 @@ namespace Discord | |||||
}; | }; | ||||
} | } | ||||
_channels = new Channels(this); | |||||
_members = new Members(this); | |||||
_messages = new Messages(this); | |||||
_roles = new Roles(this); | |||||
_servers = new Servers(this); | |||||
_users = new Users(this); | |||||
object cacheLock = new object(); | |||||
_channels = new Channels(this, cacheLock); | |||||
_members = new Members(this, cacheLock); | |||||
_messages = new Messages(this, cacheLock); | |||||
_roles = new Roles(this, cacheLock); | |||||
_servers = new Servers(this, cacheLock); | |||||
_users = new Users(this, cacheLock); | |||||
_dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message); | _dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message); | ||||
if (_config.EnableVoice) | if (_config.EnableVoice) | ||||
@@ -581,11 +588,14 @@ namespace Discord | |||||
case "VOICE_SERVER_UPDATE": | case "VOICE_SERVER_UPDATE": | ||||
{ | { | ||||
var data = e.Payload.ToObject<Events.VoiceServerUpdate>(_serializer); | var data = e.Payload.ToObject<Events.VoiceServerUpdate>(_serializer); | ||||
var server = _servers[data.GuildId]; | |||||
if (_config.EnableVoice) | |||||
if (data.GuildId == _voiceSocket.CurrentVoiceServerId) | |||||
{ | { | ||||
_voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; | |||||
await _voiceSocket.Login(data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); | |||||
var server = _servers[data.GuildId]; | |||||
if (_config.EnableVoice) | |||||
{ | |||||
_voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; | |||||
await _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); | |||||
} | |||||
} | } | ||||
} | } | ||||
break; | break; | ||||
@@ -637,9 +647,6 @@ namespace Discord | |||||
string token; | string token; | ||||
try | try | ||||
{ | { | ||||
var cancelToken = new CancellationTokenSource(); | |||||
cancelToken.CancelAfter(5000); | |||||
_api.CancelToken = cancelToken.Token; | |||||
var response = await _api.Login(email, password).ConfigureAwait(false); | var response = await _api.Login(email, password).ConfigureAwait(false); | ||||
token = response.Token; | token = response.Token; | ||||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | if (_config.LogLevel >= LogMessageSeverity.Verbose) | ||||
@@ -748,14 +755,14 @@ namespace Discord | |||||
//When the first task ends, make sure the rest do too | //When the first task ends, make sure the rest do too | ||||
await DisconnectInternal(skipAwait: true); | await DisconnectInternal(skipAwait: true); | ||||
bool wasUnexpected = _wasDisconnectUnexpected; | |||||
_wasDisconnectUnexpected = false; | |||||
await Cleanup(wasUnexpected).ConfigureAwait(false); | |||||
await Cleanup().ConfigureAwait(false); | |||||
_runTask = null; | _runTask = null; | ||||
} | } | ||||
private async Task Cleanup(bool wasUnexpected) | |||||
private async Task Cleanup() | |||||
{ | { | ||||
var wasDisconnectUnexpected = _wasDisconnectUnexpected; | |||||
_wasDisconnectUnexpected = false; | |||||
await _dataSocket.Disconnect().ConfigureAwait(false); | await _dataSocket.Disconnect().ConfigureAwait(false); | ||||
if (_config.EnableVoice) | if (_config.EnableVoice) | ||||
await _voiceSocket.Disconnect().ConfigureAwait(false); | await _voiceSocket.Disconnect().ConfigureAwait(false); | ||||
@@ -777,7 +784,7 @@ namespace Discord | |||||
_currentUserId = null; | _currentUserId = null; | ||||
_token = null; | _token = null; | ||||
if (!wasUnexpected) | |||||
if (!wasDisconnectUnexpected) | |||||
{ | { | ||||
_state = (int)DiscordClientState.Disconnected; | _state = (int)DiscordClientState.Disconnected; | ||||
_disconnectedEvent.Set(); | _disconnectedEvent.Set(); | ||||
@@ -29,7 +29,7 @@ namespace Discord | |||||
private int _messageQueueInterval = 100; | private int _messageQueueInterval = 100; | ||||
/// <summary> Gets or sets the max buffer length (in milliseconds) for outgoing voice packets. This value is the target maximum but is not guaranteed, the buffer will often go slightly above this value. </summary> | /// <summary> Gets or sets the max buffer length (in milliseconds) for outgoing voice packets. This value is the target maximum but is not guaranteed, the buffer will often go slightly above this value. </summary> | ||||
public int VoiceBufferLength { get { return _voiceBufferLength; } set { SetValue(ref _voiceBufferLength, value); } } | public int VoiceBufferLength { get { return _voiceBufferLength; } set { SetValue(ref _voiceBufferLength, value); } } | ||||
private int _voiceBufferLength = 3000; | |||||
private int _voiceBufferLength = 1000; | |||||
//Experimental Features | //Experimental Features | ||||
#if !DNXCORE50 | #if !DNXCORE50 | ||||
@@ -14,9 +14,8 @@ namespace Discord.Net.WebSockets | |||||
public sealed class KeepAlive : WebSocketMessage<ulong> | public sealed class KeepAlive : WebSocketMessage<ulong> | ||||
{ | { | ||||
public KeepAlive() : base(1, GetTimestamp()) { } | public KeepAlive() : base(1, GetTimestamp()) { } | ||||
private static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | |||||
private static ulong GetTimestamp() | |||||
=> (ulong)(DateTime.UtcNow - epoch).TotalMilliseconds; | |||||
private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | |||||
private static ulong GetTimestamp() => (ulong)(DateTime.UtcNow - epoch).TotalMilliseconds; | |||||
} | } | ||||
public sealed class Login : WebSocketMessage<Login.Data> | public sealed class Login : WebSocketMessage<Login.Data> | ||||
{ | { | ||||
@@ -20,7 +20,7 @@ namespace Discord.Net.WebSockets | |||||
public async Task Login(string token) | public async Task Login(string token) | ||||
{ | { | ||||
await Connect(); | |||||
await Connect().ConfigureAwait(false); | |||||
Commands.Login msg = new Commands.Login(); | Commands.Login msg = new Commands.Login(); | ||||
msg.Payload.Token = token; | msg.Payload.Token = token; | ||||
@@ -29,14 +29,38 @@ namespace Discord.Net.WebSockets | |||||
} | } | ||||
private async Task Redirect(string server) | private async Task Redirect(string server) | ||||
{ | { | ||||
await DisconnectInternal(isUnexpected: false); | |||||
await Connect(); | |||||
await DisconnectInternal(isUnexpected: false).ConfigureAwait(false); | |||||
await Connect().ConfigureAwait(false); | |||||
var resumeMsg = new Commands.Resume(); | var resumeMsg = new Commands.Resume(); | ||||
resumeMsg.Payload.SessionId = _sessionId; | resumeMsg.Payload.SessionId = _sessionId; | ||||
resumeMsg.Payload.Sequence = _lastSeq; | resumeMsg.Payload.Sequence = _lastSeq; | ||||
QueueMessage(resumeMsg); | QueueMessage(resumeMsg); | ||||
} | } | ||||
public async Task Reconnect(string token) | |||||
{ | |||||
try | |||||
{ | |||||
var cancelToken = ParentCancelToken; | |||||
await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
while (!cancelToken.IsCancellationRequested) | |||||
{ | |||||
try | |||||
{ | |||||
await Login(token).ConfigureAwait(false); | |||||
break; | |||||
} | |||||
catch (OperationCanceledException) { throw; } | |||||
catch (Exception ex) | |||||
{ | |||||
RaiseOnLog(LogMessageSeverity.Error, $"Reconnect failed: {ex.GetBaseException().Message}"); | |||||
//Net is down? We can keep trying to reconnect until the user runs Disconnect() | |||||
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
} | |||||
} | |||||
} | |||||
catch (OperationCanceledException) { } | |||||
} | |||||
protected override async Task ProcessMessage(string json) | protected override async Task ProcessMessage(string json) | ||||
{ | { | ||||
@@ -88,16 +112,17 @@ namespace Discord.Net.WebSockets | |||||
return new Commands.KeepAlive(); | return new Commands.KeepAlive(); | ||||
} | } | ||||
public void SendJoinVoice(Channel channel) | |||||
public void SendJoinVoice(string serverId, string channelId) | |||||
{ | { | ||||
var joinVoice = new Commands.JoinVoice(); | var joinVoice = new Commands.JoinVoice(); | ||||
joinVoice.Payload.ServerId = channel.ServerId; | |||||
joinVoice.Payload.ChannelId = channel.Id; | |||||
joinVoice.Payload.ServerId = serverId; | |||||
joinVoice.Payload.ChannelId = channelId; | |||||
QueueMessage(joinVoice); | QueueMessage(joinVoice); | ||||
} | } | ||||
public void SendLeaveVoice() | |||||
public void SendLeaveVoice(string serverId) | |||||
{ | { | ||||
var leaveVoice = new Commands.JoinVoice(); | var leaveVoice = new Commands.JoinVoice(); | ||||
leaveVoice.Payload.ServerId = serverId; | |||||
QueueMessage(leaveVoice); | QueueMessage(leaveVoice); | ||||
} | } | ||||
} | } | ||||
@@ -53,21 +53,24 @@ namespace Discord.Net.WebSockets | |||||
_targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | ||||
} | } | ||||
public async Task Login(string serverId, string userId, string sessionId, string token, CancellationToken cancelToken) | |||||
public void SetServer(string serverId) | |||||
{ | { | ||||
if (_serverId == serverId && _userId == userId && _sessionId == sessionId && _token == token) | |||||
_serverId = serverId; | |||||
} | |||||
public async Task Login(string userId, string sessionId, string token, CancellationToken cancelToken) | |||||
{ | |||||
if ((WebSocketState)_state != WebSocketState.Disconnected) | |||||
{ | { | ||||
//Adjust the host and tell the system to reconnect | //Adjust the host and tell the system to reconnect | ||||
await DisconnectInternal(new Exception("Server transfer occurred."), isUnexpected: false); | await DisconnectInternal(new Exception("Server transfer occurred."), isUnexpected: false); | ||||
return; | return; | ||||
} | } | ||||
_serverId = serverId; | |||||
_userId = userId; | _userId = userId; | ||||
_sessionId = sessionId; | _sessionId = sessionId; | ||||
_token = token; | _token = token; | ||||
await Connect(); | |||||
await Connect().ConfigureAwait(false); | |||||
} | } | ||||
public async Task Reconnect() | public async Task Reconnect() | ||||
{ | { | ||||
@@ -85,7 +88,7 @@ namespace Discord.Net.WebSockets | |||||
catch (OperationCanceledException) { throw; } | catch (OperationCanceledException) { throw; } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
RaiseOnLog(LogMessageSeverity.Error, $"DataSocket reconnect failed: {ex.GetBaseException().Message}"); | |||||
RaiseOnLog(LogMessageSeverity.Error, $"Reconnect failed: {ex.GetBaseException().Message}"); | |||||
//Net is down? We can keep trying to reconnect until the user runs Disconnect() | //Net is down? We can keep trying to reconnect until the user runs Disconnect() | ||||
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | ||||
} | } | ||||
@@ -125,7 +128,7 @@ namespace Discord.Net.WebSockets | |||||
#endif | #endif | ||||
}.Concat(base.Run()).ToArray(); | }.Concat(base.Run()).ToArray(); | ||||
} | } | ||||
protected override Task Cleanup(bool wasUnexpected) | |||||
protected override Task Cleanup() | |||||
{ | { | ||||
#if USE_THREAD | #if USE_THREAD | ||||
_sendThread.Join(); | _sendThread.Join(); | ||||
@@ -133,16 +136,15 @@ namespace Discord.Net.WebSockets | |||||
#endif | #endif | ||||
ClearPCMFrames(); | ClearPCMFrames(); | ||||
if (!wasUnexpected) | |||||
if (!_wasDisconnectUnexpected) | |||||
{ | { | ||||
_serverId = null; | |||||
_userId = null; | _userId = null; | ||||
_sessionId = null; | _sessionId = null; | ||||
_token = null; | _token = null; | ||||
} | } | ||||
_udp = null; | _udp = null; | ||||
return base.Cleanup(wasUnexpected); | |||||
return base.Cleanup(); | |||||
} | } | ||||
private async Task ReceiveVoiceAsync() | private async Task ReceiveVoiceAsync() | ||||
@@ -512,9 +514,13 @@ namespace Discord.Net.WebSockets | |||||
return new VoiceCommands.KeepAlive(); | return new VoiceCommands.KeepAlive(); | ||||
} | } | ||||
public void Wait() | |||||
public void WaitForQueue() | |||||
{ | |||||
_sendQueueEmptyWait.Wait(_cancelToken); | |||||
} | |||||
public void WaitForConnection() | |||||
{ | { | ||||
_sendQueueEmptyWait.Wait(); | |||||
_connectedEvent.Wait(); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -37,30 +37,33 @@ namespace Discord.Net.WebSockets | |||||
protected readonly IWebSocketEngine _engine; | protected readonly IWebSocketEngine _engine; | ||||
protected readonly DiscordClient _client; | protected readonly DiscordClient _client; | ||||
protected readonly LogMessageSeverity _logLevel; | protected readonly LogMessageSeverity _logLevel; | ||||
protected readonly ManualResetEventSlim _connectedEvent; | |||||
public string Host { get; set; } | |||||
protected ExceptionDispatchInfo _disconnectReason; | |||||
protected bool _wasDisconnectUnexpected; | |||||
protected WebSocketState _disconnectState; | |||||
protected int _loginTimeout, _heartbeatInterval; | protected int _loginTimeout, _heartbeatInterval; | ||||
private DateTime _lastHeartbeat; | private DateTime _lastHeartbeat; | ||||
private Task _runTask; | private Task _runTask; | ||||
public WebSocketState State => (WebSocketState)_state; | |||||
protected int _state; | |||||
protected ExceptionDispatchInfo _disconnectReason; | |||||
private bool _wasDisconnectUnexpected; | |||||
public CancellationToken ParentCancelToken { get; set; } | public CancellationToken ParentCancelToken { get; set; } | ||||
public CancellationToken CancelToken => _cancelToken; | public CancellationToken CancelToken => _cancelToken; | ||||
private CancellationTokenSource _cancelTokenSource; | private CancellationTokenSource _cancelTokenSource; | ||||
protected CancellationToken _cancelToken; | protected CancellationToken _cancelToken; | ||||
public string Host { get; set; } | |||||
public WebSocketState State => (WebSocketState)_state; | |||||
protected int _state; | |||||
public WebSocket(DiscordClient client) | public WebSocket(DiscordClient client) | ||||
{ | { | ||||
_client = client; | _client = client; | ||||
_logLevel = client.Config.LogLevel; | _logLevel = client.Config.LogLevel; | ||||
_loginTimeout = client.Config.ConnectionTimeout; | _loginTimeout = client.Config.ConnectionTimeout; | ||||
_cancelToken = new CancellationToken(true); | _cancelToken = new CancellationToken(true); | ||||
_connectedEvent = new ManualResetEventSlim(false); | |||||
_engine = new BuiltInWebSocketEngine(client.Config.WebSocketInterval); | _engine = new BuiltInWebSocketEngine(client.Config.WebSocketInterval); | ||||
_engine.ProcessMessage += async (s, e) => | _engine.ProcessMessage += async (s, e) => | ||||
@@ -78,9 +81,7 @@ namespace Discord.Net.WebSockets | |||||
try | try | ||||
{ | { | ||||
await Disconnect().ConfigureAwait(false); | |||||
_state = (int)WebSocketState.Connecting; | |||||
await Disconnect().ConfigureAwait(false); | |||||
_cancelTokenSource = new CancellationTokenSource(); | _cancelTokenSource = new CancellationTokenSource(); | ||||
if (ParentCancelToken != null) | if (ParentCancelToken != null) | ||||
@@ -91,50 +92,59 @@ namespace Discord.Net.WebSockets | |||||
await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | ||||
_lastHeartbeat = DateTime.UtcNow; | _lastHeartbeat = DateTime.UtcNow; | ||||
_state = (int)WebSocketState.Connecting; | |||||
_runTask = RunTasks(); | _runTask = RunTasks(); | ||||
} | } | ||||
catch | |||||
catch (Exception ex) | |||||
{ | { | ||||
await Disconnect().ConfigureAwait(false); | |||||
throw; | |||||
await DisconnectInternal(ex, isUnexpected: false).ConfigureAwait(false); | |||||
throw; //Dont handle this exception internally, send up it upwards | |||||
} | } | ||||
} | } | ||||
protected void CompleteConnect() | protected void CompleteConnect() | ||||
{ | { | ||||
_state = (int)WebSocketState.Connected; | _state = (int)WebSocketState.Connected; | ||||
_connectedEvent.Set(); | |||||
RaiseConnected(); | RaiseConnected(); | ||||
} | } | ||||
/*public Task Reconnect(CancellationToken cancelToken) | /*public Task Reconnect(CancellationToken cancelToken) | ||||
=> Connect(_host, _cancelToken);*/ | => Connect(_host, _cancelToken);*/ | ||||
public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | ||||
protected Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false) | |||||
protected async Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false) | |||||
{ | { | ||||
int oldState; | int oldState; | ||||
bool hasWriterLock; | bool hasWriterLock; | ||||
//If in either connecting or connected state, get a lock by being the first to switch to disconnecting | //If in either connecting or connected state, get a lock by being the first to switch to disconnecting | ||||
oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connecting); | oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connecting); | ||||
if (oldState == (int)WebSocketState.Disconnected) return TaskHelper.CompletedTask; //Already disconnected | |||||
if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected | |||||
hasWriterLock = oldState == (int)WebSocketState.Connecting; //Caused state change | hasWriterLock = oldState == (int)WebSocketState.Connecting; //Caused state change | ||||
if (!hasWriterLock) | if (!hasWriterLock) | ||||
{ | { | ||||
oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connected); | oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connected); | ||||
if (oldState == (int)WebSocketState.Disconnected) return TaskHelper.CompletedTask; //Already disconnected | |||||
if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected | |||||
hasWriterLock = oldState == (int)WebSocketState.Connected; //Caused state change | hasWriterLock = oldState == (int)WebSocketState.Connected; //Caused state change | ||||
} | } | ||||
if (hasWriterLock) | if (hasWriterLock) | ||||
{ | { | ||||
_wasDisconnectUnexpected = isUnexpected; | _wasDisconnectUnexpected = isUnexpected; | ||||
_disconnectState = (WebSocketState)oldState; | |||||
_disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null; | _disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null; | ||||
if (_disconnectState == WebSocketState.Connecting) //_runTask was never made | |||||
await Cleanup(); | |||||
_cancelTokenSource.Cancel(); | _cancelTokenSource.Cancel(); | ||||
} | } | ||||
if (!skipAwait) | if (!skipAwait) | ||||
return _runTask ?? TaskHelper.CompletedTask; | |||||
{ | |||||
Task task = _runTask ?? TaskHelper.CompletedTask; | |||||
await task; | |||||
} | |||||
else | else | ||||
return TaskHelper.CompletedTask; | |||||
await TaskHelper.CompletedTask; | |||||
} | } | ||||
protected virtual async Task RunTasks() | protected virtual async Task RunTasks() | ||||
@@ -143,19 +153,19 @@ namespace Discord.Net.WebSockets | |||||
Task firstTask = Task.WhenAny(tasks); | Task firstTask = Task.WhenAny(tasks); | ||||
Task allTasks = Task.WhenAll(tasks); | Task allTasks = Task.WhenAll(tasks); | ||||
//Wait until the first task ends/errors and capture the error | |||||
try { await firstTask.ConfigureAwait(false); } | try { await firstTask.ConfigureAwait(false); } | ||||
catch (Exception ex) { await DisconnectInternal(ex: ex, skipAwait: true).ConfigureAwait(false); } | catch (Exception ex) { await DisconnectInternal(ex: ex, skipAwait: true).ConfigureAwait(false); } | ||||
//When the first task ends, make sure the rest do too | |||||
//Ensure all other tasks are signaled to end. | |||||
await DisconnectInternal(skipAwait: true); | await DisconnectInternal(skipAwait: true); | ||||
//Wait for the remaining tasks to complete | |||||
try { await allTasks.ConfigureAwait(false); } | try { await allTasks.ConfigureAwait(false); } | ||||
catch { } | catch { } | ||||
bool wasUnexpected = _wasDisconnectUnexpected; | |||||
_wasDisconnectUnexpected = false; | |||||
await Cleanup(wasUnexpected).ConfigureAwait(false); | |||||
_runTask = null; | |||||
//Clean up state variables and raise disconnect event | |||||
await Cleanup().ConfigureAwait(false); | |||||
} | } | ||||
protected virtual Task[] Run() | protected virtual Task[] Run() | ||||
{ | { | ||||
@@ -164,12 +174,23 @@ namespace Discord.Net.WebSockets | |||||
.Concat(new Task[] { HeartbeatAsync(cancelToken) }) | .Concat(new Task[] { HeartbeatAsync(cancelToken) }) | ||||
.ToArray(); | .ToArray(); | ||||
} | } | ||||
protected virtual Task Cleanup(bool wasUnexpected) | |||||
protected virtual async Task Cleanup() | |||||
{ | { | ||||
var disconnectState = _disconnectState; | |||||
_disconnectState = WebSocketState.Disconnected; | |||||
var wasDisconnectUnexpected = _wasDisconnectUnexpected; | |||||
_wasDisconnectUnexpected = false; | |||||
//Dont reset disconnectReason, we may called ThrowError() later | |||||
await _engine.Disconnect(); | |||||
_cancelTokenSource = null; | _cancelTokenSource = null; | ||||
_state = (int)WebSocketState.Disconnected; | |||||
RaiseDisconnected(wasUnexpected, _disconnectReason?.SourceException); | |||||
return _engine.Disconnect(); | |||||
var oldState = _state; | |||||
_state = (int)WebSocketState.Disconnected; | |||||
_runTask = null; | |||||
_connectedEvent.Reset(); | |||||
if (disconnectState == WebSocketState.Connected) | |||||
RaiseDisconnected(wasDisconnectUnexpected, _disconnectReason?.SourceException); | |||||
} | } | ||||
protected abstract Task ProcessMessage(string json); | protected abstract Task ProcessMessage(string json); | ||||
@@ -1,5 +1,5 @@ | |||||
{ | { | ||||
"version": "0.7.0-beta1", | |||||
"version": "0.7.0", | |||||
"description": "An unofficial .Net API wrapper for the Discord client.", | "description": "An unofficial .Net API wrapper for the Discord client.", | ||||
"authors": [ "RogueException" ], | "authors": [ "RogueException" ], | ||||
"tags": [ "discord", "discordapp" ], | "tags": [ "discord", "discordapp" ], | ||||