Early 1.0 REST Preview

This commit is contained in:
RogueException
2016-04-04 20:15:16 -03:00
parent b888ea23dc
commit 5bdd6a7ff3
470 changed files with 6804 additions and 14042 deletions

View File

@@ -1,283 +0,0 @@
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.Threading;
using System.Threading.Tasks;
namespace Discord.Audio
{
internal class AudioClient : IAudioClient
{
private readonly DiscordConfig _config;
private readonly AsyncLock _connectionLock;
private readonly TaskManager _taskManager;
private ConnectionState _gatewayState;
internal Logger Logger { get; }
public int Id { get; }
public AudioService Service { get; }
public AudioServiceConfig Config { get; }
public RestClient ClientAPI { get; }
public GatewaySocket GatewaySocket { get; }
public VoiceSocket VoiceSocket { get; }
public JsonSerializer Serializer { get; }
public CancellationToken CancelToken { get; private set; }
public string SessionId => GatewaySocket.SessionId;
public ConnectionState State => VoiceSocket.State;
public Server Server => VoiceSocket.Server;
public VoiceChannel Channel => VoiceSocket.Channel;
public AudioClient(DiscordClient client, Server server, int id)
{
Id = id;
Service = client.GetService<AudioService>();
Config = Service.Config;
Serializer = client.Serializer;
_gatewayState = (int)ConnectionState.Disconnected;
//Logging
Logger = client.Log.CreateLogger($"AudioClient #{id}");
//Async
_taskManager = new TaskManager(Cleanup, false);
_connectionLock = new AsyncLock();
CancelToken = new CancellationToken(true);
//Networking
if (Config.EnableMultiserver)
{
//TODO: We can remove this hack when official API launches
var baseConfig = client.Config;
var builder = new DiscordConfigBuilder
{
AppName = baseConfig.AppName,
AppUrl = baseConfig.AppUrl,
AppVersion = baseConfig.AppVersion,
CacheToken = baseConfig.CacheDir != null,
ConnectionTimeout = baseConfig.ConnectionTimeout,
EnablePreUpdateEvents = false,
FailedReconnectDelay = baseConfig.FailedReconnectDelay,
LargeThreshold = 1,
LogLevel = baseConfig.LogLevel,
MessageCacheSize = 0,
ReconnectDelay = baseConfig.ReconnectDelay,
UsePermissionsCache = false
};
_config = builder.Build();
ClientAPI = new JsonRestClient(_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
{
_config = client.Config;
GatewaySocket = client.GatewaySocket;
}
GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e);
VoiceSocket = new VoiceSocket(_config, Config, client.Serializer, client.Log.CreateLogger($"Voice #{id}"));
VoiceSocket.Server = server;
}
public async Task Connect()
{
if (Config.EnableMultiserver)
await BeginGatewayConnect().ConfigureAwait(false);
else
{
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))
{
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 Disconnect()
{
await _taskManager.Stop(true).ConfigureAwait(false);
if (Config.EnableMultiserver)
ClientAPI.Token = null;
}
private async Task Cleanup()
{
var oldState = _gatewayState;
_gatewayState = ConnectionState.Disconnecting;
if (Config.EnableMultiserver)
{
if (oldState == ConnectionState.Connected)
{
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 Join(VoiceChannel 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 Task.Run(() => VoiceSocket.WaitForConnection(CancelToken)).ConfigureAwait(false);
}
private async void OnReceivedEvent(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) as VoiceChannel;
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);
}
}
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(offset));
if (VoiceSocket.Server == null) return; //Has been closed
if (count == 0) return;
VoiceSocket.SendPCMFrames(data, offset, count);
}
public void Clear()
{
if (VoiceSocket.Server == null) return; //Has been closed
VoiceSocket.ClearPCMFrames();
}
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);
}
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Discord.Audio
{
public static class AudioExtensions
{
public static DiscordClient UsingAudio(this DiscordClient client, AudioServiceConfig config = null)
{
client.AddService(new AudioService(config));
return client;
}
public static DiscordClient UsingAudio(this DiscordClient client, Action<AudioServiceConfigBuilder> configFunc = null)
{
var builder = new AudioServiceConfigBuilder();
configFunc(builder);
client.AddService(new AudioService(builder));
return client;
}
public static Task<IAudioClient> JoinAudio(this VoiceChannel channel) => channel.Client.GetService<AudioService>().Join(channel);
public static Task LeaveAudio(this VoiceChannel channel) => channel.Client.GetService<AudioService>().Leave(channel);
public static Task LeaveAudio(this Server server) => server.Client.GetService<AudioService>().Leave(server);
public static IAudioClient GetAudioClient(this Server server) => server.Client.GetService<AudioService>().GetClient(server);
}
}

View File

@@ -1,9 +0,0 @@
namespace Discord.Audio
{
public enum AudioMode : byte
{
Outgoing = 1,
Incoming = 2,
Both = Outgoing | Incoming
}
}

View File

@@ -1,193 +0,0 @@
using Nito.AsyncEx;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
namespace Discord.Audio
{
public class AudioService : IService
{
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;
public DiscordClient Client { get; private set; }
public AudioServiceConfig Config { get; }
public event EventHandler Connected = delegate { };
public event EventHandler<VoiceDisconnectedEventArgs> Disconnected = delegate { };
public event EventHandler<UserIsSpeakingEventArgs> UserIsSpeakingUpdated = delegate { };
private void OnConnected()
=> Connected(this, EventArgs.Empty);
private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex)
=> Disconnected(this, new VoiceDisconnectedEventArgs(serverId, wasUnexpected, ex));
private void OnUserIsSpeakingUpdated(User user, bool isSpeaking)
=> UserIsSpeakingUpdated(this, new UserIsSpeakingEventArgs(user, isSpeaking));
public AudioService()
: this(new AudioServiceConfigBuilder())
{
}
public AudioService(AudioServiceConfigBuilder builder)
: this(builder.Build())
{
}
public AudioService(AudioServiceConfig config)
{
Config = config;
_asyncLock = new AsyncLock();
}
void IService.Install(DiscordClient client)
{
Client = client;
if (Config.EnableMultiserver)
_voiceClients = new ConcurrentDictionary<ulong, AudioClient>();
else
{
var logger = Client.Log.CreateLogger("Voice");
_defaultClient = new AudioClient(Client, null, 0);
}
_talkingUsers = new ConcurrentDictionary<User, bool>();
client.GatewaySocket.Disconnected += async (s, e) =>
{
if (Config.EnableMultiserver)
{
var tasks = _voiceClients
.Select(x =>
{
var val = x.Value;
if (val != null)
return x.Value.Disconnect();
else
return TaskHelper.CompletedTask;
})
.ToArray();
await Task.WhenAll(tasks).ConfigureAwait(false);
_voiceClients.Clear();
}
foreach (var member in _talkingUsers)
{
bool ignored;
if (_talkingUsers.TryRemove(member.Key, out ignored))
OnUserIsSpeakingUpdated(member.Key, false);
}
};
}
public IAudioClient GetClient(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (Config.EnableMultiserver)
{
AudioClient client;
if (_voiceClients.TryGetValue(server.Id, out client))
return client;
else
return null;
}
else
{
if (server == _currentClient.Server)
return _currentClient;
else
return null;
}
}
//Called from AudioClient.Disconnect
internal async Task RemoveClient(Server server, AudioClient client)
{
using (await _asyncLock.LockAsync().ConfigureAwait(false))
{
if (_voiceClients.TryUpdate(server.Id, null, client))
_voiceClients.TryRemove(server.Id, out client);
}
}
public async Task<IAudioClient> Join(VoiceChannel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
var server = channel.Server;
using (await _asyncLock.LockAsync().ConfigureAwait(false))
{
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().ConfigureAwait(false);
_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;
}
}
}
public Task Leave(Server server) => Leave(server, null);
public Task Leave(VoiceChannel channel) => Leave(channel.Server, channel);
private async Task Leave(Server server, VoiceChannel channel)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (Config.EnableMultiserver)
{
AudioClient client;
//Potential race condition if changing channels during this call, but that's acceptable
if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel))
{
if (_voiceClients.TryRemove(server.Id, out client))
await client.Disconnect().ConfigureAwait(false);
}
}
else
{
using (await _asyncLock.LockAsync().ConfigureAwait(false))
{
var client = GetClient(server) as VirtualClient;
if (client != null && client.Channel == channel)
await _defaultClient.Disconnect().ConfigureAwait(false);
}
}
}
}
}

View File

@@ -1,51 +0,0 @@
namespace Discord.Audio
{
public class AudioServiceConfigBuilder
{
/// <summary> Enables the voice websocket and UDP client and specifies how it will be used. </summary>
public AudioMode Mode { get; set; } = AudioMode.Outgoing;
/// <summary> Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. </summary>
public bool EnableEncryption { get; set; } = true;
/// <summary>
/// Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server).
/// This option uses a lot of CPU power and network bandwidth, as a new gateway connection needs to be spun up per server. Use sparingly.
/// </summary>
public bool EnableMultiserver { get; set; } = false;
/// <summary> Gets or sets the buffer length (in milliseconds) for outgoing voice packets. </summary>
public int BufferLength { get; set; } = 1000;
/// <summary> Gets or sets the bitrate used (in kbit/s, between 1 and MaxBitrate inclusively) for outgoing voice packets. A null value will use default Opus settings. </summary>
public int? Bitrate { get; set; } = null;
/// <summary> Gets or sets the number of channels (1 or 2) used in both input provided to IAudioClient and output send to Discord. Defaults to 2 (stereo). </summary>
public int Channels { get; set; } = 2;
public AudioServiceConfig Build() => new AudioServiceConfig(this);
}
public class AudioServiceConfig
{
public const int MaxBitrate = 128;
public AudioMode Mode { get; }
public bool EnableEncryption { get; }
public bool EnableMultiserver { get; }
public int BufferLength { get; }
public int? Bitrate { get; }
public int Channels { get; }
internal AudioServiceConfig(AudioServiceConfigBuilder builder)
{
Mode = builder.Mode;
EnableEncryption = builder.EnableEncryption;
EnableMultiserver = builder.EnableMultiserver;
BufferLength = builder.BufferLength;
Bitrate = builder.Bitrate;
Channels = builder.Channels;
}
}
}

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>dff7afe3-ca77-4109-bade-b4b49a4f6648</ProjectGuid>
<RootNamespace>Discord.Audio</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -1,46 +0,0 @@
using Discord.Net.Rest;
using Discord.Net.WebSockets;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.Audio
{
public interface IAudioClient
{
/// <summary> Gets the unique identifier for this client. </summary>
int Id { get; }
/// <summary> Gets the session id for the current connection. </summary>
string SessionId { get; }
/// <summary> Gets the current state of this client. </summary>
ConnectionState State { get; }
/// <summary> Gets the channel this client is currently a member of. </summary>
VoiceChannel Channel { get; }
/// <summary> Gets the server this client is bound to. </summary>
Server Server { get; }
/// <summary> Gets a cancellation token that triggers when the client is manually disconnected. </summary>
CancellationToken CancelToken { get; }
/// <summary> Gets the internal RestClient for the Client API endpoint. </summary>
RestClient ClientAPI { get; }
/// <summary> Gets the internal WebSocket for the Gateway event stream. </summary>
GatewaySocket GatewaySocket { get; }
/// <summary> Gets the internal WebSocket for the Voice control stream. </summary>
VoiceSocket VoiceSocket { get; }
/// <summary> Moves the client to another channel on the same server. </summary>
Task Join(VoiceChannel channel);
/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary>
Task Disconnect();
/// <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="offset">Offset . </param>
/// <param name="count">Number of bytes in this frame. </param>
void Send(byte[] data, int offset, int count);
/// <summary> Clears the PCM buffer. </summary>
void Clear();
/// <summary> Blocks until the voice output buffer is empty. </summary>
void Wait();
}
}

View File

@@ -1,22 +0,0 @@
using System;
namespace Discord
{
internal class InternalFrameEventArgs : EventArgs
{
public ulong UserId { get; }
public ulong ChannelId { get; }
public byte[] Buffer { get; }
public int Offset { get; }
public int Count { get; }
public InternalFrameEventArgs(ulong userId, ulong channelId, byte[] buffer, int offset, int count)
{
UserId = userId;
ChannelId = channelId;
Buffer = buffer;
Offset = offset;
Count = count;
}
}
}

View File

@@ -1,14 +0,0 @@
namespace Discord.Audio
{
internal class InternalIsSpeakingEventArgs
{
public ulong UserId { get; }
public bool IsSpeaking { get; }
public InternalIsSpeakingEventArgs(ulong userId, bool isSpeaking)
{
UserId = userId;
IsSpeaking = isSpeaking;
}
}
}

View File

