Browse Source

Merge branch 'dev'

Conflicts:
	Discord.Net.sln
	README.md
pull/62/head
Christopher F 9 years ago
parent
commit
34196bcc79
100 changed files with 3036 additions and 435 deletions
  1. +6
    -6
      Discord.Net.sln
  2. +11
    -15
      README.md
  3. +1
    -4
      global.json
  4. +19
    -0
      src/Discord.Net.Commands/Attributes/CommandAttribute.cs
  5. +14
    -0
      src/Discord.Net.Commands/Attributes/DescriptionAttribute.cs
  6. +18
    -0
      src/Discord.Net.Commands/Attributes/GroupAttribute.cs
  7. +18
    -0
      src/Discord.Net.Commands/Attributes/ModuleAttribute.cs
  8. +9
    -0
      src/Discord.Net.Commands/Attributes/UnparsedAttribute.cs
  9. +124
    -0
      src/Discord.Net.Commands/Command.cs
  10. +20
    -0
      src/Discord.Net.Commands/CommandError.cs
  11. +35
    -0
      src/Discord.Net.Commands/CommandParameter.cs
  12. +144
    -0
      src/Discord.Net.Commands/CommandParser.cs
  13. +260
    -0
      src/Discord.Net.Commands/CommandService.cs
  14. +19
    -0
      src/Discord.Net.Commands/Discord.Net.Commands.xproj
  15. +44
    -0
      src/Discord.Net.Commands/Extensions/MessageExtensions.cs
  16. +76
    -0
      src/Discord.Net.Commands/Map/CommandMap.cs
  17. +95
    -0
      src/Discord.Net.Commands/Map/CommandMapNode.cs
  18. +54
    -0
      src/Discord.Net.Commands/Module.cs
  19. +48
    -0
      src/Discord.Net.Commands/Readers/ChannelTypeReader.cs
  20. +17
    -0
      src/Discord.Net.Commands/Readers/GenericTypeReader.cs
  21. +24
    -0
      src/Discord.Net.Commands/Readers/MessageTypeReader.cs
  22. +36
    -0
      src/Discord.Net.Commands/Readers/RoleTypeReader.cs
  23. +9
    -0
      src/Discord.Net.Commands/Readers/TypeReader.cs
  24. +62
    -0
      src/Discord.Net.Commands/Readers/UserTypeReader.cs
  25. +24
    -0
      src/Discord.Net.Commands/ReflectionUtils.cs
  26. +35
    -0
      src/Discord.Net.Commands/Results/ExecuteResult.cs
  27. +9
    -0
      src/Discord.Net.Commands/Results/IResult.cs
  28. +35
    -0
      src/Discord.Net.Commands/Results/ParseResult.cs
  29. +33
    -0
      src/Discord.Net.Commands/Results/SearchResult.cs
  30. +31
    -0
      src/Discord.Net.Commands/Results/TypeReaderResult.cs
  31. +34
    -0
      src/Discord.Net.Commands/project.json
  32. +2
    -2
      src/Discord.Net/API/Common/Attachment.cs
  33. +9
    -9
      src/Discord.Net/API/Common/Channel.cs
  34. +1
    -1
      src/Discord.Net/API/Common/Connection.cs
  35. +2
    -2
      src/Discord.Net/API/Common/Embed.cs
  36. +2
    -2
      src/Discord.Net/API/Common/EmbedThumbnail.cs
  37. +2
    -2
      src/Discord.Net/API/Common/Game.cs
  38. +5
    -1
      src/Discord.Net/API/Common/Guild.cs
  39. +1
    -1
      src/Discord.Net/API/Common/GuildEmbed.cs
  40. +2
    -2
      src/Discord.Net/API/Common/GuildMember.cs
  41. +1
    -1
      src/Discord.Net/API/Common/Integration.cs
  42. +1
    -1
      src/Discord.Net/API/Common/InviteMetadata.cs
  43. +11
    -11
      src/Discord.Net/API/Common/Message.cs
  44. +21
    -0
      src/Discord.Net/API/Common/Presence.cs
  45. +1
    -1
      src/Discord.Net/API/Common/ReadState.cs
  46. +14
    -0
      src/Discord.Net/API/Common/Relationship.cs
  47. +9
    -0
      src/Discord.Net/API/Common/RelationshipType.cs
  48. +5
    -5
      src/Discord.Net/API/Common/Role.cs
  49. +8
    -4
      src/Discord.Net/API/Common/User.cs
  50. +1
    -1
      src/Discord.Net/API/Common/VoiceState.cs
  51. +392
    -275
      src/Discord.Net/API/DiscordAPIClient.cs
  52. +254
    -0
      src/Discord.Net/API/DiscordVoiceAPIClient.cs
  53. +24
    -0
      src/Discord.Net/API/Gateway/ExtendedGuild.cs
  54. +12
    -4
      src/Discord.Net/API/Gateway/GatewayOpCode.cs
  55. +12
    -0
      src/Discord.Net/API/Gateway/GuildBanEvent.cs
  56. +12
    -0
      src/Discord.Net/API/Gateway/GuildEmojiUpdateEvent.cs
  57. +10
    -0
      src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs
  58. +12
    -0
      src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs
  59. +10
    -0
      src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs
  60. +1
    -1
      src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs
  61. +12
    -0
      src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs
  62. +1
    -1
      src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs
  63. +17
    -0
      src/Discord.Net/API/Gateway/GuildSyncEvent.cs
  64. +10
    -0
      src/Discord.Net/API/Gateway/HelloEvent.cs
  65. +13
    -0
      src/Discord.Net/API/Gateway/MessageDeleteBulkEvent.cs
  66. +5
    -8
      src/Discord.Net/API/Gateway/ReadyEvent.cs
  67. +7
    -2
      src/Discord.Net/API/Gateway/RequestMembersParams.cs
  68. +1
    -1
      src/Discord.Net/API/Gateway/ResumeParams.cs
  69. +12
    -0
      src/Discord.Net/API/Gateway/StatusUpdateParams.cs
  70. +0
    -16
      src/Discord.Net/API/Gateway/UpdateVoiceParams.cs
  71. +21
    -0
      src/Discord.Net/API/Gateway/VoiceStateUpdateParams.cs
  72. +0
    -8
      src/Discord.Net/API/IOptional.cs
  73. +2
    -0
      src/Discord.Net/API/Rest/CreateDMChannelParams.cs
  74. +4
    -1
      src/Discord.Net/API/Rest/DeleteMessagesParams.cs
  75. +1
    -0
      src/Discord.Net/API/Rest/GetChannelMessagesParams.cs
  76. +0
    -12
      src/Discord.Net/API/Rest/LoginParams.cs
  77. +0
    -10
      src/Discord.Net/API/Rest/LoginResponse.cs
  78. +1
    -8
      src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs
  79. +5
    -3
      src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs
  80. +11
    -3
      src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs
  81. +14
    -7
      src/Discord.Net/API/Rest/ModifyGuildParams.cs
  82. +8
    -0
      src/Discord.Net/API/Rest/ModifyPresenceParams.cs
  83. +16
    -0
      src/Discord.Net/API/Voice/IdentifyParams.cs
  84. +16
    -0
      src/Discord.Net/API/Voice/ReadyEvent.cs
  85. +12
    -0
      src/Discord.Net/API/Voice/SelectProtocolParams.cs
  86. +12
    -0
      src/Discord.Net/API/Voice/SessionDescriptionEvent.cs
  87. +12
    -0
      src/Discord.Net/API/Voice/SpeakingParams.cs
  88. +14
    -0
      src/Discord.Net/API/Voice/UdpProtocolInfo.cs
  89. +5
    -3
      src/Discord.Net/API/Voice/VoiceOpCode.cs
  90. +1
    -1
      src/Discord.Net/API/WebSocketMessage.cs
  91. +330
    -0
      src/Discord.Net/Audio/AudioClient.cs
  92. +13
    -0
      src/Discord.Net/Audio/AudioMode.cs
  93. +24
    -0
      src/Discord.Net/Audio/IAudioClient.cs
  94. +9
    -0
      src/Discord.Net/Audio/Opus/OpusApplication.cs
  95. +51
    -0
      src/Discord.Net/Audio/Opus/OpusConverter.cs
  96. +10
    -0
      src/Discord.Net/Audio/Opus/OpusCtl.cs
  97. +49
    -0
      src/Discord.Net/Audio/Opus/OpusDecoder.cs
  98. +77
    -0
      src/Discord.Net/Audio/Opus/OpusEncoder.cs
  99. +14
    -0
      src/Discord.Net/Audio/Opus/OpusError.cs
  100. +25
    -0
      src/Discord.Net/Audio/Sodium/SecretBox.cs

