Early 1.0 REST Preview
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Discord.Audio
|
||||
{
|
||||
public enum AudioMode : byte
|
||||
{
|
||||
Outgoing = 1,
|
||||
Incoming = 2,
|
||||
Both = Outgoing | Incoming
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Discord
|
||||
{
|
||||
public class UserIsSpeakingEventArgs : UserEventArgs
|
||||
{
|
||||
public bool IsSpeaking { get; }
|
||||
|
||||
public UserIsSpeakingEventArgs(User user, bool isSpeaking)
|
||||
: base(user)
|
||||
{
|
||||
IsSpeaking = isSpeaking;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -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": { }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Discord.Commands.Permissions
|
||||
{
|
||||
public interface IPermissionChecker
|
||||
{
|
||||
bool CanRun(Command command, User user, ITextChannel channel, out string error);
|
||||
}
|
||||
}
|
||||
@@ -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": { }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Discord.Modules
|
||||
{
|
||||
public interface IModule
|
||||
{
|
||||
void Install(ModuleManager manager);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": { }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Discord.API.Client.GatewaySocket.Events
|
||||
{
|
||||
//public class GuildEmojisUpdateEvent { }
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Discord.API.Client.GatewaySocket
|
||||
{
|
||||
//public class GuildIntegrationsUpdateEvent { }
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Discord.API.Client.GatewaySocket
|
||||
{
|
||||
public class GuildMemberRemoveEvent : Member { }
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Discord.API.Client.GatewaySocket
|
||||
{
|
||||
public class GuildMemberUpdateEvent : Member { }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Discord.API.Client.GatewaySocket
|
||||
{
|
||||
//public class UserSettingsUpdateEvent { }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Discord.API.Client
|
||||
{
|
||||
public interface ISerializable
|
||||
{
|
||||
void Write(BinaryWriter writer);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; } }
|
||||
}
|
||||
}
|
||||
22
src/Discord.Net/API/Common/Attachment.cs
Normal file
22
src/Discord.Net/API/Common/Attachment.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
37
src/Discord.Net/API/Common/Channel.cs
Normal file
37
src/Discord.Net/API/Common/Channel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
21
src/Discord.Net/API/Common/Embed.cs
Normal file
21
src/Discord.Net/API/Common/Embed.cs
Normal 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
Reference in New Issue
Block a user