@@ -1,516 +0,0 @@
using Discord.API.Client;
using Discord.API.Client.VoiceSocket;
using Discord.Audio;
using Discord.Audio.Opus;
using Discord.Audio.Sodium;
using Discord.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.Net.WebSockets
{
public partial class VoiceSocket : WebSocket
{
private const int MaxOpusSize = 4000;
private const string EncryptedMode = "xsalsa20_poly1305";
private const string UnencryptedMode = "plain";
private readonly int _targetAudioBufferLength;
private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders;
private readonly AudioServiceConfig _audioConfig;
private Task _sendTask, _receiveTask;
private VoiceBuffer _sendBuffer;
private OpusEncoder _encoder;
private uint _ssrc;
private ConcurrentDictionary<uint, ulong> _ssrcMapping;
private UdpClient _udp;
private IPEndPoint _endpoint;
private bool _isEncrypted;
private byte[] _secretKey, _encodingBuffer;
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; }
public VoiceChannel Channel { get; internal set; }
public int Ping => _ping;
internal VoiceBuffer OutputBuffer => _sendBuffer;
internal event EventHandler<InternalIsSpeakingEventArgs> UserIsSpeaking = delegate { };
internal event EventHandler<InternalFrameEventArgs> FrameReceived = delegate { };
private void OnUserIsSpeaking(ulong userId, bool isSpeaking)
=> UserIsSpeaking(this, new InternalIsSpeakingEventArgs(userId, isSpeaking));
internal void OnFrameReceived(ulong userId, ulong channelId, byte[] buffer, int offset, int count)
=> FrameReceived(this, new InternalFrameEventArgs(userId, channelId, buffer, offset, count));
internal VoiceSocket(DiscordConfig config, AudioServiceConfig audioConfig, JsonSerializer serializer, Logger logger)
: base(config, serializer, logger)
{
_audioConfig = audioConfig;
_decoders = new ConcurrentDictionary<uint, OpusDecoder>();
_targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames
_encodingBuffer = new byte[MaxOpusSize];
_ssrcMapping = new ConcurrentDictionary<uint, ulong>();
_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(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;
await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false);
while (!cancelToken.IsCancellationRequested)
{
try
{
await BeginConnect(_parentCancelToken).ConfigureAwait(false);
break;
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
Logger.Error("Reconnect failed", ex);
//Net is down? We can keep trying to reconnect until the user runs Disconnect()
await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException) { }
}
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 (_audioConfig.Mode.HasFlag(AudioMode.Outgoing))
_sendTask = Task.Run(() => SendVoiceAsync(CancelToken));
_receiveTask = Task.Run(() => ReceiveVoiceAsync(CancelToken));
SendIdentify(_userId.Value, _sessionId);
#if !DOTNET5_4
tasks.Add(WatcherAsync());
#endif
tasks.AddRange(_engine.GetTasks(CancelToken));
tasks.Add(HeartbeatAsync(CancelToken));
await _taskManager.Start(tasks, _cancelSource).ConfigureAwait(false);
}
protected override async Task Cleanup()
{
var sendThread = _sendTask;
if (sendThread != null)
{
try { await sendThread.ConfigureAwait(false); }
catch (Exception) { } //Ignore any errors during cleanup
}
_sendTask = null;
var receiveThread = _receiveTask;
if (receiveThread != null)
{
try { await receiveThread.ConfigureAwait(false); }
catch (Exception) { } //Ignore any errors during cleanup
}
_receiveTask = null;
OpusDecoder decoder;
foreach (var pair in _decoders)
{
if (_decoders.TryRemove(pair.Key, out decoder))
decoder.Dispose();
}
ClearPCMFrames();
_udp = null;
await base.Cleanup().ConfigureAwait(false);
}
private async Task ReceiveVoiceAsync(CancellationToken cancelToken)
{
var closeTask = cancelToken.Wait();
try
{
byte[] packet, decodingBuffer = null, nonce = null, result;
int packetLength, resultOffset, resultLength;
IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0);
if ((_audioConfig.Mode & AudioMode.Incoming) != 0)
{
decodingBuffer = new byte[MaxOpusSize];
nonce = new byte[24];
}
while (!cancelToken.IsCancellationRequested)
{
await Task.Delay(1).ConfigureAwait(false);
if (_udp.Available > 0)
{
#if !DOTNET5_4
packet = _udp.Receive(ref endpoint);
#else
//TODO: Is this really the only way to end a Receive call in DOTNET5_4?
var receiveTask = _udp.ReceiveAsync();
var task = Task.WhenAny(closeTask, receiveTask).Result;
if (task == closeTask)
break;
var udpPacket = receiveTask.Result;
packet = udpPacket.Buffer;
endpoint = udpPacket.RemoteEndPoint;
#endif
packetLength = packet.Length;
if (packetLength > 0 && endpoint.Equals(_endpoint))
{
if (State != ConnectionState.Connected)
{
if (packetLength != 70)
return;
string ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0');
int port = packet[68] | packet[69] << 8;
SendSelectProtocol(ip, port);
if ((_audioConfig.Mode & AudioMode.Incoming) == 0)
return; //We dont need this thread anymore
}
else
{
//Parse RTP Data
if (packetLength < 12) return;
if (packet[0] != 0x80) return; //Flags
if (packet[1] != 0x78) return; //Payload Type
ushort sequenceNumber = (ushort)((packet[2] << 8) |
packet[3] << 0);
uint timestamp = (uint)((packet[4] << 24) |
(packet[5] << 16) |
(packet[6] << 8) |
(packet[7] << 0));
uint ssrc = (uint)((packet[8] << 24) |
(packet[9] << 16) |
(packet[10] << 8) |
(packet[11] << 0));
//Decrypt
if (_isEncrypted)
{
if (packetLength < 28) //12 + 16 (RTP + Poly1305 MAC)
return;
Buffer.BlockCopy(packet, 0, nonce, 0, 12);
int ret = SecretBox.Decrypt(packet, 12, packetLength - 12, decodingBuffer, nonce, _secretKey);
if (ret != 0)
continue;
result = decodingBuffer;
resultOffset = 0;
resultLength = packetLength - 28;
}
else //Plain
{
result = packet;
resultOffset = 12;
resultLength = packetLength - 12;
}
/*if (_logLevel >= LogMessageSeverity.Debug)
RaiseOnLog(LogMessageSeverity.Debug, $"Received {buffer.Length - 12} bytes.");*/
ulong userId;
if (_ssrcMapping.TryGetValue(ssrc, out userId))
OnFrameReceived(userId, Channel.Id, result, resultOffset, resultLength);
}
}
}
}
}
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } //Includes ObjectDisposedException
}
private async Task SendVoiceAsync(CancellationToken cancelToken)
{
try
{
while (!cancelToken.IsCancellationRequested && State != ConnectionState.Connected)
await Task.Delay(1).ConfigureAwait(false);
if (cancelToken.IsCancellationRequested)
return;
byte[] frame = new byte[_encoder.FrameSize];
byte[] encodedFrame = new byte[MaxOpusSize];
byte[] voicePacket, pingPacket, nonce = null;
uint timestamp = 0;
double nextTicks = 0.0, nextPingTicks = 0.0;
long ticksPerSeconds = Stopwatch.Frequency;
double ticksPerMillisecond = Stopwatch.Frequency / 1000.0;
double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength;
double spinLockThreshold = 3 * ticksPerMillisecond;
uint samplesPerFrame = (uint)_encoder.SamplesPerFrame;
Stopwatch sw = Stopwatch.StartNew();
if (_isEncrypted)
{
nonce = new byte[24];
voicePacket = new byte[MaxOpusSize + 12 + 16];
}
else
voicePacket = new byte[MaxOpusSize + 12];
pingPacket = new byte[8];
int rtpPacketLength = 0;
voicePacket[0] = 0x80; //Flags;
voicePacket[1] = 0x78; //Payload Type
voicePacket[8] = (byte)(_ssrc >> 24);
voicePacket[9] = (byte)(_ssrc >> 16);
voicePacket[10] = (byte)(_ssrc >> 8);
voicePacket[11] = (byte)(_ssrc >> 0);
if (_isEncrypted)
Buffer.BlockCopy(voicePacket, 0, nonce, 0, 12);
bool hasFrame = false;
while (!cancelToken.IsCancellationRequested)
{
if (!hasFrame && _sendBuffer.Pop(frame))
{
ushort sequence = unchecked(_sequence++);
voicePacket[2] = (byte)(sequence >> 8);
voicePacket[3] = (byte)(sequence >> 0);
voicePacket[4] = (byte)(timestamp >> 24);
voicePacket[5] = (byte)(timestamp >> 16);
voicePacket[6] = (byte)(timestamp >> 8);
voicePacket[7] = (byte)(timestamp >> 0);
//Encode
int encodedLength = _encoder.EncodeFrame(frame, 0, encodedFrame);
//Encrypt
if (_isEncrypted)
{
Buffer.BlockCopy(voicePacket, 2, nonce, 2, 6); //Update nonce
int ret = SecretBox.Encrypt(encodedFrame, encodedLength, voicePacket, 12, nonce, _secretKey);
if (ret != 0)
continue;
rtpPacketLength = encodedLength + 12 + 16;
}
else
{
Buffer.BlockCopy(encodedFrame, 0, voicePacket, 12, encodedLength);
rtpPacketLength = encodedLength + 12;
}
timestamp = unchecked(timestamp + samplesPerFrame);
hasFrame = true;
}
long currentTicks = sw.ElapsedTicks;
double ticksToNextFrame = nextTicks - currentTicks;
if (ticksToNextFrame <= 0.0)
{
if (hasFrame)
{
try
{
_udp.Send(voicePacket, rtpPacketLength);
}
catch (SocketException ex)
{
Logger.Error("Failed to send UDP packet.", ex);
}
hasFrame = false;
}
nextTicks += ticksPerFrame;
//Is it time to send out another ping?
if (currentTicks > nextPingTicks)
{
//Increment in LE
for (int i = 0; i < 8; i++)
{
var b = pingPacket[i];
if (b == byte.MaxValue)
pingPacket[i] = 0;
else
{
pingPacket[i] = (byte)(b + 1);
break;
}
}
await _udp.SendAsync(pingPacket, pingPacket.Length).ConfigureAwait(false);
nextPingTicks = currentTicks + 5 * ticksPerSeconds;
}
}
else
{
if (hasFrame)
{
int time = (int)Math.Floor(ticksToNextFrame / ticksPerMillisecond);
if (time > 0)
await Task.Delay(time).ConfigureAwait(false);
}
else
await Task.Delay(1).ConfigureAwait(false); //Give as much time to the encrypter as possible
}
}
}
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } //Includes ObjectDisposedException
}
#if !DOTNET5_4
//Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken
private async Task WatcherAsync()
{
await CancelToken.Wait().ConfigureAwait(false);
_udp.Close();
}
#endif
protected override async Task ProcessMessage(string json)
{
await base.ProcessMessage(json).ConfigureAwait(false);
WebSocketMessage msg;
using (var reader = new JsonTextReader(new StringReader(json)))
msg = _serializer.Deserialize(reader, typeof(WebSocketMessage)) as WebSocketMessage;
var opCode = (OpCodes)msg.Operation;
switch (opCode)
{
case OpCodes.Ready:
{
if (State != ConnectionState.Connected)
{
var payload = (msg.Payload as JToken).ToObject<ReadyEvent>(_serializer);
_heartbeatInterval = payload.HeartbeatInterval;
_ssrc = payload.SSRC;
var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault();
_endpoint = new IPEndPoint(address, payload.Port);
if (_audioConfig.EnableEncryption)
{
if (payload.Modes.Contains(EncryptedMode))
{
_encryptionMode = EncryptedMode;
_isEncrypted = true;
}
else
throw new InvalidOperationException("Unexpected encryption format.");
}
else
{
_encryptionMode = UnencryptedMode;
_isEncrypted = false;
}
_udp.Connect(_endpoint);
_sequence = 0;// (ushort)_rand.Next(0, ushort.MaxValue);
//No thread issue here because SendAsync doesn't start until _isReady is true
byte[] packet = new byte[70];
packet[0] = (byte)(_ssrc >> 24);
packet[1] = (byte)(_ssrc >> 16);
packet[2] = (byte)(_ssrc >> 8);
packet[3] = (byte)(_ssrc >> 0);
await _udp.SendAsync(packet, 70).ConfigureAwait(false);
}
}
break;
case OpCodes.Heartbeat:
{
long time = EpochTime.GetMilliseconds();
var payload = (long)msg.Payload;
_ping = (int)(payload - time);
//TODO: Use this to estimate latency
}
break;
case OpCodes.SessionDescription:
{
var payload = (msg.Payload as JToken).ToObject<SessionDescriptionEvent>(_serializer);
_secretKey = payload.SecretKey;
SendSetSpeaking(true);
await EndConnect().ConfigureAwait(false);
}
break;
case OpCodes.Speaking:
{
var payload = (msg.Payload as JToken).ToObject<SpeakingEvent>(_serializer);
OnUserIsSpeaking(payload.UserId, payload.IsSpeaking);
}
break;
default:
Logger.Warning($"Unknown Opcode: {opCode}");
break;
}
}
public void SendPCMFrames(byte[] data, int offset, int count)
{
_sendBuffer.Push(data, offset, count, CancelToken);
}
public void ClearPCMFrames()
{
_sendBuffer.Clear(CancelToken);
}
public void WaitForQueue()
{
_sendBuffer.Wait(CancelToken);
}
public override void SendHeartbeat()
=> QueueMessage(new HeartbeatCommand());
public void SendIdentify(ulong id, string sessionId)
=> QueueMessage(new IdentifyCommand
{
GuildId = Server.Id,
UserId = id,
SessionId = sessionId,
Token = Token
});
public void SendSelectProtocol(string externalAddress, int externalPort)
=> QueueMessage(new SelectProtocolCommand
{
Protocol = "udp",
ExternalAddress = externalAddress,
ExternalPort = externalPort,
EncryptionMode = _encryptionMode
});
public void SendSetSpeaking(bool value)
=> QueueMessage(new SetSpeakingCommand { IsSpeaking = value, Delay = 0 });
}
}

View File