+ 6
- 6
Discord.Net.sln View File

@@ -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


+ 11
- 15
README.md View File

@@ -1,25 +1,21 @@
# Discord.Net v1.0.0-dev
[![NuGet Pre Release](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![AppVeyor](https://img.shields.io/appveyor/ci/foxbot/discord-net.svg?maxAge=2592000?style=plastic)](https://ci.appveyor.com/project/foxbot/discord-net/) [![Discord](https://discordapp.com/api/servers/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTYLhAAW)

[![NuGet Pre Release](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![AppVeyor](https://img.shields.io/appveyor/ci/foxbot/discord-net.svg?maxAge=2592000?style=plastic)](https://ci.appveyor.com/project/foxbot/discord-net/) [![Discord](https://discordapp.com/api/servers/81384788765712384/widget.png)](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
- 4
global.json View File

@@ -1,6 +1,3 @@
{
"projects": [ "src", "test" ],
"sdk": {
"version": "1.0.0-preview1-002702"
}
"projects": [ "src", "test" ]
}

+ 19
- 0
src/Discord.Net.Commands/Attributes/CommandAttribute.cs View File

@@ -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;
}
}
}

+ 14
- 0
src/Discord.Net.Commands/Attributes/DescriptionAttribute.cs View File

@@ -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;
}
}
}

