@@ -62,8 +62,8 @@ | |||
<Compile Include="..\Discord.Net.Audio\InternalIsSpeakingEventArgs.cs"> | |||
<Link>InternalIsSpeakingEventArgs.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\Net\VoiceWebSocket.cs"> | |||
<Link>Net\VoiceWebSocket.cs</Link> | |||
<Compile Include="..\Discord.Net.Audio\Net\VoiceSocket.cs"> | |||
<Link>Net\VoiceSocket.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\Opus\OpusConverter.cs"> | |||
<Link>Opus\OpusConverter.cs</Link> | |||
@@ -74,15 +74,15 @@ | |||
<Compile Include="..\Discord.Net.Audio\Opus\OpusEncoder.cs"> | |||
<Link>Opus\OpusEncoder.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\SimpleAudioClient.cs"> | |||
<Link>SimpleAudioClient.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\Sodium\SecretBox.cs"> | |||
<Link>Sodium\SecretBox.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\UserIsTalkingEventArgs.cs"> | |||
<Link>UserIsTalkingEventArgs.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\VirtualClient.cs"> | |||
<Link>VirtualClient.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\VoiceBuffer.cs"> | |||
<Link>VoiceBuffer.cs</Link> | |||
</Compile> | |||
@@ -1,9 +1,12 @@ | |||
using Discord.API.Client.GatewaySocket; | |||
using Discord.API.Client.Rest; | |||
using Discord.Logging; | |||
using Discord.Net.Rest; | |||
using Discord.Net.WebSockets; | |||
using Newtonsoft.Json; | |||
using Nito.AsyncEx; | |||
using System; | |||
using System.Diagnostics; | |||
using System.IO; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -41,124 +44,190 @@ namespace Discord.Audio | |||
} | |||
} | |||
private readonly DiscordConfig _config; | |||
private readonly AsyncLock _connectionLock; | |||
private readonly JsonSerializer _serializer; | |||
private CancellationTokenSource _cancelTokenSource; | |||
private readonly TaskManager _taskManager; | |||
private ConnectionState _gatewayState; | |||
internal AudioService Service { get; } | |||
internal Logger Logger { get; } | |||
/// <summary> Gets the unique identifier for this client. </summary> | |||
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 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; } | |||
/// <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; | |||
/// <summary> Gets the server this client is bound to. </summary> | |||
public Server Server => VoiceSocket.Server; | |||
/// <summary> Gets the channel </summary> | |||
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 = logger; | |||
OutputStream = new OutStream(this); | |||
Id = id; | |||
_config = client.Config; | |||
Service = client.Audio(); | |||
Config = Service.Config; | |||
Serializer = client.Serializer; | |||
_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.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)) | |||
{ | |||
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 | |||
{ | |||
@@ -166,11 +235,11 @@ namespace Discord.Audio | |||
{ | |||
case "VOICE_STATE_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_serializer); | |||
var data = e.Payload.ToObject<VoiceStateUpdateEvent>(Serializer); | |||
if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id) | |||
{ | |||
if (data.ChannelId == null) | |||
await Disconnect(); | |||
await Disconnect().ConfigureAwait(false); | |||
else | |||
{ | |||
var channel = Service.Client.GetChannel(data.ChannelId.Value); | |||
@@ -179,7 +248,7 @@ namespace Discord.Audio | |||
else | |||
{ | |||
Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting."); | |||
await Disconnect(); | |||
await Disconnect().ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
@@ -187,13 +256,16 @@ namespace Discord.Audio | |||
break; | |||
case "VOICE_SERVER_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | |||
var data = e.Payload.ToObject<VoiceServerUpdateEvent>(Serializer); | |||
if (data.GuildId == VoiceSocket.Server?.Id) | |||
{ | |||
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; | |||
@@ -233,15 +305,11 @@ namespace Discord.Audio | |||
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); | |||
} | |||
} | |||
} |
@@ -0,0 +1,242 @@ | |||
using Discord.API.Client.GatewaySocket; | |||
using Discord.Logging; | |||
using Discord.Net.Rest; | |||
using Discord.Net.WebSockets; | |||
using Newtonsoft.Json; | |||
using Nito.AsyncEx; | |||
using System; | |||
using System.IO; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
internal class AudioClient : IAudioClient | |||
{ | |||
private class OutStream : Stream | |||
{ | |||
public override bool CanRead => false; | |||
public override bool CanSeek => false; | |||
public override bool CanWrite => true; | |||
private readonly AudioClient _client; | |||
internal OutStream(AudioClient client) | |||
{ | |||
_client = client; | |||
} | |||
public override long Length { get { throw new InvalidOperationException(); } } | |||
public override long Position | |||
{ | |||
get { throw new InvalidOperationException(); } | |||
set { throw new InvalidOperationException(); } | |||
} | |||
public override void Flush() { throw new InvalidOperationException(); } | |||
public override long Seek(long offset, SeekOrigin origin) { throw new InvalidOperationException(); } | |||
public override void SetLength(long value) { throw new InvalidOperationException(); } | |||
public override int Read(byte[] buffer, int offset, int count) { throw new InvalidOperationException(); } | |||
public override void Write(byte[] buffer, int offset, int count) | |||
{ | |||
_client.Send(buffer, offset, count); | |||
} | |||
} | |||
private readonly JsonSerializer _serializer; | |||
private readonly bool _ownsGateway; | |||
private TaskManager _taskManager; | |||
private CancellationToken _cancelToken; | |||
internal AudioService Service { get; } | |||
internal Logger Logger { get; } | |||
public int Id { get; } | |||
public GatewaySocket GatewaySocket { get; } | |||
public VoiceSocket VoiceSocket { get; } | |||
public Stream OutputStream { get; } | |||
public ConnectionState State => VoiceSocket.State; | |||
public Server Server => VoiceSocket.Server; | |||
public Channel Channel => VoiceSocket.Channel; | |||
public AudioClient(AudioService service, int clientId, Server server, GatewaySocket gatewaySocket, bool ownsGateway, Logger logger) | |||
{ | |||
Service = service; | |||
_serializer = service.Client.Serializer; | |||
Id = clientId; | |||
GatewaySocket = gatewaySocket; | |||
_ownsGateway = ownsGateway; | |||
Logger = logger; | |||
OutputStream = new OutStream(this); | |||
_taskManager = new TaskManager(Cleanup, true); | |||
GatewaySocket.ReceivedDispatch += OnReceivedDispatch; | |||
VoiceSocket = new VoiceSocket(service.Client.Config, service.Config, service.Client.Serializer, logger); | |||
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; | |||
};*/ | |||
} | |||
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."); | |||
SendVoiceUpdate(channel.Server.Id, channel.Id); | |||
await Task.Run(() => VoiceSocket.WaitForConnection(_cancelToken)); | |||
} | |||
public async Task Connect(RestClient rest = null) | |||
{ | |||
var cancelSource = new CancellationTokenSource(); | |||
_cancelToken = cancelSource.Token; | |||
Task[] tasks; | |||
if (rest != null) | |||
tasks = new Task[] { GatewaySocket.Connect(rest, _cancelToken) }; | |||
else | |||
tasks = new Task[0]; | |||
await _taskManager.Start(tasks, cancelSource); | |||
} | |||
public Task Disconnect() => _taskManager.Stop(true); | |||
private async Task Cleanup() | |||
{ | |||
var server = VoiceSocket.Server; | |||
VoiceSocket.Server = null; | |||
VoiceSocket.Channel = null; | |||
await Service.RemoveClient(server, this).ConfigureAwait(false); | |||
SendVoiceUpdate(server.Id, null); | |||
await VoiceSocket.Disconnect().ConfigureAwait(false); | |||
if (_ownsGateway) | |||
await GatewaySocket.Disconnect().ConfigureAwait(false); | |||
} | |||
private async void OnReceivedDispatch(object sender, WebSocketEventEventArgs e) | |||
{ | |||
try | |||
{ | |||
switch (e.Type) | |||
{ | |||
case "VOICE_STATE_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_serializer); | |||
if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id) | |||
{ | |||
if (data.ChannelId == null) | |||
await Disconnect().ConfigureAwait(false); | |||
else | |||
{ | |||
var channel = Service.Client.GetChannel(data.ChannelId.Value); | |||
if (channel != null) | |||
VoiceSocket.Channel = channel; | |||
else | |||
{ | |||
Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting."); | |||
await Disconnect().ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
} | |||
break; | |||
case "VOICE_SERVER_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | |||
if (data.GuildId == VoiceSocket.Server?.Id) | |||
{ | |||
var client = Service.Client; | |||
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; | |||
} | |||
} | |||
catch (Exception ex) | |||
{ | |||
Logger.Error($"Error handling {e.Type} event", ex); | |||
} | |||
} | |||
/// <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="count">Number of bytes in this frame. </param> | |||
public void Send(byte[] data, int offset, int count) | |||
{ | |||
if (data == null) throw new ArgumentException(nameof(data)); | |||
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); | |||
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(count)); | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
if (count == 0) return; | |||
VoiceSocket.SendPCMFrames(data, offset, count); | |||
} | |||
/// <summary> Clears the PCM buffer. </summary> | |||
public void Clear() | |||
{ | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
VoiceSocket.ClearPCMFrames(); | |||
} | |||
/// <summary> Returns a task that completes once the voice output buffer is empty. </summary> | |||
public void Wait() | |||
{ | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
VoiceSocket.WaitForQueue(); | |||
} | |||
public void SendVoiceUpdate(ulong? serverId, ulong? channelId) | |||
{ | |||
GatewaySocket.SendUpdateVoice(serverId, channelId, | |||
(Service.Config.Mode | AudioMode.Outgoing) == 0, | |||
(Service.Config.Mode | AudioMode.Incoming) == 0); | |||
} | |||
} | |||
} |
@@ -1,4 +1,4 @@ | |||
using Discord.Net.WebSockets; | |||
using Nito.AsyncEx; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Linq; | |||
@@ -8,8 +8,10 @@ namespace Discord.Audio | |||
{ | |||
public class AudioService : IService | |||
{ | |||
private AudioClient _defaultClient; | |||
private ConcurrentDictionary<ulong, IAudioClient> _voiceClients; | |||
private readonly AsyncLock _asyncLock; | |||
private AudioClient _defaultClient; //Only used for single server | |||
private VirtualClient _currentClient; //Only used for single server | |||
private ConcurrentDictionary<ulong, AudioClient> _voiceClients; | |||
private ConcurrentDictionary<User, bool> _talkingUsers; | |||
private int _nextClientId; | |||
@@ -30,18 +32,20 @@ namespace Discord.Audio | |||
public AudioService(AudioServiceConfig config) | |||
{ | |||
Config = config; | |||
} | |||
_asyncLock = new AsyncLock(); | |||
} | |||
void IService.Install(DiscordClient client) | |||
{ | |||
Client = client; | |||
Config.Lock(); | |||
if (Config.EnableMultiserver) | |||
_voiceClients = new ConcurrentDictionary<ulong, IAudioClient>(); | |||
_voiceClients = new ConcurrentDictionary<ulong, AudioClient>(); | |||
else | |||
{ | |||
var logger = Client.Log.CreateLogger("Voice"); | |||
_defaultClient = new SimpleAudioClient(this, 0, logger); | |||
_defaultClient = new AudioClient(Client, null, 0); | |||
} | |||
_talkingUsers = new ConcurrentDictionary<User, bool>(); | |||
@@ -75,68 +79,30 @@ namespace Discord.Audio | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
if (!Config.EnableMultiserver) | |||
if (Config.EnableMultiserver) | |||
{ | |||
if (server == _defaultClient.Server) | |||
return (_defaultClient as SimpleAudioClient).CurrentClient; | |||
AudioClient client; | |||
if (_voiceClients.TryGetValue(server.Id, out client)) | |||
return client; | |||
else | |||
return null; | |||
} | |||
else | |||
{ | |||
IAudioClient client; | |||
if (_voiceClients.TryGetValue(server.Id, out client)) | |||
return client; | |||
if (server == _currentClient.Server) | |||
return _currentClient; | |||
else | |||
return null; | |||
} | |||
} | |||
private async Task<IAudioClient> CreateClient(Server server) | |||
{ | |||
var client = _voiceClients.GetOrAdd(server.Id, _ => null); //Placeholder, so we can't have two clients connecting at once | |||
if (client == null) | |||
{ | |||
int id = unchecked(++_nextClientId); | |||
var gatewayLogger = Client.Log.CreateLogger($"Gateway #{id}"); | |||
var voiceLogger = Client.Log.CreateLogger($"Voice #{id}"); | |||
var gatewaySocket = new GatewaySocket(Client, gatewayLogger); | |||
var voiceClient = new AudioClient(this, id, server, Client.GatewaySocket, voiceLogger); | |||
await voiceClient.Connect(true).ConfigureAwait(false); | |||
/*voiceClient.VoiceSocket.FrameReceived += (s, e) => | |||
{ | |||
OnFrameReceieved(e); | |||
}; | |||
voiceClient.VoiceSocket.UserIsSpeaking += (s, e) => | |||
{ | |||
var user = server.GetUser(e.UserId); | |||
OnUserIsSpeakingUpdated(user, e.IsSpeaking); | |||
};*/ | |||
//Update the placeholder only it still exists (RemoveClient wasnt called) | |||
if (!_voiceClients.TryUpdate(server.Id, voiceClient, null)) | |||
{ | |||
//If it was, cleanup | |||
await voiceClient.Disconnect().ConfigureAwait(false); ; | |||
await gatewaySocket.Disconnect().ConfigureAwait(false); ; | |||
} | |||
} | |||
return client; | |||
} | |||
//TODO: This isn't threadsafe | |||
internal async Task RemoveClient(Server server, IAudioClient client) | |||
//Called from AudioClient.Disconnect | |||
internal async Task RemoveClient(Server server, AudioClient client) | |||
{ | |||
if (Config.EnableMultiserver && server != null) | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
if (_voiceClients.TryRemove(server.Id, out client)) | |||
{ | |||
await client.Disconnect(); | |||
await (client as AudioClient).GatewaySocket.Disconnect(); | |||
} | |||
if (_voiceClients.TryUpdate(server.Id, null, client)) | |||
_voiceClients.TryRemove(server.Id, out client); | |||
} | |||
} | |||
@@ -144,16 +110,48 @@ namespace Discord.Audio | |||
{ | |||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
if (!Config.EnableMultiserver) | |||
{ | |||
await (_defaultClient as SimpleAudioClient).Connect(channel, false).ConfigureAwait(false); | |||
return _defaultClient; | |||
} | |||
else | |||
var server = channel.Server; | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
var client = await CreateClient(channel.Server).ConfigureAwait(false); | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
if (Config.EnableMultiserver) | |||
{ | |||
AudioClient client; | |||
if (!_voiceClients.TryGetValue(server.Id, out client)) | |||
{ | |||
client = new AudioClient(Client, server, unchecked(++_nextClientId)); | |||
_voiceClients[server.Id] = client; | |||
await client.Connect().ConfigureAwait(false); | |||
/*voiceClient.VoiceSocket.FrameReceived += (s, e) => | |||
{ | |||
OnFrameReceieved(e); | |||
}; | |||
voiceClient.VoiceSocket.UserIsSpeaking += (s, e) => | |||
{ | |||
var user = server.GetUser(e.UserId); | |||
OnUserIsSpeakingUpdated(user, e.IsSpeaking); | |||
};*/ | |||
} | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
} | |||
else | |||
{ | |||
if (_defaultClient.Server != server) | |||
{ | |||
await _defaultClient.Disconnect(); | |||
_defaultClient.VoiceSocket.Server = server; | |||
await _defaultClient.Connect().ConfigureAwait(false); | |||
} | |||
var client = new VirtualClient(_defaultClient, server); | |||
_currentClient = client; | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
} | |||
} | |||
} | |||
@@ -163,15 +161,18 @@ namespace Discord.Audio | |||
if (Config.EnableMultiserver) | |||
{ | |||
IAudioClient client; | |||
AudioClient client; | |||
if (_voiceClients.TryRemove(server.Id, out client)) | |||
await client.Disconnect().ConfigureAwait(false); | |||
} | |||
else | |||
{ | |||
IAudioClient client = GetClient(server); | |||
if (client != null) | |||
await (_defaultClient as SimpleAudioClient).Leave(client as SimpleAudioClient.VirtualClient).ConfigureAwait(false); | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
var client = GetClient(server) as VirtualClient; | |||
if (client != null) | |||
await _defaultClient.Disconnect().ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
} | |||
@@ -19,7 +19,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Net.WebSockets | |||
{ | |||
public partial class VoiceWebSocket : WebSocket | |||
public partial class VoiceSocket : WebSocket | |||
{ | |||
private const int MaxOpusSize = 4000; | |||
private const string EncryptedMode = "xsalsa20_poly1305"; | |||
@@ -27,8 +27,7 @@ namespace Discord.Net.WebSockets | |||
private readonly int _targetAudioBufferLength; | |||
private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | |||
private readonly AudioClient _audioClient; | |||
private readonly AudioServiceConfig _config; | |||
private readonly AudioServiceConfig _audioConfig; | |||
private Task _sendTask, _receiveTask; | |||
private VoiceBuffer _sendBuffer; | |||
private OpusEncoder _encoder; | |||
@@ -41,6 +40,8 @@ namespace Discord.Net.WebSockets | |||
private ushort _sequence; | |||
private string _encryptionMode; | |||
private int _ping; | |||
private ulong? _userId; | |||
private string _sessionId; | |||
public string Token { get; internal set; } | |||
public Server Server { get; internal set; } | |||
@@ -57,32 +58,37 @@ namespace Discord.Net.WebSockets | |||
internal void OnFrameReceived(ulong userId, ulong channelId, byte[] buffer, int offset, int count) | |||
=> FrameReceived(this, new InternalFrameEventArgs(userId, channelId, buffer, offset, count)); | |||
internal VoiceWebSocket(DiscordClient client, AudioClient audioClient, Logger logger) | |||
: base(client, logger) | |||
internal VoiceSocket(DiscordConfig config, AudioServiceConfig audioConfig, JsonSerializer serializer, Logger logger) | |||
: base(config, serializer, logger) | |||
{ | |||
_audioClient = audioClient; | |||
_config = client.Audio().Config; | |||
_audioConfig = audioConfig; | |||
_decoders = new ConcurrentDictionary<uint, OpusDecoder>(); | |||
_targetAudioBufferLength = _config.BufferLength / 20; //20 ms frames | |||
_targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames | |||
_encodingBuffer = new byte[MaxOpusSize]; | |||
_ssrcMapping = new ConcurrentDictionary<uint, ulong>(); | |||
_encoder = new OpusEncoder(48000, _config.Channels, 20, _config.Bitrate, OpusApplication.MusicOrMixed); | |||
_sendBuffer = new VoiceBuffer((int)Math.Ceiling(_config.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); | |||
_encoder = new OpusEncoder(48000, _audioConfig.Channels, 20, _audioConfig.Bitrate, OpusApplication.MusicOrMixed); | |||
_sendBuffer = new VoiceBuffer((int)Math.Ceiling(_audioConfig.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); | |||
} | |||
public Task Connect() | |||
=> BeginConnect(); | |||
public Task Connect(string host, string token, ulong userId, string sessionId, CancellationToken parentCancelToken) | |||
{ | |||
Host = host; | |||
Token = token; | |||
_userId = userId; | |||
_sessionId = sessionId; | |||
return BeginConnect(parentCancelToken); | |||
} | |||
private async Task Reconnect() | |||
{ | |||
try | |||
{ | |||
var cancelToken = ParentCancelToken.Value; | |||
await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||
var cancelToken = _parentCancelToken; | |||
await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
try | |||
{ | |||
await Connect().ConfigureAwait(false); | |||
await BeginConnect(_parentCancelToken).ConfigureAwait(false); | |||
break; | |||
} | |||
catch (OperationCanceledException) { throw; } | |||
@@ -90,31 +96,35 @@ namespace Discord.Net.WebSockets | |||
{ | |||
Logger.Error("Reconnect failed", ex); | |||
//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(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
catch (OperationCanceledException) { } | |||
} | |||
public Task Disconnect() => _taskManager.Stop(true); | |||
public async Task Disconnect() | |||
{ | |||
await _taskManager.Stop(true).ConfigureAwait(false); | |||
_userId = null; | |||
} | |||
protected override async Task Run() | |||
{ | |||
_udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | |||
List<Task> tasks = new List<Task>(); | |||
if (_config.Mode.HasFlag(AudioMode.Outgoing)) | |||
if (_audioConfig.Mode.HasFlag(AudioMode.Outgoing)) | |||
_sendTask = Task.Run(() => SendVoiceAsync(CancelToken)); | |||
_receiveTask = Task.Run(() => ReceiveVoiceAsync(CancelToken)); | |||
SendIdentify(); | |||
SendIdentify(_userId.Value, _sessionId); | |||
#if !DOTNET5_4 | |||
tasks.Add(WatcherAsync()); | |||
#endif | |||
tasks.AddRange(_engine.GetTasks(CancelToken)); | |||
tasks.Add(HeartbeatAsync(CancelToken)); | |||
await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); | |||
await _taskManager.Start(tasks, _cancelSource).ConfigureAwait(false); | |||
} | |||
protected override async Task Cleanup() | |||
{ | |||
@@ -148,7 +158,7 @@ namespace Discord.Net.WebSockets | |||
int packetLength, resultOffset, resultLength; | |||
IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); | |||
if ((_config.Mode & AudioMode.Incoming) != 0) | |||
if ((_audioConfig.Mode & AudioMode.Incoming) != 0) | |||
{ | |||
decodingBuffer = new byte[MaxOpusSize]; | |||
nonce = new byte[24]; | |||
@@ -184,7 +194,7 @@ namespace Discord.Net.WebSockets | |||
int port = packet[68] | packet[69] << 8; | |||
SendSelectProtocol(ip, port); | |||
if ((_config.Mode & AudioMode.Incoming) == 0) | |||
if ((_audioConfig.Mode & AudioMode.Incoming) == 0) | |||
return; //We dont need this thread anymore | |||
} | |||
else | |||
@@ -395,7 +405,7 @@ namespace Discord.Net.WebSockets | |||
var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(); | |||
_endpoint = new IPEndPoint(address, payload.Port); | |||
if (_config.EnableEncryption) | |||
if (_audioConfig.EnableEncryption) | |||
{ | |||
if (payload.Modes.Contains(EncryptedMode)) | |||
{ | |||
@@ -467,12 +477,12 @@ namespace Discord.Net.WebSockets | |||
public override void SendHeartbeat() | |||
=> QueueMessage(new HeartbeatCommand()); | |||
public void SendIdentify() | |||
public void SendIdentify(ulong id, string sessionId) | |||
=> QueueMessage(new IdentifyCommand | |||
{ | |||
GuildId = Server.Id, | |||
UserId = _client.CurrentUser.Id, | |||
SessionId = _client.SessionId, | |||
UserId = id, | |||
SessionId = sessionId, | |||
Token = Token | |||
}); | |||
public void SendSelectProtocol(string externalAddress, int externalPort) |
@@ -1,72 +0,0 @@ | |||
using Discord.Logging; | |||
using Nito.AsyncEx; | |||
using System.IO; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
internal class SimpleAudioClient : AudioClient | |||
{ | |||
internal class VirtualClient : IAudioClient | |||
{ | |||
private readonly SimpleAudioClient _client; | |||
ConnectionState IAudioClient.State => _client.VoiceSocket.State; | |||
Server IAudioClient.Server => _client.VoiceSocket.Server; | |||
Channel IAudioClient.Channel => _client.VoiceSocket.Channel; | |||
Stream IAudioClient.OutputStream => _client.OutputStream; | |||
public VirtualClient(SimpleAudioClient client) | |||
{ | |||
_client = client; | |||
} | |||
Task IAudioClient.Disconnect() => _client.Leave(this); | |||
Task IAudioClient.Join(Channel channel) => _client.Join(channel); | |||
void IAudioClient.Send(byte[] data, int offset, int count) => _client.Send(data, offset, count); | |||
void IAudioClient.Clear() => _client.Clear(); | |||
void IAudioClient.Wait() => _client.Wait(); | |||
} | |||
private readonly AsyncLock _connectionLock; | |||
internal VirtualClient CurrentClient { get; private set; } | |||
public SimpleAudioClient(AudioService service, int id, Logger logger) | |||
: base(service, id, null, service.Client.GatewaySocket, logger) | |||
{ | |||
_connectionLock = new AsyncLock(); | |||
} | |||
//Only disconnects if is current a member of this server | |||
public async Task Leave(VirtualClient client) | |||
{ | |||
using (await _connectionLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
if (CurrentClient == client) | |||
{ | |||
CurrentClient = null; | |||
await Disconnect().ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
internal async Task<IAudioClient> Connect(Channel channel, bool connectGateway) | |||
{ | |||
using (await _connectionLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
bool changeServer = channel.Server != VoiceSocket.Server; | |||
if (changeServer || CurrentClient == null) | |||
{ | |||
await Disconnect().ConfigureAwait(false); | |||
CurrentClient = new VirtualClient(this); | |||
VoiceSocket.Server = channel.Server; | |||
await Connect(connectGateway).ConfigureAwait(false); | |||
} | |||
await Join(channel).ConfigureAwait(false); | |||
return CurrentClient; | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,29 @@ | |||
using System.IO; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
internal class VirtualClient : IAudioClient | |||
{ | |||
private readonly AudioClient _client; | |||
public Server Server { get; } | |||
public ConnectionState State => _client.VoiceSocket.Server == Server ? _client.VoiceSocket.State : ConnectionState.Disconnected; | |||
public Channel Channel => _client.VoiceSocket.Server == Server ? _client.VoiceSocket.Channel : null; | |||
public Stream OutputStream => _client.VoiceSocket.Server == Server ? _client.OutputStream : null; | |||
public VirtualClient(AudioClient client, Server server) | |||
{ | |||
_client = client; | |||
Server = server; | |||
} | |||
public Task Disconnect() => _client.Service.Leave(Server); | |||
public Task Join(Channel channel) => _client.Join(channel); | |||
public void Send(byte[] data, int offset, int count) => _client.Send(data, offset, count); | |||
public void Clear() => _client.Clear(); | |||
public void Wait() => _client.Wait(); | |||
} | |||
} |
@@ -5,7 +5,8 @@ using System.Threading.Tasks; | |||
namespace Discord.Commands | |||
{ | |||
public sealed class Command | |||
//TODO: Make this more friendly and expose it to be extendable | |||
public class Command | |||
{ | |||
private string[] _aliases; | |||
internal CommandParameter[] _parameters; | |||
@@ -6,6 +6,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Commands | |||
{ | |||
//TODO: Make this more friendly and expose it to be extendable | |||
public sealed class CommandBuilder | |||
{ | |||
private readonly CommandService _service; | |||
@@ -18,17 +19,20 @@ namespace Discord.Commands | |||
public CommandService Service => _service; | |||
internal CommandBuilder(CommandService service, Command command, string prefix = "", string category = "", IEnumerable<IPermissionChecker> initialChecks = null) | |||
internal CommandBuilder(CommandService service, string text, string prefix = "", string category = "", IEnumerable<IPermissionChecker> initialChecks = null) | |||
{ | |||
_service = service; | |||
_command = command; | |||
_command.Category = category; | |||
_params = new List<CommandParameter>(); | |||
_service = service; | |||
_prefix = prefix; | |||
_command = new Command(AppendPrefix(prefix, text)); | |||
_command.Category = category; | |||
if (initialChecks != null) | |||
_checks = new List<IPermissionChecker>(initialChecks); | |||
else | |||
_checks = new List<IPermissionChecker>(); | |||
_prefix = prefix; | |||
_params = new List<CommandParameter>(); | |||
_aliases = new List<string>(); | |||
_allowRequiredParams = true; | |||
@@ -112,7 +116,7 @@ namespace Discord.Commands | |||
return prefix; | |||
} | |||
} | |||
public sealed class CommandGroupBuilder | |||
public class CommandGroupBuilder | |||
{ | |||
private readonly CommandService _service; | |||
private readonly string _prefix; | |||
@@ -154,9 +158,6 @@ namespace Discord.Commands | |||
public CommandBuilder CreateCommand() | |||
=> CreateCommand(""); | |||
public CommandBuilder CreateCommand(string cmd) | |||
{ | |||
var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); | |||
return new CommandBuilder(_service, command, _prefix, _category, _checks); | |||
} | |||
=> new CommandBuilder(_service, cmd, _prefix, _category, _checks); | |||
} | |||
} |
@@ -11,13 +11,13 @@ | |||
/// <summary> Catches all remaining text as a single optional parameter. </summary> | |||
Unparsed | |||
} | |||
public sealed class CommandParameter | |||
public class CommandParameter | |||
{ | |||
public string Name { get; } | |||
public int Id { get; internal set; } | |||
public ParameterType Type { get; } | |||
public CommandParameter(string name, ParameterType type) | |||
internal CommandParameter(string name, ParameterType type) | |||
{ | |||
Name = name; | |||
Type = type; | |||
@@ -7,7 +7,7 @@ using System.Linq; | |||
namespace Discord.Modules | |||
{ | |||
public sealed class ModuleManager | |||
public class ModuleManager | |||
{ | |||
public event EventHandler<ServerEventArgs> ServerEnabled = delegate { }; | |||
public event EventHandler<ServerEventArgs> ServerDisabled = delegate { }; | |||
@@ -5,7 +5,7 @@ namespace Discord.API.Client | |||
{ | |||
public class Channel : ChannelReference | |||
{ | |||
public sealed class PermissionOverwrite | |||
public class PermissionOverwrite | |||
{ | |||
[JsonProperty("type")] | |||
public string Type { get; set; } | |||
@@ -4,7 +4,7 @@ namespace Discord.API.Client | |||
{ | |||
public class ExtendedGuild : Guild | |||
{ | |||
public sealed class ExtendedMemberInfo : Member | |||
public class ExtendedMemberInfo : Member | |||
{ | |||
[JsonProperty("mute")] | |||
public bool? IsServerMuted { get; set; } | |||
@@ -6,7 +6,7 @@ namespace Discord.API.Client | |||
{ | |||
public class Guild : GuildReference | |||
{ | |||
public sealed class EmojiData | |||
public class EmojiData | |||
{ | |||
[JsonProperty("id")] | |||
public string Id { get; set; } | |||
@@ -4,7 +4,7 @@ namespace Discord.API.Client | |||
{ | |||
public class InviteReference | |||
{ | |||
public sealed class GuildData : GuildReference | |||
public class GuildData : GuildReference | |||
{ | |||
[JsonProperty("splash_hash")] | |||
public string Splash { get; set; } | |||
@@ -5,7 +5,7 @@ namespace Discord.API.Client | |||
{ | |||
public class MemberPresence : MemberReference | |||
{ | |||
public sealed class GameInfo | |||
public class GameInfo | |||
{ | |||
[JsonProperty("name")] | |||
public string Name { get; set; } | |||
@@ -5,7 +5,7 @@ namespace Discord.API.Client | |||
{ | |||
public class Message : MessageReference | |||
{ | |||
public sealed class Attachment | |||
public class Attachment | |||
{ | |||
[JsonProperty("id")] | |||
public string Id { get; set; } | |||
@@ -23,9 +23,9 @@ namespace Discord.API.Client | |||
public int Height { get; set; } | |||
} | |||
public sealed class Embed | |||
public class Embed | |||
{ | |||
public sealed class Reference | |||
public class Reference | |||
{ | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
@@ -33,7 +33,7 @@ namespace Discord.API.Client | |||
public string Name { get; set; } | |||
} | |||
public sealed class ThumbnailInfo | |||
public class ThumbnailInfo | |||
{ | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
@@ -44,7 +44,7 @@ namespace Discord.API.Client | |||
[JsonProperty("height")] | |||
public int Height { get; set; } | |||
} | |||
public sealed class VideoInfo | |||
public class VideoInfo | |||
{ | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class HeartbeatCommand : IWebSocketMessage | |||
public class HeartbeatCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat; | |||
object IWebSocketMessage.Payload => EpochTime.GetMilliseconds(); | |||
@@ -4,7 +4,7 @@ using System.Collections.Generic; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class IdentifyCommand : IWebSocketMessage | |||
public class IdentifyCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.Identify; | |||
object IWebSocketMessage.Payload => this; | |||
@@ -4,7 +4,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class RequestMembersCommand : IWebSocketMessage | |||
public class RequestMembersCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.RequestGuildMembers; | |||
object IWebSocketMessage.Payload => this; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class ResumeCommand : IWebSocketMessage | |||
public class ResumeCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.Resume; | |||
object IWebSocketMessage.Payload => this; | |||
@@ -3,13 +3,13 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class UpdateStatusCommand : IWebSocketMessage | |||
public class UpdateStatusCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.StatusUpdate; | |||
object IWebSocketMessage.Payload => this; | |||
bool IWebSocketMessage.IsPrivate => false; | |||
public sealed class GameInfo | |||
public class GameInfo | |||
{ | |||
[JsonProperty("name")] | |||
public string Name { get; set; } | |||
@@ -4,7 +4,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class UpdateVoiceCommand : IWebSocketMessage | |||
public class UpdateVoiceCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.VoiceStateUpdate; | |||
object IWebSocketMessage.Payload => this; | |||
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class ChannelCreateEvent : Channel { } | |||
public class ChannelCreateEvent : Channel { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class ChannelDeleteEvent : Channel { } | |||
public class ChannelDeleteEvent : Channel { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class ChannelUpdateEvent : Channel { } | |||
public class ChannelUpdateEvent : Channel { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildBanAddEvent : MemberReference { } | |||
public class GuildBanAddEvent : MemberReference { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildBanRemoveEvent : MemberReference { } | |||
public class GuildBanRemoveEvent : MemberReference { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildCreateEvent : ExtendedGuild { } | |||
public class GuildCreateEvent : ExtendedGuild { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildDeleteEvent : ExtendedGuild { } | |||
public class GuildDeleteEvent : ExtendedGuild { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket.Events | |||
{ | |||
//public sealed class GuildEmojisUpdateEvent { } | |||
//public class GuildEmojisUpdateEvent { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
//public sealed class GuildIntegrationsUpdateEvent { } | |||
//public class GuildIntegrationsUpdateEvent { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildMemberAddEvent : Member { } | |||
public class GuildMemberAddEvent : Member { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildMemberRemoveEvent : Member { } | |||
public class GuildMemberRemoveEvent : Member { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildMemberUpdateEvent : Member { } | |||
public class GuildMemberUpdateEvent : Member { } | |||
} |
@@ -3,7 +3,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildMembersChunkEvent | |||
public class GuildMembersChunkEvent | |||
{ | |||
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong GuildId { get; set; } | |||
@@ -3,7 +3,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildRoleCreateEvent | |||
public class GuildRoleCreateEvent | |||
{ | |||
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong GuildId { get; set; } | |||
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildRoleDeleteEvent : RoleReference { } | |||
public class GuildRoleDeleteEvent : RoleReference { } | |||
} |
@@ -3,7 +3,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildRoleUpdateEvent | |||
public class GuildRoleUpdateEvent | |||
{ | |||
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong GuildId { get; set; } | |||
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class GuildUpdateEvent : Guild { } | |||
public class GuildUpdateEvent : Guild { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class MessageAckEvent : MessageReference { } | |||
public class MessageAckEvent : MessageReference { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class MessageCreateEvent : Message { } | |||
public class MessageCreateEvent : Message { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class MessageDeleteEvent : MessageReference { } | |||
public class MessageDeleteEvent : MessageReference { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class MessageUpdateEvent : Message { } | |||
public class MessageUpdateEvent : Message { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class PresenceUpdateEvent : MemberPresence { } | |||
public class PresenceUpdateEvent : MemberPresence { } | |||
} |
@@ -2,9 +2,9 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class ReadyEvent | |||
public class ReadyEvent | |||
{ | |||
public sealed class ReadState | |||
public class ReadState | |||
{ | |||
[JsonProperty("id")] | |||
public string ChannelId { get; set; } | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class RedirectEvent | |||
public class RedirectEvent | |||
{ | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class ResumedEvent | |||
public class ResumedEvent | |||
{ | |||
[JsonProperty("heartbeat_interval")] | |||
public int HeartbeatInterval { get; set; } | |||
@@ -3,7 +3,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class TypingStartEvent | |||
public class TypingStartEvent | |||
{ | |||
[JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong UserId { get; set; } | |||
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
//public sealed class UserSettingsUpdateEvent { } | |||
//public class UserSettingsUpdateEvent { } | |||
} |
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class UserUpdateEvent : User { } | |||
public class UserUpdateEvent : User { } | |||
} |
@@ -3,7 +3,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class VoiceServerUpdateEvent | |||
public class VoiceServerUpdateEvent | |||
{ | |||
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong GuildId { get; set; } | |||
@@ -1,4 +1,4 @@ | |||
namespace Discord.API.Client.GatewaySocket | |||
{ | |||
public sealed class VoiceStateUpdateEvent : MemberVoiceState { } | |||
public class VoiceStateUpdateEvent : MemberVoiceState { } | |||
} |
@@ -8,7 +8,7 @@ namespace Discord.API.Client | |||
object Payload { get; } | |||
bool IsPrivate { get; } | |||
} | |||
public sealed class WebSocketMessage | |||
public class WebSocketMessage | |||
{ | |||
[JsonProperty("op")] | |||
public int? Operation { get; set; } | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class AcceptInviteRequest : IRestRequest<InviteReference> | |||
public class AcceptInviteRequest : IRestRequest<InviteReference> | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"invite/{InviteId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class AckMessageRequest : IRestRequest | |||
public class AckMessageRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}/ack"; | |||
@@ -4,7 +4,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class AddChannelPermissionsRequest : IRestRequest | |||
public class AddChannelPermissionsRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "PUT"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class AddGuildBanRequest : IRestRequest | |||
public class AddGuildBanRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "PUT"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}?delete-message-days={PruneDays}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class CreateChannelRequest : IRestRequest<Channel> | |||
public class CreateChannelRequest : IRestRequest<Channel> | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class CreateGuildRequest : IRestRequest<Guild> | |||
public class CreateGuildRequest : IRestRequest<Guild> | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"guilds"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class CreateInviteRequest : IRestRequest<Invite> | |||
public class CreateInviteRequest : IRestRequest<Invite> | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/invites"; | |||
@@ -4,7 +4,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class CreatePrivateChannelRequest : IRestRequest<Channel> | |||
public class CreatePrivateChannelRequest : IRestRequest<Channel> | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"users/@me/channels"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class CreateRoleRequest : IRestRequest<Role> | |||
public class CreateRoleRequest : IRestRequest<Role> | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class DeleteChannelRequest : IRestRequest<Channel> | |||
public class DeleteChannelRequest : IRestRequest<Channel> | |||
{ | |||
string IRestRequest.Method => "DELETE"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class DeleteInviteRequest : IRestRequest<Invite> | |||
public class DeleteInviteRequest : IRestRequest<Invite> | |||
{ | |||
string IRestRequest.Method => "DELETE"; | |||
string IRestRequest.Endpoint => $"invite/{InviteCode}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class DeleteMessageRequest : IRestRequest | |||
public class DeleteMessageRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "DELETE"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class DeleteRoleRequest : IRestRequest | |||
public class DeleteRoleRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "DELETE"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class GatewayRequest : IRestRequest<GatewayResponse> | |||
public class GatewayRequest : IRestRequest<GatewayResponse> | |||
{ | |||
string IRestRequest.Method => "GET"; | |||
string IRestRequest.Endpoint => $"gateway"; | |||
@@ -11,7 +11,7 @@ namespace Discord.API.Client.Rest | |||
bool IRestRequest.IsPrivate => false; | |||
} | |||
public sealed class GatewayResponse | |||
public class GatewayResponse | |||
{ | |||
[JsonProperty("url")] | |||
public string Url { get; set; } | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class GetBansRequest : IRestRequest<UserReference[]> | |||
public class GetBansRequest : IRestRequest<UserReference[]> | |||
{ | |||
string IRestRequest.Method => "GET"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/bans"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class GetInviteRequest : IRestRequest<InviteReference> | |||
public class GetInviteRequest : IRestRequest<InviteReference> | |||
{ | |||
string IRestRequest.Method => "GET"; | |||
string IRestRequest.Endpoint => $"invite/{InviteCode}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class GetInvitesRequest : IRestRequest<InviteReference[]> | |||
public class GetInvitesRequest : IRestRequest<InviteReference[]> | |||
{ | |||
string IRestRequest.Method => "GET"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/invites"; | |||
@@ -4,7 +4,7 @@ using System.Text; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class GetMessagesRequest : IRestRequest<Message[]> | |||
public class GetMessagesRequest : IRestRequest<Message[]> | |||
{ | |||
string IRestRequest.Method => "GET"; | |||
string IRestRequest.Endpoint | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class GetVoiceRegionsRequest : IRestRequest<GetVoiceRegionsResponse[]> | |||
public class GetVoiceRegionsRequest : IRestRequest<GetVoiceRegionsResponse[]> | |||
{ | |||
string IRestRequest.Method => "GET"; | |||
string IRestRequest.Endpoint => $"voice/regions"; | |||
@@ -11,7 +11,7 @@ namespace Discord.API.Client.Rest | |||
bool IRestRequest.IsPrivate => false; | |||
} | |||
public sealed class GetVoiceRegionsResponse | |||
public class GetVoiceRegionsResponse | |||
{ | |||
[JsonProperty("sample_hostname")] | |||
public string Hostname { get; set; } | |||
@@ -4,7 +4,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class GetWidgetRequest : IRestRequest<GetWidgetResponse> | |||
public class GetWidgetRequest : IRestRequest<GetWidgetResponse> | |||
{ | |||
string IRestRequest.Method => "GET"; | |||
string IRestRequest.Endpoint => $"servers/{GuildId}/widget.json"; | |||
@@ -19,9 +19,9 @@ namespace Discord.API.Client.Rest | |||
} | |||
} | |||
public sealed class GetWidgetResponse | |||
public class GetWidgetResponse | |||
{ | |||
public sealed class Channel | |||
public class Channel | |||
{ | |||
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong Id { get; set; } | |||
@@ -30,7 +30,7 @@ namespace Discord.API.Client.Rest | |||
[JsonProperty("position")] | |||
public int Position { get; set; } | |||
} | |||
public sealed class User : UserReference | |||
public class User : UserReference | |||
{ | |||
[JsonProperty("avatar_url")] | |||
public string AvatarUrl { get; set; } | |||
@@ -39,7 +39,7 @@ namespace Discord.API.Client.Rest | |||
[JsonProperty("game")] | |||
public UserGame Game { get; set; } | |||
} | |||
public sealed class UserGame | |||
public class UserGame | |||
{ | |||
[JsonProperty("id")] | |||
public int Id { get; set; } | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class KickMemberRequest : IRestRequest | |||
public class KickMemberRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "DELETE"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class LeaveGuildRequest : IRestRequest<Guild> | |||
public class LeaveGuildRequest : IRestRequest<Guild> | |||
{ | |||
string IRestRequest.Method => "DELETE"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class LoginRequest : IRestRequest<LoginResponse> | |||
public class LoginRequest : IRestRequest<LoginResponse> | |||
{ | |||
string IRestRequest.Method => Email != null ? "POST" : "GET"; | |||
string IRestRequest.Endpoint => $"auth/login"; | |||
@@ -16,7 +16,7 @@ namespace Discord.API.Client.Rest | |||
public string Password { get; set; } | |||
} | |||
public sealed class LoginResponse | |||
public class LoginResponse | |||
{ | |||
[JsonProperty("token")] | |||
public string Token { get; set; } | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class LogoutRequest : IRestRequest | |||
public class LogoutRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"auth/logout"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class PruneMembersRequest : IRestRequest<PruneMembersResponse> | |||
public class PruneMembersRequest : IRestRequest<PruneMembersResponse> | |||
{ | |||
string IRestRequest.Method => IsSimulation ? "GET" : "POST"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/prune?days={Days}"; | |||
@@ -21,7 +21,7 @@ namespace Discord.API.Client.Rest | |||
} | |||
} | |||
public sealed class PruneMembersResponse | |||
public class PruneMembersResponse | |||
{ | |||
[JsonProperty("pruned")] | |||
public int Pruned { get; set; } | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class RemoveChannelPermissionsRequest : IRestRequest | |||
public class RemoveChannelPermissionsRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "DELETE"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class RemoveGuildBanRequest : IRestRequest | |||
public class RemoveGuildBanRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "DELETE"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}"; | |||
@@ -5,7 +5,7 @@ using System.Linq; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class ReorderChannelsRequest : IRestRequest | |||
public class ReorderChannelsRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; | |||
@@ -19,7 +19,7 @@ namespace Discord.API.Client.Rest | |||
} | |||
bool IRestRequest.IsPrivate => false; | |||
public sealed class Channel | |||
public class Channel | |||
{ | |||
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong Id { get; set; } | |||
@@ -5,7 +5,7 @@ using System.Linq; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class ReorderRolesRequest : IRestRequest<Role[]> | |||
public class ReorderRolesRequest : IRestRequest<Role[]> | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; | |||
@@ -19,7 +19,7 @@ namespace Discord.API.Client.Rest | |||
} | |||
bool IRestRequest.IsPrivate => false; | |||
public sealed class Role | |||
public class Role | |||
{ | |||
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong Id { get; set; } | |||
@@ -4,7 +4,7 @@ using System.IO; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class SendFileRequest : IRestFileRequest<Message> | |||
public class SendFileRequest : IRestFileRequest<Message> | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/messages"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class SendIsTypingRequest : IRestRequest | |||
public class SendIsTypingRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/typing"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class SendMessageRequest : IRestRequest<Message> | |||
public class SendMessageRequest : IRestRequest<Message> | |||
{ | |||
string IRestRequest.Method => "POST"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/messages"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class UpdateChannelRequest : IRestRequest<Channel> | |||
public class UpdateChannelRequest : IRestRequest<Channel> | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}"; | |||
@@ -4,7 +4,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class UpdateGuildRequest : IRestRequest<Guild> | |||
public class UpdateGuildRequest : IRestRequest<Guild> | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}"; | |||
@@ -5,7 +5,7 @@ using System.Collections.Generic; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class UpdateMemberRequest : IRestRequest | |||
public class UpdateMemberRequest : IRestRequest | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; | |||
@@ -4,7 +4,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class UpdateMessageRequest : IRestRequest<Message> | |||
public class UpdateMessageRequest : IRestRequest<Message> | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class UpdateProfileRequest : IRestRequest<User> | |||
public class UpdateProfileRequest : IRestRequest<User> | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"users/@me"; | |||
@@ -3,7 +3,7 @@ | |||
namespace Discord.API.Client.Rest | |||
{ | |||
[JsonObject(MemberSerialization.OptIn)] | |||
public sealed class UpdateRoleRequest : IRestRequest<Role> | |||
public class UpdateRoleRequest : IRestRequest<Role> | |||
{ | |||
string IRestRequest.Method => "PATCH"; | |||
string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; | |||
@@ -1,6 +1,6 @@ | |||
namespace Discord.API.Client.VoiceSocket | |||
{ | |||
public sealed class HeartbeatCommand : IWebSocketMessage | |||
public class HeartbeatCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat; | |||
object IWebSocketMessage.Payload => EpochTime.GetMilliseconds(); | |||
@@ -3,7 +3,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.VoiceSocket | |||
{ | |||
public sealed class IdentifyCommand : IWebSocketMessage | |||
public class IdentifyCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.Identify; | |||
object IWebSocketMessage.Payload => this; | |||
@@ -2,13 +2,13 @@ | |||
namespace Discord.API.Client.VoiceSocket | |||
{ | |||
public sealed class SelectProtocolCommand : IWebSocketMessage | |||
public class SelectProtocolCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.SelectProtocol; | |||
object IWebSocketMessage.Payload => this; | |||
bool IWebSocketMessage.IsPrivate => false; | |||
public sealed class Data | |||
public class Data | |||
{ | |||
[JsonProperty("address")] | |||
public string Address { get; set; } | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Client.VoiceSocket | |||
{ | |||
public sealed class SetSpeakingCommand : IWebSocketMessage | |||
public class SetSpeakingCommand : IWebSocketMessage | |||
{ | |||
int IWebSocketMessage.OpCode => (int)OpCodes.Speaking; | |||
object IWebSocketMessage.Payload => this; | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Client.VoiceSocket | |||
{ | |||
public sealed class ReadyEvent | |||
public class ReadyEvent | |||
{ | |||
[JsonProperty("ssrc")] | |||
public uint SSRC { get; set; } | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.API.Client.VoiceSocket | |||
{ | |||
public sealed class SessionDescriptionEvent | |||
public class SessionDescriptionEvent | |||
{ | |||
[JsonProperty("secret_key")] | |||
public byte[] SecretKey { get; set; } | |||
@@ -3,7 +3,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Client.VoiceSocket | |||
{ | |||
public sealed class SpeakingEvent | |||
public class SpeakingEvent | |||
{ | |||
[JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] | |||
public ulong UserId { get; set; } | |||
@@ -4,7 +4,7 @@ using System.Collections.Generic; | |||
namespace Discord.API.Converters | |||
{ | |||
public sealed class LongStringConverter : JsonConverter | |||
public class LongStringConverter : JsonConverter | |||
{ | |||
public override bool CanConvert(Type objectType) | |||
=> objectType == typeof(ulong); | |||
@@ -14,7 +14,7 @@ namespace Discord.API.Converters | |||
=> writer.WriteValue(((ulong)value).ToIdString()); | |||
} | |||
public sealed class NullableLongStringConverter : JsonConverter | |||
public class NullableLongStringConverter : JsonConverter | |||
{ | |||
public override bool CanConvert(Type objectType) | |||
=> objectType == typeof(ulong?); | |||
@@ -24,7 +24,7 @@ namespace Discord.API.Converters | |||
=> writer.WriteValue(((ulong?)value).ToIdString()); | |||
} | |||
/*public sealed class LongStringEnumerableConverter : JsonConverter | |||
/*public class LongStringEnumerableConverter : JsonConverter | |||
{ | |||
public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable<ulong>); | |||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||
@@ -55,7 +55,7 @@ namespace Discord.API.Converters | |||
} | |||
}*/ | |||
internal sealed class LongStringArrayConverter : JsonConverter | |||
internal class LongStringArrayConverter : JsonConverter | |||
{ | |||
public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable<ulong[]>); | |||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||