@@ -1,111 +0,0 @@
using System;
using System.Runtime.InteropServices;
#if NET45
using System.Security;
#endif
namespace Discord.Audio.Opus
{
internal enum OpusApplication : int
{
Voice = 2048,
MusicOrMixed = 2049,
LowLatency = 2051
}
internal enum OpusError : int
{
OK = 0,
BadArg = -1,
BufferToSmall = -2,
InternalError = -3,
InvalidPacket = -4,
Unimplemented = -5,
InvalidState = -6,
AllocFail = -7
}
internal abstract class OpusConverter : IDisposable
{
protected enum Ctl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}
#if NET45
[SuppressUnmanagedCodeSecurity]
#endif
protected unsafe static class UnsafeNativeMethods
{
[DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error);
[DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyEncoder(IntPtr encoder);
[DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)]
public static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes);
[DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)]
public static extern int EncoderCtl(IntPtr st, Ctl request, int value);
[DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error);
[DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyDecoder(IntPtr decoder);
[DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)]
public static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec);
}
protected IntPtr _ptr;
/// <summary> Gets the bit rate of this converter. </summary>
public const int BitsPerSample = 16;
/// <summary> Gets the input sampling rate of this converter. </summary>
public int InputSamplingRate { get; }
/// <summary> Gets the number of channels of this converter. </summary>
public int InputChannels { get; }
/// <summary> Gets the milliseconds per frame. </summary>
public int FrameLength { get; }
/// <summary> Gets the number of samples per frame. </summary>
public int SamplesPerFrame { get; }
/// <summary> Gets the bytes per frame. </summary>
public int FrameSize { get; }
/// <summary> Gets the bytes per sample. </summary>
public int SampleSize { get; }
protected OpusConverter(int samplingRate, int channels, int frameLength)
{
if (samplingRate != 8000 && samplingRate != 12000 &&
samplingRate != 16000 && samplingRate != 24000 &&
samplingRate != 48000)
throw new ArgumentOutOfRangeException(nameof(samplingRate));
if (channels != 1 && channels != 2)
throw new ArgumentOutOfRangeException(nameof(channels));
InputSamplingRate = samplingRate;
InputChannels = channels;
FrameLength = frameLength;
SampleSize = (BitsPerSample / 8) * channels;
SamplesPerFrame = samplingRate / 1000 * FrameLength;
FrameSize = SamplesPerFrame * SampleSize;
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
disposedValue = true;
}
~OpusConverter() {
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@@ -1,43 +0,0 @@
using System;
namespace Discord.Audio.Opus
{
internal class OpusDecoder : OpusConverter
{
/// <summary> Creates a new Opus decoder. </summary>
/// <param name="samplingRate">Sampling rate of the input PCM (in Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000</param>
/// <param name="frameLength">Length, in milliseconds, of each frame. Supported Values: 2.5, 5, 10, 20, 40, or 60</param>
public OpusDecoder(int samplingRate, int channels, int frameLength)
: base(samplingRate, channels, frameLength)
{
OpusError error;
_ptr = UnsafeNativeMethods.CreateDecoder(samplingRate, channels, out error);
if (error != OpusError.OK)
throw new InvalidOperationException($"Error occured while creating decoder: {error}");
}
/// <summary> Produces PCM samples from Opus-encoded audio. </summary>
/// <param name="input">PCM samples to decode.</param>
/// <param name="inputOffset">Offset of the frame in input.</param>
/// <param name="output">Buffer to store the decoded frame.</param>
public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output)
{
int result = 0;
fixed (byte* inPtr = input)
result = UnsafeNativeMethods.Decode(_ptr, inPtr + inputOffset, inputCount, output, SamplesPerFrame, 0);
if (result < 0)
throw new Exception(((OpusError)result).ToString());
return result;
}
protected override void Dispose(bool disposing)
{
if (_ptr != IntPtr.Zero)
{
UnsafeNativeMethods.DestroyDecoder(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}

View File

@@ -1,78 +0,0 @@
using System;
namespace Discord.Audio.Opus
{
internal class OpusEncoder : OpusConverter
{
/// <summary> Gets the bit rate in kbit/s. </summary>
public int? BitRate { get; }
/// <summary> Gets the coding mode of the encoder. </summary>
public OpusApplication Application { get; }
/// <summary> Creates a new Opus encoder. </summary>
/// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000</param>
/// <param name="channels">Number of channels in input signal. Supported Values: 1 or 2</param>
/// <param name="frameLength">Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60</param>
/// <param name="bitrate">Bitrate (kbit/s) used for this encoder. Supported Values: 1-512. Null will use the recommended bitrate. </param>
/// <param name="application">Coding mode.</param>
public OpusEncoder(int samplingRate, int channels, int frameLength, int? bitrate, OpusApplication application)
: base(samplingRate, channels, frameLength)
{
if (bitrate != null && (bitrate < 1 || bitrate > AudioServiceConfig.MaxBitrate))
throw new ArgumentOutOfRangeException(nameof(bitrate));
BitRate = bitrate;
Application = application;
OpusError error;
_ptr = UnsafeNativeMethods.CreateEncoder(samplingRate, channels, (int)application, out error);
if (error != OpusError.OK)
throw new InvalidOperationException($"Error occured while creating encoder: {error}");
SetForwardErrorCorrection(true);
if (bitrate != null)
SetBitrate(bitrate.Value);
}
/// <summary> Produces Opus encoded audio from PCM samples. </summary>
/// <param name="input">PCM samples to encode.</param>
/// <param name="inputOffset">Offset of the frame in pcmSamples.</param>
/// <param name="output">Buffer to store the encoded frame.</param>
/// <returns>Length of the frame contained in outputBuffer.</returns>
public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output)
{
int result = 0;
fixed (byte* inPtr = input)
result = UnsafeNativeMethods.Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length);
if (result < 0)
throw new Exception(((OpusError)result).ToString());
return result;
}
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetForwardErrorCorrection(bool value)
{
var result = UnsafeNativeMethods.EncoderCtl(_ptr, Ctl.SetInbandFECRequest, value ? 1 : 0);
if (result < 0)
throw new Exception(((OpusError)result).ToString());
}
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetBitrate(int value)
{
var result = UnsafeNativeMethods.EncoderCtl(_ptr, Ctl.SetBitrateRequest, value * 1000);
if (result < 0)
throw new Exception(((OpusError)result).ToString());
}
protected override void Dispose(bool disposing)
{
if (_ptr != IntPtr.Zero)
{
UnsafeNativeMethods.DestroyEncoder(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}

View File

@@ -1,32 +0,0 @@
using System.Runtime.InteropServices;
#if NET45
using System.Security;
#endif
namespace Discord.Audio.Sodium
{
internal unsafe static class SecretBox
{
#if NET45
[SuppressUnmanagedCodeSecurity]
#endif
private static class SafeNativeMethods
{
[DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)]
public static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret);
[DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)]
public static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret);
}
public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret)
{
fixed (byte* outPtr = output)
return SafeNativeMethods.SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret);
}
public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret)
{
fixed (byte* inPtr = input)
return SafeNativeMethods.SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret);
}
}
}

View File

@@ -1,13 +0,0 @@
namespace Discord
{
public class UserIsSpeakingEventArgs : UserEventArgs
{
public bool IsSpeaking { get; }
public UserIsSpeakingEventArgs(User user, bool isSpeaking)
: base(user)
{
IsSpeaking = isSpeaking;
}
}
}

View File