+ 18
- 0
src/Discord.Net.Commands/Attributes/GroupAttribute.cs View File

@@ -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;
}
}
}

+ 18
- 0
src/Discord.Net.Commands/Attributes/ModuleAttribute.cs View File

@@ -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;
}
}
}

+ 9
- 0
src/Discord.Net.Commands/Attributes/UnparsedAttribute.cs View File

@@ -0,0 +1,9 @@
using System;

namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Parameter)]
public class UnparsedAttribute : Attribute
{
}
}

+ 124
- 0
src/Discord.Net.Commands/Command.cs View File

@@ -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})";
}
}

+ 20
- 0
src/Discord.Net.Commands/CommandError.cs View File

@@ -0,0 +1,20 @@
namespace Discord.Commands
{
public enum CommandError
{
//Search
UnknownCommand,

//Parse
ParseFailed,
BadArgCount,

//Parse (Type Reader)
CastFailed,
ObjectNotFound,
MultipleMatches,

//Execute
Exception,
}
}

+ 35
- 0
src/Discord.Net.Commands/CommandParameter.cs View File

@@ -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)" : "")}";
}
}

+ 144
- 0
src/Discord.Net.Commands/CommandParser.cs View File

@@ -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());
}
}
}

+ 260
- 0
src/Discord.Net.Commands/CommandService.cs View File

@@ -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.");
}
}
}

+ 19
- 0
src/Discord.Net.Commands/Discord.Net.Commands.xproj View File

@@ -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>

+ 44
- 0
src/Discord.Net.Commands/Extensions/MessageExtensions.cs View File

@@ -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;
}
}
}

+ 76
- 0
src/Discord.Net.Commands/Map/CommandMap.cs View File

