| @@ -136,15 +136,6 @@ | |||||
| <Compile Include="..\Discord.Net\DiscordAPIClient.cs"> | <Compile Include="..\Discord.Net\DiscordAPIClient.cs"> | ||||
| <Link>DiscordAPIClient.cs</Link> | <Link>DiscordAPIClient.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\DiscordBaseClient.cs"> | |||||
| <Link>DiscordBaseClient.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\DiscordBaseClient.Events.cs"> | |||||
| <Link>DiscordBaseClient.Events.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\DiscordBaseClient.Voice.cs"> | |||||
| <Link>DiscordBaseClient.Voice.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\DiscordClient.API.cs"> | <Compile Include="..\Discord.Net\DiscordClient.API.cs"> | ||||
| <Link>DiscordClient.API.cs</Link> | <Link>DiscordClient.API.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -160,6 +151,15 @@ | |||||
| <Compile Include="..\Discord.Net\DiscordClientConfig.cs"> | <Compile Include="..\Discord.Net\DiscordClientConfig.cs"> | ||||
| <Link>DiscordClientConfig.cs</Link> | <Link>DiscordClientConfig.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\DiscordSimpleClient.cs"> | |||||
| <Link>DiscordSimpleClient.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\DiscordSimpleClient.Events.cs"> | |||||
| <Link>DiscordSimpleClient.Events.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\DiscordSimpleClient.Voice.cs"> | |||||
| <Link>DiscordSimpleClient.Voice.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\Enums\ChannelTypes.cs"> | <Compile Include="..\Discord.Net\Enums\ChannelTypes.cs"> | ||||
| <Link>Enums\ChannelTypes.cs</Link> | <Link>Enums\ChannelTypes.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -6,19 +6,22 @@ using Discord.WebSockets.Data; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Linq; | |||||
| using System.Net; | using System.Net; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using VoiceWebSocket = Discord.WebSockets.Voice.VoiceWebSocket; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> Provides a connection to the DiscordApp service. </summary> | /// <summary> Provides a connection to the DiscordApp service. </summary> | ||||
| public partial class DiscordClient : DiscordBaseClient | |||||
| public partial class DiscordClient : DiscordSimpleClient | |||||
| { | { | ||||
| protected readonly DiscordAPIClient _api; | protected readonly DiscordAPIClient _api; | ||||
| private readonly Random _rand; | private readonly Random _rand; | ||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| private readonly ConcurrentQueue<Message> _pendingMessages; | private readonly ConcurrentQueue<Message> _pendingMessages; | ||||
| private readonly ConcurrentDictionary<string, DiscordSimpleClient> _voiceClients; | |||||
| /// <summary> Returns the current logged-in user. </summary> | /// <summary> Returns the current logged-in user. </summary> | ||||
| public User CurrentUser => _currentUser; | public User CurrentUser => _currentUser; | ||||
| @@ -52,6 +55,8 @@ namespace Discord | |||||
| _api = new DiscordAPIClient(_config.LogLevel, _config.APITimeout); | _api = new DiscordAPIClient(_config.LogLevel, _config.APITimeout); | ||||
| if (_config.UseMessageQueue) | if (_config.UseMessageQueue) | ||||
| _pendingMessages = new ConcurrentQueue<Message>(); | _pendingMessages = new ConcurrentQueue<Message>(); | ||||
| if (_config.EnableVoiceMultiserver) | |||||
| _voiceClients = new ConcurrentDictionary<string, DiscordSimpleClient>(); | |||||
| object cacheLock = new object(); | object cacheLock = new object(); | ||||
| _channels = new Channels(this, cacheLock); | _channels = new Channels(this, cacheLock); | ||||
| @@ -61,38 +66,20 @@ namespace Discord | |||||
| _servers = new Servers(this, cacheLock); | _servers = new Servers(this, cacheLock); | ||||
| _users = new Users(this, cacheLock); | _users = new Users(this, cacheLock); | ||||
| if (Config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| this.Connected += (s,e) => _api.CancelToken = CancelToken; | |||||
| VoiceDisconnected += (s, e) => | |||||
| { | { | ||||
| this.VoiceDisconnected += (s, e) => | |||||
| { | |||||
| foreach (var member in _members) | |||||
| { | |||||
| if (member.IsSpeaking) | |||||
| { | |||||
| member.IsSpeaking = false; | |||||
| RaiseUserIsSpeaking(member, false); | |||||
| } | |||||
| } | |||||
| }; | |||||
| _voiceSocket.IsSpeaking += (s, e) => | |||||
| foreach (var member in _members) | |||||
| { | { | ||||
| if (_voiceSocket.State == WebSocketState.Connected) | |||||
| if (member.ServerId == e.ServerId && member.IsSpeaking) | |||||
| { | { | ||||
| var member = _members[e.UserId, _voiceSocket.CurrentServerId]; | |||||
| bool value = e.IsSpeaking; | |||||
| if (member.IsSpeaking != value) | |||||
| { | |||||
| member.IsSpeaking = value; | |||||
| RaiseUserIsSpeaking(member, value); | |||||
| if (Config.TrackActivity) | |||||
| member.UpdateActivity(); | |||||
| } | |||||
| member.IsSpeaking = false; | |||||
| RaiseUserIsSpeaking(member, false); | |||||
| } | } | ||||
| }; | |||||
| } | |||||
| } | |||||
| }; | |||||
| this.Connected += (s,e) => _api.CancelToken = CancelToken; | |||||
| if (_config.LogLevel >= LogMessageSeverity.Verbose) | if (_config.LogLevel >= LogMessageSeverity.Verbose) | ||||
| { | { | ||||
| bool isDebug = _config.LogLevel >= LogMessageSeverity.Debug; | bool isDebug = _config.LogLevel >= LogMessageSeverity.Debug; | ||||
| @@ -207,6 +194,27 @@ namespace Discord | |||||
| #endif | #endif | ||||
| } | } | ||||
| internal override VoiceWebSocket CreateVoiceSocket() | |||||
| { | |||||
| var socket = base.CreateVoiceSocket(); | |||||
| socket.IsSpeaking += (s, e) => | |||||
| { | |||||
| if (_voiceSocket.State == WebSocketState.Connected) | |||||
| { | |||||
| var member = _members[e.UserId, socket.CurrentServerId]; | |||||
| bool value = e.IsSpeaking; | |||||
| if (member.IsSpeaking != value) | |||||
| { | |||||
| member.IsSpeaking = value; | |||||
| RaiseUserIsSpeaking(member, value); | |||||
| if (_config.TrackActivity) | |||||
| member.UpdateActivity(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| return socket; | |||||
| } | |||||
| /// <summary> Connects to the Discord server with the provided email and password. </summary> | /// <summary> Connects to the Discord server with the provided email and password. </summary> | ||||
| /// <returns> Returns a token for future connections. </returns> | /// <returns> Returns a token for future connections. </returns> | ||||
| public new async Task<string> Connect(string email, string password) | public new async Task<string> Connect(string email, string password) | ||||
| @@ -249,6 +257,18 @@ namespace Discord | |||||
| { | { | ||||
| await base.Cleanup().ConfigureAwait(false); | await base.Cleanup().ConfigureAwait(false); | ||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| { | |||||
| if (_config.EnableVoiceMultiserver) | |||||
| { | |||||
| var tasks = _voiceClients | |||||
| .Select(x => x.Value.Disconnect()) | |||||
| .ToArray(); | |||||
| _voiceClients.Clear(); | |||||
| await Task.WhenAll(tasks).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| if (_config.UseMessageQueue) | if (_config.UseMessageQueue) | ||||
| { | { | ||||
| Message ignored; | Message ignored; | ||||
| @@ -624,20 +644,6 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| case "VOICE_SERVER_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | |||||
| if (data.GuildId == _voiceSocket.CurrentServerId) | |||||
| { | |||||
| var server = _servers[data.GuildId]; | |||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| { | |||||
| _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; | |||||
| await _voiceSocket.Login(CurrentUserId, _dataSocket.SessionId, data.Token, CancelToken).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | |||||
| break; | |||||
| //Settings | //Settings | ||||
| case "USER_UPDATE": | case "USER_UPDATE": | ||||
| @@ -657,11 +663,76 @@ namespace Discord | |||||
| } | } | ||||
| break; | break; | ||||
| //Internal (handled in DiscordSimpleClient) | |||||
| case "VOICE_SERVER_UPDATE": | |||||
| break; | |||||
| //Others | //Others | ||||
| default: | default: | ||||
| RaiseOnLog(LogMessageSeverity.Warning, LogMessageSource.DataWebSocket, $"Unknown message type: {e.Type}"); | RaiseOnLog(LogMessageSeverity.Warning, LogMessageSource.DataWebSocket, $"Unknown message type: {e.Type}"); | ||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| public IDiscordVoiceClient GetVoiceClient(string serverId) | |||||
| { | |||||
| if (serverId == null) throw new ArgumentNullException(nameof(serverId)); | |||||
| if (!_config.EnableVoiceMultiserver) | |||||
| { | |||||
| if (serverId == _voiceServerId) | |||||
| return this; | |||||
| else | |||||
| return null; | |||||
| } | |||||
| DiscordSimpleClient client; | |||||
| if (_voiceClients.TryGetValue(serverId, out client)) | |||||
| return client; | |||||
| else | |||||
| return null; | |||||
| } | |||||
| private async Task<IDiscordVoiceClient> CreateVoiceClient(string serverId) | |||||
| { | |||||
| if (!_config.EnableVoiceMultiserver) | |||||
| { | |||||
| _voiceServerId = serverId; | |||||
| return this; | |||||
| } | |||||
| var client = _voiceClients.GetOrAdd(serverId, _ => | |||||
| { | |||||
| var config = _config.Clone(); | |||||
| config.LogLevel = (LogMessageSeverity)Math.Min((int)_config.LogLevel, (int)LogMessageSeverity.Warning); | |||||
| config.EnableVoiceMultiserver = false; | |||||
| return new DiscordSimpleClient(config, serverId); | |||||
| }); | |||||
| await client.Connect(_gateway, _token).ConfigureAwait(false); | |||||
| return client; | |||||
| } | |||||
| public Task JoinVoiceServer(Channel channel) | |||||
| => JoinVoiceServer(channel?.ServerId, channel?.Id); | |||||
| public Task JoinVoiceServer(Server server, string channelId) | |||||
| => JoinVoiceServer(server?.Id, channelId); | |||||
| public async Task JoinVoiceServer(string serverId, string channelId) | |||||
| { | |||||
| CheckReady(); //checkVoice is done inside the voice client | |||||
| if (serverId == null) throw new ArgumentNullException(nameof(serverId)); | |||||
| if (channelId == null) throw new ArgumentNullException(nameof(channelId)); | |||||
| var client = await CreateVoiceClient(serverId).ConfigureAwait(false); | |||||
| await client.JoinChannel(channelId).ConfigureAwait(false); | |||||
| } | |||||
| async Task LeaveVoiceServer(string serverId) | |||||
| { | |||||
| CheckReady(); //checkVoice is done inside the voice client | |||||
| if (serverId == null) throw new ArgumentNullException(nameof(serverId)); | |||||
| DiscordSimpleClient client; | |||||
| if (_voiceClients.TryRemove(serverId, out client)) | |||||
| await client.Disconnect(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -11,7 +11,7 @@ namespace Discord | |||||
| Both = Outgoing | Incoming | Both = Outgoing | Incoming | ||||
| } | } | ||||
| public class DiscordClientConfig | |||||
| public sealed class DiscordClientConfig | |||||
| { | { | ||||
| /// <summary> Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. </summary> | /// <summary> Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. </summary> | ||||
| public LogMessageSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | public LogMessageSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | ||||
| @@ -72,5 +72,12 @@ namespace Discord | |||||
| throw new InvalidOperationException("Unable to modify a discord client's configuration after it has been created."); | throw new InvalidOperationException("Unable to modify a discord client's configuration after it has been created."); | ||||
| storage = value; | storage = value; | ||||
| } | } | ||||
| } | |||||
| public DiscordClientConfig Clone() | |||||
| { | |||||
| var config = this.MemberwiseClone() as DiscordClientConfig; | |||||
| config._isLocked = false; | |||||
| return config; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -32,6 +32,16 @@ namespace Discord | |||||
| Error = error; | Error = error; | ||||
| } | } | ||||
| } | } | ||||
| public class VoiceDisconnectedEventArgs : DisconnectedEventArgs | |||||
| { | |||||
| public readonly string ServerId; | |||||
| internal VoiceDisconnectedEventArgs(string serverId, DisconnectedEventArgs e) | |||||
| : base(e.WasUnexpected, e.Error) | |||||
| { | |||||
| ServerId = serverId; | |||||
| } | |||||
| } | |||||
| public sealed class LogMessageEventArgs : EventArgs | public sealed class LogMessageEventArgs : EventArgs | ||||
| { | { | ||||
| public LogMessageSeverity Severity { get; } | public LogMessageSeverity Severity { get; } | ||||
| @@ -63,7 +73,7 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| public abstract partial class DiscordBaseClient | |||||
| public partial class DiscordSimpleClient | |||||
| { | { | ||||
| public event EventHandler Connected; | public event EventHandler Connected; | ||||
| private void RaiseConnected() | private void RaiseConnected() | ||||
| @@ -90,11 +100,11 @@ namespace Discord | |||||
| if (VoiceConnected != null) | if (VoiceConnected != null) | ||||
| RaiseEvent(nameof(VoiceConnected), () => VoiceConnected(this, EventArgs.Empty)); | RaiseEvent(nameof(VoiceConnected), () => VoiceConnected(this, EventArgs.Empty)); | ||||
| } | } | ||||
| public event EventHandler<DisconnectedEventArgs> VoiceDisconnected; | |||||
| private void RaiseVoiceDisconnected(DisconnectedEventArgs e) | |||||
| public event EventHandler<VoiceDisconnectedEventArgs> VoiceDisconnected; | |||||
| private void RaiseVoiceDisconnected(string serverId, DisconnectedEventArgs e) | |||||
| { | { | ||||
| if (VoiceDisconnected != null) | if (VoiceDisconnected != null) | ||||
| RaiseEvent(nameof(VoiceDisconnected), () => VoiceDisconnected(this, e)); | |||||
| RaiseEvent(nameof(VoiceDisconnected), () => VoiceDisconnected(this, new VoiceDisconnectedEventArgs(serverId, e))); | |||||
| } | } | ||||
| public event EventHandler<VoicePacketEventArgs> OnVoicePacket; | public event EventHandler<VoicePacketEventArgs> OnVoicePacket; | ||||
| @@ -6,47 +6,53 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public partial class DiscordBaseClient | |||||
| public interface IDiscordVoiceClient | |||||
| { | { | ||||
| public Task JoinVoiceServer(Channel channel) | |||||
| => JoinVoiceServer(channel?.ServerId, channel?.Id); | |||||
| public Task JoinVoiceServer(Server server, string channelId) | |||||
| => JoinVoiceServer(server?.Id, channelId); | |||||
| public async Task JoinVoiceServer(string serverId, string channelId) | |||||
| Task JoinChannel(string channelId); | |||||
| Task Disconnect(); | |||||
| void SendVoicePCM(byte[] data, int count); | |||||
| void ClearVoicePCM(); | |||||
| Task WaitVoice(); | |||||
| } | |||||
| public partial class DiscordSimpleClient : IDiscordVoiceClient | |||||
| { | |||||
| async Task IDiscordVoiceClient.JoinChannel(string channelId) | |||||
| { | { | ||||
| CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
| if (serverId == null) throw new ArgumentNullException(nameof(serverId)); | |||||
| if (channelId == null) throw new ArgumentNullException(nameof(channelId)); | if (channelId == null) throw new ArgumentNullException(nameof(channelId)); | ||||
| await LeaveVoiceServer().ConfigureAwait(false); | |||||
| _voiceSocket.SetChannel(serverId, channelId); | |||||
| _dataSocket.SendJoinVoice(serverId, channelId); | |||||
| await ((IDiscordVoiceClient)this).Disconnect().ConfigureAwait(false); | |||||
| _voiceSocket.SetChannel(_voiceServerId, channelId); | |||||
| _dataSocket.SendJoinVoice(_voiceServerId, channelId); | |||||
| CancellationTokenSource tokenSource = new CancellationTokenSource(); | CancellationTokenSource tokenSource = new CancellationTokenSource(); | ||||
| try | try | ||||
| { | { | ||||
| await Task.Run(() => _voiceSocket.WaitForConnection(tokenSource.Token)) | |||||
| await Task.Run(() => _voiceSocket.WaitForConnection(tokenSource.Token)) | |||||
| .Timeout(_config.ConnectionTimeout, tokenSource) | .Timeout(_config.ConnectionTimeout, tokenSource) | ||||
| .ConfigureAwait(false); | .ConfigureAwait(false); | ||||
| } | } | ||||
| catch (TimeoutException) | catch (TimeoutException) | ||||
| { | { | ||||
| tokenSource.Cancel(); | tokenSource.Cancel(); | ||||
| await LeaveVoiceServer().ConfigureAwait(false); | |||||
| await ((IDiscordVoiceClient)this).Disconnect().ConfigureAwait(false); | |||||
| throw; | throw; | ||||
| } | } | ||||
| } | } | ||||
| public async Task LeaveVoiceServer() | |||||
| async Task IDiscordVoiceClient.Disconnect() | |||||
| { | { | ||||
| CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
| if (_voiceSocket.State != WebSocketState.Disconnected) | if (_voiceSocket.State != WebSocketState.Disconnected) | ||||
| { | { | ||||
| var serverId = _voiceSocket.CurrentServerId; | |||||
| if (serverId != null) | |||||
| if (_voiceSocket.CurrentServerId != null) | |||||
| { | { | ||||
| await _voiceSocket.Disconnect().ConfigureAwait(false); | await _voiceSocket.Disconnect().ConfigureAwait(false); | ||||
| _dataSocket.SendLeaveVoice(serverId); | |||||
| _dataSocket.SendLeaveVoice(_voiceSocket.CurrentServerId); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -54,7 +60,7 @@ namespace Discord | |||||
| /// <summary> Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. </summary> | /// <summary> Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. </summary> | ||||
| /// <param name="data">PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. </param> | /// <param name="data">PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. </param> | ||||
| /// <param name="count">Number of bytes in this frame. </param> | /// <param name="count">Number of bytes in this frame. </param> | ||||
| public void SendVoicePCM(byte[] data, int count) | |||||
| void IDiscordVoiceClient.SendVoicePCM(byte[] data, int count) | |||||
| { | { | ||||
| CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
| if (data == null) throw new ArgumentException(nameof(data)); | if (data == null) throw new ArgumentException(nameof(data)); | ||||
| @@ -64,7 +70,7 @@ 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() | |||||
| void IDiscordVoiceClient.ClearVoicePCM() | |||||
| { | { | ||||
| CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
| @@ -72,7 +78,7 @@ namespace Discord | |||||
| } | } | ||||
| /// <summary> Returns a task that completes once the voice output buffer is empty. </summary> | /// <summary> Returns a task that completes once the voice output buffer is empty. </summary> | ||||
| public async Task WaitVoice() | |||||
| async Task IDiscordVoiceClient.WaitVoice() | |||||
| { | { | ||||
| CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
| @@ -1,10 +1,6 @@ | |||||
| using Discord.API; | |||||
| using Discord.Collections; | |||||
| using Discord.Helpers; | |||||
| using Discord.Helpers; | |||||
| using Discord.WebSockets.Data; | using Discord.WebSockets.Data; | ||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | |||||
| using System.Net; | |||||
| using System.Runtime.ExceptionServices; | using System.Runtime.ExceptionServices; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -21,18 +17,24 @@ namespace Discord | |||||
| } | } | ||||
| /// <summary> Provides a barebones connection to the Discord service </summary> | /// <summary> Provides a barebones connection to the Discord service </summary> | ||||
| public partial class DiscordBaseClient | |||||
| public partial class DiscordSimpleClient | |||||
| { | { | ||||
| internal readonly DataWebSocket _dataSocket; | internal readonly DataWebSocket _dataSocket; | ||||
| internal readonly VoiceWebSocket _voiceSocket; | internal readonly VoiceWebSocket _voiceSocket; | ||||
| protected readonly ManualResetEvent _disconnectedEvent; | protected readonly ManualResetEvent _disconnectedEvent; | ||||
| protected readonly ManualResetEventSlim _connectedEvent; | protected readonly ManualResetEventSlim _connectedEvent; | ||||
| protected readonly bool _enableVoice; | |||||
| protected string _gateway, _token; | |||||
| protected string _voiceServerId; | |||||
| private Task _runTask; | private Task _runTask; | ||||
| private string _gateway, _token; | |||||
| protected ExceptionDispatchInfo _disconnectReason; | protected ExceptionDispatchInfo _disconnectReason; | ||||
| private bool _wasDisconnectUnexpected; | private bool _wasDisconnectUnexpected; | ||||
| /// <summary> Returns the configuration object used to make this client. Note that this object cannot be edited directly - to change the configuration of this client, use the DiscordClient(DiscordClientConfig config) constructor. </summary> | |||||
| public DiscordClientConfig Config => _config; | |||||
| protected readonly DiscordClientConfig _config; | |||||
| /// <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; | ||||
| @@ -43,64 +45,78 @@ namespace Discord | |||||
| public DiscordClientState State => (DiscordClientState)_state; | public DiscordClientState State => (DiscordClientState)_state; | ||||
| private int _state; | private int _state; | ||||
| /// <summary> Returns the configuration object used to make this client. Note that this object cannot be edited directly - to change the configuration of this client, use the DiscordClient(DiscordClientConfig config) constructor. </summary> | |||||
| public DiscordClientConfig Config => _config; | |||||
| protected readonly DiscordClientConfig _config; | |||||
| public CancellationToken CancelToken => _cancelToken; | public CancellationToken CancelToken => _cancelToken; | ||||
| private CancellationTokenSource _cancelTokenSource; | private CancellationTokenSource _cancelTokenSource; | ||||
| private CancellationToken _cancelToken; | private CancellationToken _cancelToken; | ||||
| /// <summary> Initializes a new instance of the DiscordClient class. </summary> | /// <summary> Initializes a new instance of the DiscordClient class. </summary> | ||||
| public DiscordBaseClient(DiscordClientConfig config = null) | |||||
| public DiscordSimpleClient(DiscordClientConfig config = null) | |||||
| { | { | ||||
| _config = config ?? new DiscordClientConfig(); | _config = config ?? new DiscordClientConfig(); | ||||
| _config.Lock(); | _config.Lock(); | ||||
| _enableVoice = config.VoiceMode != DiscordVoiceMode.Disabled && !config.EnableVoiceMultiserver; | |||||
| _state = (int)DiscordClientState.Disconnected; | _state = (int)DiscordClientState.Disconnected; | ||||
| _cancelToken = new CancellationToken(true); | _cancelToken = new CancellationToken(true); | ||||
| _disconnectedEvent = new ManualResetEvent(true); | _disconnectedEvent = new ManualResetEvent(true); | ||||
| _connectedEvent = new ManualResetEventSlim(false); | _connectedEvent = new ManualResetEventSlim(false); | ||||
| _dataSocket = new DataWebSocket(this); | |||||
| _dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); }; | |||||
| _dataSocket.Disconnected += async (s, e) => | |||||
| _dataSocket = CreateDataSocket(); | |||||
| if (_enableVoice) | |||||
| _voiceSocket = CreateVoiceSocket(); | |||||
| } | |||||
| internal DiscordSimpleClient(DiscordClientConfig config = null, string serverId = null) | |||||
| : this(config) | |||||
| { | |||||
| _voiceServerId = serverId; | |||||
| } | |||||
| internal virtual DataWebSocket CreateDataSocket() | |||||
| { | |||||
| var socket = new DataWebSocket(this); | |||||
| socket.Connected += (s, e) => | |||||
| { | |||||
| if (_state == (int)DiscordClientState.Connecting) | |||||
| CompleteConnect(); } | |||||
| ; | |||||
| socket.Disconnected += async (s, e) => | |||||
| { | { | ||||
| RaiseDisconnected(e); | RaiseDisconnected(e); | ||||
| if (e.WasUnexpected) | if (e.WasUnexpected) | ||||
| await _dataSocket.Reconnect(_token); | |||||
| await socket.Reconnect(_token); | |||||
| }; | }; | ||||
| if (Config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| socket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message); | |||||
| if (_config.LogLevel >= LogMessageSeverity.Info) | |||||
| { | { | ||||
| _voiceSocket = new VoiceWebSocket(this); | |||||
| _voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||||
| _voiceSocket.Disconnected += async (s, e) => | |||||
| { | |||||
| RaiseVoiceDisconnected(e); | |||||
| if (e.WasUnexpected) | |||||
| await _voiceSocket.Reconnect(); | |||||
| }; | |||||
| socket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected"); | |||||
| socket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected"); | |||||
| } | } | ||||
| _dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message); | |||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| _voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.VoiceWebSocket, e.Message); | |||||
| socket.ReceivedEvent += (s, e) => OnReceivedEvent(e); | |||||
| return socket; | |||||
| } | |||||
| internal virtual VoiceWebSocket CreateVoiceSocket() | |||||
| { | |||||
| var socket = new VoiceWebSocket(this); | |||||
| socket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.VoiceWebSocket, e.Message); | |||||
| socket.Connected += (s, e) => RaiseVoiceConnected(); | |||||
| socket.Disconnected += async (s, e) => | |||||
| { | |||||
| RaiseVoiceDisconnected(socket.CurrentServerId, e); | |||||
| if (e.WasUnexpected) | |||||
| await socket.Reconnect(); | |||||
| }; | |||||
| if (_config.LogLevel >= LogMessageSeverity.Info) | if (_config.LogLevel >= LogMessageSeverity.Info) | ||||
| { | { | ||||
| _dataSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected"); | |||||
| _dataSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected"); | |||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| { | |||||
| _voiceSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Connected"); | |||||
| _voiceSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected"); | |||||
| } | |||||
| socket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Connected"); | |||||
| socket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected"); | |||||
| } | } | ||||
| _dataSocket.ReceivedEvent += (s, e) => OnReceivedEvent(e); | |||||
| return socket; | |||||
| } | } | ||||
| //Connection | //Connection | ||||
| protected async Task<string> Connect(string gateway, string token) | |||||
| public async Task<string> Connect(string gateway, string token) | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| @@ -132,7 +148,6 @@ namespace Discord | |||||
| } | } | ||||
| //_state = (int)DiscordClientState.Connected; | //_state = (int)DiscordClientState.Connected; | ||||
| _token = token; | |||||
| return token; | return token; | ||||
| } | } | ||||
| catch | catch | ||||
| @@ -222,7 +237,7 @@ namespace Discord | |||||
| protected virtual async Task Cleanup() | protected virtual async Task Cleanup() | ||||
| { | { | ||||
| await _dataSocket.Disconnect().ConfigureAwait(false); | await _dataSocket.Disconnect().ConfigureAwait(false); | ||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| if (_enableVoice) | |||||
| await _voiceSocket.Disconnect().ConfigureAwait(false); | await _voiceSocket.Disconnect().ConfigureAwait(false); | ||||
| _currentUserId = null; | _currentUserId = null; | ||||
| @@ -249,7 +264,7 @@ namespace Discord | |||||
| throw new InvalidOperationException("The client is connecting."); | throw new InvalidOperationException("The client is connecting."); | ||||
| } | } | ||||
| if (checkVoice && _config.VoiceMode == DiscordVoiceMode.Disabled) | |||||
| if (checkVoice && !_enableVoice) | |||||
| throw new InvalidOperationException("Voice is not enabled for this client."); | throw new InvalidOperationException("Voice is not enabled for this client."); | ||||
| } | } | ||||
| protected void RaiseEvent(string name, Action action) | protected void RaiseEvent(string name, Action action) | ||||
| @@ -264,8 +279,24 @@ namespace Discord | |||||
| internal virtual Task OnReceivedEvent(WebSocketEventEventArgs e) | internal virtual Task OnReceivedEvent(WebSocketEventEventArgs e) | ||||
| { | { | ||||
| if (e.Type == "READY") | |||||
| _currentUserId = e.Payload["user"].Value<string>("id"); | |||||
| switch (e.Type) | |||||
| { | |||||
| case "READY": | |||||
| _currentUserId = e.Payload["user"].Value<string>("id"); | |||||
| break; | |||||
| case "VOICE_SERVER_UPDATE": | |||||
| { | |||||
| string guildId = e.Payload.Value<string>("guild_id"); | |||||
| if (_enableVoice && guildId == _voiceSocket.CurrentServerId) | |||||
| { | |||||
| string token = e.Payload.Value<string>("token"); | |||||
| _voiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; | |||||
| return _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, token, CancelToken); | |||||
| } | |||||
| } | |||||
| break; | |||||
| } | |||||
| return TaskHelper.CompletedTask; | return TaskHelper.CompletedTask; | ||||
| } | } | ||||
| } | } | ||||
| @@ -12,7 +12,7 @@ namespace Discord.WebSockets.Data | |||||
| public string SessionId => _sessionId; | public string SessionId => _sessionId; | ||||
| private string _sessionId; | private string _sessionId; | ||||
| public DataWebSocket(DiscordBaseClient client) | |||||
| public DataWebSocket(DiscordSimpleClient client) | |||||
| : base(client) | : base(client) | ||||
| { | { | ||||
| } | } | ||||
| @@ -5,6 +5,7 @@ using Newtonsoft.Json; | |||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Net; | using System.Net; | ||||
| @@ -45,7 +46,7 @@ namespace Discord.WebSockets.Voice | |||||
| public string CurrentServerId => _serverId; | public string CurrentServerId => _serverId; | ||||
| public string CurrentChannelId => _channelId; | public string CurrentChannelId => _channelId; | ||||
| public VoiceWebSocket(DiscordBaseClient client) | |||||
| public VoiceWebSocket(DiscordSimpleClient client) | |||||
| : base(client) | : base(client) | ||||
| { | { | ||||
| _rand = new Random(); | _rand = new Random(); | ||||
| @@ -121,25 +122,32 @@ namespace Discord.WebSockets.Voice | |||||
| msg.Payload.UserId = _userId; | msg.Payload.UserId = _userId; | ||||
| QueueMessage(msg); | QueueMessage(msg); | ||||
| List<Task> tasks = new List<Task>(); | |||||
| if ((_client.Config.VoiceMode & DiscordVoiceMode.Outgoing) != 0) | |||||
| { | |||||
| #if USE_THREAD | #if USE_THREAD | ||||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | |||||
| _sendThread.Start(); | |||||
| _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken))); | |||||
| _receiveThread.Start(); | |||||
| #if !DNXCORE50 | |||||
| return new Task[] { WatcherAsync() }.Concat(base.Run()).ToArray(); | |||||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | |||||
| _sendThread.Start(); | |||||
| #else | #else | ||||
| return base.Run(); | |||||
| tasks.Add(SendVoiceAsync()); | |||||
| #endif | #endif | ||||
| #else //!USE_THREAD | |||||
| return new Task[] { Task.WhenAll( | |||||
| ReceiveVoiceAsync(), | |||||
| SendVoiceAsync(), | |||||
| #if !DNXCORE50 | |||||
| WatcherAsync() | |||||
| } | |||||
| if ((_client.Config.VoiceMode & DiscordVoiceMode.Incoming) != 0) | |||||
| { | |||||
| #if USE_THREAD | |||||
| _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken))); | |||||
| _receiveThread.Start(); | |||||
| #else | |||||
| tasks.Add(ReceiveVoiceAsync()); | |||||
| #endif | #endif | ||||
| )}.Concat(base.Run()).ToArray(); | |||||
| } | |||||
| #if !DNXCORE50 | |||||
| tasks.Add(WatcherAsync()); | |||||
| #endif | #endif | ||||
| tasks.AddRange(base.Run()); | |||||
| return tasks.ToArray(); | |||||
| } | } | ||||
| protected override Task Cleanup() | protected override Task Cleanup() | ||||
| { | { | ||||
| @@ -35,7 +35,7 @@ namespace Discord.WebSockets | |||||
| internal abstract partial class WebSocket | internal abstract partial class WebSocket | ||||
| { | { | ||||
| protected readonly IWebSocketEngine _engine; | protected readonly IWebSocketEngine _engine; | ||||
| protected readonly DiscordBaseClient _client; | |||||
| protected readonly DiscordSimpleClient _client; | |||||
| protected readonly LogMessageSeverity _logLevel; | protected readonly LogMessageSeverity _logLevel; | ||||
| protected readonly ManualResetEventSlim _connectedEvent; | protected readonly ManualResetEventSlim _connectedEvent; | ||||
| @@ -57,7 +57,7 @@ namespace Discord.WebSockets | |||||
| public WebSocketState State => (WebSocketState)_state; | public WebSocketState State => (WebSocketState)_state; | ||||
| protected int _state; | protected int _state; | ||||
| public WebSocket(DiscordBaseClient client) | |||||
| public WebSocket(DiscordSimpleClient client) | |||||
| { | { | ||||
| _client = client; | _client = client; | ||||
| _logLevel = client.Config.LogLevel; | _logLevel = client.Config.LogLevel; | ||||