@@ -1,39 +0,0 @@
using Discord.Net.Rest;
using Discord.Net.WebSockets;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.Audio
{
internal class VirtualClient : IAudioClient
{
private readonly AudioClient _client;
public Server Server { get; }
public int Id => 0;
public string SessionId => _client.Server == Server ? _client.SessionId : null;
public ConnectionState State => _client.Server == Server ? _client.State : ConnectionState.Disconnected;
public VoiceChannel Channel => _client.Server == Server ? _client.Channel : null;
public CancellationToken CancelToken => _client.Server == Server ? _client.CancelToken : CancellationToken.None;
public RestClient ClientAPI => _client.Server == Server ? _client.ClientAPI : null;
public GatewaySocket GatewaySocket => _client.Server == Server ? _client.GatewaySocket : null;
public VoiceSocket VoiceSocket => _client.Server == Server ? _client.VoiceSocket : null;
public VirtualClient(AudioClient client, Server server)
{
_client = client;
Server = server;
}
public Task Disconnect() => _client.Service.Leave(Server);
public Task Join(VoiceChannel 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();
}
}

View File

@@ -1,140 +0,0 @@
using Nito.AsyncEx;
using System;
using System.Threading;
namespace Discord.Audio
{
internal class VoiceBuffer
{
private readonly int _frameSize, _frameCount, _bufferSize;
private readonly byte[] _buffer;
private readonly byte[] _blankFrame;
private ushort _readCursor, _writeCursor;
private ManualResetEventSlim _notOverflowEvent;
private bool _isClearing;
private AsyncLock _lock;
public int FrameSize => _frameSize;
public int FrameCount => _frameCount;
public ushort ReadPos => _readCursor;
public ushort WritePos => _writeCursor;
public VoiceBuffer(int frameCount, int frameSize)
{
_frameSize = frameSize;
_frameCount = frameCount;
_bufferSize = _frameSize * _frameCount;
_readCursor = 0;
_writeCursor = 0;
_buffer = new byte[_bufferSize];
_blankFrame = new byte[_frameSize];
_notOverflowEvent = new ManualResetEventSlim(); //Notifies when an overflow is solved
_lock = new AsyncLock();
}
public void Push(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{
if (cancelToken.IsCancellationRequested)
throw new OperationCanceledException("Client is disconnected.", cancelToken);
int wholeFrames = count / _frameSize;
int expectedBytes = wholeFrames * _frameSize;
int lastFrameSize = count - expectedBytes;
using (_lock.Lock())
{
for (int i = 0, pos = offset; i <= wholeFrames; i++, pos += _frameSize)
{
//If the read cursor is in the next position, wait for it to move.
ushort nextPosition = _writeCursor;
AdvanceCursorPos(ref nextPosition);
if (_readCursor == nextPosition)
{
_notOverflowEvent.Reset();
try
{
_notOverflowEvent.Wait(cancelToken);
}
catch (OperationCanceledException ex)
{
throw new OperationCanceledException("Client is disconnected.", ex, cancelToken);
}
}
if (i == wholeFrames)
{
//If there are no partial frames, skip this step
if (lastFrameSize == 0)
break;
//Copy partial frame
Buffer.BlockCopy(buffer, pos, _buffer, _writeCursor * _frameSize, lastFrameSize);
//Wipe the end of the buffer
Buffer.BlockCopy(_blankFrame, 0, _buffer, _writeCursor * _frameSize + lastFrameSize, _frameSize - lastFrameSize);
}
else
{
//Copy full frame
Buffer.BlockCopy(buffer, pos, _buffer, _writeCursor * _frameSize, _frameSize);
}
//Advance the write cursor to the next position
AdvanceCursorPos(ref _writeCursor);
}
}
}
public bool Pop(byte[] buffer)
{
//using (_lock.Lock())
//{
if (_writeCursor == _readCursor)
{
_notOverflowEvent.Set();
return false;
}
bool isClearing = _isClearing;
if (!isClearing)
Buffer.BlockCopy(_buffer, _readCursor * _frameSize, buffer, 0, _frameSize);
//Advance the read cursor to the next position
AdvanceCursorPos(ref _readCursor);
_notOverflowEvent.Set();
return !isClearing;
//}
}
public void Clear(CancellationToken cancelToken)
{
using (_lock.Lock())
{
_isClearing = true;
for (int i = 0; i < _frameCount; i++)
Buffer.BlockCopy(_blankFrame, 0, _buffer, i * _frameCount, i++);
_writeCursor = 0;
_readCursor = 0;
_isClearing = false;
}
}
public void Wait(CancellationToken cancelToken)
{
while (true)
{
_notOverflowEvent.Wait(cancelToken);
if (_writeCursor == _readCursor)
break;
}
}
private void AdvanceCursorPos(ref ushort pos)
{
pos++;
if (pos == _frameCount)
pos = 0;
}
}
}

View File

@@ -1,15 +0,0 @@
using System;
namespace Discord
{
public class VoiceDisconnectedEventArgs : DisconnectedEventArgs
{
public ulong ServerId { get; }
public VoiceDisconnectedEventArgs(ulong serverId, bool wasUnexpected, Exception ex)
: base(wasUnexpected, ex)
{
ServerId = serverId;
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,27 +0,0 @@
{
"version": "1.0.0-alpha1",
"description": "A Discord.Net extension adding voice support.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
},
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
"contentFiles": [ "libsodium.dll", "opus.dll" ],
"compilationOptions": {
"allowUnsafe": true,
"warningsAsErrors": true
},
"dependencies": {
"Discord.Net": "1.0.0-alpha1"
},
"frameworks": {
"net45": { },
"dotnet5.4": { }
}
}

View File

@@ -1,83 +0,0 @@
using Discord.Commands.Permissions;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Discord.Commands
{
//TODO: Make this more friendly and expose it to be extendable
public class Command
{
private string[] _aliases;
internal CommandParameter[] _parameters;
private IPermissionChecker[] _checks;
private Func<CommandEventArgs, Task> _runFunc;
internal readonly Dictionary<string, CommandParameter> _parametersByName;
public string Text { get; }
public string Category { get; internal set; }
public bool IsHidden { get; internal set; }
public string Description { get; internal set; }
public IEnumerable<string> Aliases => _aliases;
public IEnumerable<CommandParameter> Parameters => _parameters;
public CommandParameter this[string name] => _parametersByName[name];
internal Command(string text)
{
Text = text;
IsHidden = false;
_aliases = new string[0];
_parameters = new CommandParameter[0];
_parametersByName = new Dictionary<string, CommandParameter>();
}
internal void SetAliases(string[] aliases)
{
_aliases = aliases;
}
internal void SetParameters(CommandParameter[] parameters)
{
_parametersByName.Clear();
for (int i = 0; i < parameters.Length; i++)
{
parameters[i].Id = i;
_parametersByName[parameters[i].Name] = parameters[i];
}
_parameters = parameters;
}
internal void SetChecks(IPermissionChecker[] checks)
{
_checks = checks;
}
internal bool CanRun(User user, ITextChannel channel, out string error)
{
for (int i = 0; i < _checks.Length; i++)
{
if (!_checks[i].CanRun(this, user, channel, out error))
return false;
}
error = null;
return true;
}
internal void SetRunFunc(Func<CommandEventArgs, Task> func)
{
_runFunc = func;
}
internal void SetRunFunc(Action<CommandEventArgs> func)
{
_runFunc = TaskHelper.ToAsync(func);
}
internal Task Run(CommandEventArgs args)
{
var task = _runFunc(args);
if (task != null)
return task;
else
return TaskHelper.CompletedTask;
}
}
}

View File

@@ -1,163 +0,0 @@
using Discord.Commands.Permissions;
using System;
using System.Collections.Generic;
using System.Linq;
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;
private readonly Command _command;
private readonly List<CommandParameter> _params;
private readonly List<IPermissionChecker> _checks;
private readonly List<string> _aliases;
private readonly string _prefix;
private bool _allowRequiredParams, _areParamsClosed;
public CommandService Service => _service;
internal CommandBuilder(CommandService service, string text, string prefix = "", string category = "", IEnumerable<IPermissionChecker> initialChecks = null)
{
_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>();
_params = new List<CommandParameter>();
_aliases = new List<string>();
_allowRequiredParams = true;
_areParamsClosed = false;
}
public CommandBuilder Alias(params string[] aliases)
{
_aliases.AddRange(aliases);
return this;
}
/*public CommandBuilder Category(string category)
{
_command.Category = category;
return this;
}*/
public CommandBuilder Description(string description)
{
_command.Description = description;
return this;
}
public CommandBuilder Parameter(string name, ParameterType type = ParameterType.Required)
{
if (_areParamsClosed)
throw new Exception($"No parameters may be added after a {nameof(ParameterType.Multiple)} or {nameof(ParameterType.Unparsed)} parameter.");
if (!_allowRequiredParams && type == ParameterType.Required)
throw new Exception($"{nameof(ParameterType.Required)} parameters may not be added after an optional one");
_params.Add(new CommandParameter(name, type));
if (type == ParameterType.Optional)
_allowRequiredParams = false;
if (type == ParameterType.Multiple || type == ParameterType.Unparsed)
_areParamsClosed = true;
return this;
}
public CommandBuilder Hide()
{
_command.IsHidden = true;
return this;
}
public CommandBuilder AddCheck(IPermissionChecker check)
{
_checks.Add(check);
return this;
}
public CommandBuilder AddCheck(Func<Command, User, ITextChannel, bool> checkFunc, string errorMsg = null)
{
_checks.Add(new GenericPermissionChecker(checkFunc, errorMsg));
return this;
}
public void Do(Func<CommandEventArgs, Task> func)
{
_command.SetRunFunc(func);
Build();
}
public void Do(Action<CommandEventArgs> func)
{
_command.SetRunFunc(func);
Build();
}
private void Build()
{
_command.SetParameters(_params.ToArray());
_command.SetChecks(_checks.ToArray());
_command.SetAliases(_aliases.Select(x => AppendPrefix(_prefix, x)).ToArray());
_service.AddCommand(_command);
}
internal static string AppendPrefix(string prefix, string cmd)
{
if (cmd != "")
{
if (prefix != "")
return prefix + ' ' + cmd;
else
return cmd;
}
else
return prefix;
}
}
public class CommandGroupBuilder
{
private readonly CommandService _service;
private readonly string _prefix;
private readonly List<IPermissionChecker> _checks;
private string _category;
public CommandService Service => _service;
internal CommandGroupBuilder(CommandService service, string prefix = "", string category = null, IEnumerable<IPermissionChecker> initialChecks = null)
{
_service = service;
_prefix = prefix;
_category = category;
if (initialChecks != null)
_checks = new List<IPermissionChecker>(initialChecks);
else
_checks = new List<IPermissionChecker>();
}
public CommandGroupBuilder Category(string category)
{
_category = category;
return this;
}
public void AddCheck(IPermissionChecker checker)
{
_checks.Add(checker);
}
public void AddCheck(Func<Command, User, ITextChannel, bool> checkFunc, string errorMsg = null)
{
_checks.Add(new GenericPermissionChecker(checkFunc, errorMsg));
}
public CommandGroupBuilder CreateGroup(string cmd, Action<CommandGroupBuilder> config)
{
config(new CommandGroupBuilder(_service, CommandBuilder.AppendPrefix(_prefix, cmd), _category, _checks));
return this;
}
public CommandBuilder CreateCommand()
=> CreateCommand("");
public CommandBuilder CreateCommand(string cmd)
=> new CommandBuilder(_service, cmd, _prefix, _category, _checks);
}
}

View File

@@ -1,18 +0,0 @@
using System;
namespace Discord.Commands
{
public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput }
public class CommandErrorEventArgs : CommandEventArgs
{
public CommandErrorType ErrorType { get; }
public Exception Exception { get; }
public CommandErrorEventArgs(CommandErrorType errorType, CommandEventArgs baseArgs, Exception ex)
: base(baseArgs.Message, baseArgs.Command, baseArgs.Args)
{
Exception = ex;
ErrorType = errorType;
}
}
}

View File

@@ -1,26 +0,0 @@
using System;
namespace Discord.Commands
{
public class CommandEventArgs : EventArgs
{
private readonly string[] _args;
public Message Message { get; }
public Command Command { get; }
public User User => Message.User;
public ITextChannel Channel => Message.Channel;
public CommandEventArgs(Message message, Command command, string[] args)
{
Message = message;
Command = command;
_args = args;
}
public string[] Args => _args;
public string GetArg(int index) => _args[index];
public string GetArg(string name) => _args[Command[name].Id];
}
}

View File

@@ -1,20 +0,0 @@
using System;
namespace Discord.Commands
{
public static class CommandExtensions
{
public static DiscordClient UsingCommands(this DiscordClient client, CommandServiceConfig config = null)
{
client.AddService(new CommandService(config));
return client;
}
public static DiscordClient UsingCommands(this DiscordClient client, Action<CommandServiceConfigBuilder> configFunc = null)
{
var builder = new CommandServiceConfigBuilder();
configFunc(builder);
client.AddService(new CommandService(builder));
return client;
}
}
}

View File

@@ -1,141 +0,0 @@
using System.Collections.Generic;
namespace Discord.Commands
{
//Represents either a single function, command group, or both
internal class CommandMap
{
private readonly CommandMap _parent;
private readonly string _name, _fullName;
private readonly List<Command> _commands;
private readonly Dictionary<string, CommandMap> _items;
private bool _isVisible, _hasNonAliases, _hasSubGroups;
public string Name => _name;
public string FullName => _fullName;
public bool IsVisible => _isVisible;
public bool HasNonAliases => _hasNonAliases;
public bool HasSubGroups => _hasSubGroups;
public IEnumerable<Command> Commands => _commands;
public IEnumerable<CommandMap> SubGroups => _items.Values;
public CommandMap()
{
_items = new Dictionary<string, CommandMap>();
_commands = new List<Command>();
_isVisible = false;
_hasNonAliases = false;
_hasSubGroups = false;
}
public CommandMap(CommandMap parent, string name, string fullName)
: this()
{
_parent = parent;
_name = name;
_fullName = fullName;
}
public CommandMap GetItem(string text)
{
return GetItem(0, text.Split(' '));
}
public CommandMap GetItem(int index, string[] parts)
{
if (index != parts.Length)
{
string nextPart = parts[index];
CommandMap nextGroup;
if (_items.TryGetValue(nextPart.ToLowerInvariant(), out nextGroup))
return nextGroup.GetItem(index + 1, parts);
else
return null;
}
return this;
}
public IEnumerable<Command> GetCommands()
{
if (_commands.Count > 0)
return _commands;
else if (_parent != null)
return _parent.GetCommands();
else
return null;
}
public IEnumerable<Command> GetCommands(string text)
{
return GetCommands(0, text.Split(' '));
}
public IEnumerable<Command> GetCommands(int index, string[] parts)
{
if (index != parts.Length)
{
string nextPart = parts[index];
CommandMap nextGroup;
if (_items.TryGetValue(nextPart.ToLowerInvariant(), out nextGroup))
{
var cmd = nextGroup.GetCommands(index + 1, parts);
if (cmd != null)
return cmd;
}
}
if (_commands != null)
return _commands;
return null;
}
public void AddCommand(string text, Command command, bool isAlias)
{
AddCommand(0, text.Split(' '), command, isAlias);
}
private void AddCommand(int index, string[] parts, Command command, bool isAlias)
{
if (!command.IsHidden)
_isVisible = true;
if (index != parts.Length)
{
CommandMap nextGroup;
string name = parts[index].ToLowerInvariant();
string fullName = string.Join(" ", parts, 0, index + 1);
if (!_items.TryGetValue(name, out nextGroup))
{
nextGroup = new CommandMap(this, name, fullName);
_items.Add(name, nextGroup);
_hasSubGroups = true;
}
nextGroup.AddCommand(index + 1, parts, command, isAlias);
}
else
{
_commands.Add(command);
if (!isAlias)
_hasNonAliases = true;
}
}
public bool CanRun(User user, ITextChannel channel, out string error)
{
error = null;
if (_commands.Count > 0)
{
foreach (var cmd in _commands)
{
if (cmd.CanRun(user, channel, out error))
return true;
}
}
if (_items.Count > 0)
{
foreach (var item in _items)
{
if (item.Value.CanRun(user, channel, out error))
return true;
}
}
return false;
}
}
}

View File

@@ -1,26 +0,0 @@
namespace Discord.Commands
{
public enum ParameterType
{
/// <summary> Catches a single required parameter. </summary>
Required,
/// <summary> Catches a single optional parameter. </summary>
Optional,
/// <summary> Catches a zero or more optional parameters. </summary>
Multiple,
/// <summary> Catches all remaining text as a single optional parameter. </summary>
Unparsed
}
public class CommandParameter
{
public string Name { get; }
public int Id { get; internal set; }
public ParameterType Type { get; }
internal CommandParameter(string name, ParameterType type)
{
Name = name;
Type = type;
}
}
}

View File

@@ -1,188 +0,0 @@
using System.Collections.Generic;
namespace Discord.Commands
{
internal static class CommandParser
{
private enum ParserPart
{
None,
Parameter,
QuotedParameter,
DoubleQuotedParameter
}
public static bool ParseCommand(string input, CommandMap map, out IEnumerable<Command> commands, out int endPos)
{
int startPosition = 0;
int endPosition = 0;
int inputLength = input.Length;
bool isEscaped = false;
commands = null;
endPos = 0;
if (input == "")
return false;
while (endPosition < inputLength)
{
char currentChar = input[endPosition++];
if (isEscaped)
isEscaped = false;
else if (currentChar == '\\')
isEscaped = true;
bool isWhitespace = IsWhiteSpace(currentChar);
if ((!isEscaped && isWhitespace) || endPosition >= inputLength)
{
int length = (isWhitespace ? endPosition - 1 : endPosition) - startPosition;
string temp = input.Substring(startPosition, length);
if (temp == "")
startPosition = endPosition;
else
{
var newMap = map.GetItem(temp);
if (newMap != null)
{
map = newMap;
endPos = endPosition;
}
else
break;
startPosition = endPosition;
}
}
}
commands = map.GetCommands(); //Work our way backwards to find a command that matches our input
return commands != null;
}
private static bool IsWhiteSpace(char c) => c == ' ' || c == '\n' || c == '\r' || c == '\t';
//TODO: Check support for escaping
public static CommandErrorType? ParseArgs(string input, int startPos, Command command, out string[] args)
{
ParserPart currentPart = ParserPart.None;
int startPosition = startPos;
int endPosition = startPos;
int inputLength = input.Length;
bool isEscaped = false;
var expectedArgs = command._parameters;
List<string> argList = new List<string>();
CommandParameter parameter = null;
args = null;
if (input == "")
return CommandErrorType.InvalidInput;
while (endPosition < inputLength)
{
if (startPosition == endPosition && (parameter == null || parameter.Type != ParameterType.Multiple)) //Is first char of a new arg
{
if (argList.Count >= expectedArgs.Length)
return CommandErrorType.BadArgCount; //Too many args
parameter = expectedArgs[argList.Count];
if (parameter.Type == ParameterType.Unparsed)
{
argList.Add(input.Substring(startPosition));
break;
}
}
char currentChar = input[endPosition++];
if (isEscaped)
isEscaped = false;
else if (currentChar == '\\')
isEscaped = true;
bool isWhitespace = IsWhiteSpace(currentChar);
if (endPosition == startPosition + 1 && isWhitespace) //Has no text yet, and is another whitespace
{
startPosition = endPosition;
continue;
}
switch (currentPart)
{
case ParserPart.None:
if ((!isEscaped && currentChar == '\"'))
{
currentPart = ParserPart.DoubleQuotedParameter;
startPosition = endPosition;
}
else if ((!isEscaped && currentChar == '\''))
{
currentPart = ParserPart.QuotedParameter;
startPosition = endPosition;
}
else if ((!isEscaped && isWhitespace) || endPosition >= inputLength)
{
int length = (isWhitespace ? endPosition - 1 : endPosition) - startPosition;
if (length == 0)
startPosition = endPosition;
else
{
string temp = input.Substring(startPosition, length);
argList.Add(temp);
currentPart = ParserPart.None;
startPosition = endPosition;
}
}
break;
case ParserPart.QuotedParameter:
if ((!isEscaped && currentChar == '\''))
{
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
argList.Add(temp);
currentPart = ParserPart.None;
startPosition = endPosition;
}
else if (endPosition >= inputLength)
return CommandErrorType.InvalidInput;
break;
case ParserPart.DoubleQuotedParameter:
if ((!isEscaped && currentChar == '\"'))
{
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
argList.Add(temp);
currentPart = ParserPart.None;
startPosition = endPosition;
}
else if (endPosition >= inputLength)
return CommandErrorType.InvalidInput;
break;
}
}
//Unclosed quotes
if (currentPart == ParserPart.QuotedParameter ||
currentPart == ParserPart.DoubleQuotedParameter)
return CommandErrorType.InvalidInput;
//Too few args
for (int i = argList.Count; i < expectedArgs.Length; i++)
{
var param = expectedArgs[i];
switch (param.Type)
{
case ParameterType.Required:
return CommandErrorType.BadArgCount;
case ParameterType.Optional:
case ParameterType.Unparsed:
argList.Add("");
break;
}
}
/*if (argList.Count > expectedArgs.Length)
{
if (expectedArgs.Length == 0 || expectedArgs[expectedArgs.Length - 1].Type != ParameterType.Multiple)
return CommandErrorType.BadArgCount;
}*/
args = argList.ToArray();
return null;
}
}
}

View File

@@ -1,347 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Discord.Commands
{
public partial class CommandService : IService
{
private readonly List<Command> _allCommands;
private readonly Dictionary<string, CommandMap> _categories;
private readonly CommandMap _map; //Command map stores all commands by their input text, used for fast resolving and parsing
public CommandServiceConfig Config { get; }
public CommandGroupBuilder Root { get; }
public DiscordClient Client { get; private set; }
//AllCommands store a flattened collection of all commands
public IEnumerable<Command> AllCommands => _allCommands;
//Groups store all commands by their module, used for more informative help
internal IEnumerable<CommandMap> Categories => _categories.Values;
public event EventHandler<CommandEventArgs> CommandExecuted = delegate { };
public event EventHandler<CommandErrorEventArgs> CommandErrored = delegate { };
private void OnCommand(CommandEventArgs args)
=> CommandExecuted(this, args);
private void OnCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null)
=> CommandErrored(this, new CommandErrorEventArgs(errorType, args, ex));
public CommandService()
: this(new CommandServiceConfigBuilder())
{
}
public CommandService(CommandServiceConfigBuilder builder)
: this(builder.Build())
{
if (builder.ExecuteHandler != null)
CommandExecuted += builder.ExecuteHandler;
if (builder.ErrorHandler != null)
CommandErrored += builder.ErrorHandler;
}
public CommandService(CommandServiceConfig config)
{
Config = config;
_allCommands = new List<Command>();
_map = new CommandMap();
_categories = new Dictionary<string, CommandMap>();
Root = new CommandGroupBuilder(this);
}
void IService.Install(DiscordClient client)
{
Client = client;
if (Config.HelpMode != HelpMode.Disabled)
{
CreateCommand("help")
.Parameter("command", ParameterType.Multiple)
.Hide()
.Description("Returns information about commands.")
.Do(async e =>
{
ITextChannel replyChannel = Config.HelpMode == HelpMode.Public ? e.Channel : await e.User.CreatePMChannel().ConfigureAwait(false);
if (e.Args.Length > 0) //Show command help
{
var map = _map.GetItem(string.Join(" ", e.Args));
if (map != null)
await ShowCommandHelp(map, e.User, e.Channel, replyChannel).ConfigureAwait(false);
else
await replyChannel.SendMessage("Unable to display help: Unknown command.").ConfigureAwait(false);
}
else //Show general help
await ShowGeneralHelp(e.User, e.Channel, replyChannel).ConfigureAwait(false);
});
}
client.MessageReceived += async (s, e) =>
{
if (_allCommands.Count == 0) return;
if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return;
string msg = e.Message.RawText;
if (msg.Length == 0) return;
string cmdMsg = null;
//Check for command char
if (Config.PrefixChar.HasValue)
{
if (msg[0] == Config.PrefixChar.Value)
cmdMsg = msg.Substring(1);
}
//Check for mention
if (cmdMsg == null && Config.AllowMentionPrefix)
{
string mention = client.CurrentUser.Mention;
if (msg.StartsWith(mention) && msg.Length > mention.Length)
cmdMsg = msg.Substring(mention.Length + 1);
else
{
mention = $"@{client.CurrentUser.Name}";
if (msg.StartsWith(mention) && msg.Length > mention.Length)
cmdMsg = msg.Substring(mention.Length + 1);
}
}
//Check using custom activator
if (cmdMsg == null && Config.CustomPrefixHandler != null)
{
int index = Config.CustomPrefixHandler(e.Message);
if (index >= 0)
cmdMsg = msg.Substring(index);
}
if (cmdMsg == null) return;
//Parse command
IEnumerable<Command> commands;
int argPos;
CommandParser.ParseCommand(cmdMsg, _map, out commands, out argPos);
if (commands == null)
{
CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null);
OnCommandError(CommandErrorType.UnknownCommand, errorArgs);
return;
}
else
{
foreach (var command in commands)
{
//Parse arguments
string[] args;
var error = CommandParser.ParseArgs(cmdMsg, argPos, command, out args);
if (error != null)
{
if (error == CommandErrorType.BadArgCount)
continue;
else
{
var errorArgs = new CommandEventArgs(e.Message, command, null);
OnCommandError(error.Value, errorArgs);
return;
}
}
var eventArgs = new CommandEventArgs(e.Message, command, args);
// Check permissions
string errorText;
if (!command.CanRun(eventArgs.User, eventArgs.Channel, out errorText))
{
OnCommandError(CommandErrorType.BadPermissions, eventArgs, errorText != null ? new Exception(errorText) : null);
return;
}
// Run the command
try
{
OnCommand(eventArgs);
await command.Run(eventArgs).ConfigureAwait(false);
}
catch (Exception ex)
{
OnCommandError(CommandErrorType.Exception, eventArgs, ex);
}
return;
}
var errorArgs2 = new CommandEventArgs(e.Message, null, null);
OnCommandError(CommandErrorType.BadArgCount, errorArgs2);
}
};
}
public Task ShowGeneralHelp(User user, ITextChannel channel, ITextChannel replyChannel = null)
{
StringBuilder output = new StringBuilder();
bool isFirstCategory = true;
foreach (var category in _categories)
{
bool isFirstItem = true;
foreach (var group in category.Value.SubGroups)
{
string error;
if (group.IsVisible && (group.HasSubGroups || group.HasNonAliases) && group.CanRun(user, channel, out error))
{
if (isFirstItem)
{
isFirstItem = false;
//This is called for the first item in each category. If we never get here, we dont bother writing the header for a category type (since it's empty)
if (isFirstCategory)
{
isFirstCategory = false;
//Called for the first non-empty category
output.AppendLine("These are the commands you can use:");
}
else
output.AppendLine();
if (category.Key != "")
{
output.Append(Format.Bold(category.Key));
output.Append(": ");
}
}
else
output.Append(", ");
output.Append('`');
output.Append(group.Name);
if (group.HasSubGroups)
output.Append("*");
output.Append('`');
}
}
}
if (output.Length == 0)
output.Append("There are no commands you have permission to run.");
else
output.AppendLine("\n\nRun `help <command>` for more information.");
return (replyChannel ?? channel).SendMessage(output.ToString());
}
private Task ShowCommandHelp(CommandMap map, User user, ITextChannel channel, ITextChannel replyChannel = null)
{
StringBuilder output = new StringBuilder();
IEnumerable<Command> cmds = map.Commands;
bool isFirstCmd = true;
string error;
if (cmds.Any())
{
foreach (var cmd in cmds)
{
if (cmd.CanRun(user, channel, out error))
{
if (isFirstCmd)
isFirstCmd = false;
else
output.AppendLine();
ShowCommandHelpInternal(cmd, user, channel, output);
}
}
}
else
{
output.Append('`');
output.Append(map.FullName);
output.Append("`\n");
}
bool isFirstSubCmd = true;
foreach (var subCmd in map.SubGroups.Where(x => x.CanRun(user, channel, out error) && x.IsVisible))
{
if (isFirstSubCmd)
{
isFirstSubCmd = false;
output.AppendLine("Sub Commands: ");
}
else
output.Append(", ");
output.Append('`');
output.Append(subCmd.Name);
if (subCmd.SubGroups.Any())
output.Append("*");
output.Append('`');
}
if (isFirstCmd && isFirstSubCmd) //Had no commands and no subcommands
{
output.Clear();
output.AppendLine("There are no commands you have permission to run.");
}
return (replyChannel ?? channel).SendMessage(output.ToString());
}
public Task ShowCommandHelp(Command command, User user, ITextChannel channel, ITextChannel replyChannel = null)
{
StringBuilder output = new StringBuilder();
string error;
if (!command.CanRun(user, channel, out error))
output.AppendLine(error ?? "You do not have permission to access this command.");
else
ShowCommandHelpInternal(command, user, channel, output);
return (replyChannel ?? channel).SendMessage(output.ToString());
}
private void ShowCommandHelpInternal(Command command, User user, ITextChannel channel, StringBuilder output)
{
output.Append('`');
output.Append(command.Text);
foreach (var param in command.Parameters)
{
switch (param.Type)
{
case ParameterType.Required:
output.Append($" <{param.Name}>");
break;
case ParameterType.Optional:
output.Append($" [{param.Name}]");
break;
case ParameterType.Multiple:
output.Append($" [{param.Name}...]");
break;
case ParameterType.Unparsed:
output.Append($" [-]");
break;
}
}
output.AppendLine("`");
output.AppendLine($"{command.Description ?? "No description."}");
if (command.Aliases.Any())
output.AppendLine($"Aliases: `" + string.Join("`, `", command.Aliases) + '`');
}
public void CreateGroup(string cmd, Action<CommandGroupBuilder> config = null) => Root.CreateGroup(cmd, config);
public CommandBuilder CreateCommand(string cmd) => Root.CreateCommand(cmd);
internal void AddCommand(Command command)
{
_allCommands.Add(command);
//Get category
CommandMap category;
string categoryName = command.Category ?? "";
if (!_categories.TryGetValue(categoryName, out category))
{
category = new CommandMap();
_categories.Add(categoryName, category);
}
//Add main command
category.AddCommand(command.Text, command, false);
_map.AddCommand(command.Text, command, false);
//Add aliases
foreach (var alias in command.Aliases)
{
category.AddCommand(alias, command, true);
_map.AddCommand(alias, command, true);
}
}
}
}