@@ -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>();
}
}
}
}

+ 95
- 0
src/Discord.Net.Commands/Map/CommandMapNode.cs View File

@@ -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;
}
}
}
}
}

+ 54
- 0
src/Discord.Net.Commands/Module.cs View File

@@ -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;
}
}

+ 48
- 0
src/Discord.Net.Commands/Readers/ChannelTypeReader.cs View File

@@ -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);
}
}
}

+ 17
- 0
src/Discord.Net.Commands/Readers/GenericTypeReader.cs View File

@@ -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);
}
}

+ 24
- 0
src/Discord.Net.Commands/Readers/MessageTypeReader.cs View File

@@ -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."));
}
}
}

+ 36
- 0
src/Discord.Net.Commands/Readers/RoleTypeReader.cs View File

@@ -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."));
}
}
}

+ 9
- 0
src/Discord.Net.Commands/Readers/TypeReader.cs View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;

namespace Discord.Commands
{
public abstract class TypeReader
{
public abstract Task<TypeReaderResult> Read(IMessage context, string input);
}
}

+ 62
- 0
src/Discord.Net.Commands/Readers/UserTypeReader.cs View File

@@ -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);
}
}
}

+ 24
- 0
src/Discord.Net.Commands/ReflectionUtils.cs View File

@@ -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);
}
}
}
}

+ 35
- 0
src/Discord.Net.Commands/Results/ExecuteResult.cs View File

@@ -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}";
}
}

+ 9
- 0
src/Discord.Net.Commands/Results/IResult.cs View File

@@ -0,0 +1,9 @@
namespace Discord.Commands
{
public interface IResult
{
CommandError? Error { get; }
string ErrorReason { get; }
bool IsSuccess { get; }
}
}

+ 35
- 0
src/Discord.Net.Commands/Results/ParseResult.cs View File

@@ -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}";
}
}

+ 33
- 0
src/Discord.Net.Commands/Results/SearchResult.cs View File

@@ -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}";
}
}

+ 31
- 0
src/Discord.Net.Commands/Results/TypeReaderResult.cs View File

@@ -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}";
}
}

+ 34
- 0
src/Discord.Net.Commands/project.json View File

@@ -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"
]
}
}
}

+ 2
- 2
src/Discord.Net/API/Common/Attachment.cs View File

@@ -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; }
}
}

+ 9
- 9
src/Discord.Net/API/Common/Channel.cs View File

@@ -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; }
}
}

+ 1
- 1
src/Discord.Net/API/Common/Connection.cs View File

@@ -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; }
}
}

+ 2
- 2
src/Discord.Net/API/Common/Embed.cs View File

@@ -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; }
}
}

+ 2
- 2
src/Discord.Net/API/Common/EmbedThumbnail.cs View File

@@ -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; }
}
}

+ 2
- 2
src/Discord.Net/API/Common/Game.cs View File

@@ -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; }
}
}

+ 5
- 1
src/Discord.Net/API/Common/Guild.cs View File

@@ -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; }
}
}

+ 1
- 1
src/Discord.Net/API/Common/GuildEmbed.cs View File

@@ -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; }
}
}

+ 2
- 2
src/Discord.Net/API/Common/GuildMember.cs View File

@@ -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")]


+ 1
- 1
src/Discord.Net/API/Common/Integration.cs View File

@@ -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; }
}
}

+ 1
- 1
src/Discord.Net/API/Common/InviteMetadata.cs View File

@@ -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; }
}


+ 11
- 11
src/Discord.Net/API/Common/Message.cs View File

@@ -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; }
}
}

+ 21
- 0
src/Discord.Net/API/Common/Presence.cs View File

@@ -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; }
}
}

+ 1
- 1
src/Discord.Net/API/Common/ReadState.cs View File

@@ -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; }
}
}

+ 14
- 0
src/Discord.Net/API/Common/Relationship.cs View File

@@ -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; }
}
}

