| @@ -3,24 +3,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00 | |||
| # Visual Studio 14 | |||
| VisualStudioVersion = 14.0.25123.0 | |||
| MinimumVisualStudioVersion = 10.0.40219.1 | |||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{855D6B1D-847B-42DA-BE6A-23683EA89511}" | |||
| EndProject | |||
| Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" | |||
| EndProject | |||
| Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.xproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" | |||
| EndProject | |||
| Global | |||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
| Debug|Any CPU = Debug|Any CPU | |||
| Release|Any CPU = Release|Any CPU | |||
| EndGlobalSection | |||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | |||
| {855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
| {855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
| {855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
| {855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.Build.0 = Release|Any CPU | |||
| {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
| {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
| {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
| {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Release|Any CPU | |||
| {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
| {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
| {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
| {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.Build.0 = Release|Any CPU | |||
| EndGlobalSection | |||
| GlobalSection(SolutionProperties) = preSolution | |||
| HideSolutionNode = FALSE | |||
| @@ -1,25 +1,21 @@ | |||
| # Discord.Net v1.0.0-dev | |||
| [](https://www.nuget.org/packages/Discord.Net) [](https://ci.appveyor.com/project/foxbot/discord-net/) [](https://discord.gg/0SBTUU1wZTYLhAAW) | |||
| [](https://www.nuget.org/packages/Discord.Net) [](https://ci.appveyor.com/project/foxbot/discord-net/) [](https://discord.gg/0SBTUU1wZTYLhAAW) | |||
| Discord.Net is an API wrapper for [Discord](http://discordapp.com) written in C#. | |||
| Check out the [documentation](https://discordnet.readthedocs.org/en/latest/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). | |||
| ## Installing | |||
| **NuGet is not up to date with 1.0.0-dev.** | |||
| You can download Discord.Net and its extensions from NuGet: | |||
| ### Installation | |||
| You can download Discord.Net and its command extension from NuGet: | |||
| - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) | |||
| - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) | |||
| - [Discord.Net.Modules](https://www.nuget.org/packages/Discord.Net.Modules/) | |||
| - [Discord.Net.Audio](https://www.nuget.org/packages/Discord.Net.Audio/) | |||
| ### Compiling | |||
| In order to compile Discord.Net, you require at least the following: | |||
| - [Visual Studio 2015](https://www.visualstudio.com/downloads/download-visual-studio-vs) | |||
| - [Visual Studio 2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx) | |||
| - [Visual Studio .Net Core Plugin](https://www.microsoft.com/net/core#windows) | |||
| - NuGet 3.3+ (available through Visual Studio) | |||
| In order to compile Discord.Net, you require the following: | |||
| #### Visual Studio 2015 | |||
| - [VS2015 Update 3](https://www.microsoft.com/net/core#windows) | |||
| - [.Net Core 1.0 VS Plugin](https://www.microsoft.com/net/core#windows) | |||
| #### CLI | |||
| - [.Net Core 1.0 SDK](https://www.microsoft.com/net/core) | |||
| @@ -1,6 +1,3 @@ | |||
| { | |||
| "projects": [ "src", "test" ], | |||
| "sdk": { | |||
| "version": "1.0.0-preview1-002702" | |||
| } | |||
| "projects": [ "src", "test" ] | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| using System; | |||
| namespace Discord.Commands | |||
| { | |||
| [AttributeUsage(AttributeTargets.Method)] | |||
| public class CommandAttribute : Attribute | |||
| { | |||
| public string Text { get; } | |||
| public CommandAttribute() | |||
| { | |||
| Text = null; | |||
| } | |||
| public CommandAttribute(string text) | |||
| { | |||
| Text = text; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| using System; | |||
| namespace Discord.Commands | |||
| { | |||
| [AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter)] | |||
| public class DescriptionAttribute : Attribute | |||
| { | |||
| public string Text { get; } | |||
| public DescriptionAttribute(string text) | |||
| { | |||
| Text = text; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| using System; | |||
| namespace Discord.Commands | |||
| { | |||
| [AttributeUsage(AttributeTargets.Class)] | |||
| public class GroupAttribute : Attribute | |||
| { | |||
| public string Prefix { get; } | |||
| public GroupAttribute() | |||
| { | |||
| Prefix = null; | |||
| } | |||
| public GroupAttribute(string prefix) | |||
| { | |||
| Prefix = prefix; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| using System; | |||
| namespace Discord.Commands | |||
| { | |||
| [AttributeUsage(AttributeTargets.Class)] | |||
| public class ModuleAttribute : Attribute | |||
| { | |||
| public string Prefix { get; } | |||
| public ModuleAttribute() | |||
| { | |||
| Prefix = null; | |||
| } | |||
| public ModuleAttribute(string prefix) | |||
| { | |||
| Prefix = prefix; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| using System; | |||
| namespace Discord.Commands | |||
| { | |||
| [AttributeUsage(AttributeTargets.Parameter)] | |||
| public class UnparsedAttribute : Attribute | |||
| { | |||
| } | |||
| } | |||
| @@ -0,0 +1,124 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.Reflection; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class Command | |||
| { | |||
| private readonly object _instance; | |||
| private readonly Func<IMessage, IReadOnlyList<object>, Task> _action; | |||
| public string Name { get; } | |||
| public string Description { get; } | |||
| public string Text { get; } | |||
| public Module Module { get; } | |||
| public IReadOnlyList<CommandParameter> Parameters { get; } | |||
| internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo, string groupPrefix) | |||
| { | |||
| Module = module; | |||
| _instance = instance; | |||
| Name = methodInfo.Name; | |||
| Text = groupPrefix + attribute.Text; | |||
| var description = methodInfo.GetCustomAttribute<DescriptionAttribute>(); | |||
| if (description != null) | |||
| Description = description.Text; | |||
| Parameters = BuildParameters(methodInfo); | |||
| _action = BuildAction(methodInfo); | |||
| } | |||
| public async Task<ParseResult> Parse(IMessage msg, SearchResult searchResult) | |||
| { | |||
| if (!searchResult.IsSuccess) | |||
| return ParseResult.FromError(searchResult); | |||
| return await CommandParser.ParseArgs(this, msg, searchResult.Text.Substring(Text.Length), 0).ConfigureAwait(false); | |||
| } | |||
| public async Task<ExecuteResult> Execute(IMessage msg, ParseResult parseResult) | |||
| { | |||
| if (!parseResult.IsSuccess) | |||
| return ExecuteResult.FromError(parseResult); | |||
| try | |||
| { | |||
| await _action.Invoke(msg, parseResult.Values);//Note: This code may need context | |||
| return ExecuteResult.FromSuccess(); | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| return ExecuteResult.FromError(ex); | |||
| } | |||
| } | |||
| private IReadOnlyList<CommandParameter> BuildParameters(MethodInfo methodInfo) | |||
| { | |||
| var parameters = methodInfo.GetParameters(); | |||
| var paramBuilder = ImmutableArray.CreateBuilder<CommandParameter>(parameters.Length - 1); | |||
| for (int i = 0; i < parameters.Length; i++) | |||
| { | |||
| var parameter = parameters[i]; | |||
| var type = parameter.ParameterType; | |||
| if (i == 0) | |||
| { | |||
| if (type != typeof(IMessage)) | |||
| throw new InvalidOperationException("The first parameter of a command must be IMessage."); | |||
| else | |||
| continue; | |||
| } | |||
| var typeInfo = type.GetTypeInfo(); | |||
| if (typeInfo.IsEnum) | |||
| type = Enum.GetUnderlyingType(type); | |||
| var reader = Module.Service.GetTypeReader(type); | |||
| if (reader == null) | |||
| throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?"); | |||
| bool isUnparsed = parameter.GetCustomAttribute<UnparsedAttribute>() != null; | |||
| if (isUnparsed) | |||
| { | |||
| if (type != typeof(string)) | |||
| throw new InvalidOperationException("Unparsed parameters only support the string type."); | |||
| else if (i != parameters.Length - 1) | |||
| throw new InvalidOperationException("Unparsed parameters must be the last parameter in a command."); | |||
| } | |||
| string name = parameter.Name; | |||
| string description = typeInfo.GetCustomAttribute<DescriptionAttribute>()?.Text; | |||
| bool isOptional = parameter.IsOptional; | |||
| object defaultValue = parameter.HasDefaultValue ? parameter.DefaultValue : null; | |||
| paramBuilder.Add(new CommandParameter(name, description, reader, isOptional, isUnparsed, defaultValue)); | |||
| } | |||
| return paramBuilder.ToImmutable(); | |||
| } | |||
| private Func<IMessage, IReadOnlyList<object>, Task> BuildAction(MethodInfo methodInfo) | |||
| { | |||
| if (methodInfo.ReturnType != typeof(Task)) | |||
| throw new InvalidOperationException("Commands must return a non-generic Task."); | |||
| //TODO: Temporary reflection hack. Lets build an actual expression tree here. | |||
| return (msg, args) => | |||
| { | |||
| object[] newArgs = new object[args.Count + 1]; | |||
| newArgs[0] = msg; | |||
| for (int i = 0; i < args.Count; i++) | |||
| newArgs[i + 1] = args[i]; | |||
| var result = methodInfo.Invoke(_instance, newArgs); | |||
| return result as Task ?? Task.CompletedTask; | |||
| }; | |||
| } | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => $"{Module.Name}.{Name} ({Text})"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| namespace Discord.Commands | |||
| { | |||
| public enum CommandError | |||
| { | |||
| //Search | |||
| UnknownCommand, | |||
| //Parse | |||
| ParseFailed, | |||
| BadArgCount, | |||
| //Parse (Type Reader) | |||
| CastFailed, | |||
| ObjectNotFound, | |||
| MultipleMatches, | |||
| //Execute | |||
| Exception, | |||
| } | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| using System.Diagnostics; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| //TODO: Add support for Multiple | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class CommandParameter | |||
| { | |||
| private readonly TypeReader _reader; | |||
| public string Name { get; } | |||
| public string Description { get; } | |||
| public bool IsOptional { get; } | |||
| public bool IsUnparsed { get; } | |||
| internal object DefaultValue { get; } | |||
| public CommandParameter(string name, string description, TypeReader reader, bool isOptional, bool isUnparsed, object defaultValue) | |||
| { | |||
| _reader = reader; | |||
| Name = name; | |||
| IsOptional = isOptional; | |||
| IsUnparsed = isUnparsed; | |||
| DefaultValue = defaultValue; | |||
| } | |||
| public async Task<TypeReaderResult> Parse(IMessage context, string input) | |||
| { | |||
| return await _reader.Read(context, input).ConfigureAwait(false); | |||
| } | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsUnparsed ? " (Unparsed)" : "")}"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,144 @@ | |||
| using System.Collections.Immutable; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| internal static class CommandParser | |||
| { | |||
| private enum ParserPart | |||
| { | |||
| None, | |||
| Parameter, | |||
| QuotedParameter | |||
| } | |||
| //TODO: Check support for escaping | |||
| public static async Task<ParseResult> ParseArgs(Command command, IMessage context, string input, int startPos) | |||
| { | |||
| CommandParameter curParam = null; | |||
| StringBuilder argBuilder = new StringBuilder(input.Length); | |||
| int endPos = input.Length; | |||
| var curPart = ParserPart.None; | |||
| int lastArgEndPos = int.MinValue; | |||
| var argList = ImmutableArray.CreateBuilder<object>(); | |||
| bool isEscaping = false; | |||
| char c; | |||
| for (int curPos = startPos; curPos <= endPos; curPos++) | |||
| { | |||
| if (curPos < endPos) | |||
| c = input[curPos]; | |||
| else | |||
| c = '\0'; | |||
| //If this character is escaped, skip it | |||
| if (isEscaping) | |||
| { | |||
| if (curPos != endPos) | |||
| { | |||
| argBuilder.Append(c); | |||
| isEscaping = false; | |||
| continue; | |||
| } | |||
| } | |||
| //Are we escaping the next character? | |||
| if (c == '\\') | |||
| { | |||
| isEscaping = true; | |||
| continue; | |||
| } | |||
| //If we're processing an unparsed parameter, ignore all other logic | |||
| if (curParam != null && curParam.IsUnparsed && curPos != endPos) | |||
| { | |||
| argBuilder.Append(c); | |||
| continue; | |||
| } | |||
| //If we're not currently processing one, are we starting the next argument yet? | |||
| if (curPart == ParserPart.None) | |||
| { | |||
| if (char.IsWhiteSpace(c) || curPos == endPos) | |||
| continue; //Skip whitespace between arguments | |||
| else if (curPos == lastArgEndPos) | |||
| return ParseResult.FromError(CommandError.ParseFailed, "There must be at least one character of whitespace between arguments."); | |||
| else | |||
| { | |||
| curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; | |||
| if (curParam != null && curParam.IsUnparsed) | |||
| { | |||
| argBuilder.Append(c); | |||
| continue; | |||
| } | |||
| if (c == '\"') | |||
| { | |||
| curPart = ParserPart.QuotedParameter; | |||
| continue; | |||
| } | |||
| curPart = ParserPart.Parameter; | |||
| } | |||
| } | |||
| //Has this parameter ended yet? | |||
| string argString = null; | |||
| if (curPart == ParserPart.Parameter) | |||
| { | |||
| if (curPos == endPos || char.IsWhiteSpace(c)) | |||
| { | |||
| argString = argBuilder.ToString(); | |||
| lastArgEndPos = curPos; | |||
| } | |||
| else | |||
| argBuilder.Append(c); | |||
| } | |||
| else if (curPart == ParserPart.QuotedParameter) | |||
| { | |||
| if (c == '\"') | |||
| { | |||
| argString = argBuilder.ToString(); //Remove quotes | |||
| lastArgEndPos = curPos + 1; | |||
| } | |||
| else | |||
| argBuilder.Append(c); | |||
| } | |||
| if (argString != null) | |||
| { | |||
| if (curParam == null) | |||
| return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); | |||
| var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false); | |||
| if (!typeReaderResult.IsSuccess) | |||
| return ParseResult.FromError(typeReaderResult); | |||
| argList.Add(typeReaderResult.Value); | |||
| curParam = null; | |||
| curPart = ParserPart.None; | |||
| argBuilder.Clear(); | |||
| } | |||
| } | |||
| if (curParam != null && curParam.IsUnparsed) | |||
| argList.Add(argBuilder.ToString()); | |||
| if (isEscaping) | |||
| return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape."); | |||
| if (curPart == ParserPart.QuotedParameter) | |||
| return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete"); | |||
| if (argList.Count < command.Parameters.Count) | |||
| { | |||
| for (int i = argList.Count; i < command.Parameters.Count; i++) | |||
| { | |||
| var param = command.Parameters[i]; | |||
| if (!param.IsOptional) | |||
| return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); | |||
| argList.Add(param.DefaultValue); | |||
| } | |||
| } | |||
| return ParseResult.FromSuccess(argList.ToImmutable()); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,260 @@ | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Globalization; | |||
| using System.Linq; | |||
| using System.Reflection; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| public class CommandService | |||
| { | |||
| private readonly SemaphoreSlim _moduleLock; | |||
| private readonly ConcurrentDictionary<object, Module> _modules; | |||
| private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders; | |||
| private readonly CommandMap _map; | |||
| public IEnumerable<Module> Modules => _modules.Select(x => x.Value); | |||
| public IEnumerable<Command> Commands => _modules.SelectMany(x => x.Value.Commands); | |||
| public CommandService() | |||
| { | |||
| _moduleLock = new SemaphoreSlim(1, 1); | |||
| _modules = new ConcurrentDictionary<object, Module>(); | |||
| _map = new CommandMap(); | |||
| _typeReaders = new ConcurrentDictionary<Type, TypeReader> | |||
| { | |||
| [typeof(string)] = new GenericTypeReader((m, s) => Task.FromResult(TypeReaderResult.FromSuccess(s))), | |||
| [typeof(byte)] = new GenericTypeReader((m, s) => | |||
| { | |||
| byte value; | |||
| if (byte.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Byte")); | |||
| }), | |||
| [typeof(sbyte)] = new GenericTypeReader((m, s) => | |||
| { | |||
| sbyte value; | |||
| if (sbyte.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse SByte")); | |||
| }), | |||
| [typeof(ushort)] = new GenericTypeReader((m, s) => | |||
| { | |||
| ushort value; | |||
| if (ushort.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt16")); | |||
| }), | |||
| [typeof(short)] = new GenericTypeReader((m, s) => | |||
| { | |||
| short value; | |||
| if (short.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int16")); | |||
| }), | |||
| [typeof(uint)] = new GenericTypeReader((m, s) => | |||
| { | |||
| uint value; | |||
| if (uint.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt32")); | |||
| }), | |||
| [typeof(int)] = new GenericTypeReader((m, s) => | |||
| { | |||
| int value; | |||
| if (int.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int32")); | |||
| }), | |||
| [typeof(ulong)] = new GenericTypeReader((m, s) => | |||
| { | |||
| ulong value; | |||
| if (ulong.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt64")); | |||
| }), | |||
| [typeof(long)] = new GenericTypeReader((m, s) => | |||
| { | |||
| long value; | |||
| if (long.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int64")); | |||
| }), | |||
| [typeof(float)] = new GenericTypeReader((m, s) => | |||
| { | |||
| float value; | |||
| if (float.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Single")); | |||
| }), | |||
| [typeof(double)] = new GenericTypeReader((m, s) => | |||
| { | |||
| double value; | |||
| if (double.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Double")); | |||
| }), | |||
| [typeof(decimal)] = new GenericTypeReader((m, s) => | |||
| { | |||
| decimal value; | |||
| if (decimal.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Decimal")); | |||
| }), | |||
| [typeof(DateTime)] = new GenericTypeReader((m, s) => | |||
| { | |||
| DateTime value; | |||
| if (DateTime.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse DateTime")); | |||
| }), | |||
| [typeof(DateTimeOffset)] = new GenericTypeReader((m, s) => | |||
| { | |||
| DateTimeOffset value; | |||
| if (DateTimeOffset.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse DateTimeOffset")); | |||
| }), | |||
| [typeof(IMessage)] = new MessageTypeReader(), | |||
| [typeof(IChannel)] = new ChannelTypeReader<IChannel>(), | |||
| [typeof(IGuildChannel)] = new ChannelTypeReader<IGuildChannel>(), | |||
| [typeof(ITextChannel)] = new ChannelTypeReader<ITextChannel>(), | |||
| [typeof(IVoiceChannel)] = new ChannelTypeReader<IVoiceChannel>(), | |||
| [typeof(IRole)] = new RoleTypeReader(), | |||
| [typeof(IUser)] = new UserTypeReader<IUser>(), | |||
| [typeof(IGuildUser)] = new UserTypeReader<IGuildUser>() | |||
| }; | |||
| } | |||
| public void AddTypeReader<T>(TypeReader reader) | |||
| { | |||
| _typeReaders[typeof(T)] = reader; | |||
| } | |||
| public void AddTypeReader(Type type, TypeReader reader) | |||
| { | |||
| _typeReaders[type] = reader; | |||
| } | |||
| internal TypeReader GetTypeReader(Type type) | |||
| { | |||
| TypeReader reader; | |||
| if (_typeReaders.TryGetValue(type, out reader)) | |||
| return reader; | |||
| return null; | |||
| } | |||
| public async Task<Module> Load(object moduleInstance) | |||
| { | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| if (_modules.ContainsKey(moduleInstance)) | |||
| throw new ArgumentException($"This module has already been loaded."); | |||
| var typeInfo = moduleInstance.GetType().GetTypeInfo(); | |||
| var moduleAttr = typeInfo.GetCustomAttribute<ModuleAttribute>(); | |||
| if (moduleAttr == null) | |||
| throw new ArgumentException($"Modules must be marked with ModuleAttribute."); | |||
| return LoadInternal(moduleInstance, moduleAttr, typeInfo); | |||
| } | |||
| finally | |||
| { | |||
| _moduleLock.Release(); | |||
| } | |||
| } | |||
| private Module LoadInternal(object moduleInstance, ModuleAttribute moduleAttr, TypeInfo typeInfo) | |||
| { | |||
| var loadedModule = new Module(this, moduleInstance, moduleAttr, typeInfo); | |||
| _modules[moduleInstance] = loadedModule; | |||
| foreach (var cmd in loadedModule.Commands) | |||
| _map.AddCommand(cmd); | |||
| return loadedModule; | |||
| } | |||
| public async Task<IEnumerable<Module>> LoadAssembly(Assembly assembly) | |||
| { | |||
| var modules = ImmutableArray.CreateBuilder<Module>(); | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| foreach (var type in assembly.ExportedTypes) | |||
| { | |||
| var typeInfo = type.GetTypeInfo(); | |||
| var moduleAttr = typeInfo.GetCustomAttribute<ModuleAttribute>(); | |||
| if (moduleAttr != null) | |||
| { | |||
| var moduleInstance = ReflectionUtils.CreateObject(typeInfo); | |||
| modules.Add(LoadInternal(moduleInstance, moduleAttr, typeInfo)); | |||
| } | |||
| } | |||
| return modules.ToImmutable(); | |||
| } | |||
| finally | |||
| { | |||
| _moduleLock.Release(); | |||
| } | |||
| } | |||
| public async Task<bool> Unload(Module module) | |||
| { | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| return UnloadInternal(module.Instance); | |||
| } | |||
| finally | |||
| { | |||
| _moduleLock.Release(); | |||
| } | |||
| } | |||
| public async Task<bool> Unload(object moduleInstance) | |||
| { | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| return UnloadInternal(moduleInstance); | |||
| } | |||
| finally | |||
| { | |||
| _moduleLock.Release(); | |||
| } | |||
| } | |||
| private bool UnloadInternal(object module) | |||
| { | |||
| Module unloadedModule; | |||
| if (_modules.TryRemove(module, out unloadedModule)) | |||
| { | |||
| foreach (var cmd in unloadedModule.Commands) | |||
| _map.RemoveCommand(cmd); | |||
| return true; | |||
| } | |||
| else | |||
| return false; | |||
| } | |||
| public SearchResult Search(IMessage message, int argPos) => Search(message, message.RawText.Substring(argPos)); | |||
| public SearchResult Search(IMessage message, string input) | |||
| { | |||
| string lowerInput = input.ToLowerInvariant(); | |||
| var matches = _map.GetCommands(input).ToImmutableArray(); | |||
| if (matches.Length > 0) | |||
| return SearchResult.FromSuccess(input, matches); | |||
| else | |||
| return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | |||
| } | |||
| public Task<IResult> Execute(IMessage message, int argPos) => Execute(message, message.RawText.Substring(argPos)); | |||
| public async Task<IResult> Execute(IMessage message, string input) | |||
| { | |||
| var searchResult = Search(message, input); | |||
| if (!searchResult.IsSuccess) | |||
| return searchResult; | |||
| var commands = searchResult.Commands; | |||
| for (int i = commands.Count - 1; i >= 0; i--) | |||
| { | |||
| var parseResult = await commands[i].Parse(message, searchResult); | |||
| if (!parseResult.IsSuccess) | |||
| continue; | |||
| var executeResult = await commands[i].Execute(message, parseResult); | |||
| return executeResult; | |||
| } | |||
| return ParseResult.FromError(CommandError.ParseFailed, "This input does not match any overload."); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| <?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)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
| <PropertyGroup Label="Globals"> | |||
| <ProjectGuid>078dd7e6-943d-4d09-afc2-d2ba58b76c9c</ProjectGuid> | |||
| <RootNamespace>Discord.Commands</RootNamespace> | |||
| <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> | |||
| <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> | |||
| <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> | |||
| </PropertyGroup> | |||
| <PropertyGroup> | |||
| <SchemaVersion>2.0</SchemaVersion> | |||
| </PropertyGroup> | |||
| <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
| </Project> | |||
| @@ -0,0 +1,44 @@ | |||
| namespace Discord.Commands | |||
| { | |||
| public static class MessageExtensions | |||
| { | |||
| public static bool HasCharPrefix(this IMessage msg, char c, ref int argPos) | |||
| { | |||
| var text = msg.RawText; | |||
| if (text.Length > 0 && text[0] == c) | |||
| { | |||
| argPos = 1; | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| public static bool HasStringPrefix(this IMessage msg, string str, ref int argPos) | |||
| { | |||
| var text = msg.RawText; | |||
| //str = str + ' '; | |||
| if (text.StartsWith(str)) | |||
| { | |||
| argPos = str.Length; | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| public static bool HasMentionPrefix(this IMessage msg, IUser user, ref int argPos) | |||
| { | |||
| var text = msg.RawText; | |||
| string mention = user.Mention + ' '; | |||
| if (text.StartsWith(mention)) | |||
| { | |||
| argPos = mention.Length; | |||
| return true; | |||
| } | |||
| string nickMention = user.NicknameMention + ' '; | |||
| if (text.StartsWith(mention)) | |||
| { | |||
| argPos = nickMention.Length; | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| namespace Discord.Commands | |||
| { | |||
| internal class CommandMap | |||
| { | |||
| private readonly ConcurrentDictionary<string, CommandMapNode> _nodes; | |||
| public CommandMap() | |||
| { | |||
| _nodes = new ConcurrentDictionary<string, CommandMapNode>(); | |||
| } | |||
| public void AddCommand(Command command) | |||
| { | |||
| string text = command.Text; | |||
| int nextSpace = text.IndexOf(' '); | |||
| string name; | |||
| lock (this) | |||
| { | |||
| if (nextSpace == -1) | |||
| name = command.Text; | |||
| else | |||
| name = command.Text.Substring(0, nextSpace); | |||
| var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(x)); | |||
| nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); | |||
| } | |||
| } | |||
| public void RemoveCommand(Command command) | |||
| { | |||
| string text = command.Text; | |||
| int nextSpace = text.IndexOf(' '); | |||
| string name; | |||
| lock (this) | |||
| { | |||
| if (nextSpace == -1) | |||
| name = command.Text; | |||
| else | |||
| name = command.Text.Substring(0, nextSpace); | |||
| CommandMapNode nextNode; | |||
| if (_nodes.TryGetValue(name, out nextNode)) | |||
| { | |||
| nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); | |||
| if (nextNode.IsEmpty) | |||
| _nodes.TryRemove(name, out nextNode); | |||
| } | |||
| } | |||
| } | |||
| public IEnumerable<Command> GetCommands(string text) | |||
| { | |||
| int nextSpace = text.IndexOf(' '); | |||
| string name; | |||
| lock (this) | |||
| { | |||
| if (nextSpace == -1) | |||
| name = text; | |||
| else | |||
| name = text.Substring(0, nextSpace); | |||
| CommandMapNode nextNode; | |||
| if (_nodes.TryGetValue(name, out nextNode)) | |||
| return nextNode.GetCommands(text, nextSpace + 1); | |||
| else | |||
| return Enumerable.Empty<Command>(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,95 @@ | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| namespace Discord.Commands | |||
| { | |||
| internal class CommandMapNode | |||
| { | |||
| private readonly ConcurrentDictionary<string, CommandMapNode> _nodes; | |||
| private readonly string _name; | |||
| private ImmutableArray<Command> _commands; | |||
| public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; | |||
| public CommandMapNode(string name) | |||
| { | |||
| _name = name; | |||
| _nodes = new ConcurrentDictionary<string, CommandMapNode>(); | |||
| _commands = ImmutableArray.Create<Command>(); | |||
| } | |||
| public void AddCommand(string text, int index, Command command) | |||
| { | |||
| int nextSpace = text.IndexOf(' ', index); | |||
| string name; | |||
| lock (this) | |||
| { | |||
| if (text == "") | |||
| _commands = _commands.Add(command); | |||
| else | |||
| { | |||
| if (nextSpace == -1) | |||
| name = text.Substring(index); | |||
| else | |||
| name = text.Substring(index, nextSpace - index); | |||
| var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(x)); | |||
| nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); | |||
| } | |||
| } | |||
| } | |||
| public void RemoveCommand(string text, int index, Command command) | |||
| { | |||
| int nextSpace = text.IndexOf(' ', index); | |||
| string name; | |||
| lock (this) | |||
| { | |||
| if (text == "") | |||
| _commands = _commands.Remove(command); | |||
| else | |||
| { | |||
| if (nextSpace == -1) | |||
| name = text.Substring(index); | |||
| else | |||
| name = text.Substring(index, nextSpace - index); | |||
| CommandMapNode nextNode; | |||
| if (_nodes.TryGetValue(name, out nextNode)) | |||
| { | |||
| nextNode.RemoveCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); | |||
| if (nextNode.IsEmpty) | |||
| _nodes.TryRemove(name, out nextNode); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| public IEnumerable<Command> GetCommands(string text, int index) | |||
| { | |||
| int nextSpace = text.IndexOf(' ', index); | |||
| string name; | |||
| var commands = _commands; | |||
| for (int i = 0; i < commands.Length; i++) | |||
| yield return _commands[i]; | |||
| if (text != "") | |||
| { | |||
| if (nextSpace == -1) | |||
| name = text.Substring(index); | |||
| else | |||
| name = text.Substring(index, nextSpace - index); | |||
| CommandMapNode nextNode; | |||
| if (_nodes.TryGetValue(name, out nextNode)) | |||
| { | |||
| foreach (var cmd in nextNode.GetCommands(nextSpace == -1 ? "" : text, nextSpace + 1)) | |||
| yield return cmd; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| using System.Collections.Generic; | |||
| using System.Diagnostics; | |||
| using System.Reflection; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class Module | |||
| { | |||
| public CommandService Service { get; } | |||
| public string Name { get; } | |||
| public IEnumerable<Command> Commands { get; } | |||
| internal object Instance { get; } | |||
| internal Module(CommandService service, object instance, ModuleAttribute moduleAttr, TypeInfo typeInfo) | |||
| { | |||
| Service = service; | |||
| Name = typeInfo.Name; | |||
| Instance = instance; | |||
| List<Command> commands = new List<Command>(); | |||
| SearchClass(instance, commands, typeInfo, moduleAttr.Prefix ?? ""); | |||
| Commands = commands; | |||
| } | |||
| private void SearchClass(object instance, List<Command> commands, TypeInfo typeInfo, string groupPrefix) | |||
| { | |||
| if (groupPrefix != "") | |||
| groupPrefix += " "; | |||
| foreach (var method in typeInfo.DeclaredMethods) | |||
| { | |||
| var cmdAttr = method.GetCustomAttribute<CommandAttribute>(); | |||
| if (cmdAttr != null) | |||
| commands.Add(new Command(this, instance, cmdAttr, method, groupPrefix)); | |||
| } | |||
| foreach (var type in typeInfo.DeclaredNestedTypes) | |||
| { | |||
| var groupAttrib = type.GetCustomAttribute<GroupAttribute>(); | |||
| if (groupAttrib != null) | |||
| { | |||
| string nextGroupPrefix; | |||
| if (groupAttrib.Prefix != null) | |||
| nextGroupPrefix = groupPrefix + groupAttrib.Prefix ?? type.Name; | |||
| else | |||
| nextGroupPrefix = groupPrefix; | |||
| SearchClass(ReflectionUtils.CreateObject(type), commands, type, nextGroupPrefix); | |||
| } | |||
| } | |||
| } | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => Name; | |||
| } | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| using System; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| internal class ChannelTypeReader<T> : TypeReader | |||
| where T : class, IChannel | |||
| { | |||
| public override async Task<TypeReaderResult> Read(IMessage context, string input) | |||
| { | |||
| IGuildChannel guildChannel = context.Channel as IGuildChannel; | |||
| IChannel result = null; | |||
| if (guildChannel != null) | |||
| { | |||
| //By Id | |||
| ulong id; | |||
| if (MentionUtils.TryParseChannel(input, out id) || ulong.TryParse(input, out id)) | |||
| { | |||
| var channel = await guildChannel.Guild.GetChannelAsync(id).ConfigureAwait(false); | |||
| if (channel != null) | |||
| result = channel; | |||
| } | |||
| //By Name | |||
| if (result == null) | |||
| { | |||
| var channels = await guildChannel.Guild.GetChannelsAsync().ConfigureAwait(false); | |||
| var filteredChannels = channels.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)).ToArray(); | |||
| if (filteredChannels.Length > 1) | |||
| return TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple channels found."); | |||
| else if (filteredChannels.Length == 1) | |||
| result = filteredChannels[0]; | |||
| } | |||
| } | |||
| if (result == null) | |||
| return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found."); | |||
| T castResult = result as T; | |||
| if (castResult == null) | |||
| return TypeReaderResult.FromError(CommandError.CastFailed, $"Channel is not a {typeof(T).Name}."); | |||
| else | |||
| return TypeReaderResult.FromSuccess(castResult); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| internal class GenericTypeReader : TypeReader | |||
| { | |||
| private readonly Func<IMessage, string, Task<TypeReaderResult>> _action; | |||
| public GenericTypeReader(Func<IMessage, string, Task<TypeReaderResult>> action) | |||
| { | |||
| _action = action; | |||
| } | |||
| public override Task<TypeReaderResult> Read(IMessage context, string input) => _action(context, input); | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| using System.Globalization; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| internal class MessageTypeReader : TypeReader | |||
| { | |||
| public override Task<TypeReaderResult> Read(IMessage context, string input) | |||
| { | |||
| //By Id | |||
| ulong id; | |||
| if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | |||
| { | |||
| var msg = context.Channel.GetCachedMessage(id); | |||
| if (msg == null) | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.")); | |||
| else | |||
| return Task.FromResult(TypeReaderResult.FromSuccess(msg)); | |||
| } | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Message Id.")); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,36 @@ | |||
| using System; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| internal class RoleTypeReader : TypeReader | |||
| { | |||
| public override Task<TypeReaderResult> Read(IMessage context, string input) | |||
| { | |||
| IGuildChannel guildChannel = context.Channel as IGuildChannel; | |||
| if (guildChannel != null) | |||
| { | |||
| //By Id | |||
| ulong id; | |||
| if (MentionUtils.TryParseRole(input, out id) || ulong.TryParse(input, out id)) | |||
| { | |||
| var channel = guildChannel.Guild.GetRole(id); | |||
| if (channel != null) | |||
| return Task.FromResult(TypeReaderResult.FromSuccess(channel)); | |||
| } | |||
| //By Name | |||
| var roles = guildChannel.Guild.Roles; | |||
| var filteredRoles = roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)).ToArray(); | |||
| if (filteredRoles.Length > 1) | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple roles found.")); | |||
| else if (filteredRoles.Length == 1) | |||
| return Task.FromResult(TypeReaderResult.FromSuccess(filteredRoles[0])); | |||
| } | |||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found.")); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| public abstract class TypeReader | |||
| { | |||
| public abstract Task<TypeReaderResult> Read(IMessage context, string input); | |||
| } | |||
| } | |||
| @@ -0,0 +1,62 @@ | |||
| using System; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| internal class UserTypeReader<T> : TypeReader | |||
| where T : class, IUser | |||
| { | |||
| public override async Task<TypeReaderResult> Read(IMessage context, string input) | |||
| { | |||
| IUser result = null; | |||
| //By Id | |||
| ulong id; | |||
| if (MentionUtils.TryParseUser(input, out id) || ulong.TryParse(input, out id)) | |||
| { | |||
| var user = await context.Channel.GetUserAsync(id).ConfigureAwait(false); | |||
| if (user != null) | |||
| result = user; | |||
| } | |||
| //By Username + Discriminator | |||
| if (result == null) | |||
| { | |||
| int index = input.LastIndexOf('#'); | |||
| if (index >= 0) | |||
| { | |||
| string username = input.Substring(0, index); | |||
| ushort discriminator; | |||
| if (ushort.TryParse(input.Substring(index + 1), out discriminator)) | |||
| { | |||
| var users = await context.Channel.GetUsersAsync().ConfigureAwait(false); | |||
| result = users.Where(x => | |||
| x.DiscriminatorValue == discriminator && | |||
| string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); | |||
| } | |||
| } | |||
| } | |||
| //By Username | |||
| if (result == null) | |||
| { | |||
| var users = await context.Channel.GetUsersAsync().ConfigureAwait(false); | |||
| var filteredUsers = users.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)).ToArray(); | |||
| if (filteredUsers.Length > 1) | |||
| return TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple users found."); | |||
| else if (filteredUsers.Length == 1) | |||
| result = filteredUsers[0]; | |||
| } | |||
| if (result == null) | |||
| return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); | |||
| T castResult = result as T; | |||
| if (castResult == null) | |||
| return TypeReaderResult.FromError(CommandError.CastFailed, $"User is not a {typeof(T).Name}."); | |||
| else | |||
| return TypeReaderResult.FromSuccess(castResult); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| using System; | |||
| using System.Linq; | |||
| using System.Reflection; | |||
| namespace Discord.Commands | |||
| { | |||
| internal class ReflectionUtils | |||
| { | |||
| internal static object CreateObject(TypeInfo typeInfo) | |||
| { | |||
| var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); | |||
| if (constructor == null) | |||
| throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); | |||
| try | |||
| { | |||
| return constructor.Invoke(null); | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| using System; | |||
| using System.Diagnostics; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public struct ExecuteResult : IResult | |||
| { | |||
| public Exception Exception { get; } | |||
| public CommandError? Error { get; } | |||
| public string ErrorReason { get; } | |||
| public bool IsSuccess => !Error.HasValue; | |||
| private ExecuteResult(Exception exception, CommandError? error, string errorReason) | |||
| { | |||
| Exception = exception; | |||
| Error = error; | |||
| ErrorReason = errorReason; | |||
| } | |||
| internal static ExecuteResult FromSuccess() | |||
| => new ExecuteResult(null, null, null); | |||
| internal static ExecuteResult FromError(CommandError error, string reason) | |||
| => new ExecuteResult(null, error, reason); | |||
| internal static ExecuteResult FromError(Exception ex) | |||
| => new ExecuteResult(ex, CommandError.Exception, ex.Message); | |||
| internal static ExecuteResult FromError(ParseResult result) | |||
| => new ExecuteResult(null, result.Error, result.ErrorReason); | |||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
| private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| namespace Discord.Commands | |||
| { | |||
| public interface IResult | |||
| { | |||
| CommandError? Error { get; } | |||
| string ErrorReason { get; } | |||
| bool IsSuccess { get; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| using System.Collections.Generic; | |||
| using System.Diagnostics; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public struct ParseResult : IResult | |||
| { | |||
| public IReadOnlyList<object> Values { get; } | |||
| public CommandError? Error { get; } | |||
| public string ErrorReason { get; } | |||
| public bool IsSuccess => !Error.HasValue; | |||
| private ParseResult(IReadOnlyList<object> values, CommandError? error, string errorReason) | |||
| { | |||
| Values = values; | |||
| Error = error; | |||
| ErrorReason = errorReason; | |||
| } | |||
| internal static ParseResult FromSuccess(IReadOnlyList<object> values) | |||
| => new ParseResult(values, null, null); | |||
| internal static ParseResult FromError(CommandError error, string reason) | |||
| => new ParseResult(null, error, reason); | |||
| internal static ParseResult FromError(SearchResult result) | |||
| => new ParseResult(null, result.Error, result.ErrorReason); | |||
| internal static ParseResult FromError(TypeReaderResult result) | |||
| => new ParseResult(null, result.Error, result.ErrorReason); | |||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
| private string DebuggerDisplay => IsSuccess ? $"Success ({Values.Count} Values)" : $"{Error}: {ErrorReason}"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| using System.Collections.Generic; | |||
| using System.Diagnostics; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public struct SearchResult : IResult | |||
| { | |||
| public string Text { get; } | |||
| public IReadOnlyList<Command> Commands { get; } | |||
| public CommandError? Error { get; } | |||
| public string ErrorReason { get; } | |||
| public bool IsSuccess => !Error.HasValue; | |||
| private SearchResult(string text, IReadOnlyList<Command> commands, CommandError? error, string errorReason) | |||
| { | |||
| Text = text; | |||
| Commands = commands; | |||
| Error = error; | |||
| ErrorReason = errorReason; | |||
| } | |||
| internal static SearchResult FromSuccess(string text, IReadOnlyList<Command> commands) | |||
| => new SearchResult(text, commands, null, null); | |||
| internal static SearchResult FromError(CommandError error, string reason) | |||
| => new SearchResult(null, null, error, reason); | |||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
| private string DebuggerDisplay => IsSuccess ? $"Success ({Commands.Count} Results)" : $"{Error}: {ErrorReason}"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| using System.Diagnostics; | |||
| using System.Runtime.InteropServices; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public struct TypeReaderResult : IResult | |||
| { | |||
| public object Value { get; } | |||
| public CommandError? Error { get; } | |||
| public string ErrorReason { get; } | |||
| public bool IsSuccess => !Error.HasValue; | |||
| private TypeReaderResult(object value, CommandError? error, string errorReason) | |||
| { | |||
| Value = value; | |||
| Error = error; | |||
| ErrorReason = errorReason; | |||
| } | |||
| public static TypeReaderResult FromSuccess(object value) | |||
| => new TypeReaderResult(value, null, null); | |||
| public static TypeReaderResult FromError(CommandError error, string reason) | |||
| => new TypeReaderResult(null, error, reason); | |||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
| private string DebuggerDisplay => IsSuccess ? $"Success ({Value})" : $"{Error}: {ErrorReason}"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| { | |||
| "version": "1.0.0-dev", | |||
| "description": "A Discord.Net extension adding command support.", | |||
| "authors": [ "RogueException" ], | |||
| "packOptions": { | |||
| "tags": [ "discord", "discordapp" ], | |||
| "licenseUrl": "http://opensource.org/licenses/MIT", | |||
| "projectUrl": "https://github.com/RogueException/Discord.Net", | |||
| "repository": { | |||
| "type": "git", | |||
| "url": "git://github.com/RogueException/Discord.Net" | |||
| } | |||
| }, | |||
| "buildOptions": { | |||
| "allowUnsafe": true, | |||
| "warningsAsErrors": false | |||
| }, | |||
| "dependencies": { | |||
| "Discord.Net": "1.0.0-dev" | |||
| }, | |||
| "frameworks": { | |||
| "netstandard1.3": { | |||
| "imports": [ | |||
| "dotnet5.4", | |||
| "dnxcore50", | |||
| "portable-net45+win8" | |||
| ] | |||
| } | |||
| } | |||
| } | |||
| @@ -15,8 +15,8 @@ namespace Discord.API | |||
| [JsonProperty("proxy_url")] | |||
| public string ProxyUrl { get; set; } | |||
| [JsonProperty("height")] | |||
| public int? Height { get; set; } | |||
| public Optional<int> Height { get; set; } | |||
| [JsonProperty("width")] | |||
| public int? Width { get; set; } | |||
| public Optional<int> Width { get; set; } | |||
| } | |||
| } | |||
| @@ -14,28 +14,28 @@ namespace Discord.API | |||
| //GuildChannel | |||
| [JsonProperty("guild_id")] | |||
| public ulong? GuildId { get; set; } | |||
| public Optional<ulong> GuildId { get; set; } | |||
| [JsonProperty("name")] | |||
| public string Name { get; set; } | |||
| public Optional<string> Name { get; set; } | |||
| [JsonProperty("type")] | |||
| public ChannelType Type { get; set; } | |||
| public Optional<ChannelType> Type { get; set; } | |||
| [JsonProperty("position")] | |||
| public int Position { get; set; } | |||
| public Optional<int> Position { get; set; } | |||
| [JsonProperty("permission_overwrites")] | |||
| public Overwrite[] PermissionOverwrites { get; set; } | |||
| public Optional<Overwrite[]> PermissionOverwrites { get; set; } | |||
| //TextChannel | |||
| [JsonProperty("topic")] | |||
| public string Topic { get; set; } | |||
| public Optional<string> Topic { get; set; } | |||
| //VoiceChannel | |||
| [JsonProperty("bitrate")] | |||
| public int Bitrate { get; set; } | |||
| public Optional<int> Bitrate { get; set; } | |||
| [JsonProperty("user_limit")] | |||
| public int UserLimit { get; set; } | |||
| public Optional<int> UserLimit { get; set; } | |||
| //DMChannel | |||
| [JsonProperty("recipient")] | |||
| public User Recipient { get; set; } | |||
| public Optional<User> Recipient { get; set; } | |||
| } | |||
| } | |||
| @@ -15,6 +15,6 @@ namespace Discord.API | |||
| public bool Revoked { get; set; } | |||
| [JsonProperty("integrations")] | |||
| public IEnumerable<ulong> Integrations { get; set; } | |||
| public IReadOnlyCollection<ulong> Integrations { get; set; } | |||
| } | |||
| } | |||
| @@ -13,8 +13,8 @@ namespace Discord.API | |||
| [JsonProperty("url")] | |||
| public string Url { get; set; } | |||
| [JsonProperty("thumbnail")] | |||
| public EmbedThumbnail Thumbnail { get; set; } | |||
| public Optional<EmbedThumbnail> Thumbnail { get; set; } | |||
| [JsonProperty("provider")] | |||
| public EmbedProvider Provider { get; set; } | |||
| public Optional<EmbedProvider> Provider { get; set; } | |||
| } | |||
| } | |||
| @@ -9,8 +9,8 @@ namespace Discord.API | |||
| [JsonProperty("proxy_url")] | |||
| public string ProxyUrl { get; set; } | |||
| [JsonProperty("height")] | |||
| public int? Height { get; set; } | |||
| public Optional<int> Height { get; set; } | |||
| [JsonProperty("width")] | |||
| public int? Width { get; set; } | |||
| public Optional<int> Width { get; set; } | |||
| } | |||
| } | |||
| @@ -7,8 +7,8 @@ namespace Discord.API | |||
| [JsonProperty("name")] | |||
| public string Name { get; set; } | |||
| [JsonProperty("url")] | |||
| public string StreamUrl { get; set; } | |||
| public Optional<string> StreamUrl { get; set; } | |||
| [JsonProperty("type")] | |||
| public StreamType StreamType { get; set; } | |||
| public Optional<StreamType?> StreamType { get; set; } | |||
| } | |||
| } | |||
| @@ -25,7 +25,7 @@ namespace Discord.API | |||
| [JsonProperty("embed_channel_id")] | |||
| public ulong? EmbedChannelId { get; set; } | |||
| [JsonProperty("verification_level")] | |||
| public int VerificationLevel { get; set; } | |||
| public VerificationLevel VerificationLevel { get; set; } | |||
| [JsonProperty("voice_states")] | |||
| public VoiceState[] VoiceStates { get; set; } | |||
| [JsonProperty("roles")] | |||
| @@ -34,5 +34,9 @@ namespace Discord.API | |||
| public Emoji[] Emojis { get; set; } | |||
| [JsonProperty("features")] | |||
| public string[] Features { get; set; } | |||
| [JsonProperty("mfa_level")] | |||
| public MfaLevel MfaLevel { get; set; } | |||
| [JsonProperty("default_message_notifications")] | |||
| public DefaultMessageNotifications DefaultMessageNotifications { get; set; } | |||
| } | |||
| } | |||
| @@ -7,6 +7,6 @@ namespace Discord.API | |||
| [JsonProperty("enabled")] | |||
| public bool Enabled { get; set; } | |||
| [JsonProperty("channel_id")] | |||
| public ulong? ChannelId { get; set; } | |||
| public ulong ChannelId { get; set; } | |||
| } | |||
| } | |||
| @@ -8,11 +8,11 @@ namespace Discord.API | |||
| [JsonProperty("user")] | |||
| public User User { get; set; } | |||
| [JsonProperty("nick")] | |||
| public string Nick { get; set; } | |||
| public Optional<string> Nick { get; set; } | |||
| [JsonProperty("roles")] | |||
| public ulong[] Roles { get; set; } | |||
| [JsonProperty("joined_at")] | |||
| public DateTime?JoinedAt { get; set; } | |||
| public DateTimeOffset JoinedAt { get; set; } | |||
| [JsonProperty("deaf")] | |||
| public bool Deaf { get; set; } | |||
| [JsonProperty("mute")] | |||
| @@ -26,6 +26,6 @@ namespace Discord.API | |||
| [JsonProperty("account")] | |||
| public IntegrationAccount Account { get; set; } | |||
| [JsonProperty("synced_at")] | |||
| public DateTime SyncedAt { get; set; } | |||
| public DateTimeOffset SyncedAt { get; set; } | |||
| } | |||
| } | |||
| @@ -16,7 +16,7 @@ namespace Discord.API | |||
| [JsonProperty("temporary")] | |||
| public bool Temporary { get; set; } | |||
| [JsonProperty("created_at")] | |||
| public DateTime CreatedAt { get; set; } | |||
| public DateTimeOffset CreatedAt { get; set; } | |||
| [JsonProperty("revoked")] | |||
| public bool Revoked { get; set; } | |||
| } | |||
| @@ -10,24 +10,24 @@ namespace Discord.API | |||
| [JsonProperty("channel_id")] | |||
| public ulong ChannelId { get; set; } | |||
| [JsonProperty("author")] | |||
| public User Author { get; set; } | |||
| public Optional<User> Author { get; set; } | |||
| [JsonProperty("content")] | |||
| public string Content { get; set; } | |||
| public Optional<string> Content { get; set; } | |||
| [JsonProperty("timestamp")] | |||
| public DateTime Timestamp { get; set; } | |||
| public Optional<DateTimeOffset> Timestamp { get; set; } | |||
| [JsonProperty("edited_timestamp")] | |||
| public DateTime? EditedTimestamp { get; set; } | |||
| public Optional<DateTimeOffset?> EditedTimestamp { get; set; } | |||
| [JsonProperty("tts")] | |||
| public bool IsTextToSpeech { get; set; } | |||
| public Optional<bool> IsTextToSpeech { get; set; } | |||
| [JsonProperty("mention_everyone")] | |||
| public bool IsMentioningEveryone { get; set; } | |||
| public Optional<bool> MentionEveryone { get; set; } | |||
| [JsonProperty("mentions")] | |||
| public User[] Mentions { get; set; } | |||
| public Optional<User[]> Mentions { get; set; } | |||
| [JsonProperty("attachments")] | |||
| public Attachment[] Attachments { get; set; } | |||
| public Optional<Attachment[]> Attachments { get; set; } | |||
| [JsonProperty("embeds")] | |||
| public Embed[] Embeds { get; set; } | |||
| [JsonProperty("nonce")] | |||
| public uint? Nonce { get; set; } | |||
| public Optional<Embed[]> Embeds { get; set; } | |||
| [JsonProperty("pinned")] | |||
| public Optional<bool> Pinned { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| { | |||
| public class Presence | |||
| { | |||
| [JsonProperty("user")] | |||
| public User User { get; set; } | |||
| [JsonProperty("guild_id")] | |||
| public Optional<ulong> GuildId { get; set; } | |||
| [JsonProperty("status")] | |||
| public UserStatus Status { get; set; } | |||
| [JsonProperty("game")] | |||
| public Game Game { get; set; } | |||
| [JsonProperty("roles")] | |||
| public Optional<ulong[]> Roles { get; set; } | |||
| [JsonProperty("nick")] | |||
| public Optional<string> Nick { get; set; } | |||
| } | |||
| } | |||
| @@ -9,6 +9,6 @@ namespace Discord.API | |||
| [JsonProperty("mention_count")] | |||
| public int MentionCount { get; set; } | |||
| [JsonProperty("last_message_id")] | |||
| public ulong? LastMessageId { get; set; } | |||
| public Optional<ulong> LastMessageId { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| { | |||
| public class Relationship | |||
| { | |||
| [JsonProperty("id")] | |||
| public ulong Id { get; set; } | |||
| [JsonProperty("user")] | |||
| public User User { get; set; } | |||
| [JsonProperty("type")] | |||
| public RelationshipType Type { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| namespace Discord.API | |||
| { | |||
| public enum RelationshipType | |||
| { | |||
| Friend = 1, | |||
| Blocked = 2, | |||
| Pending = 4 | |||
| } | |||
| } | |||
| @@ -9,14 +9,14 @@ namespace Discord.API | |||
| [JsonProperty("name")] | |||
| public string Name { get; set; } | |||
| [JsonProperty("color")] | |||
| public uint? Color { get; set; } | |||
| public uint Color { get; set; } | |||
| [JsonProperty("hoist")] | |||
| public bool? Hoist { get; set; } | |||
| public bool Hoist { get; set; } | |||
| [JsonProperty("position")] | |||
| public int? Position { get; set; } | |||
| public int Position { get; set; } | |||
| [JsonProperty("permissions"), Int53] | |||
| public ulong? Permissions { get; set; } | |||
| public ulong Permissions { get; set; } | |||
| [JsonProperty("managed")] | |||
| public bool? Managed { get; set; } | |||
| public bool Managed { get; set; } | |||
| } | |||
| } | |||
| @@ -9,14 +9,18 @@ namespace Discord.API | |||
| [JsonProperty("username")] | |||
| public string Username { get; set; } | |||
| [JsonProperty("discriminator")] | |||
| public ushort Discriminator { get; set; } | |||
| public string Discriminator { get; set; } | |||
| [JsonProperty("bot")] | |||
| public bool Bot { get; set; } | |||
| [JsonProperty("avatar")] | |||
| public string Avatar { get; set; } | |||
| //CurrentUser | |||
| [JsonProperty("verified")] | |||
| public bool IsVerified { get; set; } | |||
| public bool Verified { get; set; } | |||
| [JsonProperty("email")] | |||
| public string Email { get; set; } | |||
| [JsonProperty("bot")] | |||
| public bool Bot { get; set; } | |||
| [JsonProperty("mfa_enabled")] | |||
| public bool MfaEnabled { get; set; } | |||
| } | |||
| } | |||
| @@ -7,7 +7,7 @@ namespace Discord.API | |||
| [JsonProperty("guild_id")] | |||
| public ulong? GuildId { get; set; } | |||
| [JsonProperty("channel_id")] | |||
| public ulong ChannelId { get; set; } | |||
| public ulong? ChannelId { get; set; } | |||
| [JsonProperty("user_id")] | |||
| public ulong UserId { get; set; } | |||
| [JsonProperty("session_id")] | |||
| @@ -0,0 +1,254 @@ | |||
| using Discord.API; | |||
| using Discord.API.Voice; | |||
| using Discord.Net.Converters; | |||
| using Discord.Net.WebSockets; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Diagnostics; | |||
| using System.Globalization; | |||
| using System.IO; | |||
| using System.IO.Compression; | |||
| using System.Text; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| using System.Net.Sockets; | |||
| using System.Net; | |||
| namespace Discord.Audio | |||
| { | |||
| public class DiscordVoiceAPIClient | |||
| { | |||
| public const int MaxBitrate = 128; | |||
| public const string Mode = "xsalsa20_poly1305"; | |||
| public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>(); | |||
| public event Func<VoiceOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<VoiceOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<VoiceOpCode, Task>>(); | |||
| public event Func<Task> SentDiscovery { add { _sentDiscoveryEvent.Add(value); } remove { _sentDiscoveryEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Task>> _sentDiscoveryEvent = new AsyncEvent<Func<Task>>(); | |||
| public event Func<VoiceOpCode, object, Task> ReceivedEvent { add { _receivedEvent.Add(value); } remove { _receivedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<VoiceOpCode, object, Task>> _receivedEvent = new AsyncEvent<Func<VoiceOpCode, object, Task>>(); | |||
| public event Func<byte[], Task> ReceivedPacket { add { _receivedPacketEvent.Add(value); } remove { _receivedPacketEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<byte[], Task>> _receivedPacketEvent = new AsyncEvent<Func<byte[], Task>>(); | |||
| public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||
| private readonly JsonSerializer _serializer; | |||
| private readonly IWebSocketClient _webSocketClient; | |||
| private readonly SemaphoreSlim _connectionLock; | |||
| private CancellationTokenSource _connectCancelToken; | |||
| private UdpClient _udp; | |||
| private IPEndPoint _udpEndpoint; | |||
| private Task _udpRecieveTask; | |||
| private bool _isDisposed; | |||
| public ulong GuildId { get; } | |||
| public ConnectionState ConnectionState { get; private set; } | |||
| internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) | |||
| { | |||
| GuildId = guildId; | |||
| _connectionLock = new SemaphoreSlim(1, 1); | |||
| _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | |||
| _webSocketClient = webSocketProvider(); | |||
| //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) | |||
| _webSocketClient.BinaryMessage += async (data, index, count) => | |||
| { | |||
| using (var compressed = new MemoryStream(data, index + 2, count - 2)) | |||
| using (var decompressed = new MemoryStream()) | |||
| { | |||
| using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) | |||
| zlib.CopyTo(decompressed); | |||
| decompressed.Position = 0; | |||
| using (var reader = new StreamReader(decompressed)) | |||
| { | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(reader.ReadToEnd()); | |||
| await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); | |||
| } | |||
| } | |||
| }; | |||
| _webSocketClient.TextMessage += async text => | |||
| { | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text); | |||
| await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); | |||
| }; | |||
| _webSocketClient.Closed += async ex => | |||
| { | |||
| await DisconnectAsync().ConfigureAwait(false); | |||
| await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | |||
| }; | |||
| _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
| } | |||
| private void Dispose(bool disposing) | |||
| { | |||
| if (!_isDisposed) | |||
| { | |||
| if (disposing) | |||
| { | |||
| _connectCancelToken?.Dispose(); | |||
| (_webSocketClient as IDisposable)?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| public void Dispose() => Dispose(true); | |||
| public async Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) | |||
| { | |||
| byte[] bytes = null; | |||
| payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; | |||
| if (payload != null) | |||
| bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | |||
| await _webSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false); | |||
| await _sentGatewayMessageEvent.InvokeAsync(opCode); | |||
| } | |||
| public async Task SendAsync(byte[] data, int bytes) | |||
| { | |||
| if (_udpEndpoint != null) | |||
| { | |||
| await _udp.SendAsync(data, bytes, _udpEndpoint).ConfigureAwait(false); | |||
| await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); | |||
| } | |||
| } | |||
| //WebSocket | |||
| public async Task SendHeartbeatAsync(RequestOptions options = null) | |||
| { | |||
| await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task SendIdentityAsync(ulong userId, string sessionId, string token) | |||
| { | |||
| await SendAsync(VoiceOpCode.Identify, new IdentifyParams | |||
| { | |||
| GuildId = GuildId, | |||
| UserId = userId, | |||
| SessionId = sessionId, | |||
| Token = token | |||
| }); | |||
| } | |||
| public async Task SendSelectProtocol(string externalIp, int externalPort) | |||
| { | |||
| await SendAsync(VoiceOpCode.SelectProtocol, new SelectProtocolParams | |||
| { | |||
| Protocol = "udp", | |||
| Data = new UdpProtocolInfo | |||
| { | |||
| Address = externalIp, | |||
| Port = externalPort, | |||
| Mode = Mode | |||
| } | |||
| }); | |||
| } | |||
| public async Task SendSetSpeaking(bool value) | |||
| { | |||
| await SendAsync(VoiceOpCode.Speaking, new SpeakingParams | |||
| { | |||
| IsSpeaking = value, | |||
| Delay = 0 | |||
| }); | |||
| } | |||
| public async Task ConnectAsync(string url) | |||
| { | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| await ConnectInternalAsync(url).ConfigureAwait(false); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task ConnectInternalAsync(string url) | |||
| { | |||
| ConnectionState = ConnectionState.Connecting; | |||
| try | |||
| { | |||
| _connectCancelToken = new CancellationTokenSource(); | |||
| _webSocketClient.SetCancelToken(_connectCancelToken.Token); | |||
| await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); | |||
| _udpRecieveTask = ReceiveAsync(_connectCancelToken.Token); | |||
| ConnectionState = ConnectionState.Connected; | |||
| } | |||
| catch (Exception) | |||
| { | |||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||
| throw; | |||
| } | |||
| } | |||
| public async Task DisconnectAsync() | |||
| { | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task DisconnectInternalAsync() | |||
| { | |||
| if (ConnectionState == ConnectionState.Disconnected) return; | |||
| ConnectionState = ConnectionState.Disconnecting; | |||
| try { _connectCancelToken?.Cancel(false); } | |||
| catch { } | |||
| //Wait for tasks to complete | |||
| await _udpRecieveTask.ConfigureAwait(false); | |||
| await _webSocketClient.DisconnectAsync().ConfigureAwait(false); | |||
| ConnectionState = ConnectionState.Disconnected; | |||
| } | |||
| //Udp | |||
| public async Task SendDiscoveryAsync(uint ssrc) | |||
| { | |||
| var 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 SendAsync(packet, 70).ConfigureAwait(false); | |||
| } | |||
| public void SetUdpEndpoint(IPEndPoint endpoint) | |||
| { | |||
| _udpEndpoint = endpoint; | |||
| } | |||
| private async Task ReceiveAsync(CancellationToken cancelToken) | |||
| { | |||
| var closeTask = Task.Delay(-1, cancelToken); | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| var receiveTask = _udp.ReceiveAsync(); | |||
| var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); | |||
| if (task == closeTask) | |||
| break; | |||
| await _receivedPacketEvent.InvokeAsync(receiveTask.Result.Buffer).ConfigureAwait(false); | |||
| } | |||
| } | |||
| //Helpers | |||
| private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); | |||
| private string SerializeJson(object value) | |||
| { | |||
| var sb = new StringBuilder(256); | |||
| using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) | |||
| using (JsonWriter writer = new JsonTextWriter(text)) | |||
| _serializer.Serialize(writer, value); | |||
| return sb.ToString(); | |||
| } | |||
| private T DeserializeJson<T>(Stream jsonStream) | |||
| { | |||
| using (TextReader text = new StreamReader(jsonStream)) | |||
| using (JsonReader reader = new JsonTextReader(text)) | |||
| return _serializer.Deserialize<T>(reader); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class ExtendedGuild : Guild | |||
| { | |||
| [JsonProperty("unavailable")] | |||
| public bool? Unavailable { get; set; } | |||
| [JsonProperty("member_count")] | |||
| public int MemberCount { get; set; } | |||
| [JsonProperty("large")] | |||
| public bool Large { get; set; } | |||
| [JsonProperty("presences")] | |||
| public Presence[] Presences { get; set; } | |||
| [JsonProperty("members")] | |||
| public GuildMember[] Members { get; set; } | |||
| [JsonProperty("channels")] | |||
| public Channel[] Channels { get; set; } | |||
| [JsonProperty("joined_at")] | |||
| public DateTimeOffset JoinedAt { get; set; } | |||
| } | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public enum GatewayOpCodes : byte | |||
| public enum GatewayOpCode : byte | |||
| { | |||
| /// <summary> C←S - Used to send most events. </summary> | |||
| Dispatch = 0, | |||
| @@ -12,13 +12,21 @@ | |||
| StatusUpdate = 3, | |||
| /// <summary> C→S - Used to join a particular voice channel. </summary> | |||
| VoiceStateUpdate = 4, | |||
| /// <summary> C→S - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. </summary> | |||
| /// <summary> C→S - Used to ensure the guild's voice server is alive. </summary> | |||
| VoiceServerPing = 5, | |||
| /// <summary> C→S - Used to resume a connection after a redirect occurs. </summary> | |||
| Resume = 6, | |||
| /// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary> | |||
| Reconnect = 7, | |||
| /// <summary> C→S - Used to request all members that were withheld by large_threshold </summary> | |||
| RequestGuildMembers = 8 | |||
| /// <summary> C→S - Used to request members that were withheld by large_threshold </summary> | |||
| RequestGuildMembers = 8, | |||
| /// <summary> C←S - Used to notify the client that their session has expired and cannot be resumed. </summary> | |||
| InvalidSession = 9, | |||
| /// <summary> C←S - Used to provide information to the client immediately on connection. </summary> | |||
| Hello = 10, | |||
| /// <summary> C←S - Used to reply to a client's heartbeat. </summary> | |||
| HeartbeatAck = 11, | |||
| /// <summary> C→S - Used to request presence updates from particular guilds. </summary> | |||
| GuildSync = 12 | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class GuildBanEvent | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId { get; set; } | |||
| [JsonProperty("user")] | |||
| public User User { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class GuildEmojiUpdateEvent | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId; | |||
| [JsonProperty("emojis")] | |||
| public Emoji[] Emojis; | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class GuildMemberAddEvent : GuildMember | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class GuildMemberRemoveEvent | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId { get; set; } | |||
| [JsonProperty("user")] | |||
| public User User { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class GuildMemberUpdateEvent : GuildMember | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId { get; set; } | |||
| } | |||
| } | |||
| @@ -7,6 +7,6 @@ namespace Discord.API.Gateway | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId { get; set; } | |||
| [JsonProperty("role")] | |||
| public Role Data { get; set; } | |||
| public Role Role { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class GuildRoleDeleteEvent | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId { get; set; } | |||
| [JsonProperty("role_id")] | |||
| public ulong RoleId { get; set; } | |||
| } | |||
| } | |||
| @@ -7,6 +7,6 @@ namespace Discord.API.Gateway | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId { get; set; } | |||
| [JsonProperty("role")] | |||
| public Role Data { get; set; } | |||
| public Role Role { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class GuildSyncEvent | |||
| { | |||
| [JsonProperty("id")] | |||
| public ulong Id { get; set; } | |||
| [JsonProperty("large")] | |||
| public bool Large { get; set; } | |||
| [JsonProperty("presences")] | |||
| public Presence[] Presences { get; set; } | |||
| [JsonProperty("members")] | |||
| public GuildMember[] Members { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class HelloEvent | |||
| { | |||
| [JsonProperty("heartbeat_interval")] | |||
| public int HeartbeatInterval { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class MessageDeleteBulkEvent | |||
| { | |||
| [JsonProperty("channel_id")] | |||
| public ulong ChannelId { get; set; } | |||
| [JsonProperty("ids")] | |||
| public IEnumerable<ulong> Ids { get; set; } | |||
| } | |||
| } | |||
| @@ -23,18 +23,15 @@ namespace Discord.API.Gateway | |||
| [JsonProperty("read_state")] | |||
| public ReadState[] ReadStates { get; set; } | |||
| [JsonProperty("guilds")] | |||
| public Guild[] Guilds { get; set; } | |||
| public ExtendedGuild[] Guilds { get; set; } | |||
| [JsonProperty("private_channels")] | |||
| public Channel[] PrivateChannels { get; set; } | |||
| [JsonProperty("heartbeat_interval")] | |||
| public int HeartbeatInterval { get; set; } | |||
| [JsonProperty("relationships")] | |||
| public Relationship[] Relationships { get; set; } | |||
| //Ignored | |||
| [JsonProperty("user_settings")] | |||
| public object UserSettings { get; set; } | |||
| /*[JsonProperty("user_settings")] | |||
| [JsonProperty("user_guild_settings")] | |||
| public object UserGuildSettings { get; set; } | |||
| [JsonProperty("tutorial")] | |||
| public object Tutorial { get; set; } | |||
| [JsonProperty("tutorial")]*/ | |||
| } | |||
| } | |||
| @@ -1,14 +1,19 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class RequestMembersParams | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong[] GuildId { get; set; } | |||
| [JsonProperty("query")] | |||
| public string Query { get; set; } | |||
| [JsonProperty("limit")] | |||
| public int Limit { get; set; } | |||
| [JsonProperty("guild_id")] | |||
| public IEnumerable<ulong> GuildIds { get; set; } | |||
| [JsonIgnore] | |||
| public IEnumerable<IGuild> Guilds { set { GuildIds = value.Select(x => x.Id); } } | |||
| } | |||
| } | |||
| @@ -7,6 +7,6 @@ namespace Discord.API.Gateway | |||
| [JsonProperty("session_id")] | |||
| public string SessionId { get; set; } | |||
| [JsonProperty("seq")] | |||
| public uint Sequence { get; set; } | |||
| public int Sequence { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class StatusUpdateParams | |||
| { | |||
| [JsonProperty("idle_since"), Int53] | |||
| public long? IdleSince { get; set; } | |||
| [JsonProperty("game")] | |||
| public Game Game { get; set; } | |||
| } | |||
| } | |||
| @@ -1,16 +0,0 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class UpdateVoiceParams | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong? GuildId { get; set; } | |||
| [JsonProperty("channel_id")] | |||
| public ulong? ChannelId { get; set; } | |||
| [JsonProperty("self_mute")] | |||
| public bool IsSelfMuted { get; set; } | |||
| [JsonProperty("self_deaf")] | |||
| public bool IsSelfDeafened { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class VoiceStateUpdateParams | |||
| { | |||
| [JsonProperty("self_mute")] | |||
| public bool SelfMute { get; set; } | |||
| [JsonProperty("self_deaf")] | |||
| public bool SelfDeaf { get; set; } | |||
| [JsonProperty("guild_id")] | |||
| public ulong GuildId { get; set; } | |||
| [JsonIgnore] | |||
| public IGuild Guild { set { GuildId = value.Id; } } | |||
| [JsonProperty("channel_id")] | |||
| public ulong? ChannelId { get; set; } | |||
| [JsonIgnore] | |||
| public IChannel Channel { set { ChannelId = value?.Id; } } | |||
| } | |||
| } | |||
| @@ -1,8 +0,0 @@ | |||
| namespace Discord.API | |||
| { | |||
| public interface IOptional | |||
| { | |||
| object Value { get; } | |||
| bool IsSpecified { get; } | |||
| } | |||
| } | |||
| @@ -6,5 +6,7 @@ namespace Discord.API.Rest | |||
| { | |||
| [JsonProperty("recipient_id")] | |||
| public ulong RecipientId { get; set; } | |||
| [JsonIgnore] | |||
| public IUser Recipient { set { RecipientId = value.Id; } } | |||
| } | |||
| } | |||
| @@ -1,11 +1,14 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord.API.Rest | |||
| { | |||
| public class DeleteMessagesParam | |||
| public class DeleteMessagesParams | |||
| { | |||
| [JsonProperty("messages")] | |||
| public IEnumerable<ulong> MessageIds { get; set; } | |||
| [JsonIgnore] | |||
| public IEnumerable<IMessage> Messages { set { MessageIds = value.Select(x => x.Id); } } | |||
| } | |||
| } | |||
| @@ -6,5 +6,6 @@ | |||
| public Direction RelativeDirection { get; set; } = Direction.Before; | |||
| public Optional<ulong> RelativeMessageId { get; set; } | |||
| public Optional<IMessage> RelativeMessage { set { RelativeMessageId = value.IsSpecified ? value.Value.Id : Optional.Create<ulong>(); } } | |||
| } | |||
| } | |||
| @@ -1,12 +0,0 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Rest | |||
| { | |||
| public class LoginParams | |||
| { | |||
| [JsonProperty("email")] | |||
| public string Email { get; set; } | |||
| [JsonProperty("password")] | |||
| public string Password { get; set; } | |||
| } | |||
| } | |||
| @@ -1,10 +0,0 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Rest | |||
| { | |||
| public class LoginResponse | |||
| { | |||
| [JsonProperty("token")] | |||
| public string Token { get; set; } | |||
| } | |||
| } | |||
| @@ -1,5 +1,4 @@ | |||
| using Discord.Net.Converters; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json; | |||
| using System.IO; | |||
| namespace Discord.API.Rest | |||
| @@ -8,12 +7,6 @@ namespace Discord.API.Rest | |||
| { | |||
| [JsonProperty("username")] | |||
| public Optional<string> Username { get; set; } | |||
| [JsonProperty("email")] | |||
| public Optional<string> Email { get; set; } | |||
| [JsonProperty("password")] | |||
| public Optional<string> Password { get; set; } | |||
| [JsonProperty("new_password")] | |||
| public Optional<string> NewPassword { get; set; } | |||
| [JsonProperty("avatar"), Image] | |||
| public Optional<Stream> Avatar { get; set; } | |||
| } | |||
| @@ -1,5 +1,4 @@ | |||
| using Discord.Net.Converters; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Rest | |||
| { | |||
| @@ -7,7 +6,10 @@ namespace Discord.API.Rest | |||
| { | |||
| [JsonProperty("enabled")] | |||
| public Optional<bool> Enabled { get; set; } | |||
| [JsonProperty("channel")] | |||
| public Optional<IVoiceChannel> Channel { get; set; } | |||
| public Optional<ulong> ChannelId { get; set; } | |||
| [JsonIgnore] | |||
| public Optional<IVoiceChannel> Channel { set { ChannelId = value.IsSpecified ? value.Value.Id : Optional.Create<ulong>(); } } | |||
| } | |||
| } | |||
| @@ -1,18 +1,26 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord.API.Rest | |||
| { | |||
| public class ModifyGuildMemberParams | |||
| { | |||
| [JsonProperty("roles")] | |||
| public Optional<ulong[]> Roles { get; set; } | |||
| [JsonProperty("mute")] | |||
| public Optional<bool> Mute { get; set; } | |||
| [JsonProperty("deaf")] | |||
| public Optional<bool> Deaf { get; set; } | |||
| [JsonProperty("nick")] | |||
| public Optional<string> Nickname { get; set; } | |||
| [JsonProperty("roles")] | |||
| public Optional<IEnumerable<ulong>> RoleIds { get; set; } | |||
| [JsonIgnore] | |||
| public Optional<IEnumerable<IRole>> Roles { set { RoleIds = value.IsSpecified ? Optional.Create(value.Value.Select(x => x.Id)) : Optional.Create<IEnumerable<ulong>>(); } } | |||
| [JsonProperty("channel_id")] | |||
| public Optional<IVoiceChannel> VoiceChannel { get; set; } | |||
| public Optional<ulong> VoiceChannelId { get; set; } | |||
| [JsonIgnore] | |||
| public Optional<IVoiceChannel> VoiceChannel { set { VoiceChannelId = value.IsSpecified ? value.Value.Id : Optional.Create<ulong>(); } } | |||
| } | |||
| } | |||
| @@ -1,5 +1,4 @@ | |||
| using Discord.Net.Converters; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json; | |||
| using System.IO; | |||
| namespace Discord.API.Rest | |||
| @@ -11,16 +10,24 @@ namespace Discord.API.Rest | |||
| [JsonProperty("region")] | |||
| public Optional<IVoiceRegion> Region { get; set; } | |||
| [JsonProperty("verification_level")] | |||
| public Optional<int> VerificationLevel { get; set; } | |||
| [JsonProperty("afk_channel_id")] | |||
| public Optional<ulong?> AFKChannelId { get; set; } | |||
| public Optional<VerificationLevel> VerificationLevel { get; set; } | |||
| [JsonProperty("default_message_notifications")] | |||
| public Optional<DefaultMessageNotifications> DefaultMessageNotifications { get; set; } | |||
| [JsonProperty("afk_timeout")] | |||
| public Optional<int> AFKTimeout { get; set; } | |||
| [JsonProperty("icon"), Image] | |||
| public Optional<Stream> Icon { get; set; } | |||
| [JsonProperty("owner_id")] | |||
| public Optional<GuildMember> Owner { get; set; } | |||
| [JsonProperty("splash"), Image] | |||
| public Optional<Stream> Splash { get; set; } | |||
| [JsonProperty("afk_channel_id")] | |||
| public Optional<ulong?> AFKChannelId { get; set; } | |||
| [JsonIgnore] | |||
| public Optional<IVoiceChannel> AFKChannel { set { OwnerId = value.IsSpecified ? value.Value.Id : Optional.Create<ulong>(); } } | |||
| [JsonProperty("owner_id")] | |||
| public Optional<ulong> OwnerId { get; set; } | |||
| [JsonIgnore] | |||
| public Optional<IGuildUser> Owner { set { OwnerId = value.IsSpecified ? value.Value.Id : Optional.Create<ulong>(); } } | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| namespace Discord.API.Rest | |||
| { | |||
| public class ModifyPresenceParams | |||
| { | |||
| public Optional<UserStatus> Status { get; set; } | |||
| public Optional<Discord.Game> Game { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class IdentifyParams | |||
| { | |||
| [JsonProperty("server_id")] | |||
| public ulong GuildId { get; set; } | |||
| [JsonProperty("user_id")] | |||
| public ulong UserId { get; set; } | |||
| [JsonProperty("session_id")] | |||
| public string SessionId { get; set; } | |||
| [JsonProperty("token")] | |||
| public string Token { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class ReadyEvent | |||
| { | |||
| [JsonProperty("ssrc")] | |||
| public uint SSRC { get; set; } | |||
| [JsonProperty("port")] | |||
| public ushort Port { get; set; } | |||
| [JsonProperty("modes")] | |||
| public string[] Modes { get; set; } | |||
| [JsonProperty("heartbeat_interval")] | |||
| public int HeartbeatInterval { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class SelectProtocolParams | |||
| { | |||
| [JsonProperty("protocol")] | |||
| public string Protocol { get; set; } | |||
| [JsonProperty("data")] | |||
| public UdpProtocolInfo Data { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class SessionDescriptionEvent | |||
| { | |||
| [JsonProperty("secret_key")] | |||
| public byte[] SecretKey { get; set; } | |||
| [JsonProperty("mode")] | |||
| public string Mode { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class SpeakingParams | |||
| { | |||
| [JsonProperty("speaking")] | |||
| public bool IsSpeaking { get; set; } | |||
| [JsonProperty("delay")] | |||
| public int Delay { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class UdpProtocolInfo | |||
| { | |||
| [JsonProperty("address")] | |||
| public string Address { get; set; } | |||
| [JsonProperty("port")] | |||
| public int Port { get; set; } | |||
| [JsonProperty("mode")] | |||
| public string Mode { get; set; } | |||
| } | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| namespace Discord.API.Gateway | |||
| namespace Discord.API.Voice | |||
| { | |||
| public enum VoiceOpCodes : byte | |||
| public enum VoiceOpCode : byte | |||
| { | |||
| /// <summary> C→S - Used to associate a connection with a token. </summary> | |||
| Identify = 0, | |||
| @@ -8,8 +8,10 @@ | |||
| SelectProtocol = 1, | |||
| /// <summary> C←S - Used to notify that the voice connection was successful and informs the client of available protocols. </summary> | |||
| Ready = 2, | |||
| /// <summary> C↔S - Used to keep the connection alive and measure latency. </summary> | |||
| /// <summary> C→S - Used to keep the connection alive and measure latency. </summary> | |||
| Heartbeat = 3, | |||
| /// <summary> C←S - Used to reply to a client's heartbeat. </summary> | |||
| HeartbeatAck = 3, | |||
| /// <summary> C←S - Used to provide an encryption key to the client. </summary> | |||
| SessionDescription = 4, | |||
| /// <summary> C↔S - Used to inform that a certain user is speaking. </summary> | |||
| @@ -9,7 +9,7 @@ namespace Discord.API | |||
| [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] | |||
| public string Type { get; set; } | |||
| [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] | |||
| public uint? Sequence { get; set; } | |||
| public int? Sequence { get; set; } | |||
| [JsonProperty("d")] | |||
| public object Payload { get; set; } | |||
| } | |||
| @@ -0,0 +1,330 @@ | |||
| using Discord.API.Voice; | |||
| using Discord.Logging; | |||
| using Discord.Net.Converters; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Text; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Audio | |||
| { | |||
| internal class AudioClient : IAudioClient, IDisposable | |||
| { | |||
| public const int SampleRate = 48000; | |||
| public event Func<Task> Connected | |||
| { | |||
| add { _connectedEvent.Add(value); } | |||
| remove { _connectedEvent.Remove(value); } | |||
| } | |||
| private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | |||
| public event Func<Exception, Task> Disconnected | |||
| { | |||
| add { _disconnectedEvent.Add(value); } | |||
| remove { _disconnectedEvent.Remove(value); } | |||
| } | |||
| private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||
| public event Func<int, int, Task> LatencyUpdated | |||
| { | |||
| add { _latencyUpdatedEvent.Add(value); } | |||
| remove { _latencyUpdatedEvent.Remove(value); } | |||
| } | |||
| private readonly AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>(); | |||
| private readonly ILogger _audioLogger; | |||
| #if BENCHMARK | |||
| private readonly ILogger _benchmarkLogger; | |||
| #endif | |||
| internal readonly SemaphoreSlim _connectionLock; | |||
| private readonly JsonSerializer _serializer; | |||
| private TaskCompletionSource<bool> _connectTask; | |||
| private CancellationTokenSource _cancelToken; | |||
| private Task _heartbeatTask; | |||
| private long _heartbeatTime; | |||
| private string _url; | |||
| private bool _isDisposed; | |||
| private uint _ssrc; | |||
| private byte[] _secretKey; | |||
| public CachedGuild Guild { get; } | |||
| public DiscordVoiceAPIClient ApiClient { get; private set; } | |||
| public ConnectionState ConnectionState { get; private set; } | |||
| public int Latency { get; private set; } | |||
| private DiscordSocketClient Discord => Guild.Discord; | |||
| /// <summary> Creates a new REST/WebSocket discord client. </summary> | |||
| public AudioClient(CachedGuild guild, int id) | |||
| { | |||
| Guild = guild; | |||
| _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); | |||
| #if BENCHMARK | |||
| _benchmarkLogger = logManager.CreateLogger("Benchmark"); | |||
| #endif | |||
| _connectionLock = new SemaphoreSlim(1, 1); | |||
| _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
| _serializer.Error += (s, e) => | |||
| { | |||
| _audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); | |||
| e.ErrorContext.Handled = true; | |||
| }; | |||
| ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider); | |||
| ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); | |||
| ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false); | |||
| ApiClient.ReceivedEvent += ProcessMessageAsync; | |||
| ApiClient.ReceivedPacket += ProcessPacketAsync; | |||
| ApiClient.Disconnected += async ex => | |||
| { | |||
| if (ex != null) | |||
| await _audioLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | |||
| else | |||
| await _audioLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); | |||
| }; | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) | |||
| { | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) | |||
| { | |||
| var state = ConnectionState; | |||
| if (state == ConnectionState.Connecting || state == ConnectionState.Connected) | |||
| await DisconnectInternalAsync(null).ConfigureAwait(false); | |||
| ConnectionState = ConnectionState.Connecting; | |||
| await _audioLogger.InfoAsync("Connecting").ConfigureAwait(false); | |||
| try | |||
| { | |||
| _url = url; | |||
| _connectTask = new TaskCompletionSource<bool>(); | |||
| _cancelToken = new CancellationTokenSource(); | |||
| await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false); | |||
| await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false); | |||
| await _connectTask.Task.ConfigureAwait(false); | |||
| await _connectedEvent.InvokeAsync().ConfigureAwait(false); | |||
| ConnectionState = ConnectionState.Connected; | |||
| await _audioLogger.InfoAsync("Connected").ConfigureAwait(false); | |||
| } | |||
| catch (Exception) | |||
| { | |||
| await DisconnectInternalAsync(null).ConfigureAwait(false); | |||
| throw; | |||
| } | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task DisconnectAsync() | |||
| { | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| await DisconnectInternalAsync(null).ConfigureAwait(false); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task DisconnectAsync(Exception ex) | |||
| { | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| await DisconnectInternalAsync(ex).ConfigureAwait(false); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task DisconnectInternalAsync(Exception ex) | |||
| { | |||
| if (ConnectionState == ConnectionState.Disconnected) return; | |||
| ConnectionState = ConnectionState.Disconnecting; | |||
| await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false); | |||
| //Signal tasks to complete | |||
| try { _cancelToken.Cancel(); } catch { } | |||
| //Disconnect from server | |||
| await ApiClient.DisconnectAsync().ConfigureAwait(false); | |||
| //Wait for tasks to complete | |||
| var heartbeatTask = _heartbeatTask; | |||
| if (heartbeatTask != null) | |||
| await heartbeatTask.ConfigureAwait(false); | |||
| _heartbeatTask = null; | |||
| ConnectionState = ConnectionState.Disconnected; | |||
| await _audioLogger.InfoAsync("Disconnected").ConfigureAwait(false); | |||
| await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | |||
| } | |||
| public void Send(byte[] data, int count) | |||
| { | |||
| //TODO: Queue these? | |||
| ApiClient.SendAsync(data, count).ConfigureAwait(false); | |||
| } | |||
| public RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) | |||
| { | |||
| return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); | |||
| } | |||
| public OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int channels = 2, | |||
| OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) | |||
| { | |||
| return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, SampleRate, bitrate, channels, application, bufferSize); | |||
| } | |||
| private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) | |||
| { | |||
| #if BENCHMARK | |||
| Stopwatch stopwatch = Stopwatch.StartNew(); | |||
| try | |||
| { | |||
| #endif | |||
| try | |||
| { | |||
| switch (opCode) | |||
| { | |||
| case VoiceOpCode.Ready: | |||
| { | |||
| await _audioLogger.DebugAsync("Received Ready").ConfigureAwait(false); | |||
| var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
| _ssrc = data.SSRC; | |||
| if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) | |||
| throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); | |||
| _heartbeatTime = 0; | |||
| _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); | |||
| var entry = await Dns.GetHostEntryAsync(_url).ConfigureAwait(false); | |||
| ApiClient.SetUdpEndpoint(new IPEndPoint(entry.AddressList[0], data.Port)); | |||
| await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); | |||
| } | |||
| break; | |||
| case VoiceOpCode.SessionDescription: | |||
| { | |||
| await _audioLogger.DebugAsync("Received SessionDescription").ConfigureAwait(false); | |||
| var data = (payload as JToken).ToObject<SessionDescriptionEvent>(_serializer); | |||
| if (data.Mode != DiscordVoiceAPIClient.Mode) | |||
| throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); | |||
| _secretKey = data.SecretKey; | |||
| await ApiClient.SendSetSpeaking(true).ConfigureAwait(false); | |||
| _connectTask.TrySetResult(true); | |||
| } | |||
| break; | |||
| case VoiceOpCode.HeartbeatAck: | |||
| { | |||
| await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); | |||
| var heartbeatTime = _heartbeatTime; | |||
| if (heartbeatTime != 0) | |||
| { | |||
| int latency = (int)(Environment.TickCount - _heartbeatTime); | |||
| _heartbeatTime = 0; | |||
| await _audioLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); | |||
| int before = Latency; | |||
| Latency = latency; | |||
| await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); | |||
| } | |||
| } | |||
| break; | |||
| default: | |||
| await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); | |||
| return; | |||
| } | |||
| #if BENCHMARK | |||
| } | |||
| finally | |||
| { | |||
| stopwatch.Stop(); | |||
| double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); | |||
| await _benchmarkLogger.DebugAsync($"{millis} ms").ConfigureAwait(false); | |||
| } | |||
| #endif | |||
| } | |||
| private async Task ProcessPacketAsync(byte[] packet) | |||
| { | |||
| if (!_connectTask.Task.IsCompleted) | |||
| { | |||
| if (packet.Length == 70) | |||
| { | |||
| string ip; | |||
| int port; | |||
| try | |||
| { | |||
| ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); | |||
| port = packet[68] | packet[69] << 8; | |||
| } | |||
| catch { return; } | |||
| await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); | |||
| await ApiClient.SendSelectProtocol(ip, port); | |||
| } | |||
| } | |||
| } | |||
| private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) | |||
| { | |||
| //Clean this up when Discord's session patch is live | |||
| try | |||
| { | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); | |||
| if (_heartbeatTime != 0) //Server never responded to our last heartbeat | |||
| { | |||
| if (ConnectionState == ConnectionState.Connected) | |||
| { | |||
| await _audioLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | |||
| await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| else | |||
| _heartbeatTime = Environment.TickCount; | |||
| await ApiClient.SendHeartbeatAsync().ConfigureAwait(false); | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| } | |||
| internal virtual void Dispose(bool disposing) | |||
| { | |||
| if (!_isDisposed) | |||
| _isDisposed = true; | |||
| ApiClient.Dispose(); | |||
| } | |||
| /// <inheritdoc /> | |||
| public void Dispose() => Dispose(true); | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| using System; | |||
| namespace Discord.Audio | |||
| { | |||
| [Flags] | |||
| public enum AudioMode : byte | |||
| { | |||
| Disabled = 0, | |||
| Outgoing = 1, | |||
| Incoming = 2, | |||
| Both = Outgoing | Incoming | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Audio | |||
| { | |||
| public interface IAudioClient | |||
| { | |||
| event Func<Task> Connected; | |||
| event Func<Exception, Task> Disconnected; | |||
| event Func<int, int, Task> LatencyUpdated; | |||
| DiscordVoiceAPIClient ApiClient { get; } | |||
| /// <summary> Gets the current connection state of this client. </summary> | |||
| ConnectionState ConnectionState { get; } | |||
| /// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary> | |||
| int Latency { get; } | |||
| Task DisconnectAsync(); | |||
| RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000); | |||
| OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int channels = 2, | |||
| OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000); | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| namespace Discord.Audio | |||
| { | |||
| public enum OpusApplication : int | |||
| { | |||
| Voice = 2048, | |||
| MusicOrMixed = 2049, | |||
| LowLatency = 2051 | |||
| } | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| using System; | |||
| namespace Discord.Audio | |||
| { | |||
| internal abstract class OpusConverter : IDisposable | |||
| { | |||
| protected IntPtr _ptr; | |||
| /// <summary> Gets the bit rate of this converter. </summary> | |||
| public const int BitsPerSample = 16; | |||
| /// <summary> Gets the bytes per sample. </summary> | |||
| public const int SampleSize = (BitsPerSample / 8) * MaxChannels; | |||
| /// <summary> Gets the maximum amount of channels this encoder supports. </summary> | |||
| public const int MaxChannels = 2; | |||
| /// <summary> Gets the input sampling rate of this converter. </summary> | |||
| public int SamplingRate { get; } | |||
| /// <summary> Gets the number of samples per second for this stream. </summary> | |||
| public int Channels { get; } | |||
| protected OpusConverter(int samplingRate, int channels) | |||
| { | |||
| 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)); | |||
| SamplingRate = samplingRate; | |||
| Channels = channels; | |||
| } | |||
| 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| namespace Discord.Audio | |||
| { | |||
| internal enum OpusCtl : int | |||
| { | |||
| SetBitrateRequest = 4002, | |||
| GetBitrateRequest = 4003, | |||
| SetInbandFECRequest = 4012, | |||
| GetInbandFECRequest = 4013 | |||
| } | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| using System; | |||
| using System.Runtime.InteropServices; | |||
| namespace Discord.Audio | |||
| { | |||
| internal unsafe class OpusDecoder : OpusConverter | |||
| { | |||
| [DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)] | |||
| private static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error); | |||
| [DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] | |||
| private static extern void DestroyDecoder(IntPtr decoder); | |||
| [DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] | |||
| private static extern int Decode(IntPtr st, byte* data, int len, byte* pcm, int max_frame_size, int decode_fec); | |||
| public OpusDecoder(int samplingRate, int channels) | |||
| : base(samplingRate, channels) | |||
| { | |||
| OpusError error; | |||
| _ptr = 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 outputOffset) | |||
| { | |||
| int result = 0; | |||
| fixed (byte* inPtr = input) | |||
| fixed (byte* outPtr = output) | |||
| result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize / MaxChannels, 0); | |||
| if (result < 0) | |||
| throw new Exception(((OpusError)result).ToString()); | |||
| return result; | |||
| } | |||
| protected override void Dispose(bool disposing) | |||
| { | |||
| if (_ptr != IntPtr.Zero) | |||
| { | |||
| DestroyDecoder(_ptr); | |||
| _ptr = IntPtr.Zero; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,77 @@ | |||
| using System; | |||
| using System.Runtime.InteropServices; | |||
| namespace Discord.Audio | |||
| { | |||
| internal unsafe class OpusEncoder : OpusConverter | |||
| { | |||
| [DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)] | |||
| private static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error); | |||
| [DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] | |||
| private static extern void DestroyEncoder(IntPtr encoder); | |||
| [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] | |||
| private 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)] | |||
| private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); | |||
| /// <summary> Gets the coding mode of the encoder. </summary> | |||
| public OpusApplication Application { get; } | |||
| public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed) | |||
| : base(samplingRate, channels) | |||
| { | |||
| Application = application; | |||
| OpusError error; | |||
| _ptr = CreateEncoder(samplingRate, channels, (int)application, out error); | |||
| if (error != OpusError.OK) | |||
| throw new InvalidOperationException($"Error occured while creating encoder: {error}"); | |||
| } | |||
| /// <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, int inputCount, byte[] output, int outputOffset) | |||
| { | |||
| int result = 0; | |||
| fixed (byte* inPtr = input) | |||
| fixed (byte* outPtr = output) | |||
| result = Encode(_ptr, inPtr + inputOffset, inputCount / SampleSize, outPtr + outputOffset, output.Length - outputOffset); | |||
| 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 = EncoderCtl(_ptr, OpusCtl.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) | |||
| { | |||
| if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate) | |||
| throw new ArgumentOutOfRangeException(nameof(value)); | |||
| var result = EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value * 1000); | |||
| if (result < 0) | |||
| throw new Exception(((OpusError)result).ToString()); | |||
| } | |||
| protected override void Dispose(bool disposing) | |||
| { | |||
| if (_ptr != IntPtr.Zero) | |||
| { | |||
| DestroyEncoder(_ptr); | |||
| _ptr = IntPtr.Zero; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| namespace Discord.Audio | |||
| { | |||
| internal enum OpusError : int | |||
| { | |||
| OK = 0, | |||
| BadArg = -1, | |||
| BufferToSmall = -2, | |||
| InternalError = -3, | |||
| InvalidPacket = -4, | |||
| Unimplemented = -5, | |||
| InvalidState = -6, | |||
| AllocFail = -7 | |||
| } | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| using System.Runtime.InteropServices; | |||
| namespace Discord.Audio | |||
| { | |||
| public unsafe static class SecretBox | |||
| { | |||
| [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] | |||
| private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); | |||
| [DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] | |||
| private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); | |||
| public static int Encrypt(byte[] input, int inputOffset, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) | |||
| { | |||
| fixed (byte* inPtr = input) | |||
| fixed (byte* outPtr = output) | |||
| return SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); | |||
| } | |||
| public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) | |||
| { | |||
| fixed (byte* inPtr = input) | |||
| fixed (byte* outPtr = output) | |||
| return SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); | |||
| } | |||
| } | |||
| } | |||