View File

@@ -1,46 +0,0 @@
using System;
namespace Discord.Commands
{
public class CommandServiceConfigBuilder
{
/// <summary> Gets or sets the prefix character used to trigger commands, if ActivationMode has the Char flag set. </summary>
public char? PrefixChar { get; set; } = null;
/// <summary> Gets or sets whether a message beginning with a mention to the logged-in user should be treated as a command. </summary>
public bool AllowMentionPrefix { get; set; } = true;
/// <summary>
/// Gets or sets a custom function used to detect messages that should be treated as commands.
/// This function should a positive one indicating the index of where the in the message's RawText the command begins,
/// and a negative value if the message should be ignored.
/// </summary>
public Func<Message, int> CustomPrefixHandler { get; set; } = null;
/// <summary> Gets or sets whether a help function should be automatically generated. </summary>
public HelpMode HelpMode { get; set; } = HelpMode.Disabled;
/// <summary> Gets or sets a handler that is called on any successful command execution. </summary>
public EventHandler<CommandEventArgs> ExecuteHandler { get; set; }
/// <summary> Gets or sets a handler that is called on any error during command parsing or execution. </summary>
public EventHandler<CommandErrorEventArgs> ErrorHandler { get; set; }
public CommandServiceConfig Build() => new CommandServiceConfig(this);
}
public class CommandServiceConfig
{
public char? PrefixChar { get; }
public bool AllowMentionPrefix { get; }
public Func<Message, int> CustomPrefixHandler { get; }
/// <summary> Gets or sets whether a help function should be automatically generated. </summary>
public HelpMode HelpMode { get; set; } = HelpMode.Disabled;
internal CommandServiceConfig(CommandServiceConfigBuilder builder)
{
PrefixChar = builder.PrefixChar;
AllowMentionPrefix = builder.AllowMentionPrefix;
CustomPrefixHandler = builder.CustomPrefixHandler;
HelpMode = builder.HelpMode;
}
}
}

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>19793545-ef89-48f4-8100-3ebaad0a9141</ProjectGuid>
<RootNamespace>Discord.Commands</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -1,22 +0,0 @@
using System;
namespace Discord.Commands.Permissions
{
internal class GenericPermissionChecker : IPermissionChecker
{
private readonly Func<Command, User, ITextChannel, bool> _checkFunc;
private readonly string _error;
public GenericPermissionChecker(Func<Command, User, ITextChannel, bool> checkFunc, string error = null)
{
_checkFunc = checkFunc;
_error = error;
}
public bool CanRun(Command command, User user, ITextChannel channel, out string error)
{
error = _error;
return _checkFunc(command, user, channel);
}
}
}

View File