+ 9
- 0
src/Discord.Net/API/Common/RelationshipType.cs View File

@@ -0,0 +1,9 @@
namespace Discord.API
{
public enum RelationshipType
{
Friend = 1,
Blocked = 2,
Pending = 4
}
}

+ 5
- 5
src/Discord.Net/API/Common/Role.cs View File

@@ -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; }
}
}

+ 8
- 4
src/Discord.Net/API/Common/User.cs View File

@@ -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; }
}
}

+ 1
- 1
src/Discord.Net/API/Common/VoiceState.cs View File

@@ -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")]


+ 392
- 275
src/Discord.Net/API/DiscordAPIClient.cs
File diff suppressed because it is too large
View File


+ 254
- 0
src/Discord.Net/API/DiscordVoiceAPIClient.cs View File

@@ -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);
}
}
}

+ 24
- 0
src/Discord.Net/API/Gateway/ExtendedGuild.cs View File

@@ -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; }
}
}

src/Discord.Net/API/Gateway/GatewayOpCodes.cs → src/Discord.Net/API/Gateway/GatewayOpCode.cs View File

@@ -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
}
}

+ 12
- 0
src/Discord.Net/API/Gateway/GuildBanEvent.cs View File

@@ -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; }
}
}

+ 12
- 0
src/Discord.Net/API/Gateway/GuildEmojiUpdateEvent.cs View File

@@ -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;
}
}

+ 10
- 0
src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Gateway
{
public class GuildMemberAddEvent : GuildMember
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

+ 12
- 0
src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs View File

@@ -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; }
}
}

+ 10
- 0
src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Gateway
{
public class GuildMemberUpdateEvent : GuildMember
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

+ 1
- 1
src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs View File

@@ -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; }
}
}

+ 12
- 0
src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs View File

@@ -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; }
}
}

+ 1
- 1
src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs View File

@@ -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; }
}
}

+ 17
- 0
src/Discord.Net/API/Gateway/GuildSyncEvent.cs View File

@@ -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; }
}
}

+ 10
- 0
src/Discord.Net/API/Gateway/HelloEvent.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Gateway
{
public class HelloEvent
{
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval { get; set; }
}
}

+ 13
- 0
src/Discord.Net/API/Gateway/MessageDeleteBulkEvent.cs View File

@@ -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; }
}
}

+ 5
- 8
src/Discord.Net/API/Gateway/ReadyEvent.cs View File

@@ -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")]*/
}
}

+ 7
- 2
src/Discord.Net/API/Gateway/RequestMembersParams.cs View File

@@ -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); } }
}
}

+ 1
- 1
src/Discord.Net/API/Gateway/ResumeParams.cs View File

@@ -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; }
}
}

+ 12
- 0
src/Discord.Net/API/Gateway/StatusUpdateParams.cs View File

@@ -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; }
}
}

+ 0
- 16
src/Discord.Net/API/Gateway/UpdateVoiceParams.cs View File

@@ -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; }
}
}

+ 21
- 0
src/Discord.Net/API/Gateway/VoiceStateUpdateParams.cs View File

@@ -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; } }
}
}

+ 0
- 8
src/Discord.Net/API/IOptional.cs View File

@@ -1,8 +0,0 @@
namespace Discord.API
{
public interface IOptional
{
object Value { get; }
bool IsSpecified { get; }
}
}

+ 2
- 0
src/Discord.Net/API/Rest/CreateDMChannelParams.cs View File

@@ -6,5 +6,7 @@ namespace Discord.API.Rest
{
[JsonProperty("recipient_id")]
public ulong RecipientId { get; set; }
[JsonIgnore]
public IUser Recipient { set { RecipientId = value.Id; } }
}
}

src/Discord.Net/API/Rest/DeleteMessagesParam.cs → src/Discord.Net/API/Rest/DeleteMessagesParams.cs View File

@@ -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); } }
}
}

+ 1
- 0
src/Discord.Net/API/Rest/GetChannelMessagesParams.cs View File

