@@ -1,9 +1,12 @@
using Discord.API.Client.GatewaySocket;
using Discord.API.Client.GatewaySocket;
using Discord.API.Client.Rest;
using Discord.Logging;
using Discord.Logging;
using Discord.Net.Rest;
using Discord.Net.WebSockets;
using Discord.Net.WebSockets;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Nito.AsyncEx;
using Nito.AsyncEx;
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.IO;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
@@ -41,124 +44,190 @@ namespace Discord.Audio
}
}
}
}
private readonly DiscordConfig _config;
private readonly AsyncLock _connectionLock;
private readonly AsyncLock _connectionLock;
private readonly JsonSerializer _serializ er;
private CancellationTokenSource _cancelTokenSourc e;
private readonly TaskManager _taskManag er;
private ConnectionState _gatewayStat e;
internal AudioService Service { get; }
internal Logger Logger { get; }
internal Logger Logger { get; }
/// <summary> Gets the unique identifier for this client. </summary>
public int Id { get; }
public int Id { get; }
/// <summary> Gets the service managing this client. </summary>
public AudioService Service { get; }
/// <summary> Gets the configuration object used to make this client. </summary>
public AudioServiceConfig Config { get; }
/// <summary> Gets the internal RestClient for the Client API endpoint. </summary>
public RestClient ClientAPI { get; }
/// <summary> Gets the internal WebSocket for the Gateway event stream. </summary>
public GatewaySocket GatewaySocket { get; }
public GatewaySocket GatewaySocket { get; }
public VoiceWebSocket VoiceSocket { get; }
/// <summary> Gets the internal WebSocket for the Voice control stream. </summary>
public VoiceSocket VoiceSocket { get; }
/// <summary> Gets the JSON serializer used by this client. </summary>
public JsonSerializer Serializer { get; }
/// <summary> </summary>
public Stream OutputStream { get; }
public Stream OutputStream { get; }
/// <summary> Gets a cancellation token that triggers when the client is manually disconnected. </summary>
public CancellationToken CancelToken { get; private set; }
/// <summary> Gets the session id for the current connection. </summary>
public string SessionId { get; private set; }
/// <summary> Gets the current state of this client. </summary>
public ConnectionState State => VoiceSocket.State;
public ConnectionState State => VoiceSocket.State;
/// <summary> Gets the server this client is bound to. </summary>
public Server Server => VoiceSocket.Server;
public Server Server => VoiceSocket.Server;
/// <summary> Gets the channel </summary>
public Channel Channel => VoiceSocket.Channel;
public Channel Channel => VoiceSocket.Channel;
public AudioClient(AudioService service, int clientId, Server server, GatewaySocket gatewaySocket, Logger logger)
public AudioClient(DiscordClient client, Server server, int id )
{
{
Service = service ;
_serializer = service.Client.Serializer ;
Id = clientId ;
GatewaySocket = gatewaySocket ;
Logger = logg er;
OutputStream = new OutStream(this) ;
Id = id ;
_config = client.Config ;
Service = client.Audio() ;
Config = Service.Config ;
Serializer = client.Serializ er;
_gatewayState = (int)ConnectionState.Disconnected ;
_connectionLock = new AsyncLock();
//Logging
Logger = client.Log.CreateLogger($"AudioClient #{id}");
GatewaySocket.ReceivedDispatch += OnReceivedDispatch;
//Async
_taskManager = new TaskManager(Cleanup, false);
_connectionLock = new AsyncLock();
CancelToken = new CancellationToken(true);
VoiceSocket = new VoiceWebSocket(service.Client, this, logger);
//Networking
if (Config.EnableMultiserver)
{
ClientAPI = new RestClient(_config, DiscordConfig.ClientAPIUrl, client.Log.CreateLogger($"ClientAPI #{id}"));
GatewaySocket = new GatewaySocket(_config, client.Serializer, client.Log.CreateLogger($"Gateway #{id}"));
GatewaySocket.Connected += (s, e) =>
{
if (_gatewayState == ConnectionState.Connecting)
EndGatewayConnect();
};
}
else
GatewaySocket = client.GatewaySocket;
GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e);
VoiceSocket = new VoiceSocket(_config, Config, client.Serializer, client.Log.CreateLogger($"Voice #{id}"));
VoiceSocket.Server = server;
VoiceSocket.Server = server;
/*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected();
_voiceSocket.Disconnected += async (s, e) =>
{
_voiceSocket.CurrentServerId;
if (voiceServerId != null)
_gatewaySocket.SendLeaveVoice(voiceServerId.Value);
await _voiceSocket.Disconnect().ConfigureAwait(false);
RaiseVoiceDisconnected(socket.CurrentServerId.Value, e);
if (e.WasUnexpected)
await socket.Reconnect().ConfigureAwait(false);
};*/
/*_voiceSocket.IsSpeaking += (s, e) =>
{
if (_voiceSocket.State == WebSocketState.Connected)
{
var user = _users[e.UserId, socket.CurrentServerId];
bool value = e.IsSpeaking;
if (user.IsSpeaking != value)
{
user.IsSpeaking = value;
var channel = _channels[_voiceSocket.CurrentChannelId];
RaiseUserIsSpeaking(user, channel, value);
if (Config.TrackActivity)
user.UpdateActivity();
}
}
};*/
/*this.Connected += (s, e) =>
{
_voiceSocket.ParentCancelToken = _cancelToken;
};*/
OutputStream = new OutStream(this);
}
}
public async Task Join(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (channel.Type != ChannelType.Voice)
throw new ArgumentException("Channel must be a voice channel.", nameof(channel));
if (channel.Server != VoiceSocket.Server)
throw new ArgumentException("This is channel is not part of the current server.", nameof(channel));
if (channel == VoiceSocket.Channel) return;
if (VoiceSocket.Server == null)
throw new InvalidOperationException("This client has been closed.");
using (await _connectionLock.LockAsync().ConfigureAwait(false))
/// <summary> Connects to the Discord server with the provided token. </summary>
public async Task Connect()
{
if (Config.EnableMultiserver)
await BeginGatewayConnect().ConfigureAwait(false);
else
{
{
VoiceSocket.Channel = channel;
await Task.Run(() =>
var cancelSource = new CancellationTokenSource();
CancelToken = cancelSource.Token;
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false);
}
}
private async Task BeginGatewayConnect()
{
try
{
using (await _connectionLock.LockAsync().ConfigureAwait(false))
{
{
SendVoiceUpdate();
VoiceSocket.WaitForConnection(_cancelTokenSource.Token);
});
await Disconnect().ConfigureAwait(false);
_taskManager.ClearException();
ClientAPI.Token = Service.Client.ClientAPI.Token;
Stopwatch stopwatch = null;
if (_config.LogLevel >= LogSeverity.Verbose)
stopwatch = Stopwatch.StartNew();
_gatewayState = ConnectionState.Connecting;
var cancelSource = new CancellationTokenSource();
CancelToken = cancelSource.Token;
ClientAPI.CancelToken = CancelToken;
await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false);
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false);
GatewaySocket.WaitForConnection(CancelToken);
if (_config.LogLevel >= LogSeverity.Verbose)
{
stopwatch.Stop();
double seconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerSecond, 2);
Logger.Verbose($"Connection took {seconds} sec");
}
}
}
catch (Exception ex)
{
await _taskManager.SignalError(ex).ConfigureAwait(false);
throw;
}
}
}
}
private void EndGatewayConnect()
{
_gatewayState = ConnectionState.Connected;
}
public async Task Connect(bool connectGateway)
/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary>
public async Task Disconnect()
{
{
using (await _connectionLock.LockAsync().ConfigureAwait(false))
{
_cancelTokenSource = new CancellationTokenSource();
var cancelToken = _cancelTokenSource.Token;
VoiceSocket.ParentCancelToken = cancelToken;
await _taskManager.Stop(true).ConfigureAwait(false);
if (Config.EnableMultiserver)
ClientAPI.Token = null;
}
private async Task Cleanup()
{
var oldState = _gatewayState;
_gatewayState = ConnectionState.Disconnecting;
if (connectGateway)
if (Config.EnableMultiserver)
{
if (oldState == ConnectionState.Connected)
{
{
GatewaySocket.ParentCancelToken = cancelToken;
await GatewaySocket.Connect().ConfigureAwait(false);
GatewaySocket.WaitForConnection(cancelToken);
try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); }
catch (OperationCanceledException) { }
}
}
await GatewaySocket.Disconnect().ConfigureAwait(false);
ClientAPI.Token = null;
}
}
var server = VoiceSocket.Server;
VoiceSocket.Server = null;
VoiceSocket.Channel = null;
if (Config.EnableMultiserver)
await Service.RemoveClient(server, this).ConfigureAwait(false);
SendVoiceUpdate(server.Id, null);
await VoiceSocket.Disconnect().ConfigureAwait(false);
if (Config.EnableMultiserver)
await GatewaySocket.Disconnect().ConfigureAwait(false);
_gatewayState = (int)ConnectionState.Disconnected;
}
}
public async Task Disconnect()
public async Task Join(Channel channel )
{
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (channel.Type != ChannelType.Voice)
throw new ArgumentException("Channel must be a voice channel.", nameof(channel));
if (channel == VoiceSocket.Channel) return;
var server = channel.Server;
if (server != VoiceSocket.Server)
throw new ArgumentException("This is channel is not part of the current server.", nameof(channel));
if (VoiceSocket.Server == null)
throw new InvalidOperationException("This client has been closed.");
SendVoiceUpdate(channel.Server.Id, channel.Id);
using (await _connectionLock.LockAsync().ConfigureAwait(false))
using (await _connectionLock.LockAsync().ConfigureAwait(false))
{
await Service.RemoveClient(VoiceSocket.Server, this).ConfigureAwait(false);
VoiceSocket.Channel = null;
SendVoiceUpdate();
await VoiceSocket.Disconnect();
}
await Task.Run(() => VoiceSocket.WaitForConnection(CancelToken));
}
}
private async void OnReceivedDispatch(object sender, WebSocketEventEventArgs e)
private async void OnReceivedEvent(WebSocketEventEventArgs e)
{
{
try
try
{
{
@@ -166,11 +235,11 @@ namespace Discord.Audio
{
{
case "VOICE_STATE_UPDATE":
case "VOICE_STATE_UPDATE":
{
{
var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_s erializer);
var data = e.Payload.ToObject<VoiceStateUpdateEvent>(S erializer);
if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id)
if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id)
{
{
if (data.ChannelId == null)
if (data.ChannelId == null)
await Disconnect();
await Disconnect().ConfigureAwait(false) ;
else
else
{
{
var channel = Service.Client.GetChannel(data.ChannelId.Value);
var channel = Service.Client.GetChannel(data.ChannelId.Value);
@@ -179,7 +248,7 @@ namespace Discord.Audio
else
else
{
{
Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting.");
Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting.");
await Disconnect();
await Disconnect().ConfigureAwait(false) ;
}
}
}
}
}
}
@@ -187,13 +256,16 @@ namespace Discord.Audio
break;
break;
case "VOICE_SERVER_UPDATE":
case "VOICE_SERVER_UPDATE":
{
{
var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_s erializer);
var data = e.Payload.ToObject<VoiceServerUpdateEvent>(S erializer);
if (data.GuildId == VoiceSocket.Server?.Id)
if (data.GuildId == VoiceSocket.Server?.Id)
{
{
var client = Service.Client;
var client = Service.Client;
VoiceSocket.Token = data.Token;
VoiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0];
await VoiceSocket.Connect().ConfigureAwait(false);
var id = client.CurrentUser?.Id;
if (id != null)
{
var host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0];
await VoiceSocket.Connect(host, data.Token, id.Value, GatewaySocket.SessionId, CancelToken).ConfigureAwait(false);
}
}
}
}
}
break;
break;
@@ -233,15 +305,11 @@ namespace Discord.Audio
VoiceSocket.WaitForQueue();
VoiceSocket.WaitForQueue();
}
}
private void SendVoiceUpdate( )
public void SendVoiceUpdate(ulong? serverId, ulong? channelId )
{
{
var serverId = VoiceSocket.Server?.Id;
if (serverId != null)
{
GatewaySocket.SendUpdateVoice(serverId, VoiceSocket.Channel?.Id,
(Service.Config.Mode | AudioMode.Outgoing) == 0,
(Service.Config.Mode | AudioMode.Incoming) == 0);
}
GatewaySocket.SendUpdateVoice(serverId, channelId,
(Service.Config.Mode | AudioMode.Outgoing) == 0,
(Service.Config.Mode | AudioMode.Incoming) == 0);
}
}
}
}
}
}