@@ -1,12 +0,0 @@
namespace Discord.Commands
{
public enum HelpMode
{
/// <summary> Disable the automatic help command. </summary>
Disabled,
/// <summary> Use the automatic help command and respond in the channel the command is used. </summary>
Public,
/// <summary> Use the automatic help command and respond in a private message. </summary>
Private
}
}

View File

@@ -1,7 +0,0 @@
namespace Discord.Commands.Permissions
{
public interface IPermissionChecker
{
bool CanRun(Command command, User user, ITextChannel channel, out string error);
}
}

View File

@@ -1,25 +0,0 @@
{
"version": "1.0.0-alpha1",
"description": "A Discord.Net extension adding basic command support.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
},
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
"compilationOptions": {
"warningsAsErrors": true
},
"dependencies": {
"Discord.Net": "1.0.0-alpha1"
},
"frameworks": {
"net45": { },
"dotnet5.4": { }
}
}

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>01584e8a-78da-486f-9ef9-a894e435841b</ProjectGuid>
<RootNamespace>Discord.Modules</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -1,7 +0,0 @@
namespace Discord.Modules
{
public interface IModule
{
void Install(ModuleManager manager);
}
}

View File

@@ -1,33 +0,0 @@
using Discord.Commands;
using Discord.Commands.Permissions;
namespace Discord.Modules
{
public class ModuleChecker : IPermissionChecker
{
private readonly ModuleManager _manager;
private readonly ModuleFilter _filterType;
internal ModuleChecker(ModuleManager manager)
{
_manager = manager;
_filterType = manager.FilterType;
}
public bool CanRun(Command command, User user, ITextChannel channel, out string error)
{
if (_filterType == ModuleFilter.None ||
_filterType == ModuleFilter.AlwaysAllowPrivate ||
(channel.IsPublic && _manager.HasChannel(channel)))
{
error = null;
return true;
}
else
{
error = "This module is currently disabled.";
return false;
}
}
}
}

View File

@@ -1,29 +0,0 @@
namespace Discord.Modules
{
public static class ModuleExtensions
{
public static DiscordClient UsingModules(this DiscordClient client)
{
client.AddService(new ModuleService());
return client;
}
public static void AddModule(this DiscordClient client, IModule instance, string name = null, ModuleFilter filter = ModuleFilter.None)
{
client.GetService<ModuleService>().Add(instance, name, filter);
}
public static void AddModule<T>(this DiscordClient client, string name = null, ModuleFilter filter = ModuleFilter.None)
where T : class, IModule, new()
{
client.GetService<ModuleService>().Add<T>(name, filter);
}
public static void AddModule<T>(this DiscordClient client, T instance, string name = null, ModuleFilter filter = ModuleFilter.None)
where T : class, IModule
{
client.GetService<ModuleService>().Add(instance, name, filter);
}
public static ModuleManager<T> GetModule<T>(this DiscordClient client)
where T : class, IModule
=> client.GetService<ModuleService>().Get<T>();
}
}

View File

@@ -1,17 +0,0 @@
using System;
namespace Discord.Modules
{
[Flags]
public enum ModuleFilter
{
/// <summary> Disables the event and command filters. </summary>
None = 0x0,
/// <summary> Uses the server whitelist to filter events and commands. </summary>
ServerWhitelist = 0x1,
/// <summary> Uses the channel whitelist to filter events and commands. </summary>
ChannelWhitelist = 0x2,
/// <summary> Enables this module in all private messages. </summary>
AlwaysAllowPrivate = 0x4
}
}

View File

@@ -1,278 +0,0 @@
using Discord.Commands;
using Nito.AsyncEx;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace Discord.Modules
{
public class ModuleManager<T> : ModuleManager
where T : class, IModule
{
public new T Instance => base.Instance as T;
internal ModuleManager(DiscordClient client, T instance, string name, ModuleFilter filterType)
: base(client, instance, name, filterType)
{
}
}
public class ModuleManager
{
public event EventHandler<ServerEventArgs> JoinedServer = delegate { };
public event EventHandler<ServerEventArgs> LeftServer = delegate { };
public event EventHandler<ServerUpdatedEventArgs> ServerUpdated = delegate { };
public event EventHandler<ServerEventArgs> ServerUnavailable = delegate { };
public event EventHandler<ServerEventArgs> ServerAvailable = delegate { };
public event EventHandler<ChannelEventArgs> ChannelCreated = delegate { };
public event EventHandler<ChannelEventArgs> ChannelDestroyed = delegate { };
public event EventHandler<ChannelUpdatedEventArgs> ChannelUpdated = delegate { };
public event EventHandler<RoleEventArgs> RoleCreated = delegate { };
public event EventHandler<RoleUpdatedEventArgs> RoleUpdated = delegate { };
public event EventHandler<RoleEventArgs> RoleDeleted = delegate { };
public event EventHandler<UserEventArgs> UserBanned = delegate { };
public event EventHandler<UserEventArgs> UserJoined = delegate { };
public event EventHandler<UserEventArgs> UserLeft = delegate { };
public event EventHandler<UserUpdatedEventArgs> UserUpdated = delegate { };
public event EventHandler<UserEventArgs> UserUnbanned = delegate { };
public event EventHandler<TypingEventArgs> UserIsTyping = delegate { };
public event EventHandler<MessageEventArgs> MessageReceived = delegate { };
public event EventHandler<MessageEventArgs> MessageSent = delegate { };
public event EventHandler<MessageEventArgs> MessageDeleted = delegate { };
public event EventHandler<MessageUpdatedEventArgs> MessageUpdated = delegate { };
public event EventHandler<MessageEventArgs> MessageReadRemotely = delegate { };
private readonly bool _useServerWhitelist, _useChannelWhitelist, _allowAll, _allowPrivate;
private readonly ConcurrentDictionary<ulong, Server> _enabledServers;
private readonly ConcurrentDictionary<ulong, IChannel> _enabledChannels;
private readonly ConcurrentDictionary<ulong, int> _indirectServers;
private readonly AsyncLock _lock;
public DiscordClient Client { get; }
public IModule Instance { get; }
public string Name { get; }
public string Id { get; }
public ModuleFilter FilterType { get; }
public IEnumerable<Server> EnabledServers => _enabledServers.Select(x => x.Value);
public IEnumerable<IChannel> EnabledChannels => _enabledChannels.Select(x => x.Value);
internal ModuleManager(DiscordClient client, IModule instance, string name, ModuleFilter filterType)
{
Client = client;
Instance = instance;
Name = name;
FilterType = filterType;
Id = name.ToLowerInvariant();
_lock = new AsyncLock();
_allowAll = filterType == ModuleFilter.None;
_useServerWhitelist = filterType.HasFlag(ModuleFilter.ServerWhitelist);
_useChannelWhitelist = filterType.HasFlag(ModuleFilter.ChannelWhitelist);
_allowPrivate = filterType.HasFlag(ModuleFilter.AlwaysAllowPrivate);
_enabledServers = new ConcurrentDictionary<ulong, Server>();
_enabledChannels = new ConcurrentDictionary<ulong, IChannel>();
_indirectServers = new ConcurrentDictionary<ulong, int>();
if (_allowAll || _useServerWhitelist) //Server-only events
{
client.ChannelCreated += (s, e) =>
{
var server = (e.Channel as PublicChannel)?.Server;
if (HasServer(server))
ChannelCreated(s, e);
};
//TODO: This *is* a channel update if the before/after voice channel is whitelisted
//client.UserVoiceStateUpdated += (s, e) => { if (HasServer(e.Server)) UserVoiceStateUpdated(s, e); };
}
client.ChannelDestroyed += (s, e) => { if (HasChannel(e.Channel)) ChannelDestroyed(s, e); };
client.ChannelUpdated += (s, e) => { if (HasChannel(e.After)) ChannelUpdated(s, e); };
client.MessageReceived += (s, e) => { if (HasChannel(e.Channel)) MessageReceived(s, e); };
client.MessageSent += (s, e) => { if (HasChannel(e.Channel)) MessageSent(s, e); };
client.MessageDeleted += (s, e) => { if (HasChannel(e.Channel)) MessageDeleted(s, e); };
client.MessageUpdated += (s, e) => { if (HasChannel(e.Channel)) MessageUpdated(s, e); };
client.MessageAcknowledged += (s, e) => { if (HasChannel(e.Channel)) MessageReadRemotely(s, e); };
client.RoleCreated += (s, e) => { if (HasIndirectServer(e.Server)) RoleCreated(s, e); };
client.RoleUpdated += (s, e) => { if (HasIndirectServer(e.Server)) RoleUpdated(s, e); };
client.RoleDeleted += (s, e) => { if (HasIndirectServer(e.Server)) RoleDeleted(s, e); };
client.JoinedServer += (s, e) => { if (_allowAll) JoinedServer(s, e); };
client.LeftServer += (s, e) => { if (HasIndirectServer(e.Server)) LeftServer(s, e); };
client.ServerUpdated += (s, e) => { if (HasIndirectServer(e.After)) ServerUpdated(s, e); };
client.ServerUnavailable += (s, e) => { if (HasIndirectServer(e.Server)) ServerUnavailable(s, e); };
client.ServerAvailable += (s, e) => { if (HasIndirectServer(e.Server)) ServerAvailable(s, e); };
client.UserJoined += (s, e) => { if (HasIndirectServer(e.Server)) UserJoined(s, e); };
client.UserLeft += (s, e) => { if (HasIndirectServer(e.Server)) UserLeft(s, e); };
//TODO: We aren't getting events from UserPresence if AllowPrivate is enabled, but the server we know that user through isn't on the whitelist
client.UserUpdated += (s, e) => { if (HasIndirectServer(e.Server)) UserUpdated(s, e); };
client.UserIsTyping += (s, e) => { if (HasChannel(e.Channel)) UserIsTyping(s, e); };
client.UserBanned += (s, e) => { if (HasIndirectServer(e.Server)) UserBanned(s, e); };
client.UserUnbanned += (s, e) => { if (HasIndirectServer(e.Server)) UserUnbanned(s, e); };
}
public void CreateCommands(string prefix, Action<CommandGroupBuilder> config)
{
var commandService = Client.GetService<CommandService>();
commandService.CreateGroup(prefix, x =>
{
x.Category(Name);
x.AddCheck(new ModuleChecker(this));
config(x);
});
}
public bool EnableServer(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist.");
using (_lock.Lock())
return EnableServerInternal(server);
}
public void EnableServers(IEnumerable<Server> servers)
{
if (servers == null) throw new ArgumentNullException(nameof(servers));
if (servers.Contains(null)) throw new ArgumentException("Collection cannot contain null.", nameof(servers));
if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist.");
using (_lock.Lock())
{
foreach (var server in servers)
EnableServerInternal(server);
}
}
private bool EnableServerInternal(Server server) => _enabledServers.TryAdd(server.Id, server);
public bool DisableServer(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (!_useServerWhitelist) return false;
using (_lock.Lock())
return _enabledServers.TryRemove(server.Id, out server);
}
public void DisableAllServers()
{
if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist.");
if (!_useServerWhitelist) return;
using (_lock.Lock())
_enabledServers.Clear();
}
public bool EnableChannel(ITextChannel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (!_useChannelWhitelist) throw new InvalidOperationException("This module is not configured to use a channel whitelist.");
using (_lock.Lock())
return EnableChannelInternal(channel);
}
public void EnableChannels(IEnumerable<ITextChannel> channels)
{
if (channels == null) throw new ArgumentNullException(nameof(channels));
if (channels.Contains(null)) throw new ArgumentException("Collection cannot contain null.", nameof(channels));
if (!_useChannelWhitelist) throw new InvalidOperationException("This module is not configured to use a channel whitelist.");
using (_lock.Lock())
{
foreach (var channel in channels)
EnableChannelInternal(channel);
}
}
private bool EnableChannelInternal(ITextChannel channel)
{
if (_enabledChannels.TryAdd(channel.Id, channel))
{
if (channel.Type != ChannelType.Private)
{
var server = (channel as PublicChannel)?.Server;
int value = 0;
_indirectServers.TryGetValue(server.Id, out value);
value++;
_indirectServers[server.Id] = value;
}
return true;
}
return false;
}
public bool DisableChannel(IChannel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (!_useChannelWhitelist) return false;
IChannel ignored;
if (_enabledChannels.TryRemove(channel.Id, out ignored))
{
using (_lock.Lock())
{
if (channel.Type != ChannelType.Private)
{
var server = (channel as PublicChannel)?.Server;
int value = 0;
_indirectServers.TryGetValue(server.Id, out value);
value--;
if (value <= 0)
_indirectServers.TryRemove(server.Id, out value);
else
_indirectServers[server.Id] = value;
}
return true;
}
}
return false;
}
public void DisableAllChannels()
{
if (!_useChannelWhitelist) return;
using (_lock.Lock())
{
_enabledChannels.Clear();
_indirectServers.Clear();
}
}
public void DisableAll()
{
if (_useServerWhitelist)
DisableAllServers();
if (_useChannelWhitelist)
DisableAllChannels();
}
internal bool HasServer(Server server) =>
_allowAll ||
(_useServerWhitelist && _enabledServers.ContainsKey(server.Id));
internal bool HasIndirectServer(Server server) =>
_allowAll ||
(_useServerWhitelist && _enabledServers.ContainsKey(server.Id)) ||
(_useChannelWhitelist && _indirectServers.ContainsKey(server.Id));
internal bool HasChannel(IChannel channel)
{
if (_allowAll) return true;
if (channel.Type == ChannelType.Private) return _allowPrivate;
if (_useChannelWhitelist && _enabledChannels.ContainsKey(channel.Id)) return true;
if (_useServerWhitelist && channel.IsPublic)
{
var server = (channel as PublicChannel).Server;
if (server == null) return false;
if (_enabledServers.ContainsKey(server.Id)) return true;
}
return false;
}
}
}