@@ -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>(); } }
}
}

+ 0
- 12
src/Discord.Net/API/Rest/LoginParams.cs View File

@@ -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; }
}
}

+ 0
- 10
src/Discord.Net/API/Rest/LoginResponse.cs View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;

namespace Discord.API.Rest
{
public class LoginResponse
{
[JsonProperty("token")]
public string Token { get; set; }
}
}

+ 1
- 8
src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs View File

@@ -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; }
}


+ 5
- 3
src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs View File

@@ -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>(); } }
}
}

+ 11
- 3
src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs View File

@@ -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>(); } }
}
}

+ 14
- 7
src/Discord.Net/API/Rest/ModifyGuildParams.cs View File

@@ -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>(); } }
}
}

+ 8
- 0
src/Discord.Net/API/Rest/ModifyPresenceParams.cs View File

@@ -0,0 +1,8 @@
namespace Discord.API.Rest
{
public class ModifyPresenceParams
{
public Optional<UserStatus> Status { get; set; }
public Optional<Discord.Game> Game { get; set; }
}
}

+ 16
- 0
src/Discord.Net/API/Voice/IdentifyParams.cs View File

@@ -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; }
}
}

+ 16
- 0
src/Discord.Net/API/Voice/ReadyEvent.cs View File

@@ -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; }
}
}

+ 12
- 0
src/Discord.Net/API/Voice/SelectProtocolParams.cs View File

@@ -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; }
}
}

+ 12
- 0
src/Discord.Net/API/Voice/SessionDescriptionEvent.cs View File

@@ -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; }
}
}

+ 12
- 0
src/Discord.Net/API/Voice/SpeakingParams.cs View File

@@ -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; }
}
}

+ 14
- 0
src/Discord.Net/API/Voice/UdpProtocolInfo.cs View File

@@ -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; }
}
}

src/Discord.Net/API/Voice/VoiceOpCodes.cs → src/Discord.Net/API/Voice/VoiceOpCode.cs View File

@@ -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> CS - Used to keep the connection alive and measure latency. </summary>
/// <summary> CS - 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>

+ 1
- 1
src/Discord.Net/API/WebSocketMessage.cs View File

@@ -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; }
}


+ 330
- 0
src/Discord.Net/Audio/AudioClient.cs View File

@@ -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);
}
}

+ 13
- 0
src/Discord.Net/Audio/AudioMode.cs View File

@@ -0,0 +1,13 @@
using System;

namespace Discord.Audio
{
[Flags]
public enum AudioMode : byte
{
Disabled = 0,
Outgoing = 1,
Incoming = 2,
Both = Outgoing | Incoming
}
}

+ 24
- 0
src/Discord.Net/Audio/IAudioClient.cs View File

@@ -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);
}
}

+ 9
- 0
src/Discord.Net/Audio/Opus/OpusApplication.cs View File

@@ -0,0 +1,9 @@
namespace Discord.Audio
{
public enum OpusApplication : int
{
Voice = 2048,
MusicOrMixed = 2049,
LowLatency = 2051
}
}

+ 51
- 0
src/Discord.Net/Audio/Opus/OpusConverter.cs View File

@@ -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);
}
}
}

+ 10
- 0
src/Discord.Net/Audio/Opus/OpusCtl.cs View File

@@ -0,0 +1,10 @@
namespace Discord.Audio
{
internal enum OpusCtl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}
}

+ 49
- 0
src/Discord.Net/Audio/Opus/OpusDecoder.cs View File

@@ -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;
}
}
}
}

+ 77
- 0
src/Discord.Net/Audio/Opus/OpusEncoder.cs View File

@@ -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;
}
}
}
}

+ 14
- 0
src/Discord.Net/Audio/Opus/OpusError.cs View File

@@ -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
}
}

+ 25
- 0
src/Discord.Net/Audio/Sodium/SecretBox.cs View File

@@ -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);
}
}
}

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

Loading…
Cancel
Save