View File

@@ -1,61 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Discord.Modules
{
public class ModuleService : IService
{
public DiscordClient Client { get; private set; }
private static readonly MethodInfo addMethod = typeof(ModuleService).GetTypeInfo().GetDeclaredMethods(nameof(Add))
.Single(x => x.IsGenericMethodDefinition && x.GetParameters().Length == 3);
public IEnumerable<ModuleManager> Modules => _modules.Values;
private readonly Dictionary<Type, ModuleManager> _modules;
public ModuleService()
{
_modules = new Dictionary<Type, ModuleManager>();
}
void IService.Install(DiscordClient client)
{
Client = client;
}
public void Add(IModule instance, string name, ModuleFilter filter)
{
Type type = instance.GetType();
addMethod.MakeGenericMethod(type).Invoke(this, new object[] { instance, name, filter });
}
public void Add<T>(string name, ModuleFilter filter)
where T : class, IModule, new()
=> Add(new T(), name, filter);
public void Add<T>(T instance, string name, ModuleFilter filter)
where T : class, IModule
{
if (instance == null) throw new ArgumentNullException(nameof(instance));
if (Client == null)
throw new InvalidOperationException("Service needs to be added to a DiscordClient before modules can be installed.");
Type type = typeof(T);
if (name == null) name = type.Name;
if (_modules.ContainsKey(type))
throw new InvalidOperationException("This module has already been added.");
var manager = new ModuleManager<T>(Client, instance, name, filter);
_modules.Add(type, manager);
instance.Install(manager);
}
public ModuleManager<T> Get<T>()
where T : class, IModule
{
ModuleManager manager;
if (_modules.TryGetValue(typeof(T), out manager))
return manager as ModuleManager<T>;
return null;
}
}
}

View File

@@ -1,26 +0,0 @@
{
"version": "1.0.0-alpha1",
"description": "A Discord.Net extension adding basic plugin support.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
},
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
"compilationOptions": {
"warningsAsErrors": true
},
"dependencies": {
"Discord.Net": "1.0.0-alpha1",
"Discord.Net.Commands": "1.0.0-alpha1"
},
"frameworks": {
"net45": { },
"dotnet5.4": { }
}
}

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>2875deb5-f248-4105-8ea2-5141e3de8025</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Discord.Net.Shared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)EpochTime.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TaskExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TaskHelper.cs" />
</ItemGroup>
</Project>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>2875deb5-f248-4105-8ea2-5141e3de8025</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="Discord.Net.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

View File

@@ -1,11 +0,0 @@
using System;
namespace Discord
{
internal class EpochTime
{
private const long epoch = 621355968000000000L; //1/1/1970 in Ticks
public static long GetMilliseconds() => (DateTime.UtcNow.Ticks - epoch) / TimeSpan.TicksPerMillisecond;
}
}

View File

@@ -1,68 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Discord
{
internal static class TaskExtensions
{
public static async Task Timeout(this Task task, int milliseconds)
{
Task timeoutTask = Task.Delay(milliseconds);
Task finishedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false);
if (finishedTask == timeoutTask)
throw new TimeoutException();
else
await task.ConfigureAwait(false);
}
public static async Task<T> Timeout<T>(this Task<T> task, int milliseconds)
{
Task timeoutTask = Task.Delay(milliseconds);
Task finishedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false);
if (finishedTask == timeoutTask)
throw new TimeoutException();
else
return await task.ConfigureAwait(false);
}
public static async Task Timeout(this Task task, int milliseconds, CancellationTokenSource timeoutToken)
{
try
{
timeoutToken.CancelAfter(milliseconds);
await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
if (timeoutToken.IsCancellationRequested)
throw new TimeoutException();
throw;
}
}
public static async Task<T> Timeout<T>(this Task<T> task, int milliseconds, CancellationTokenSource timeoutToken)
{
try
{
timeoutToken.CancelAfter(milliseconds);
return await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
if (timeoutToken.IsCancellationRequested)
throw new TimeoutException();
throw;
}
}
public static async Task Wait(this CancellationTokenSource tokenSource)
{
var token = tokenSource.Token;
try { await Task.Delay(-1, token).ConfigureAwait(false); }
catch (OperationCanceledException) { } //Expected
}
public static async Task Wait(this CancellationToken token)
{
try { await Task.Delay(-1, token).ConfigureAwait(false); }
catch (OperationCanceledException) { } //Expected
}
}
}

View File

@@ -1,29 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Discord
{
internal static class TaskHelper
{
#if DOTNET54
public static Task CompletedTask => Task.CompletedTask;
#else
public static Task CompletedTask => Task.Delay(0);
#endif
public static Func<Task> ToAsync(Action action)
{
return () =>
{
action(); return CompletedTask;
};
}
public static Func<T, Task> ToAsync<T>(Action<T> action)
{
return x =>
{
action(x); return CompletedTask;
};
}
}
}

View File

@@ -1,35 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class Channel : ChannelReference
{
public class PermissionOverwrite
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("deny")]
public uint Deny { get; set; }
[JsonProperty("allow")]
public uint Allow { get; set; }
}
[JsonProperty("last_message_id"), JsonConverter(typeof(NullableLongStringConverter))]
public ulong? LastMessageId { get; set; }
[JsonProperty("is_private")]
public bool? IsPrivate { get; set; }
[JsonProperty("position")]
public int? Position { get; set; }
[JsonProperty("topic")]
public string Topic { get; set; }
[JsonProperty("permission_overwrites")]
public PermissionOverwrite[] PermissionOverwrites { get; set; }
[JsonProperty("recipient")]
public UserReference Recipient { get; set; }
[JsonProperty("bitrate")]
public int Bitrate { get; set; }
}
}

View File

@@ -1,17 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class ChannelReference
{
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("guild_id"), JsonConverter(typeof(NullableLongStringConverter))]
public ulong? GuildId { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
}
}

View File

@@ -1,12 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class ExtendedMember : Member
{
[JsonProperty("mute")]
public bool? IsServerMuted { get; set; }
[JsonProperty("deaf")]
public bool? IsServerDeafened { get; set; }
}
}

View File

@@ -1,50 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
using System;
namespace Discord.API.Client
{
public class Guild : GuildReference
{
public class EmojiData
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))]
public ulong[] RoleIds { get; set; }
[JsonProperty("require_colons")]
public bool RequireColons { get; set; }
[JsonProperty("managed")]
public bool IsManaged { get; set; }
}
[JsonProperty("afk_channel_id"), JsonConverter(typeof(NullableLongStringConverter))]
public ulong? AFKChannelId { get; set; }
[JsonProperty("afk_timeout")]
public int? AFKTimeout { get; set; }
[JsonProperty("embed_channel_id"), JsonConverter(typeof(NullableLongStringConverter))]
public ulong? EmbedChannelId { get; set; }
[JsonProperty("embed_enabled")]
public bool EmbedEnabled { get; set; }
[JsonProperty("icon")]
public string Icon { get; set; }
[JsonProperty("joined_at")]
public DateTime? JoinedAt { get; set; }
[JsonProperty("owner_id"), JsonConverter(typeof(NullableLongStringConverter))]
public ulong? OwnerId { get; set; }
[JsonProperty("region")]
public string Region { get; set; }
[JsonProperty("roles")]
public Role[] Roles { get; set; }
[JsonProperty("features")]
public string[] Features { get; set; }
[JsonProperty("emojis")]
public EmojiData[] Emojis { get; set; }
[JsonProperty("splash")]
public string Splash { get; set; }
[JsonProperty("verification_level")]
public int VerificationLevel { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class GuildReference
{
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
}

View File

@@ -1,23 +0,0 @@
using Newtonsoft.Json;
using System;
namespace Discord.API.Client
{
public class Invite : InviteReference
{
[JsonProperty("max_age")]
public int? MaxAge { get; set; }
[JsonProperty("max_uses")]
public int? MaxUses { get; set; }
[JsonProperty("revoked")]
public bool? IsRevoked { get; set; }
[JsonProperty("temporary")]
public bool? IsTemporary { get; set; }
[JsonProperty("uses")]
public int? Uses { get; set; }
[JsonProperty("created_at")]
public DateTime? CreatedAt { get; set; }
[JsonProperty("inviter")]
public UserReference Inviter { get; set; }
}
}

View File

@@ -1,22 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class InviteReference
{
public class GuildData : GuildReference
{
[JsonProperty("splash_hash")]
public string Splash { get; set; }
}
[JsonProperty("guild")]
public GuildData Guild { get; set; }
[JsonProperty("channel")]
public ChannelReference Channel { get; set; }
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("xkcdpass")]
public string XkcdPass { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
using System;
namespace Discord.API.Client
{
public class Member : MemberReference
{
[JsonProperty("joined_at")]
public DateTime? JoinedAt { get; set; }
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))]
public ulong[] Roles { get; set; }
}
}

View File

@@ -1,20 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class MemberPresence : MemberReference
{
public class GameInfo
{
[JsonProperty("name")]
public string Name { get; set; }
}
[JsonProperty("game")]
public GameInfo Game { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))]
public ulong[] Roles { get; set; } //TODO: Might be temporary
}
}

View File

@@ -1,13 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class MemberReference
{
[JsonProperty("guild_id"), JsonConverter(typeof(NullableLongStringConverter))]
public ulong? GuildId { get; set; }
[JsonProperty("user")]
public UserReference User { get; set; }
}
}

View File

@@ -1,96 +0,0 @@
using Newtonsoft.Json;
using System;
namespace Discord.API.Client
{
public class Message : MessageReference
{
public class Attachment
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("proxy_url")]
public string ProxyUrl { get; set; }
[JsonProperty("size")]
public int Size { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("width")]
public int? Width { get; set; }
[JsonProperty("height")]
public int? Height { get; set; }
}
public class Embed
{
public class Reference
{
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
public class ThumbnailInfo
{
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("proxy_url")]
public string ProxyUrl { get; set; }
[JsonProperty("width")]
public int? Width { get; set; }
[JsonProperty("height")]
public int? Height { get; set; }
}
public class VideoInfo
{
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("width")]
public int? Width { get; set; }
[JsonProperty("height")]
public int? Height { get; set; }
}
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("author")]
public Reference Author { get; set; }
[JsonProperty("provider")]
public Reference Provider { get; set; }
[JsonProperty("thumbnail")]
public ThumbnailInfo Thumbnail { get; set; }
[JsonProperty("video")]
public VideoInfo Video { get; set; }
}
[JsonProperty("tts")]
public bool? IsTextToSpeech { get; set; }
[JsonProperty("mention_everyone")]
public bool? IsMentioningEveryone { get; set; }
[JsonProperty("timestamp")]
public DateTime? Timestamp { get; set; }
[JsonProperty("edited_timestamp")]
public DateTime? EditedTimestamp { get; set; }
[JsonProperty("mentions")]
public UserReference[] Mentions { get; set; }
[JsonProperty("embeds")]
public Embed[] Embeds { get; set; } //TODO: Parse this
[JsonProperty("attachments")]
public Attachment[] Attachments { get; set; }
[JsonProperty("content")]
public string Content { get; set; }
[JsonProperty("author")]
public UserReference Author { get; set; }
[JsonProperty("nonce")]
public string Nonce { get; set; }
}
}

View File

@@ -1,15 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class MessageReference
{
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("message_id"), JsonConverter(typeof(LongStringConverter))] //Only used in MESSAGE_ACK
public ulong MessageId { get { return Id; } set { Id = value; } }
[JsonProperty("channel_id"), JsonConverter(typeof(LongStringConverter))]
public ulong ChannelId { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class RoleReference
{
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))]
public ulong GuildId { get; set; }
[JsonProperty("role_id"), JsonConverter(typeof(LongStringConverter))]
public ulong RoleId { get; set; }
}
}

View File

@@ -1,12 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class User : UserReference
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("verified")]
public bool? IsVerified { get; set; }
}
}

View File

@@ -1,17 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client
{
public class UserReference
{
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("discriminator")]
public ushort? Discriminator { get; set; }
[JsonProperty("avatar")]
public string Avatar { get; set; }
}
}

View File

@@ -1,12 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.GatewaySocket
{
[JsonObject(MemberSerialization.OptIn)]
public class HeartbeatCommand : IWebSocketMessage
{
int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat;
object IWebSocketMessage.Payload => EpochTime.GetMilliseconds();
bool IWebSocketMessage.IsPrivate => false;
}
}

View File

@@ -1,4 +0,0 @@
namespace Discord.API.Client.GatewaySocket.Events
{
//public class GuildEmojisUpdateEvent { }
}

View File

@@ -1,4 +0,0 @@
namespace Discord.API.Client.GatewaySocket
{
//public class GuildIntegrationsUpdateEvent { }
}

View File

@@ -1,4 +0,0 @@
namespace Discord.API.Client.GatewaySocket
{
public class GuildMemberRemoveEvent : Member { }
}

View File

@@ -1,4 +0,0 @@
namespace Discord.API.Client.GatewaySocket
{
public class GuildMemberUpdateEvent : Member { }
}

View File

@@ -1,13 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client.GatewaySocket
{
public class GuildRoleCreateEvent
{
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))]
public ulong GuildId { get; set; }
[JsonProperty("role")]
public Role Data { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client.GatewaySocket
{
public class GuildRoleUpdateEvent
{
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))]
public ulong GuildId { get; set; }
[JsonProperty("role")]
public Role Data { get; set; }
}
}

View File

@@ -1,15 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client.GatewaySocket
{
public class TypingStartEvent
{
[JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))]
public ulong UserId { get; set; }
[JsonProperty("channel_id"), JsonConverter(typeof(LongStringConverter))]
public ulong ChannelId { get; set; }
[JsonProperty("timestamp")]
public int Timestamp { get; set; }
}
}

View File

@@ -1,4 +0,0 @@
namespace Discord.API.Client.GatewaySocket
{
//public class UserSettingsUpdateEvent { }
}

View File

@@ -1,15 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client.GatewaySocket
{
public class VoiceServerUpdateEvent
{
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))]
public ulong GuildId { get; set; }
[JsonProperty("endpoint")]
public string Endpoint { get; set; }
[JsonProperty("token")]
public string Token { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
using System.IO;
namespace Discord.API.Client
{
public interface ISerializable
{
void Write(BinaryWriter writer);
}
}

View File

@@ -1,19 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class AcceptInviteRequest : IRestRequest<InviteReference>
{
string IRestRequest.Method => "POST";
string IRestRequest.Endpoint => $"invite/{InviteId}";
object IRestRequest.Payload => null;
public string InviteId { get; set; }
public AcceptInviteRequest(string inviteId)
{
InviteId = inviteId;
}
}
}

View File

@@ -1,23 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class AddGuildBanRequest : IRestRequest
{
string IRestRequest.Method => "PUT";
string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}?delete-message-days={PruneDays}";
object IRestRequest.Payload => null;
public ulong GuildId { get; set; }
public ulong UserId { get; set; }
public int PruneDays { get; set; } = 0;
public AddGuildBanRequest(ulong guildId, ulong userId)
{
GuildId = guildId;
UserId = userId;
}
}
}

View File

@@ -1,21 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class DeleteRoleRequest : IRestRequest
{
string IRestRequest.Method => "DELETE";
string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}";
object IRestRequest.Payload => null;
public ulong GuildId { get; set; }
public ulong RoleId { get; set; }
public DeleteRoleRequest(ulong guildId, ulong roleId)
{
GuildId = guildId;
RoleId = roleId;
}
}
}

View File

@@ -1,18 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class GatewayRequest : IRestRequest<GatewayResponse>
{
string IRestRequest.Method => "GET";
string IRestRequest.Endpoint => $"gateway";
object IRestRequest.Payload => null;
}
public class GatewayResponse
{
[JsonProperty("url")]
public string Url { get; set; }
}
}

View File

@@ -1,19 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class GetBansRequest : IRestRequest<UserReference[]>
{
string IRestRequest.Method => "GET";
string IRestRequest.Endpoint => $"guilds/{GuildId}/bans";
object IRestRequest.Payload => null;
public ulong GuildId { get; set; }
public GetBansRequest(ulong guildId)
{
GuildId = guildId;
}
}
}

View File

@@ -1,19 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class GetInviteRequest : IRestRequest<InviteReference>
{
string IRestRequest.Method => "GET";
string IRestRequest.Endpoint => $"invite/{InviteCode}";
object IRestRequest.Payload => null;
public string InviteCode { get; set; }
public GetInviteRequest(string inviteCode)
{
InviteCode = inviteCode;
}
}
}

View File

@@ -1,19 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class GetInvitesRequest : IRestRequest<InviteReference[]>
{
string IRestRequest.Method => "GET";
string IRestRequest.Endpoint => $"guilds/{GuildId}/invites";
object IRestRequest.Payload => null;
public ulong GuildId { get; set; }
public GetInvitesRequest(ulong guildId)
{
GuildId = guildId;
}
}
}

View File

@@ -1,34 +0,0 @@
using Newtonsoft.Json;
using System.Text;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class GetMessagesRequest : IRestRequest<Message[]>
{
string IRestRequest.Method => "GET";
string IRestRequest.Endpoint
{
get
{
StringBuilder query = new StringBuilder();
this.AddQueryParam(query, "limit", Limit.ToString());
if (RelativeDir != null)
this.AddQueryParam(query, RelativeDir, RelativeId.ToString());
return $"channels/{ChannelId}/messages{query}";
}
}
object IRestRequest.Payload => null;
public ulong ChannelId { get; set; }
public int Limit { get; set; } = 100;
public string RelativeDir { get; set; } = null;
public ulong RelativeId { get; set; } = 0;
public GetMessagesRequest(ulong channelId)
{
ChannelId = channelId;
}
}
}

View File

@@ -1,60 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class GetWidgetRequest : IRestRequest<GetWidgetResponse>
{
string IRestRequest.Method => "GET";
string IRestRequest.Endpoint => $"servers/{GuildId}/widget.json";
object IRestRequest.Payload => null;
public ulong GuildId { get; set; }
public GetWidgetRequest(ulong guildId)
{
GuildId = guildId;
}
}
public class GetWidgetResponse
{
public class Channel
{
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
}
public class User : UserReference
{
[JsonProperty("avatar_url")]
public string AvatarUrl { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("game")]
public UserGame Game { get; set; }
}
public class UserGame
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("channels")]
public Channel[] Channels { get; set; }
[JsonProperty("members")]
public MemberReference[] Members { get; set; }
[JsonProperty("instant_invite")]
public string InstantInviteUrl { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
}

View File

@@ -1,21 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class KickMemberRequest : IRestRequest
{
string IRestRequest.Method => "DELETE";
string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}";
object IRestRequest.Payload => null;
public ulong GuildId { get; set; }
public ulong UserId { get; set; }
public KickMemberRequest(ulong guildId, ulong userId)
{
GuildId = guildId;
UserId = userId;
}
}
}

View File

@@ -1,23 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class LoginRequest : IRestRequest<LoginResponse>
{
string IRestRequest.Method => Email != null ? "POST" : "GET";
string IRestRequest.Endpoint => $"auth/login";
object IRestRequest.Payload => this;
[JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)]
public string Email { get; set; }
[JsonProperty("password", NullValueHandling = NullValueHandling.Ignore)]
public string Password { get; set; }
}
public class LoginResponse
{
[JsonProperty("token")]
public string Token { get; set; }
}
}

View File

@@ -1,12 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class LogoutRequest : IRestRequest
{
string IRestRequest.Method => "POST";
string IRestRequest.Endpoint => $"auth/logout";
object IRestRequest.Payload => null;
}
}

View File

@@ -1,28 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class PruneMembersRequest : IRestRequest<PruneMembersResponse>
{
string IRestRequest.Method => IsSimulation ? "GET" : "POST";
string IRestRequest.Endpoint => $"guilds/{GuildId}/prune?days={Days}";
object IRestRequest.Payload => null;
public ulong GuildId { get; set; }
public int Days { get; set; } = 30;
public bool IsSimulation { get; set; } = false;
public PruneMembersRequest(ulong guildId)
{
GuildId = guildId;
}
}
public class PruneMembersResponse
{
[JsonProperty("pruned")]
public int Pruned { get; set; }
}
}

View File

@@ -1,21 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class RemoveChannelPermissionsRequest : IRestRequest
{
string IRestRequest.Method => "DELETE";
string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}";
object IRestRequest.Payload => null;
public ulong ChannelId { get; set; }
public ulong TargetId { get; set; }
public RemoveChannelPermissionsRequest(ulong channelId, ulong targetId)
{
ChannelId = channelId;
TargetId = targetId;
}
}
}

View File

@@ -1,45 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
using System.Linq;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class ReorderChannelsRequest : IRestRequest
{
string IRestRequest.Method => "PATCH";
string IRestRequest.Endpoint => $"guilds/{GuildId}/channels";
object IRestRequest.Payload
{
get
{
int pos = StartPos;
return ChannelIds.Select(x => new Channel(x, pos++));
}
}
public class Channel
{
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
public Channel(ulong id, int position)
{
Id = id;
Position = position;
}
}
public ulong GuildId { get; set; }
public ulong[] ChannelIds { get; set; } = new ulong[0];
public int StartPos { get; set; } = 0;
public ReorderChannelsRequest(ulong guildId)
{
GuildId = guildId;
}
}
}

View File

@@ -1,45 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
using System.Linq;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class ReorderRolesRequest : IRestRequest<Role[]>
{
string IRestRequest.Method => "PATCH";
string IRestRequest.Endpoint => $"guilds/{GuildId}/roles";
object IRestRequest.Payload
{
get
{
int pos = StartPos;
return RoleIds.Select(x => new Role(x, pos++));
}
}
public class Role
{
[JsonProperty("id"), JsonConverter(typeof(LongStringConverter))]
public ulong Id { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
public Role(ulong id, int pos)
{
Id = id;
Position = pos;
}
}
public ulong GuildId { get; set; }
public ulong[] RoleIds { get; set; } = new ulong[0];
public int StartPos { get; set; } = 0;
public ReorderRolesRequest(ulong guildId)
{
GuildId = guildId;
}
}
}

View File

@@ -1,19 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class SendIsTypingRequest : IRestRequest
{
string IRestRequest.Method => "POST";
string IRestRequest.Endpoint => $"channels/{ChannelId}/typing";
object IRestRequest.Payload => null;
public ulong ChannelId { get; set; }
public SendIsTypingRequest(ulong channelId)
{
ChannelId = channelId;
}
}
}

View File

@@ -1,33 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class UpdateGuildRequest : IRestRequest<Guild>
{
string IRestRequest.Method => "PATCH";
string IRestRequest.Endpoint => $"guilds/{GuildId}";
object IRestRequest.Payload => this;
public ulong GuildId { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("region")]
public string Region { get; set; }
[JsonProperty("icon")]
public string IconBase64 { get; set; }
[JsonProperty("afk_channel_id"), JsonConverter(typeof(NullableLongStringConverter))]
public ulong? AFKChannelId { get; set; }
[JsonProperty("afk_timeout")]
public int AFKTimeout { get; set; }
[JsonProperty("splash")]
public object Splash { get; set; }
public UpdateGuildRequest(ulong guildId)
{
GuildId = guildId;
}
}
}

View File

@@ -1,30 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class UpdateRoleRequest : IRestRequest<Role>
{
string IRestRequest.Method => "PATCH";
string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}";
object IRestRequest.Payload => this;
public ulong GuildId { get; set; }
public ulong RoleId { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("permissions")]
public uint Permissions { get; set; }
[JsonProperty("hoist")]
public bool IsHoisted { get; set; }
[JsonProperty("color")]
public uint Color { get; set; }
public UpdateRoleRequest(ulong guildId, ulong roleId)
{
GuildId = guildId;
RoleId = roleId;
}
}
}

View File

@@ -1,9 +0,0 @@
namespace Discord.API.Client.VoiceSocket
{
public class HeartbeatCommand : IWebSocketMessage
{
int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat;
object IWebSocketMessage.Payload => EpochTime.GetMilliseconds();
bool IWebSocketMessage.IsPrivate => false;
}
}

View File

@@ -1,21 +0,0 @@
using Discord.API.Converters;
using Newtonsoft.Json;
namespace Discord.API.Client.VoiceSocket
{
public class IdentifyCommand : IWebSocketMessage
{
int IWebSocketMessage.OpCode => (int)OpCodes.Identify;
object IWebSocketMessage.Payload => this;
bool IWebSocketMessage.IsPrivate => true;
[JsonProperty("server_id"), JsonConverter(typeof(LongStringConverter))]
public ulong GuildId { get; set; }
[JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))]
public ulong UserId { get; set; }
[JsonProperty("session_id")]
public string SessionId { get; set; }
[JsonProperty("token")]
public string Token { get; set; }
}
}

View File

@@ -1,29 +0,0 @@
using Newtonsoft.Json;
namespace Discord.API.Client.VoiceSocket
{
public class SelectProtocolCommand : IWebSocketMessage
{
int IWebSocketMessage.OpCode => (int)OpCodes.SelectProtocol;
object IWebSocketMessage.Payload => this;
bool IWebSocketMessage.IsPrivate => false;
public class Data
{
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("port")]
public int Port { get; set; }
[JsonProperty("mode")]
public string Mode { get; set; }
}
[JsonProperty("protocol")]
public string Protocol { get; set; } = "udp";
[JsonProperty("data")]
private Data ProtocolData { get; } = new Data();
public string ExternalAddress { get { return ProtocolData.Address; } set { ProtocolData.Address = value; } }
public int ExternalPort { get { return ProtocolData.Port; } set { ProtocolData.Port = value; } }
public string EncryptionMode { get { return ProtocolData.Mode; } set { ProtocolData.Mode = value; } }
}
}

View File

@@ -0,0 +1,22 @@
using Newtonsoft.Json;
namespace Discord.API
{
public class Attachment
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("size")]
public int Size { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("proxy_url")]
public string ProxyUrl { get; set; }
[JsonProperty("height")]
public int? Height { get; set; }
[JsonProperty("width")]
public int? Width { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
#pragma warning disable CA1721
using Newtonsoft.Json;
namespace Discord.API
{
public class Channel
{
//Shared
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("is_private")]
public bool IsPrivate { get; set; }
[JsonProperty("last_message_id")]
public ulong LastMessageId { get; set; }
//GuildChannel
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
[JsonProperty("permission_overwrites")]
public Overwrite[] PermissionOverwrites { get; set; }
[JsonProperty("topic")]
public string Topic { get; set; }
[JsonProperty("bitrate")]
public int Bitrate { get; set; }
//DMChannel
[JsonProperty("recipient")]
public User Recipient { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
#pragma warning disable CA1721
using Newtonsoft.Json;
namespace Discord.API
{
public class Embed
{
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("thumbnail")]
public EmbedThumbnail Thumbnail { get; set; }
[JsonProperty("provider")]
public EmbedProvider Provider { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More