Compare commits

...

130 Commits
dev ... 0.9

Author SHA1 Message Date
  RogueException 8115aeabbf Merge pull request #399 from Grimitsu/master 8 years ago
  Grimitsu 4e0cf92384 Fix BuildInEngine.SendFile() error 8 years ago
  RogueException d7507957ad Merge pull request #360 from Sentinent/master 8 years ago
  Sentinent af478f224f Fixed connecting with user tokens and updated gateway OP codes 8 years ago
  RogueException 34064ffe59 Merge pull request #316 from jvitkauskas/returnAfterThrow 8 years ago
  RogueException 2fbb4633a0 Merge pull request #317 from jvitkauskas/fixTypo 8 years ago
  Julius Vitkauskas 1956d1df89 fix typo 8 years ago
  Julius Vitkauskas 95b262a9fe remove return after throw (unreachable code) 8 years ago
  RogueException 001dc525a3 0.9.6 8 years ago
  RogueException 840ed338e5 Merge pull request #303 from Jeezymang/master 8 years ago
  Jon 0435ac933c Amended to add invisible status 8 years ago
  Jon 8fcb643cd0 Added support for DND 8 years ago
  Khionu Sybiern 04e4d9069a Update README.md 8 years ago
  RogueException 42f2573358 Merge pull request #288 from moiph/moiph/sharding 8 years ago
  Pat Murphy 8c1c16fe7b Adds sharding support 8 years ago
  RogueException cf3aa40c8a Dealing with nuget's versioning restrictions 8 years ago
  RogueException bbe61e118e Redeployed Discord.Net.Audio 8 years ago
  RogueException e220a30bf2 Updated deps to 0.9.5 8 years ago
  RogueException d7b1c4577d Fixed compile errors 8 years ago
  RogueException 22a7dab14c 0.9.5 8 years ago
  RogueException 23a53a7688 Fixed README's Discord widget 8 years ago
  RogueException aa0cb12ae7 Merge pull request #189 from Googie2149/master 8 years ago
  Googie2149 2a19820c58 Removed Bearer token type 8 years ago
  Googie2149 541ca565fb Added bot authorization headers 8 years ago
  RogueException 24c83f6674 Removed use of obsolete events 9 years ago
  Khionu Terabite 6bfeaaddf0 Replaced expired invite 9 years ago
  RogueException b07e96d517 Merge pull request #105 from AdenFlorian/event_summaries 9 years ago
  David Valachovic 5195e6b230 Added [Obsolete] attrib to 2 events 9 years ago
  David Valachovic 28d4041a5a Added XML Doc summaries to the Client Events to help users of the library decide which events to use. 9 years ago
  RogueException 8db6eff8cb Updated to .Net Core 1.0 RTM, 0.9.4 9 years ago
  RogueException b3b5d45dca Removed build status badge 9 years ago
  RogueException 7808b1129c Removed index.html 9 years ago
  RogueException 86406e2641 Merge pull request #101 from 420foxbot/patch-1 9 years ago
  Christopher F 1de60a358e Update docs link in README to legacy 9 years ago
  RogueException d0e53bca6a Updated README version and docs link 9 years ago
  RogueException e7e5540d47 0.9.3.2 9 years ago
  RogueException 6036de7736 Merge pull request #96 from khionu/master 9 years ago
  Khionu Terabite 99637d0a21 Separated the conditions 9 years ago
  Khionu Terabite 402d47959c Included unsaved local changes 9 years ago
  Khionu Terabite aeebe5d596 Added OutOfBounds exception for DeleteMessages, for when Message Count is less than 2 or greater than 100 9 years ago
  RogueException d1ae0db741 Dont send nickname in User.Edit unless one is specified 9 years ago
  RogueException 608d4a9cdd Removed LegacyExtensions.Run() 9 years ago
  RogueException 1fb6a03e1d Removed DiscordClient.Wait() 9 years ago
  RogueException 2b5bc17e98 Added "around" direction 9 years ago
  RogueException 03c0f670f1 Padded user discrminators 9 years ago
  RogueException 4e2b919de8 Merge pull request #74 from khionu/master 9 years ago
  Khionu Terabite e3dea5b7c2 Add BulkMessageDelete Functionality 9 years ago
  RogueException f62948188d 0.9.3.1 9 years ago
  RogueException eb14cfdb88 Removed PreAuthenticate = false 9 years ago
  RogueException a4b08bee8b Fixed updating members without ManageNicknames 9 years ago
  RogueException 14180fb761 Fixed null GameTypes 9 years ago
  RogueException 416162fd96 0.9.3 9 years ago
  RogueException 80f9d6f2de Added support for nickname parsing 9 years ago
  RogueException bcbf1bc5c6 Fixed setting other users' nicknames 9 years ago
  RogueException 12fad60fce Fixed removing own nicknames 9 years ago
  RogueException 86b0b3b266 Minor doc edit 9 years ago
  RogueException b8cc2eb20f Added commands triggering on nickname mentions 9 years ago
  RogueException 6d6137ad01 Added Profile.NicknameMention 9 years ago
  RogueException 41629525e8 Fixed game name not serializing 9 years ago
  RogueException d0f6e70dbc Several Game fixes 9 years ago
  RogueException cc68ef27f5 Fixed a couple permissions bugs 9 years ago
  Voltana eb614d17b3 Fixed a link 9 years ago
  Voltana 4d766aad7b Removed Known issues 9 years ago
  Voltana 6a896da0b4 Updated README 9 years ago
  Voltana 878ff65cf7 Readded net45 support, 0.9.2 9 years ago
  Voltana 4ea259bcef Removed preserveCompilationContext 9 years ago
  Voltana cd3d90ea80 Added .Net Core RC2 Support 9 years ago
  Voltana ec7297e99c Dont exception on SetGame if no url is passed 9 years ago
  RogueException 94ad855290 Merge pull request #58 from khionu/master 9 years ago
  Khionu Terabite 234185b603 Confirmed working now 9 years ago
  Khionu Terabite c3df2577ec Implimented the new GameObject for UserPresences 9 years ago
  Voltana a87027b7ab Fixed several permission bugs 9 years ago
  Voltana de2068140d Cleaned up nickname editing 9 years ago
  RogueException c6e919fb72 Merge pull request #57 from Lirusaito/master 9 years ago
  Lirusaito d1f3dfe26a Shove in support for changing own nick (through `User.Edit(nickname:))` 9 years ago
  Lirusaito 641401da4d Add support for changing Nicknames! 9 years ago
  RogueException 9c77a80cf5 Typo 9 years ago
  RogueException 75a5f4a739 Added Administrator, ChangeNick and ManageNick permissions 9 years ago
  RogueException 7606dd650f Use cancelTokens in LargeServerDownloader 9 years ago
  RogueException f04789619a Fixed DiscordClient.Disconnect stalling 9 years ago
  RogueException 64d4b0af32 Support nickname mentions in FindUsers 9 years ago
  RogueException 69b53b2ce5 Merge pull request #55 from khionu/master 9 years ago
  RogueException f74a393581 Merge pull request #56 from Lirusaito/master 9 years ago
  Lirusaito 6337266375 Restore ability to `.Mention` the `EveryoneRole` 9 years ago
  Lirusaito 549b32de6d Support Role Mentions 9 years ago
  Khionu Terabite afe1b318e8 Remove <see/> from Summary 9 years ago
  Khionu Terabite d30f462afc Added SelfBot Setting 9 years ago
  RogueException 5042ae22dd Merge pull request #54 from 420foxbot/patch-user-nickname 9 years ago
  Christopher F 440e193fdf Add Nicknames to User Model; Bump README to 0.9.1 9 years ago
  RogueException 58e9716158 Actually bumped the version to 0.9.1 9 years ago
  RogueException 033ea96204 Merge pull request #52 from 420foxbot/master-user-bot 9 years ago
  Christopher F 789419ac86 don't check for null and default to false 9 years ago
  Christopher F c012261cd1 Add `User.IsBot` field, bump version number to 0.9rc4 9 years ago
  RogueException 4f632df446 Don't cache DefaultChannel or EveryoneRole 9 years ago
  RogueException 46e5b44483 Removed EnableMultiserver 9 years ago
  RogueException 638fe71f6e Merge pull request #49 from khionu/master 9 years ago
  Khionu 58025d303f Removed hack; code now assumes client is logged in as Bot Account, without 1 voice channel limit 9 years ago
  RogueException 4d8e5b878f Made large server downloader more stable 9 years ago
  RogueException 87c9d2243b Merge pull request #44 from moiph/master 9 years ago
  RogueException 0a068bb63c PR cleanup, adjusting delay times 9 years ago
  RogueException 8a8b01cc5b Merge pull request #47 from Kwoth/master 9 years ago
  Master Kwoth e72d681b96 Removed unused variable 9 years ago
  Master Kwoth f7ba519989 Changed loglevel to Verbose for large server data message 9 years ago
  Master Kwoth 765e986018 Now getting all of the large server data properly 9 years ago
  Master Kwoth 659e63f2b4 Voice Socket error fix 9 years ago
  RogueException 8f0bc1933a Add a timeout to guild member chunking, fix voice connections, delay READY. 9 years ago
  Pat Murphy 0361f2e2d6 check for server==null in User.ServerPermissions getter 9 years ago
  RogueException a44d7de473 This is what I get for pushing to master 9 years ago
  RogueException ce23943b45 v4 bugfix 9 years ago
  RogueException 35ec23d33c Pass v4 info to the right endpoint 9 years ago
  RogueException 24e71f7c1a Request gateway v4, support reconnects 9 years ago
  RogueException a5beded156 Fixed READY crash on v4 gateway 9 years ago
  RogueException a357a06d33 Added delay to member request batching 9 years ago
  RogueException b7edf48a19 Actually batch, not just offset... 9 years ago
  RogueException db295bac3a Minor changes 9 years ago
  RogueException 4e3f726073 Added member request batching 9 years ago
  RogueException 1a18d920db Merge pull request #42 from 420foxbot/unit-testing 9 years ago
  Christopher F 20b4288f57 Update README.md for Unit Test Badge; Docs 9 years ago
  Christopher F 8522c8b5b0 Add Unit Tests for User Operations 9 years ago
  Christopher F bf41b30a9a Added more Unit Tests for Messages, Channels; Removed Settings 9 years ago
  Christopher F c887adfa4b Unit tests for Permissions 9 years ago
  Christopher F 3f484f4c19 deprecated config.json 9 years ago
  Christopher F 654396e665 Added basic Permissions testing 9 years ago
  Christopher F 9b403e872a Removed exception testing for messages 9 years ago
  Christopher F 75ee7b0913 Message Unit Tests 9 years ago
  Christopher F 9e78ca4941 Begin rewriting Unit Tests 9 years ago
  RogueException 25f148d5a8 Several bugfixes 9 years ago
  RogueException e55f1a6415 Merge branch 'master' of https://github.com/RogueException/Discord.Net.git 9 years ago
  RogueException a072a44481 Merge pull request #35 from SSStormy/patch-1 9 years ago
  SSStormy c33a1f2cd3 Fixed Server.RoleCount returning channel amount. 9 years ago
74 changed files with 2326 additions and 1850 deletions
Split View
  1. +7
    -11
      README.md
  2. +1
    -4
      global.json
  3. +2
    -2
      src/Discord.Net.Audio.Net45/Properties/AssemblyInfo.cs
  4. +20
    -75
      src/Discord.Net.Audio/AudioClient.cs
  5. +54
    -129
      src/Discord.Net.Audio/AudioService.cs
  6. +0
    -7
      src/Discord.Net.Audio/AudioServiceConfig.cs
  7. +3
    -3
      src/Discord.Net.Audio/Discord.Net.Audio.xproj
  8. +9
    -9
      src/Discord.Net.Audio/Net/VoiceSocket.cs
  9. +26
    -13
      src/Discord.Net.Audio/project.json
  10. +2
    -2
      src/Discord.Net.Commands.Net45/Properties/AssemblyInfo.cs
  11. +20
    -1
      src/Discord.Net.Commands/CommandService.cs
  12. +8
    -1
      src/Discord.Net.Commands/CommandServiceConfig.cs
  13. +3
    -3
      src/Discord.Net.Commands/Discord.Net.Commands.xproj
  14. +25
    -12
      src/Discord.Net.Commands/project.json
  15. +2
    -2
      src/Discord.Net.Modules.Net45/Properties/AssemblyInfo.cs
  16. +3
    -3
      src/Discord.Net.Modules/Discord.Net.Modules.xproj
  17. +2
    -5
      src/Discord.Net.Modules/ModuleManager.cs
  18. +26
    -13
      src/Discord.Net.Modules/project.json
  19. +20
    -5
      src/Discord.Net.Net45/Discord.Net.csproj
  20. +2
    -2
      src/Discord.Net.Net45/Properties/AssemblyInfo.cs
  21. +5
    -5
      src/Discord.Net.Shared/TaskExtensions.cs
  22. +14
    -0
      src/Discord.Net/API/Client/Common/Game.cs
  23. +2
    -0
      src/Discord.Net/API/Client/Common/Member.cs
  24. +1
    -6
      src/Discord.Net/API/Client/Common/MemberPresence.cs
  25. +2
    -0
      src/Discord.Net/API/Client/Common/Role.cs
  26. +2
    -0
      src/Discord.Net/API/Client/Common/UserReference.cs
  27. +2
    -2
      src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs
  28. +6
    -8
      src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs
  29. +6
    -0
      src/Discord.Net/API/Client/GatewaySocket/Events/Reconnect.cs
  30. +0
    -10
      src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs
  31. +9
    -3
      src/Discord.Net/API/Client/GatewaySocket/OpCodes.cs
  32. +23
    -0
      src/Discord.Net/API/Client/Rest/BulkMessageDelete.cs
  33. +4
    -4
      src/Discord.Net/API/Client/Rest/Gateway.cs
  34. +3
    -0
      src/Discord.Net/API/Client/Rest/UpdateMember.cs
  35. +23
    -0
      src/Discord.Net/API/Client/Rest/UpdateOwnNick.cs
  36. +2
    -0
      src/Discord.Net/API/Client/Rest/UpdateRole.cs
  37. +3
    -3
      src/Discord.Net/Discord.Net.xproj
  38. +33
    -2
      src/Discord.Net/DiscordClient.Events.cs
  39. +96
    -53
      src/Discord.Net/DiscordClient.cs
  40. +10
    -0
      src/Discord.Net/DiscordConfig.cs
  41. +466
    -466
      src/Discord.Net/ETF/ETFReader.cs
  42. +447
    -447
      src/Discord.Net/ETF/ETFWriter.cs
  43. +8
    -0
      src/Discord.Net/Enums/GameType.cs
  44. +6
    -2
      src/Discord.Net/Enums/PermissionBits.cs
  45. +1
    -1
      src/Discord.Net/Enums/Relative.cs
  46. +8
    -0
      src/Discord.Net/Enums/TokenType.cs
  47. +8
    -0
      src/Discord.Net/Enums/UserStatus.cs
  48. +19
    -14
      src/Discord.Net/InternalExtensions.cs
  49. +0
    -5
      src/Discord.Net/Legacy.cs
  50. +6
    -6
      src/Discord.Net/Logging/ILogger.cs
  51. +2
    -2
      src/Discord.Net/Logging/LogManager.cs
  52. +1
    -1
      src/Discord.Net/Logging/Logger.cs
  53. +36
    -17
      src/Discord.Net/Models/Channel.cs
  54. +22
    -0
      src/Discord.Net/Models/Game.cs
  55. +43
    -24
      src/Discord.Net/Models/Message.cs
  56. +52
    -34
      src/Discord.Net/Models/Permissions.cs
  57. +4
    -2
      src/Discord.Net/Models/Profile.cs
  58. +9
    -4
      src/Discord.Net/Models/Role.cs
  59. +41
    -42
      src/Discord.Net/Models/Server.cs
  60. +51
    -13
      src/Discord.Net/Models/User.cs
  61. +3
    -4
      src/Discord.Net/Net/Rest/BuiltInEngine.cs
  62. +0
    -27
      src/Discord.Net/Net/Rest/ETFRestClient.cs
  63. +4
    -2
      src/Discord.Net/Net/Rest/RestClient.cs
  64. +1
    -1
      src/Discord.Net/Net/Rest/SharpRestEngine.cs
  65. +3
    -4
      src/Discord.Net/Net/WebSockets/BuiltInEngine.cs
  66. +18
    -18
      src/Discord.Net/Net/WebSockets/GatewaySocket.cs
  67. +1
    -1
      src/Discord.Net/Net/WebSockets/WS4NetEngine.cs
  68. +71
    -71
      src/Discord.Net/Net/WebSockets/WebSocket.cs
  69. +1
    -0
      src/Discord.Net/TaskManager.cs
  70. +31
    -48
      src/Discord.Net/project.json
  71. +4
    -5
      test/Discord.Net.Tests/Discord.Net.Tests.csproj
  72. +0
    -32
      test/Discord.Net.Tests/Settings.cs
  73. +477
    -158
      test/Discord.Net.Tests/Tests.cs
  74. +1
    -1
      test/Discord.Net.Tests/packages.config

+ 7
- 11
README.md View File

@@ -1,10 +1,9 @@
# Discord.Net v0.9.0-rc3
An unofficial .Net API Wrapper for the Discord client (http://discordapp.com).
# Discord.Net v0.9.6
[![NuGet Pre Release](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTYLhAAW)

Check out the [documentation](https://discordnet.readthedocs.org/en/latest/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx).
An unofficial .Net API Wrapper for the Discord client (http://discordapp.com).

##### Warning: documentation is currently outdated.
It's current being rewritten. Until that's done, feel free to use my [DiscordBot](https://github.com/RogueException/DiscordBot) repo for reference.
Check out the [documentation](http://rtd.discord.foxbot.me/en/legacy/) or join the [Discord API Chat](https://discord.gg/discord-api).

### Installation
You can download Discord.Net and its extensions from NuGet:
@@ -16,9 +15,6 @@ You can download Discord.Net and its extensions from NuGet:
### 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 1 (available through Visual Studio)
- [ASP.Net 5 RC1](https://get.asp.net)
- NuGet 3.3 (available through Visual Studio)

### Known Issues
- .Net Core support is incomplete on non-Windows systems
- [Visual Studio 2015 Update 3](https://www.microsoft.com/net/core#windows)
- [Visual Studio .Net Core Plugin](https://www.microsoft.com/net/core#windows)
- NuGet 3.3+ (available through Visual Studio)

+ 1
- 4
global.json View File

@@ -1,6 +1,3 @@
{
"projects": [ "src" ],
"sdk": {
"version": "1.0.0-rc1-update1"
}
"projects": [ "src" ]
}

+ 2
- 2
src/Discord.Net.Audio.Net45/Properties/AssemblyInfo.cs View File

@@ -13,6 +13,6 @@ using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")]

[assembly: AssemblyVersion("0.9.0.0")]
[assembly: AssemblyFileVersion("0.9.0.0")]
[assembly: AssemblyVersion("0.9.6.0")]
[assembly: AssemblyFileVersion("0.9.6.0")]


+ 20
- 75
src/Discord.Net.Audio/AudioClient.cs View File

@@ -1,5 +1,4 @@
using Discord.API.Client.GatewaySocket;
using Discord.API.Client.Rest;
using Discord.Logging;
using Discord.Net.Rest;
using Discord.Net.WebSockets;
@@ -13,7 +12,7 @@ using System.Threading.Tasks;

namespace Discord.Audio
{
internal class AudioClient : IAudioClient
internal class AudioClient : IAudioClient
{
private class OutStream : Stream
{
@@ -50,7 +49,7 @@ namespace Discord.Audio
private ConnectionState _gatewayState;

internal Logger Logger { get; }
public int Id { get; }
public AudioService Service { get; }
public AudioServiceConfig Config { get; }
@@ -59,7 +58,7 @@ namespace Discord.Audio
public VoiceSocket VoiceSocket { get; }
public JsonSerializer Serializer { get; }
public Stream OutputStream { get; }
public CancellationToken CancelToken { get; private set; }
public string SessionId => GatewaySocket.SessionId;

@@ -68,7 +67,7 @@ namespace Discord.Audio
public Channel Channel => VoiceSocket.Channel;

public AudioClient(DiscordClient client, Server server, int id)
{
{
Id = id;
Service = client.GetService<AudioService>();
Config = Service.Config;
@@ -84,40 +83,8 @@ namespace Discord.Audio
CancelToken = new CancellationToken(true);

//Networking
if (Config.EnableMultiserver)
{
//TODO: We can remove this hack when official API launches
var baseConfig = client.Config;
var builder = new DiscordConfigBuilder
{
AppName = baseConfig.AppName,
AppUrl = baseConfig.AppUrl,
AppVersion = baseConfig.AppVersion,
CacheToken = baseConfig.CacheDir != null,
ConnectionTimeout = baseConfig.ConnectionTimeout,
EnablePreUpdateEvents = false,
FailedReconnectDelay = baseConfig.FailedReconnectDelay,
LargeThreshold = 1,
LogLevel = baseConfig.LogLevel,
MessageCacheSize = 0,
ReconnectDelay = baseConfig.ReconnectDelay,
UsePermissionsCache = false
};
_config = builder.Build();

ClientAPI = new JsonRestClient(_config, DiscordConfig.ClientAPIUrl, client.Log.CreateLogger($"ClientAPI #{id}"));
GatewaySocket = new GatewaySocket(_config, client.Serializer, client.Log.CreateLogger($"Gateway #{id}"));
GatewaySocket.Connected += (s, e) =>
{
if (_gatewayState == ConnectionState.Connecting)
EndGatewayConnect();
};
}
else
{
_config = client.Config;
GatewaySocket = client.GatewaySocket;
}
_config = client.Config;
GatewaySocket = client.GatewaySocket;
GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e);
VoiceSocket = new VoiceSocket(_config, Config, client.Serializer, client.Log.CreateLogger($"Voice #{id}"));
VoiceSocket.Server = server;
@@ -126,14 +93,9 @@ namespace Discord.Audio

public async Task Connect()
{
if (Config.EnableMultiserver)
await BeginGatewayConnect().ConfigureAwait(false);
else
{
var cancelSource = new CancellationTokenSource();
CancelToken = cancelSource.Token;
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false);
}
var cancelSource = new CancellationTokenSource();
CancelToken = cancelSource.Token;
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false);
}
private async Task BeginGatewayConnect()
{
@@ -154,7 +116,7 @@ namespace Discord.Audio
var cancelSource = new CancellationTokenSource();
CancelToken = cancelSource.Token;
ClientAPI.CancelToken = CancelToken;
await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false);

await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false);
@@ -178,40 +140,23 @@ namespace Discord.Audio
{
_gatewayState = ConnectionState.Connected;
}
public async Task Disconnect()
{
await _taskManager.Stop(true).ConfigureAwait(false);
if (Config.EnableMultiserver)
ClientAPI.Token = null;
}
private async Task Cleanup()
{
var oldState = _gatewayState;
_gatewayState = ConnectionState.Disconnecting;

if (Config.EnableMultiserver)
{
if (oldState == ConnectionState.Connected)
{
try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); }
catch (OperationCanceledException) { }
}

await GatewaySocket.Disconnect().ConfigureAwait(false);
ClientAPI.Token = null;
}

var server = VoiceSocket.Server;
VoiceSocket.Server = null;
VoiceSocket.Channel = null;
if (Config.EnableMultiserver)
await Service.RemoveClient(server, this).ConfigureAwait(false);
await Service.RemoveClient(server, this).ConfigureAwait(false);
SendVoiceUpdate(server.Id, null);

await VoiceSocket.Disconnect().ConfigureAwait(false);
if (Config.EnableMultiserver)
await GatewaySocket.Disconnect().ConfigureAwait(false);

_gatewayState = (int)ConnectionState.Disconnected;
}
@@ -224,7 +169,7 @@ namespace Discord.Audio
if (channel == VoiceSocket.Channel) return;
var server = channel.Server;
if (server != VoiceSocket.Server)
throw new ArgumentException("This is channel is not part of the current server.", nameof(channel));
throw new ArgumentException("This channel is not part of the current server.", nameof(channel));
if (VoiceSocket.Server == null)
throw new InvalidOperationException("This client has been closed.");

@@ -284,26 +229,26 @@ namespace Discord.Audio
}

public void Send(byte[] data, int offset, int count)
{
{
if (data == null) throw new ArgumentException(nameof(data));
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(count));
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset));
if (VoiceSocket.Server == null) return; //Has been closed
if (count == 0) return;

VoiceSocket.SendPCMFrames(data, offset, count);
}
VoiceSocket.SendPCMFrames(data, offset, count);
}

public void Clear()
{
if (VoiceSocket.Server == null) return; //Has been closed
VoiceSocket.ClearPCMFrames();
}
public void Wait()
public void Wait()
{
if (VoiceSocket.Server == null) return; //Has been closed
VoiceSocket.WaitForQueue();
}
}

public void SendVoiceUpdate(ulong? serverId, ulong? channelId)
{
@@ -311,5 +256,5 @@ namespace Discord.Audio
(Service.Config.Mode | AudioMode.Outgoing) == 0,
(Service.Config.Mode | AudioMode.Incoming) == 0);
}
}
}
}

+ 54
- 129
src/Discord.Net.Audio/AudioService.cs View File

@@ -1,22 +1,19 @@
using Nito.AsyncEx;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Audio
{
public class AudioService : IService
public class AudioService : IService
{
private readonly AsyncLock _asyncLock;
private AudioClient _defaultClient; //Only used for single server
private VirtualClient _currentClient; //Only used for single server
private ConcurrentDictionary<ulong, AudioClient> _voiceClients;
private ConcurrentDictionary<User, bool> _talkingUsers;
private int _nextClientId;
private ConcurrentDictionary<User, bool> _talkingUsers;
private int _nextClientId;

public DiscordClient Client { get; private set; }
public AudioServiceConfig Config { get; }
public AudioServiceConfig Config { get; }

public event EventHandler Connected = delegate { };
public event EventHandler<VoiceDisconnectedEventArgs> Disconnected = delegate { };
@@ -24,9 +21,9 @@ namespace Discord.Audio

private void OnConnected()
=> Connected(this, EventArgs.Empty);
private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex)
private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex)
=> Disconnected(this, new VoiceDisconnectedEventArgs(serverId, wasUnexpected, ex));
private void OnUserIsSpeakingUpdated(User user, bool isSpeaking)
private void OnUserIsSpeakingUpdated(User user, bool isSpeaking)
=> UserIsSpeakingUpdated(this, new UserIsSpeakingEventArgs(user, isSpeaking));

public AudioService()
@@ -38,72 +35,41 @@ namespace Discord.Audio
{
}
public AudioService(AudioServiceConfig config)
{
{
Config = config;
_asyncLock = new AsyncLock();

}
void IService.Install(DiscordClient client)
{
Client = client;

if (Config.EnableMultiserver)
_voiceClients = new ConcurrentDictionary<ulong, AudioClient>();
else
{
var logger = Client.Log.CreateLogger("Voice");
_defaultClient = new AudioClient(Client, null, 0);
}
_talkingUsers = new ConcurrentDictionary<User, bool>();

client.GatewaySocket.Disconnected += async (s, e) =>
{
if (Config.EnableMultiserver)
{
var tasks = _voiceClients
.Select(x =>
{
var val = x.Value;
if (val != null)
return x.Value.Disconnect();
else
return TaskHelper.CompletedTask;
})
.ToArray();
await Task.WhenAll(tasks).ConfigureAwait(false);
_voiceClients.Clear();
}
foreach (var member in _talkingUsers)
{
bool ignored;
if (_talkingUsers.TryRemove(member.Key, out ignored))
OnUserIsSpeakingUpdated(member.Key, false);
}
};
}

public IAudioClient GetClient(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));

if (Config.EnableMultiserver)
void IService.Install(DiscordClient client)
{
Client = client;

_voiceClients = new ConcurrentDictionary<ulong, AudioClient>();

_talkingUsers = new ConcurrentDictionary<User, bool>();

client.GatewaySocket.Disconnected += (s, e) =>
{
AudioClient client;
if (_voiceClients.TryGetValue(server.Id, out client))
return client;
else
return null;
}
foreach (var member in _talkingUsers)
{
bool ignored;
if (_talkingUsers.TryRemove(member.Key, out ignored))
OnUserIsSpeakingUpdated(member.Key, false);
}
};
}

public IAudioClient GetClient(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
AudioClient client;
if (_voiceClients.TryGetValue(server.Id, out client))
return client;
else
{
if (server == _currentClient.Server)
return _currentClient;
else
return null;
}
}
//Called from AudioClient.Disconnect
return null;
}

//Called from AudioClient.Cleanup
internal async Task RemoveClient(Server server, AudioClient client)
{
using (await _asyncLock.LockAsync().ConfigureAwait(false))
@@ -113,81 +79,40 @@ namespace Discord.Audio
}
}

public async Task<IAudioClient> Join(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
public async Task<IAudioClient> Join(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
var server = channel.Server;
using (await _asyncLock.LockAsync().ConfigureAwait(false))
{
if (Config.EnableMultiserver)
{
AudioClient client;
if (!_voiceClients.TryGetValue(server.Id, out client))
{
client = new AudioClient(Client, server, unchecked(++_nextClientId));
_voiceClients[server.Id] = client;

await client.Connect().ConfigureAwait(false);

/*voiceClient.VoiceSocket.FrameReceived += (s, e) =>
{
OnFrameReceieved(e);
};
voiceClient.VoiceSocket.UserIsSpeaking += (s, e) =>
{
var user = server.GetUser(e.UserId);
OnUserIsSpeakingUpdated(user, e.IsSpeaking);
};*/
}

await client.Join(channel).ConfigureAwait(false);
return client;
}
else
AudioClient client;
if (!_voiceClients.TryGetValue(server.Id, out client))
{
if (_defaultClient.Server != server)
{
await _defaultClient.Disconnect().ConfigureAwait(false);
_defaultClient.VoiceSocket.Server = server;
await _defaultClient.Connect().ConfigureAwait(false);
}
var client = new VirtualClient(_defaultClient, server);
_currentClient = client;

await client.Join(channel).ConfigureAwait(false);
return client;
client = new AudioClient(Client, server, unchecked(++_nextClientId));
_voiceClients[server.Id] = client;

await client.Connect().ConfigureAwait(false);
}

await client.Join(channel).ConfigureAwait(false);
return client;
}
}
}

public Task Leave(Server server) => Leave(server, null);
public Task Leave(Server server) => Leave(server, null);
public Task Leave(Channel channel) => Leave(channel.Server, channel);
private async Task Leave(Server server, Channel channel)
{
if (server == null) throw new ArgumentNullException(nameof(server));

if (Config.EnableMultiserver)
AudioClient client;
//Potential race condition if changing channels during this call, but that's acceptable
if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel))
{
AudioClient client;
//Potential race condition if changing channels during this call, but that's acceptable
if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel))
{
if (_voiceClients.TryRemove(server.Id, out client))
await client.Disconnect().ConfigureAwait(false);
}
}
else
{
using (await _asyncLock.LockAsync().ConfigureAwait(false))
{
var client = GetClient(server) as VirtualClient;
if (client != null && client.Channel == channel)
await _defaultClient.Disconnect().ConfigureAwait(false);
}
if (_voiceClients.TryRemove(server.Id, out client))
await client.Disconnect().ConfigureAwait(false);
}

}
}
}

+ 0
- 7
src/Discord.Net.Audio/AudioServiceConfig.cs View File

@@ -7,11 +7,6 @@

/// <summary> Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. </summary>
public bool EnableEncryption { get; set; } = true;
/// <summary>
/// Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server).
/// This option uses a lot of CPU power and network bandwidth, as a new gateway connection needs to be spun up per server. Use sparingly.
/// </summary>
public bool EnableMultiserver { get; set; } = false;

/// <summary> Gets or sets the buffer length (in milliseconds) for outgoing voice packets. </summary>
public int BufferLength { get; set; } = 1000;
@@ -30,7 +25,6 @@
public AudioMode Mode { get; }

public bool EnableEncryption { get; }
public bool EnableMultiserver { get; }

public int BufferLength { get; }
public int? Bitrate { get; }
@@ -41,7 +35,6 @@
Mode = builder.Mode;

EnableEncryption = builder.EnableEncryption;
EnableMultiserver = builder.EnableMultiserver;

BufferLength = builder.BufferLength;
Bitrate = builder.Bitrate;


+ 3
- 3
src/Discord.Net.Audio/Discord.Net.Audio.xproj View File

@@ -4,12 +4,12 @@
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>dff7afe3-ca77-4109-bade-b4b49a4f6648</ProjectGuid>
<RootNamespace>Discord.Audio</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
@@ -17,5 +17,5 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

+ 9
- 9
src/Discord.Net.Audio/Net/VoiceSocket.cs View File

@@ -120,7 +120,7 @@ namespace Discord.Net.WebSockets

SendIdentify(_userId.Value, _sessionId);

#if !DOTNET5_4
#if !NETSTANDARD1_3
tasks.Add(WatcherAsync());
#endif
tasks.AddRange(_engine.GetTasks(CancelToken));
@@ -178,7 +178,7 @@ namespace Discord.Net.WebSockets
await Task.Delay(1).ConfigureAwait(false);
if (_udp.Available > 0)
{
#if !DOTNET5_4
#if !NETSTANDARD1_3
packet = _udp.Receive(ref endpoint);
#else
//TODO: Is this really the only way to end a Receive call in DOTNET5_4?
@@ -346,7 +346,7 @@ namespace Discord.Net.WebSockets
{
try
{
_udp.Send(voicePacket, rtpPacketLength);
await _udp.SendAsync(voicePacket, rtpPacketLength, _endpoint).ConfigureAwait(false);
}
catch (SocketException ex)
{
@@ -371,7 +371,7 @@ namespace Discord.Net.WebSockets
break;
}
}
await _udp.SendAsync(pingPacket, pingPacket.Length).ConfigureAwait(false);
await _udp.SendAsync(pingPacket, pingPacket.Length, _endpoint).ConfigureAwait(false);
nextPingTicks = currentTicks + 5 * ticksPerSeconds;
}
}
@@ -391,7 +391,7 @@ namespace Discord.Net.WebSockets
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } //Includes ObjectDisposedException
}
#if !DOTNET5_4
#if !NETSTANDARD1_3
//Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken
private async Task WatcherAsync()
{
@@ -407,7 +407,7 @@ namespace Discord.Net.WebSockets
WebSocketMessage msg;
using (var reader = new JsonTextReader(new StringReader(json)))
msg = _serializer.Deserialize(reader, typeof(WebSocketMessage)) as WebSocketMessage;
var opCode = (OpCodes)msg.Operation;
switch (opCode)
{
@@ -418,7 +418,8 @@ namespace Discord.Net.WebSockets
var payload = (msg.Payload as JToken).ToObject<ReadyEvent>(_serializer);
_heartbeatInterval = payload.HeartbeatInterval;
_ssrc = payload.SSRC;
var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault();
string hostname = Host.Replace("wss://", "");
var address = (await Dns.GetHostAddressesAsync(hostname).ConfigureAwait(false)).FirstOrDefault();
_endpoint = new IPEndPoint(address, payload.Port);

if (_audioConfig.EnableEncryption)
@@ -436,7 +437,6 @@ namespace Discord.Net.WebSockets
_encryptionMode = UnencryptedMode;
_isEncrypted = false;
}
_udp.Connect(_endpoint);

_sequence = 0;// (ushort)_rand.Next(0, ushort.MaxValue);
//No thread issue here because SendAsync doesn't start until _isReady is true
@@ -445,7 +445,7 @@ namespace Discord.Net.WebSockets
packet[1] = (byte)(_ssrc >> 16);
packet[2] = (byte)(_ssrc >> 8);
packet[3] = (byte)(_ssrc >> 0);
await _udp.SendAsync(packet, 70).ConfigureAwait(false);
await _udp.SendAsync(packet, 70, _endpoint).ConfigureAwait(false);
}
}
break;


+ 26
- 13
src/Discord.Net.Audio/project.json View File

@@ -1,27 +1,40 @@
{
"version": "0.9.0-rc3",
"version": "0.9.6",
"description": "A Discord.Net extension adding voice support.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
"packOptions": {
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
},
"contentFiles": [ "libsodium.dll", "opus.dll" ]
},
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
"contentFiles": [ "libsodium.dll", "opus.dll" ],

"compilationOptions": {
"buildOptions": {
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
"allowUnsafe": true,
"warningsAsErrors": true
},

"dependencies": {
"Discord.Net": "0.9.0-rc3-3"
"Discord.Net": "0.9.6"
},

"frameworks": {
"net45": { },
"dotnet5.4": { }
"netstandard1.3": {
"dependencies": {
"NETStandard.Library": "1.6.0"
},
"imports": [
"dotnet5.4",
"dnxcore50",
"portable-net45+win8"
]
},
"net45": { }
}
}

+ 2
- 2
src/Discord.Net.Commands.Net45/Properties/AssemblyInfo.cs View File

@@ -13,6 +13,6 @@ using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")]

[assembly: AssemblyVersion("0.9.0.0")]
[assembly: AssemblyFileVersion("0.9.0.0")]
[assembly: AssemblyVersion("0.9.6.0")]
[assembly: AssemblyFileVersion("0.9.6.0")]


+ 20
- 1
src/Discord.Net.Commands/CommandService.cs View File

@@ -80,7 +80,13 @@ namespace Discord.Commands
client.MessageReceived += async (s, e) =>
{
if (_allCommands.Count == 0) return;
if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return;

if (Config.IsSelfBot)
{
if (e.Message.User == null || e.Message.User.Id != Client.CurrentUser.Id) return; // Will only listen to Self
}
else
if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return; // Normal expected behavior for bots

string msg = e.Message.RawText;
if (msg.Length == 0) return;
@@ -106,6 +112,19 @@ namespace Discord.Commands
if (msg.StartsWith(mention) && msg.Length > mention.Length)
cmdMsg = msg.Substring(mention.Length + 1);
}

string mention2 = client.CurrentUser.NicknameMention;
if (mention2 != null)
{
if (msg.StartsWith(mention2) && msg.Length > mention2.Length)
cmdMsg = msg.Substring(mention2.Length + 1);
else
{
mention2 = $"@{client.CurrentUser.Name}";
if (msg.StartsWith(mention2) && msg.Length > mention2.Length)
cmdMsg = msg.Substring(mention2.Length + 1);
}
}
}
//Check using custom activator


+ 8
- 1
src/Discord.Net.Commands/CommandServiceConfig.cs View File

@@ -14,6 +14,11 @@ namespace Discord.Commands
/// and a negative value if the message should be ignored.
/// </summary>
public Func<Message, int> CustomPrefixHandler { get; set; } = null;
/// <summary>
/// Changing this to true makes the bot ignore all messages, except when the messages are from its own account.
/// This is desired behavior for "Self Bots" only, so unless this bot is being run under a normal user's account, leave it alone!!
/// </summary>
public bool IsSelfBot { get; set; } = false;

/// <summary> Gets or sets whether a help function should be automatically generated. </summary>
public HelpMode HelpMode { get; set; } = HelpMode.Disabled;
@@ -28,9 +33,10 @@ namespace Discord.Commands
}
public class CommandServiceConfig
{
public char? PrefixChar { get; }
public char? PrefixChar { get; }
public bool AllowMentionPrefix { get; }
public Func<Message, int> CustomPrefixHandler { get; }
public bool IsSelfBot { get; }

/// <summary> Gets or sets whether a help function should be automatically generated. </summary>
public HelpMode HelpMode { get; set; } = HelpMode.Disabled;
@@ -41,6 +47,7 @@ namespace Discord.Commands
AllowMentionPrefix = builder.AllowMentionPrefix;
CustomPrefixHandler = builder.CustomPrefixHandler;
HelpMode = builder.HelpMode;
IsSelfBot = builder.IsSelfBot;
}
}
}

+ 3
- 3
src/Discord.Net.Commands/Discord.Net.Commands.xproj View File

@@ -4,12 +4,12 @@
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>19793545-ef89-48f4-8100-3ebaad0a9141</ProjectGuid>
<RootNamespace>Discord.Commands</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
@@ -17,5 +17,5 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

+ 25
- 12
src/Discord.Net.Commands/project.json View File

@@ -1,25 +1,38 @@
{
"version": "0.9.0-rc3-1",
"version": "0.9.6",
"description": "A Discord.Net extension adding basic command support.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"

"packOptions": {
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
}
},
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],

"compilationOptions": {
"buildOptions": {
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
"warningsAsErrors": true
},

"dependencies": {
"Discord.Net": "0.9.0-rc3-3"
"Discord.Net": "0.9.6"
},

"frameworks": {
"net45": { },
"dotnet5.4": { }
"netstandard1.3": {
"dependencies": {
"NETStandard.Library": "1.6.0"
},
"imports": [
"dotnet5.4",
"dnxcore50",
"portable-net45+win8"
]
},
"net45": {}
}
}

+ 2
- 2
src/Discord.Net.Modules.Net45/Properties/AssemblyInfo.cs View File

@@ -13,6 +13,6 @@ using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")]

[assembly: AssemblyVersion("0.9.0.0")]
[assembly: AssemblyFileVersion("0.9.0.0")]
[assembly: AssemblyVersion("0.9.6.0")]
[assembly: AssemblyFileVersion("0.9.6.0")]


+ 3
- 3
src/Discord.Net.Modules/Discord.Net.Modules.xproj View File

@@ -4,12 +4,12 @@
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>01584e8a-78da-486f-9ef9-a894e435841b</ProjectGuid>
<RootNamespace>Discord.Modules</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
@@ -17,5 +17,5 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

+ 2
- 5
src/Discord.Net.Modules/ModuleManager.cs View File

@@ -43,15 +43,15 @@ namespace Discord.Modules
public event EventHandler<UserEventArgs> UserJoined = delegate { };
public event EventHandler<UserEventArgs> UserLeft = delegate { };
public event EventHandler<UserUpdatedEventArgs> UserUpdated = delegate { };
//public event EventHandler<UserEventArgs> UserPresenceUpdated = delegate { };
//public event EventHandler<UserEventArgs> UserVoiceStateUpdated = delegate { };
public event EventHandler<UserEventArgs> UserUnbanned = delegate { };
public event EventHandler<ChannelUserEventArgs> UserIsTyping = delegate { };

public event EventHandler<MessageEventArgs> MessageReceived = delegate { };
[Obsolete]
public event EventHandler<MessageEventArgs> MessageSent = delegate { };
public event EventHandler<MessageEventArgs> MessageDeleted = delegate { };
public event EventHandler<MessageUpdatedEventArgs> MessageUpdated = delegate { };
[Obsolete]
public event EventHandler<MessageEventArgs> MessageReadRemotely = delegate { };
private readonly bool _useServerWhitelist, _useChannelWhitelist, _allowAll, _allowPrivate;
@@ -99,10 +99,8 @@ namespace Discord.Modules
client.ChannelUpdated += (s, e) => { if (HasChannel(e.After)) ChannelUpdated(s, e); };

client.MessageReceived += (s, e) => { if (HasChannel(e.Channel)) MessageReceived(s, e); };
client.MessageSent += (s, e) => { if (HasChannel(e.Channel)) MessageSent(s, e); };
client.MessageDeleted += (s, e) => { if (HasChannel(e.Channel)) MessageDeleted(s, e); };
client.MessageUpdated += (s, e) => { if (HasChannel(e.Channel)) MessageUpdated(s, e); };
client.MessageAcknowledged += (s, e) => { if (HasChannel(e.Channel)) MessageReadRemotely(s, e); };

client.RoleCreated += (s, e) => { if (HasIndirectServer(e.Server)) RoleCreated(s, e); };
client.RoleUpdated += (s, e) => { if (HasIndirectServer(e.Server)) RoleUpdated(s, e); };
@@ -185,7 +183,6 @@ namespace Discord.Modules
public void DisableAllServers()
{
if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist.");
if (!_useServerWhitelist) return;

using (_lock.Lock())
{


+ 26
- 13
src/Discord.Net.Modules/project.json View File

@@ -1,26 +1,39 @@
{
"version": "0.9.0-rc3",
"version": "0.9.6",
"description": "A Discord.Net extension adding basic plugin support.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"

"packOptions": {
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
}
},
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],

"compilationOptions": {
"buildOptions": {
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
"warningsAsErrors": true
},

"dependencies": {
"Discord.Net": "0.9.0-rc3-3",
"Discord.Net.Commands": "0.9.0-rc3-1"
"Discord.Net": "0.9.6",
"Discord.Net.Commands": "0.9.6"
},

"frameworks": {
"net45": { },
"dotnet5.4": { }
"netstandard1.3": {
"dependencies": {
"NETStandard.Library": "1.6.0"
},
"imports": [
"dotnet5.4",
"dnxcore50",
"portable-net45+win8"
]
},
"net45": {}
}
}

+ 20
- 5
src/Discord.Net.Net45/Discord.Net.csproj View File

@@ -70,6 +70,9 @@
<Compile Include="..\Discord.Net\API\Client\Common\ExtendedMember.cs">
<Link>API\Client\Common\ExtendedMember.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Client\Common\Game.cs">
<Link>API\Client\Common\Game.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Client\Common\Guild.cs">
<Link>API\Client\Common\Guild.cs</Link>
</Compile>
@@ -199,8 +202,8 @@
<Compile Include="..\Discord.Net\API\Client\GatewaySocket\Events\Ready.cs">
<Link>API\Client\GatewaySocket\Events\Ready.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Client\GatewaySocket\Events\Redirect.cs">
<Link>API\Client\GatewaySocket\Events\Redirect.cs</Link>
<Compile Include="..\Discord.Net\API\Client\GatewaySocket\Events\Reconnect.cs">
<Link>API\Client\GatewaySocket\Events\Reconnect.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Client\GatewaySocket\Events\Resumed.cs">
<Link>API\Client\GatewaySocket\Events\Resumed.cs</Link>
@@ -238,6 +241,9 @@
<Compile Include="..\Discord.Net\API\Client\Rest\AddGuildBan.cs">
<Link>API\Client\Rest\AddGuildBan.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Client\Rest\BulkMessageDelete.cs">
<Link>API\Client\Rest\BulkMessageDelete.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Client\Rest\CreateChannel.cs">
<Link>API\Client\Rest\CreateChannel.cs</Link>
</Compile>
@@ -337,6 +343,9 @@
<Compile Include="..\Discord.Net\API\Client\Rest\UpdateMessage.cs">
<Link>API\Client\Rest\UpdateMessage.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Client\Rest\UpdateOwnNick.cs">
<Link>API\Client\Rest\UpdateOwnNick.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Client\Rest\UpdateProfile.cs">
<Link>API\Client\Rest\UpdateProfile.cs</Link>
</Compile>
@@ -409,6 +418,9 @@
<Compile Include="..\Discord.Net\Enums\ConnectionState.cs">
<Link>Enums\ConnectionState.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Enums\GameType.cs">
<Link>Enums\GameType.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Enums\ImageType.cs">
<Link>Enums\ImageType.cs</Link>
</Compile>
@@ -430,6 +442,9 @@
<Compile Include="..\Discord.Net\Enums\StringEnum.cs">
<Link>Enums\StringEnum.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Enums\TokenType.cs">
<Link>Enums\TokenType.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Enums\UserStatus.cs">
<Link>Enums\UserStatus.cs</Link>
</Compile>
@@ -517,6 +532,9 @@
<Compile Include="..\Discord.Net\Models\Color.cs">
<Link>Models\Color.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Models\Game.cs">
<Link>Models\Game.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Models\Invite.cs">
<Link>Models\Invite.cs</Link>
</Compile>
@@ -547,9 +565,6 @@
<Compile Include="..\Discord.Net\Net\Rest\CompletedRequestEventArgs.cs">
<Link>Net\Rest\CompletedRequestEventArgs.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\Rest\ETFRestClient.cs">
<Link>Net\Rest\ETFRestClient.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\Rest\IRestEngine.cs">
<Link>Net\Rest\IRestEngine.cs</Link>
</Compile>


+ 2
- 2
src/Discord.Net.Net45/Properties/AssemblyInfo.cs View File

@@ -13,5 +13,5 @@ using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")]

[assembly: AssemblyVersion("0.9.0.0")]
[assembly: AssemblyFileVersion("0.9.0.0")]
[assembly: AssemblyVersion("0.9.6.0")]
[assembly: AssemblyFileVersion("0.9.6.0")]

+ 5
- 5
src/Discord.Net.Shared/TaskExtensions.cs View File

@@ -53,15 +53,15 @@ namespace Discord
}
}

public static async Task Wait(this CancellationTokenSource tokenSource)
public static async Task Wait(this CancellationTokenSource cancelTokenSource)
{
var token = tokenSource.Token;
try { await Task.Delay(-1, token).ConfigureAwait(false); }
var cancelToken = cancelTokenSource.Token;
try { await Task.Delay(-1, cancelToken).ConfigureAwait(false); }
catch (OperationCanceledException) { } //Expected
}
public static async Task Wait(this CancellationToken token)
public static async Task Wait(this CancellationToken cancelToken)
{
try { await Task.Delay(-1, token).ConfigureAwait(false); }
try { await Task.Delay(-1, cancelToken).ConfigureAwait(false); }
catch (OperationCanceledException) { } //Expected
}
}


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

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

namespace Discord.API.Client
{
public class Game
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("type")]
public GameType? Type { get; set; }
}
}

+ 2
- 0
src/Discord.Net/API/Client/Common/Member.cs View File

@@ -10,5 +10,7 @@ namespace Discord.API.Client
public DateTime? JoinedAt { get; set; }
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))]
public ulong[] Roles { get; set; }
[JsonProperty("nick")]
public string Nick { get; set; } = "";
}
}

+ 1
- 6
src/Discord.Net/API/Client/Common/MemberPresence.cs View File

@@ -5,13 +5,8 @@ namespace Discord.API.Client
{
public class MemberPresence : MemberReference
{
public class GameInfo
{
[JsonProperty("name")]
public string Name { get; set; }
}
[JsonProperty("game")]
public GameInfo Game { get; set; }
public Game Game { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))]


+ 2
- 0
src/Discord.Net/API/Client/Common/Role.cs View File

@@ -19,5 +19,7 @@ namespace Discord.API.Client
public uint? Color { get; set; }
[JsonProperty("managed")]
public bool? Managed { get; set; }
[JsonProperty("mentionable")]
public bool? Mentionable { get; set; }
}
}

+ 2
- 0
src/Discord.Net/API/Client/Common/UserReference.cs View File

@@ -13,5 +13,7 @@ namespace Discord.API.Client
public ushort? Discriminator { get; set; }
[JsonProperty("avatar")]
public string Avatar { get; set; }
[JsonProperty("bot")]
public bool? Bot { get; set; }
}
}

+ 2
- 2
src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs View File

@@ -10,8 +10,6 @@ namespace Discord.API.Client.GatewaySocket
object IWebSocketMessage.Payload => this;
bool IWebSocketMessage.IsPrivate => false;

[JsonProperty("v")]
public int Version { get; set; }
[JsonProperty("token")]
public string Token { get; set; }
[JsonProperty("properties")]
@@ -20,5 +18,7 @@ namespace Discord.API.Client.GatewaySocket
public int LargeThreshold { get; set; }
[JsonProperty("compress")]
public bool UseCompression { get; set; }
[JsonProperty("shard", NullValueHandling = NullValueHandling.Ignore)]
public int[] ShardingParams { get; set; }
}
}

+ 6
- 8
src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs View File

@@ -9,15 +9,13 @@ namespace Discord.API.Client.GatewaySocket
object IWebSocketMessage.Payload => this;
bool IWebSocketMessage.IsPrivate => false;

public class GameInfo
{
[JsonProperty("name")]
public string Name { get; set; }
}

[JsonProperty("idle_since")]
[JsonProperty("afk")]
public bool? Afk { get; set; }
[JsonProperty("since")]
public long? IdleSince { get; set; }
[JsonProperty("game")]
public GameInfo Game { get; set; }
public Game Game { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
}
}

+ 6
- 0
src/Discord.Net/API/Client/GatewaySocket/Events/Reconnect.cs View File

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

namespace Discord.API.Client.GatewaySocket
{
public class ReconnectEvent { }
}

+ 0
- 10
src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs View File

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

namespace Discord.API.Client.GatewaySocket
{
public class RedirectEvent
{
[JsonProperty("url")]
public string Url { get; set; }
}
}

+ 9
- 3
src/Discord.Net/API/Client/GatewaySocket/OpCodes.cs View File

@@ -17,8 +17,14 @@
/// <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>
Redirect = 7,
/// <summary> C→S - Used to request all members that were withheld by large_threshold </summary>
RequestGuildMembers = 8
Reconnect = 7,
/// <summary> C→S - Used to request all members that were withheld by large_threshold. </summary>
RequestGuildMembers = 8,
/// <summary> C←S - Used to notify the client of an invalid session id. </summary>
InvalidSession = 9,
/// <summary> C←S - Used to receive heartbeat_interval information and initiate the websocket connection. </summary>
Hello = 10,
/// <summary> C←S - Used to acknowledge a heartbeat by the client. </summary>
HeartbeatACK = 11
}
}

+ 23
- 0
src/Discord.Net/API/Client/Rest/BulkMessageDelete.cs View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;
namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class BulkMessageDelete : IRestRequest
{
string IRestRequest.Endpoint => $"/channels/{ChannelId}/messages/bulk_delete";
string IRestRequest.Method => "POST";
object IRestRequest.Payload => this;
public ulong ChannelId { get; set; }
public BulkMessageDelete(ulong channelId, ulong[] messageIds)
{
ChannelId = channelId;
MessageIds = messageIds;
}
[JsonProperty("messages")]
public ulong[] MessageIds { get; set; }
}
}

+ 4
- 4
src/Discord.Net/API/Client/Rest/Gateway.cs View File

@@ -9,10 +9,10 @@ namespace Discord.API.Client.Rest
string IRestRequest.Endpoint => $"gateway";
object IRestRequest.Payload => null;
}
public class GatewayResponse
{
[JsonProperty("url")]
public string Url { get; set; }
{
[JsonProperty("url")]
public string Url { get; set; }
}
}

+ 3
- 0
src/Discord.Net/API/Client/Rest/UpdateMember.cs View File

@@ -1,5 +1,6 @@
using Discord.API.Converters;
using Newtonsoft.Json;
using System.ComponentModel;

namespace Discord.API.Client.Rest
{
@@ -21,6 +22,8 @@ namespace Discord.API.Client.Rest
public ulong? VoiceChannelId { get; set; }
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))]
public ulong[] RoleIds { get; set; }
[JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)]
public string Nickname { get; set; }

public UpdateMemberRequest(ulong guildId, ulong userId)
{


+ 23
- 0
src/Discord.Net/API/Client/Rest/UpdateOwnNick.cs View File

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

namespace Discord.API.Client.Rest
{
[JsonObject(MemberSerialization.OptIn)]
public class UpdateOwnNick : IRestRequest
{
string IRestRequest.Method => "PATCH";
string IRestRequest.Endpoint => $"guilds/{GuildId}/members/@me/nick";
object IRestRequest.Payload => this;

public ulong GuildId { get; set; }

[JsonProperty("nick")]
public string Nickname { get; set; }

public UpdateOwnNick(ulong guildId, string nickname)
{
GuildId = guildId;
Nickname = nickname;
}
}
}

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

@@ -18,6 +18,8 @@ namespace Discord.API.Client.Rest
public uint Permissions { get; set; }
[JsonProperty("hoist")]
public bool IsHoisted { get; set; }
[JsonProperty("mentionable")]
public bool IsMentionable { get; set; }
[JsonProperty("color")]
public uint Color { get; set; }



+ 3
- 3
src/Discord.Net/Discord.Net.xproj View File

@@ -4,12 +4,12 @@
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>acfb060b-ec8a-4926-b293-04c01e17ee23</ProjectGuid>
<RootNamespace>Discord</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
@@ -17,5 +17,5 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

+ 33
- 2
src/Discord.Net/DiscordClient.Events.cs View File

@@ -7,42 +7,73 @@ namespace Discord
{
public event EventHandler Ready = delegate { };
//public event EventHandler<DisconnectedEventArgs> LoggedOut = delegate { };
/// <summary>When a new channel is created, relevant to the current user.
/// Discord API Event name: CHANNEL_CREATE.</summary>
public event EventHandler<ChannelEventArgs> ChannelCreated = delegate { };
/// <summary>When a channel relevant to the current user is deleted.
/// Discord API Event name: CHANNEL_DELETE.</summary>
public event EventHandler<ChannelEventArgs> ChannelDestroyed = delegate { };
/// <summary>When a channel is updated. Discord API Event name: CHANNEL_UPDATE.</summary>
public event EventHandler<ChannelUpdatedEventArgs> ChannelUpdated = delegate { };
[Obsolete]
public event EventHandler<MessageEventArgs> MessageAcknowledged = delegate { };
/// <summary>When a message is deleted. Discord API Event name: MESSAGE_DELETE.</summary>
public event EventHandler<MessageEventArgs> MessageDeleted = delegate { };
/// <summary>When a message is created. Discord API Event name: MESSAGE_CREATE.</summary>
public event EventHandler<MessageEventArgs> MessageReceived = delegate { };
[Obsolete]
public event EventHandler<MessageEventArgs> MessageSent = delegate { };
/// <summary>When a message is updated. Discord API Event name: MESSAGE_UPDATE.</summary>
public event EventHandler<MessageUpdatedEventArgs> MessageUpdated = delegate { };
/// <summary>When properties about the user change. Discord API Event name: USER_UPDATE.</summary>
public event EventHandler<ProfileUpdatedEventArgs> ProfileUpdated = delegate { };
/// <summary>When a server role is created. Discord API Event name: GUILD_ROLE_CREATE.</summary>
public event EventHandler<RoleEventArgs> RoleCreated = delegate { };
/// <summary>When a server role is updated. Discord API Event name: GUILD_ROLE_UPDATE.</summary>
public event EventHandler<RoleUpdatedEventArgs> RoleUpdated = delegate { };
/// <summary>When a server role is deleted. Discord API Event name: GUILD_ROLE_DELETE.</summary>
public event EventHandler<RoleEventArgs> RoleDeleted = delegate { };
/// <summary>When the current user joins a new server. Discord API Event name: GUILD_CREATE.</summary>
public event EventHandler<ServerEventArgs> JoinedServer = delegate { };
/// <summary>When the user leaves or is removed from a server.
/// Discord API Event name: GUILD_DELETE.</summary>
public event EventHandler<ServerEventArgs> LeftServer = delegate { };
/// <summary>When a server becomes available (current user is initially connecting,
/// or a server becomes available again).
/// Discord API Event name: GUILD_CREATE or GUILD_MEMBERS_CHUNK.</summary>
public event EventHandler<ServerEventArgs> ServerAvailable = delegate { };
/// <summary>When a server is updated. Discord API Event name: GUILD_UPDATE.</summary>
public event EventHandler<ServerUpdatedEventArgs> ServerUpdated = delegate { };
/// <summary>When a server becomes unavailable during a server outage.
/// Discord API Event name: GUILD_DELETE.</summary>
public event EventHandler<ServerEventArgs> ServerUnavailable = delegate { };
/// <summary>When a user is banned from a server. Discord API Event name: GUILD_BAN_ADD.</summary>
public event EventHandler<UserEventArgs> UserBanned = delegate { };
/// <summary>When a user starts typing in a channel. Discord API Event name: TYPING_START.</summary>
public event EventHandler<ChannelUserEventArgs> UserIsTyping = delegate { };
/// <summary>When a new user joins a server. Discord API Event name: GUILD_MEMBER_ADD.</summary>
public event EventHandler<UserEventArgs> UserJoined = delegate { };
/// <summary>When a user is removed from a server (leave/kick/ban).
/// Discord API Event name: GUILD_MEMBER_REMOVE.</summary>
public event EventHandler<UserEventArgs> UserLeft = delegate { };
/// <summary>When a server member is updated.
/// Discord API Event name: GUILD_MEMBER_UPDATE or PRESENCE_UPDATE or VOICE_STATE_UPDATE.</summary>
public event EventHandler<UserUpdatedEventArgs> UserUpdated = delegate { };
/// <summary>When a user is unbanned from a server. Discord API Event name: GUILD_BAN_REMOVE.</summary>
public event EventHandler<UserEventArgs> UserUnbanned = delegate { };

private void OnReady()
=> OnEvent(Ready);
/*private void OnLoggedOut(bool wasUnexpected, Exception ex)
=> OnEvent(LoggedOut, new DisconnectedEventArgs(wasUnexpected, ex));*/
private void OnChannelCreated(Channel channel)
=> OnEvent(ChannelCreated, new ChannelEventArgs(channel));
private void OnChannelDestroyed(Channel channel)
=> OnEvent(ChannelDestroyed, new ChannelEventArgs(channel));
private void OnChannelUpdated(Channel before, Channel after)
=> OnEvent(ChannelUpdated, new ChannelUpdatedEventArgs(before, after));
private void OnMessageAcknowledged(Message msg)
=> OnEvent(MessageAcknowledged, new MessageEventArgs(msg));
private void OnMessageDeleted(Message msg)


+ 96
- 53
src/Discord.Net/DiscordClient.cs View File

@@ -33,6 +33,7 @@ namespace Discord
private ConcurrentDictionary<ulong, Channel> _privateChannels; //Key = RecipientId
private Dictionary<string, Region> _regions;
private Stopwatch _connectionStopwatch;
private ConcurrentQueue<ulong> _largeServers;

internal Logger Logger { get; }

@@ -64,7 +65,7 @@ namespace Discord
/// <summary> Gets the status of the current user. </summary>
public UserStatus Status { get; private set; }
/// <summary> Gets the game the current user is displayed as playing. </summary>
public string CurrentGame { get; private set; }
public Game CurrentGame { get; private set; }

/// <summary> Gets a collection of all extensions added to this DiscordClient. </summary>
public IEnumerable<IService> Services => _services;
@@ -125,6 +126,7 @@ namespace Discord
_servers = new ConcurrentDictionary<ulong, Server>(2, 0);
_channels = new ConcurrentDictionary<ulong, Channel>(2, 0);
_privateChannels = new ConcurrentDictionary<ulong, Channel>(2, 0);
_largeServers = new ConcurrentQueue<ulong>();

//Serialization
Serializer = new JsonSerializer();
@@ -149,7 +151,7 @@ namespace Discord

//GatewaySocket.Disconnected += (s, e) => OnDisconnected(e.WasUnexpected, e.Exception);
GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e);
MessageQueue = new MessageQueue(ClientAPI, Log.CreateLogger("MessageQueue"));

//Extensibility
@@ -167,14 +169,14 @@ namespace Discord
return ClientAPI.Token;
}
/// <summary> Connects to the Discord server with the provided token. </summary>
public async Task Connect(string token)
public async Task Connect(string token, TokenType tokenType)
{
if (token == null) throw new ArgumentNullException(token);

await BeginConnect(null, null, token).ConfigureAwait(false);
await BeginConnect(null, null, token, tokenType).ConfigureAwait(false);
}

private async Task BeginConnect(string email, string password, string token = null)
private async Task BeginConnect(string email, string password, string token = null, TokenType tokenType = TokenType.User)
{
try
{
@@ -197,10 +199,21 @@ namespace Discord
ClientAPI.CancelToken = CancelToken;
StatusAPI.CancelToken = CancelToken;

switch (tokenType)
{
case TokenType.Bot:
token = $"Bot {token}";
break;
case TokenType.User:
break;
default:
throw new ArgumentException("Unknown oauth token type", nameof(tokenType));
}

await Login(email, password, token).ConfigureAwait(false);
await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false);

var tasks = new[] { CancelToken.Wait() }
var tasks = new[] { CancelToken.Wait(), LargeServerDownloader(CancelToken) }
.Concat(MessageQueue.Run(CancelToken));

await _taskManager.Start(tasks, cancelSource).ConfigureAwait(false);
@@ -241,12 +254,15 @@ namespace Discord
}

ClientAPI.Token = token;
var request = new LoginRequest() { Email = email, Password = password };
var response = await ClientAPI.Send(request).ConfigureAwait(false);
token = response.Token;
if (Config.CacheDir != null && token != oldToken && tokenPath != null)
SaveToken(tokenPath, cacheKey, token);
ClientAPI.Token = token;
if (email != null && password != null)
{
var request = new LoginRequest() { Email = email, Password = password };
var response = await ClientAPI.Send(request).ConfigureAwait(false);
token = response.Token;
if (Config.CacheDir != null && token != oldToken && tokenPath != null)
SaveToken(tokenPath, cacheKey, token);
ClientAPI.Token = token;
}

//Cache other stuff
var regionsResponse = (await ClientAPI.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false));
@@ -274,7 +290,7 @@ namespace Discord

/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary>
public Task Disconnect() => _taskManager.Stop(true);
private async Task Cleanup()
private async Task Cleanup()
{
var oldState = State;
State = ConnectionState.Disconnecting;
@@ -284,7 +300,10 @@ namespace Discord
try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); }
catch (OperationCanceledException) { }
}

ulong serverId;
while (_largeServers.TryDequeue(out serverId)) { }

MessageQueue.Clear();

await GatewaySocket.Disconnect().ConfigureAwait(false);
@@ -296,26 +315,36 @@ namespace Discord

PrivateUser = null;
CurrentUser = null;
State = (int)ConnectionState.Disconnected;
_connectedEvent.Reset();
_disconnectedEvent.Set();
}
public void SetStatus(UserStatus status)
{
if (status == null) throw new ArgumentNullException(nameof(status));
if (status != UserStatus.Online && status != UserStatus.Idle)
throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status));
if (status != UserStatus.Online && status != UserStatus.Idle && status != UserStatus.DoNotDisturb && status != UserStatus.Invisible)
throw new ArgumentException($"Invalid status, must be {UserStatus.Online}, {UserStatus.Idle}, {UserStatus.DoNotDisturb} or {UserStatus.Invisible}", nameof(status));

Status = status;
SendStatus();
}
public void SetGame(string game)
public void SetGame(Game game)
{
CurrentGame = game;
SendStatus();
}
public void SetGame(string game)
{
CurrentGame = new Game(game);
SendStatus();
}
public void SetGame(string game, GameType type, string url)
{
CurrentGame = new Game(game, type, url);
SendStatus();
}
private void SendStatus()
{
PrivateUser.Status = Status;
@@ -331,7 +360,7 @@ namespace Discord
}
var socket = GatewaySocket;
if (socket != null)
socket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGame);
socket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGame, Status == UserStatus.Idle, Status);
}

#region Channels
@@ -488,15 +517,10 @@ namespace Discord

var data = e.Payload.ToObject<ReadyEvent>(Serializer);

int channelCount = 0;
for (int i = 0; i < data.Guilds.Length; i++)
channelCount += data.Guilds[i].Channels.Length;

//ConcurrencyLevel = 2 (only REST and WebSocket can add/remove)
_servers = new ConcurrentDictionary<ulong, Server>(2, (int)(data.Guilds.Length * 1.05));
_channels = new ConcurrentDictionary<ulong, Channel>(2, (int)(channelCount * 1.05));
_channels = new ConcurrentDictionary<ulong, Channel>(2, (int)(data.Guilds.Length * 2 * 1.05));
_privateChannels = new ConcurrentDictionary<ulong, Channel>(2, (int)(data.PrivateChannels.Length * 1.05));
List<ulong> largeServers = new List<ulong>();

SessionId = data.SessionId;
PrivateUser = new User(this, data.User.Id, null);
@@ -511,9 +535,9 @@ namespace Discord
{
var server = AddServer(model.Id);
server.Update(model);
if (model.IsLarge)
largeServers.Add(server.Id);
}
if (model.IsLarge)
_largeServers.Enqueue(model.Id);
}
for (int i = 0; i < data.PrivateChannels.Length; i++)
{
@@ -521,10 +545,8 @@ namespace Discord
var channel = AddPrivateChannel(model.Id, model.Recipient.Id);
channel.Update(model);
}
if (largeServers.Count > 0)
GatewaySocket.SendRequestMembers(largeServers, "", 0);
else
EndConnect();

EndConnect();
}
break;

@@ -536,15 +558,19 @@ namespace Discord
{
var server = AddServer(data.Id);
server.Update(data);
if (data.Unavailable != false)
{
Logger.Info($"GUILD_CREATE: {server.Path}");
OnJoinedServer(server);
}
else
Logger.Info($"GUILD_AVAILABLE: {server.Path}");

if (data.Unavailable != false)
OnJoinedServer(server);
OnServerAvailable(server);
if (!data.IsLarge)
OnServerAvailable(server);
else
_largeServers.Enqueue(data.Id);
}
}
break;
@@ -707,16 +733,7 @@ namespace Discord
Logger.Verbose($"GUILD_MEMBERS_CHUNK: {data.Members.Length} users");

if (server.CurrentUserCount >= server.UserCount) //Finished downloading for there
{
bool isConnectComplete = true;
foreach (var server2 in _servers.Select(x => x.Value))
{
if (server2.CurrentUserCount < server2.UserCount)
isConnectComplete = false;
}
if (isConnectComplete)
EndConnect();
}
OnServerAvailable(server);
}
else
Logger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild.");
@@ -853,7 +870,7 @@ namespace Discord

msg.Update(data);
user.UpdateActivity();
Logger.Verbose($"MESSAGE_CREATE: {channel.Path} ({user.Name ?? "Unknown"})");
OnMessageReceived(msg);
}
@@ -1046,11 +1063,6 @@ namespace Discord
asyncAction().GetAwaiter().GetResult();
_disconnectedEvent.WaitOne();
}
/// <summary> Blocking call and wait until the client has been manually stopped. This is mainly intended for use in console applications. </summary>
public void Wait()
{
_disconnectedEvent.WaitOne();
}
#endregion

#region IDisposable
@@ -1068,13 +1080,44 @@ namespace Discord
_isDisposed = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion

private Task LargeServerDownloader(CancellationToken cancelToken)
{
//Temporary hotfix to download all large guilds before raising READY
return Task.Run(async () =>
{
try
{
const short batchSize = 50;
ulong[] serverIds = new ulong[batchSize];

while (!cancelToken.IsCancellationRequested && State == ConnectionState.Connecting)
await Task.Delay(1000, cancelToken).ConfigureAwait(false);

while (!cancelToken.IsCancellationRequested && State == ConnectionState.Connected)
{
if (_largeServers.Count > 0)
{
int count = 0;
while (count < batchSize && _largeServers.TryDequeue(out serverIds[count]))
count++;

if (count > 0)
GatewaySocket.SendRequestMembers(serverIds.Take(count), "", 0);
}
await Task.Delay(1250, cancelToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
});
}

//Helpers
private string GetTokenCachePath(string email)
{


+ 10
- 0
src/Discord.Net/DiscordConfig.cs View File

@@ -47,6 +47,11 @@ namespace Discord
/// </summary>
public int LargeThreshold { get; set; } = 250;

/// <summary> Gets or sets the id for this shard. Must be less than TotalShards. </summary>
public int ShardId { get; set; } = 0;
/// <summary> Gets or sets the total number of shards for this application. </summary>
public int TotalShards { get; set; } = 1;

//Events

/// <summary> Gets or sets a handler for all log messages. </summary>
@@ -61,6 +66,7 @@ namespace Discord
internal const int RestTimeout = 10000;
internal const int MessageQueueInterval = 100;
internal const int WebSocketQueueInterval = 100;
internal const int ServerBatchCount = 50;

public const string LibName = "Discord.Net";
public static string LibVersion => typeof(DiscordConfigBuilder).GetTypeInfo().Assembly.GetName().Version.ToString(3);
@@ -88,6 +94,8 @@ namespace Discord
public bool UsePermissionsCache { get; }
public bool EnablePreUpdateEvents { get; }

public int ShardId { get; }
public int TotalShards { get; }

internal DiscordConfig(DiscordConfigBuilder builder)
{
@@ -106,6 +114,8 @@ namespace Discord
MessageCacheSize = builder.MessageCacheSize;
UsePermissionsCache = builder.UsePermissionsCache;
EnablePreUpdateEvents = builder.EnablePreUpdateEvents;
ShardId = builder.ShardId;
TotalShards = builder.TotalShards;
}

private static string GetUserAgent(DiscordConfigBuilder builder)


+ 466
- 466
src/Discord.Net/ETF/ETFReader.cs View File

@@ -1,491 +1,491 @@
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
//using Newtonsoft.Json;
//using System;
//using System.Collections.Concurrent;
//using System.Collections.Generic;
//using System.IO;
//using System.Linq;
//using System.Reflection;
//using System.Reflection.Emit;
//using System.Text;

namespace Discord.ETF
{
public class ETFReader : IDisposable
{
private static readonly ConcurrentDictionary<Type, Delegate> _deserializers = new ConcurrentDictionary<Type, Delegate>();
private static readonly Dictionary<Type, MethodInfo> _readMethods = GetPrimitiveReadMethods();
//namespace Discord.ETF
//{
// public class ETFReader : IDisposable
// {
// private static readonly ConcurrentDictionary<Type, Delegate> _deserializers = new ConcurrentDictionary<Type, Delegate>();
// private static readonly Dictionary<Type, MethodInfo> _readMethods = GetPrimitiveReadMethods();

private readonly Stream _stream;
private readonly byte[] _buffer;
private readonly bool _leaveOpen;
private readonly Encoding _encoding;
// private readonly Stream _stream;
// private readonly byte[] _buffer;
// private readonly bool _leaveOpen;
// private readonly Encoding _encoding;
public ETFReader(Stream stream, bool leaveOpen = false)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
// public ETFReader(Stream stream, bool leaveOpen = false)
// {
// if (stream == null) throw new ArgumentNullException(nameof(stream));

_stream = stream;
_leaveOpen = leaveOpen;
_buffer = new byte[11];
_encoding = Encoding.UTF8;
}
// _stream = stream;
// _leaveOpen = leaveOpen;
// _buffer = new byte[11];
// _encoding = Encoding.UTF8;
// }
public bool ReadBool()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT)
{
_stream.Read(_buffer, 0, 1);
switch (_buffer[0]) //Length
{
case 4:
ReadTrue();
return true;
case 5:
ReadFalse();
return false;
}
}
throw new InvalidDataException();
}
private void ReadTrue()
{
_stream.Read(_buffer, 0, 4);
if (_buffer[0] != 't' || _buffer[1] != 'r' || _buffer[2] != 'u' || _buffer[3] != 'e')
throw new InvalidDataException();
}
private void ReadFalse()
{
_stream.Read(_buffer, 0, 5);
if (_buffer[0] != 'f' || _buffer[1] != 'a' || _buffer[2] != 'l' || _buffer[3] != 's' || _buffer[4] != 'e')
throw new InvalidDataException();
}
// public bool ReadBool()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT)
// {
// _stream.Read(_buffer, 0, 1);
// switch (_buffer[0]) //Length
// {
// case 4:
// ReadTrue();
// return true;
// case 5:
// ReadFalse();
// return false;
// }
// }
// throw new InvalidDataException();
// }
// private void ReadTrue()
// {
// _stream.Read(_buffer, 0, 4);
// if (_buffer[0] != 't' || _buffer[1] != 'r' || _buffer[2] != 'u' || _buffer[3] != 'e')
// throw new InvalidDataException();
// }
// private void ReadFalse()
// {
// _stream.Read(_buffer, 0, 5);
// if (_buffer[0] != 'f' || _buffer[1] != 'a' || _buffer[2] != 'l' || _buffer[3] != 's' || _buffer[4] != 'e')
// throw new InvalidDataException();
// }

public int ReadSByte()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return (sbyte)ReadLongInternal(type);
}
public uint ReadByte()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return (byte)ReadLongInternal(type);
}
public int ReadShort()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return (short)ReadLongInternal(type);
}
public uint ReadUShort()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return (ushort)ReadLongInternal(type);
}
public int ReadInt()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return (int)ReadLongInternal(type);
}
public uint ReadUInt()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return (uint)ReadLongInternal(type);
}
public long ReadLong()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return ReadLongInternal(type);
}
public ulong ReadULong()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return (ulong)ReadLongInternal(type);
}
public long ReadLongInternal(ETFType type)
{
switch (type)
{
case ETFType.SMALL_INTEGER_EXT:
_stream.Read(_buffer, 0, 1);
return _buffer[0];
case ETFType.INTEGER_EXT:
_stream.Read(_buffer, 0, 4);
return (_buffer[0] << 24) | (_buffer[1] << 16) | (_buffer[2] << 8) | (_buffer[3]);
case ETFType.SMALL_BIG_EXT:
_stream.Read(_buffer, 0, 2);
bool isPositive = _buffer[0] == 0;
byte count = _buffer[1];
// public int ReadSByte()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return (sbyte)ReadLongInternal(type);
// }
// public uint ReadByte()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return (byte)ReadLongInternal(type);
// }
// public int ReadShort()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return (short)ReadLongInternal(type);
// }
// public uint ReadUShort()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return (ushort)ReadLongInternal(type);
// }
// public int ReadInt()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return (int)ReadLongInternal(type);
// }
// public uint ReadUInt()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return (uint)ReadLongInternal(type);
// }
// public long ReadLong()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return ReadLongInternal(type);
// }
// public ulong ReadULong()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return (ulong)ReadLongInternal(type);
// }
// public long ReadLongInternal(ETFType type)
// {
// switch (type)
// {
// case ETFType.SMALL_INTEGER_EXT:
// _stream.Read(_buffer, 0, 1);
// return _buffer[0];
// case ETFType.INTEGER_EXT:
// _stream.Read(_buffer, 0, 4);
// return (_buffer[0] << 24) | (_buffer[1] << 16) | (_buffer[2] << 8) | (_buffer[3]);
// case ETFType.SMALL_BIG_EXT:
// _stream.Read(_buffer, 0, 2);
// bool isPositive = _buffer[0] == 0;
// byte count = _buffer[1];

int shiftValue = (count - 1) * 8;
ulong value = 0;
_stream.Read(_buffer, 0, count);
for (int i = 0; i < count; i++, shiftValue -= 8)
value = value + _buffer[i] << shiftValue;
if (!isPositive)
return -(long)value;
else
return (long)value;
}
throw new InvalidDataException();
}
// int shiftValue = (count - 1) * 8;
// ulong value = 0;
// _stream.Read(_buffer, 0, count);
// for (int i = 0; i < count; i++, shiftValue -= 8)
// value = value + _buffer[i] << shiftValue;
// if (!isPositive)
// return -(long)value;
// else
// return (long)value;
// }
// throw new InvalidDataException();
// }

public float ReadSingle()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return (float)ReadDoubleInternal(type);
}
public double ReadDouble()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
return ReadDoubleInternal(type);
}
public double ReadDoubleInternal(ETFType type)
{
throw new NotImplementedException();
}
// public float ReadSingle()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return (float)ReadDoubleInternal(type);
// }
// public double ReadDouble()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// return ReadDoubleInternal(type);
// }
// public double ReadDoubleInternal(ETFType type)
// {
// throw new NotImplementedException();
// }

public bool? ReadNullableBool()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT)
{
_stream.Read(_buffer, 0, 1);
switch (_buffer[0]) //Length
{
case 3:
if (ReadNil())
return null;
break;
case 4:
ReadTrue();
return true;
case 5:
ReadFalse();
return false;
}
}
throw new InvalidDataException();
}
public int? ReadNullableSByte()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return (sbyte)ReadLongInternal(type);
}
public uint? ReadNullableByte()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return (byte)ReadLongInternal(type);
}
public int? ReadNullableShort()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return (short)ReadLongInternal(type);
}
public uint? ReadNullableUShort()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return (ushort)ReadLongInternal(type);
}
public int? ReadNullableInt()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return (int)ReadLongInternal(type);
}
public uint? ReadNullableUInt()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return (uint)ReadLongInternal(type);
}
public long? ReadNullableLong()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return ReadLongInternal(type);
}
public ulong? ReadNullableULong()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return (ulong)ReadLongInternal(type);
}
public float? ReadNullableSingle()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return (float)ReadDoubleInternal(type);
}
public double? ReadNullableDouble()
{
_stream.Read(_buffer, 0, 1);
ETFType type = (ETFType)_buffer[0];
if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
return ReadDoubleInternal(type);
}
// public bool? ReadNullableBool()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT)
// {
// _stream.Read(_buffer, 0, 1);
// switch (_buffer[0]) //Length
// {
// case 3:
// if (ReadNil())
// return null;
// break;
// case 4:
// ReadTrue();
// return true;
// case 5:
// ReadFalse();
// return false;
// }
// }
// throw new InvalidDataException();
// }
// public int? ReadNullableSByte()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return (sbyte)ReadLongInternal(type);
// }
// public uint? ReadNullableByte()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return (byte)ReadLongInternal(type);
// }
// public int? ReadNullableShort()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return (short)ReadLongInternal(type);
// }
// public uint? ReadNullableUShort()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return (ushort)ReadLongInternal(type);
// }
// public int? ReadNullableInt()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return (int)ReadLongInternal(type);
// }
// public uint? ReadNullableUInt()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return (uint)ReadLongInternal(type);
// }
// public long? ReadNullableLong()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return ReadLongInternal(type);
// }
// public ulong? ReadNullableULong()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return (ulong)ReadLongInternal(type);
// }
// public float? ReadNullableSingle()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return (float)ReadDoubleInternal(type);
// }
// public double? ReadNullableDouble()
// {
// _stream.Read(_buffer, 0, 1);
// ETFType type = (ETFType)_buffer[0];
// if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null;
// return ReadDoubleInternal(type);
// }

public string ReadString()
{
throw new NotImplementedException();
}
public byte[] ReadByteArray()
{
throw new NotImplementedException();
}
// public string ReadString()
// {
// throw new NotImplementedException();
// }
// public byte[] ReadByteArray()
// {
// throw new NotImplementedException();
// }

public T Read<T>()
where T : new()
{
var type = typeof(T);
var typeInfo = type.GetTypeInfo();
var action = _deserializers.GetOrAdd(type, _ => CreateDeserializer<T>(type, typeInfo)) as Func<ETFReader, T>;
return action(this);
}
/*public void Read<T, U>()
where T : Nullable<T>
where U : struct, new()
{
}*/
public T[] ReadArray<T>()
{
throw new NotImplementedException();
}
public IDictionary<TKey, TValue> ReadDictionary<TKey, TValue>()
{
throw new NotImplementedException();
}
/*public object Read(object obj)
{
throw new NotImplementedException();
}*/
// public T Read<T>()
// where T : new()
// {
// var type = typeof(T);
// var typeInfo = type.GetTypeInfo();
// var action = _deserializers.GetOrAdd(type, _ => CreateDeserializer<T>(type, typeInfo)) as Func<ETFReader, T>;
// return action(this);
// }
// /*public void Read<T, U>()
// where T : Nullable<T>
// where U : struct, new()
// {
// }*/
// public T[] ReadArray<T>()
// {
// throw new NotImplementedException();
// }
// public IDictionary<TKey, TValue> ReadDictionary<TKey, TValue>()
// {
// throw new NotImplementedException();
// }
// /*public object Read(object obj)
// {
// throw new NotImplementedException();
// }*/

private bool ReadNil(bool ignoreLength = false)
{
if (!ignoreLength)
{
_stream.Read(_buffer, 0, 1);
byte length = _buffer[0];
if (length != 3) return false;
}
// private bool ReadNil(bool ignoreLength = false)
// {
// if (!ignoreLength)
// {
// _stream.Read(_buffer, 0, 1);
// byte length = _buffer[0];
// if (length != 3) return false;
// }

_stream.Read(_buffer, 0, 3);
if (_buffer[0] == 'n' && _buffer[1] == 'i' && _buffer[2] == 'l')
return true;
// _stream.Read(_buffer, 0, 3);
// if (_buffer[0] == 'n' && _buffer[1] == 'i' && _buffer[2] == 'l')
// return true;

return false;
}
// return false;
// }

#region Emit
private static Func<ETFReader, T> CreateDeserializer<T>(Type type, TypeInfo typeInfo)
where T : new()
{
var method = new DynamicMethod("DeserializeETF", type, new[] { typeof(ETFReader) }, true);
var generator = method.GetILGenerator();
// #region Emit
// private static Func<ETFReader, T> CreateDeserializer<T>(Type type, TypeInfo typeInfo)
// where T : new()
// {
// var method = new DynamicMethod("DeserializeETF", type, new[] { typeof(ETFReader) }, true);
// var generator = method.GetILGenerator();

generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
EmitReadValue(generator, type, typeInfo, true);
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
// EmitReadValue(generator, type, typeInfo, true);

generator.Emit(OpCodes.Ret);
return method.CreateDelegate(typeof(Func<ETFReader, T>)) as Func<ETFReader, T>;
}
private static void EmitReadValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop)
{
//Convert enum types to their base type
if (typeInfo.IsEnum)
{
type = Enum.GetUnderlyingType(type);
typeInfo = type.GetTypeInfo();
}
//Primitives/Enums
if (!typeInfo.IsEnum && IsType(type, typeof(sbyte), typeof(byte), typeof(short),
typeof(ushort), typeof(int), typeof(uint), typeof(long),
typeof(ulong), typeof(double), typeof(bool), typeof(string),
typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?),
typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?),
typeof(bool?), typeof(float?), typeof(double?)
/*typeof(object), typeof(DateTime)*/))
{
//No conversion needed
generator.EmitCall(OpCodes.Call, GetReadMethod(type), null);
}
//Dictionaries
/*else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
{
generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
}
//Enumerable
else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
}
//Nullable Structs
else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) &&
typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType)
{
generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
}
//Structs/Classes
else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive))
{
if (isTop)
{
typeInfo.ForEachField(f =>
{
string name;
if (!f.IsPublic || !IsETFProperty(f, out name)) return;
// generator.Emit(OpCodes.Ret);
// return method.CreateDelegate(typeof(Func<ETFReader, T>)) as Func<ETFReader, T>;
// }
// private static void EmitReadValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop)
// {
// //Convert enum types to their base type
// if (typeInfo.IsEnum)
// {
// type = Enum.GetUnderlyingType(type);
// typeInfo = type.GetTypeInfo();
// }
// //Primitives/Enums
// if (!typeInfo.IsEnum && IsType(type, typeof(sbyte), typeof(byte), typeof(short),
// typeof(ushort), typeof(int), typeof(uint), typeof(long),
// typeof(ulong), typeof(double), typeof(bool), typeof(string),
// typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?),
// typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?),
// typeof(bool?), typeof(float?), typeof(double?)
// /*typeof(object), typeof(DateTime)*/))
// {
// //No conversion needed
// generator.EmitCall(OpCodes.Call, GetReadMethod(type), null);
// }
// //Dictionaries
// /*else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces
// .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
// {
// generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
// }
// //Enumerable
// else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces
// .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
// {
// generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
// }
// //Nullable Structs
// else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) &&
// typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType)
// {
// generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
// }
// //Structs/Classes
// else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive))
// {
// if (isTop)
// {
// typeInfo.ForEachField(f =>
// {
// string name;
// if (!f.IsPublic || !IsETFProperty(f, out name)) return;

generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null);
generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj
generator.Emit(OpCodes.Ldfld, f); //ETFReader(this), obj.fieldValue
EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false);
});
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
// generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name
// generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null);
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
// generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj
// generator.Emit(OpCodes.Ldfld, f); //ETFReader(this), obj.fieldValue
// EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false);
// });

typeInfo.ForEachProperty(p =>
{
string name;
if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return;
// typeInfo.ForEachProperty(p =>
// {
// string name;
// if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return;

generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null);
generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj
generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFReader(this), obj.propValue
EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false);
});
}
else
{
//While we could drill deeper and make a large serializer that also serializes all subclasses,
//it's more efficient to serialize on a per-type basis via another Write<T> call.
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
}
}*/
//Unsupported (decimal, char)
else
throw new InvalidOperationException($"Deserializing {type.Name} is not supported.");
}
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
// generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name
// generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null);
// generator.Emit(OpCodes.Ldarg_0); //ETFReader(this)
// generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj
// generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFReader(this), obj.propValue
// EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false);
// });
// }
// else
// {
// //While we could drill deeper and make a large serializer that also serializes all subclasses,
// //it's more efficient to serialize on a per-type basis via another Write<T> call.
// generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
// }
// }*/
// //Unsupported (decimal, char)
// else
// throw new InvalidOperationException($"Deserializing {type.Name} is not supported.");
// }

private static bool IsType(Type type, params Type[] types)
{
for (int i = 0; i < types.Length; i++)
{
if (type == types[i])
return true;
}
return false;
}
private static bool IsETFProperty(FieldInfo f, out string name)
{
var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault();
if (attrib != null)
{
name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name;
return true;
}
name = null;
return false;
}
private static bool IsETFProperty(PropertyInfo p, out string name)
{
var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault();
if (attrib != null)
{
name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name;
return true;
}
name = null;
return false;
}
// private static bool IsType(Type type, params Type[] types)
// {
// for (int i = 0; i < types.Length; i++)
// {
// if (type == types[i])
// return true;
// }
// return false;
// }
// private static bool IsETFProperty(FieldInfo f, out string name)
// {
// var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault();
// if (attrib != null)
// {
// name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name;
// return true;
// }
// name = null;
// return false;
// }
// private static bool IsETFProperty(PropertyInfo p, out string name)
// {
// var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault();
// if (attrib != null)
// {
// name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name;
// return true;
// }
// name = null;
// return false;
// }

private static MethodInfo GetReadMethod(string name)
=> typeof(ETFReader).GetTypeInfo().GetDeclaredMethods(name).Single();
private static MethodInfo GetReadMethod(Type type)
{
MethodInfo method;
if (_readMethods.TryGetValue(type, out method))
return method;
return null;
}
private static Dictionary<Type, MethodInfo> GetPrimitiveReadMethods()
{
return new Dictionary<Type, MethodInfo>
{
{ typeof(bool), GetReadMethod(nameof(ReadBool)) },
{ typeof(bool?), GetReadMethod(nameof(ReadNullableBool)) },
{ typeof(byte), GetReadMethod(nameof(ReadByte)) },
{ typeof(byte?), GetReadMethod(nameof(ReadNullableByte)) },
{ typeof(sbyte), GetReadMethod(nameof(ReadSByte)) },
{ typeof(sbyte?), GetReadMethod(nameof(ReadNullableSByte)) },
{ typeof(short), GetReadMethod(nameof(ReadShort)) },
{ typeof(short?), GetReadMethod(nameof(ReadNullableShort)) },
{ typeof(ushort), GetReadMethod(nameof(ReadUShort)) },
{ typeof(ushort?), GetReadMethod(nameof(ReadNullableUShort)) },
{ typeof(int), GetReadMethod(nameof(ReadInt)) },
{ typeof(int?), GetReadMethod(nameof(ReadNullableInt)) },
{ typeof(uint), GetReadMethod(nameof(ReadUInt)) },
{ typeof(uint?), GetReadMethod(nameof(ReadNullableUInt)) },
{ typeof(long), GetReadMethod(nameof(ReadLong)) },
{ typeof(long?), GetReadMethod(nameof(ReadNullableLong)) },
{ typeof(ulong), GetReadMethod(nameof(ReadULong)) },
{ typeof(ulong?), GetReadMethod(nameof(ReadNullableULong)) },
{ typeof(float), GetReadMethod(nameof(ReadSingle)) },
{ typeof(float?), GetReadMethod(nameof(ReadNullableSingle)) },
{ typeof(double), GetReadMethod(nameof(ReadDouble)) },
{ typeof(double?), GetReadMethod(nameof(ReadNullableDouble)) },
};
}
#endregion
// private static MethodInfo GetReadMethod(string name)
// => typeof(ETFReader).GetTypeInfo().GetDeclaredMethods(name).Single();
// private static MethodInfo GetReadMethod(Type type)
// {
// MethodInfo method;
// if (_readMethods.TryGetValue(type, out method))
// return method;
// return null;
// }
// private static Dictionary<Type, MethodInfo> GetPrimitiveReadMethods()
// {
// return new Dictionary<Type, MethodInfo>
// {
// { typeof(bool), GetReadMethod(nameof(ReadBool)) },
// { typeof(bool?), GetReadMethod(nameof(ReadNullableBool)) },
// { typeof(byte), GetReadMethod(nameof(ReadByte)) },
// { typeof(byte?), GetReadMethod(nameof(ReadNullableByte)) },
// { typeof(sbyte), GetReadMethod(nameof(ReadSByte)) },
// { typeof(sbyte?), GetReadMethod(nameof(ReadNullableSByte)) },
// { typeof(short), GetReadMethod(nameof(ReadShort)) },
// { typeof(short?), GetReadMethod(nameof(ReadNullableShort)) },
// { typeof(ushort), GetReadMethod(nameof(ReadUShort)) },
// { typeof(ushort?), GetReadMethod(nameof(ReadNullableUShort)) },
// { typeof(int), GetReadMethod(nameof(ReadInt)) },
// { typeof(int?), GetReadMethod(nameof(ReadNullableInt)) },
// { typeof(uint), GetReadMethod(nameof(ReadUInt)) },
// { typeof(uint?), GetReadMethod(nameof(ReadNullableUInt)) },
// { typeof(long), GetReadMethod(nameof(ReadLong)) },
// { typeof(long?), GetReadMethod(nameof(ReadNullableLong)) },
// { typeof(ulong), GetReadMethod(nameof(ReadULong)) },
// { typeof(ulong?), GetReadMethod(nameof(ReadNullableULong)) },
// { typeof(float), GetReadMethod(nameof(ReadSingle)) },
// { typeof(float?), GetReadMethod(nameof(ReadNullableSingle)) },
// { typeof(double), GetReadMethod(nameof(ReadDouble)) },
// { typeof(double?), GetReadMethod(nameof(ReadNullableDouble)) },
// };
// }
// #endregion

#region IDisposable
private bool _isDisposed = false;
// #region IDisposable
// private bool _isDisposed = false;

protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
if (_leaveOpen)
_stream.Flush();
else
_stream.Dispose();
}
_isDisposed = true;
}
}
// protected virtual void Dispose(bool disposing)
// {
// if (!_isDisposed)
// {
// if (disposing)
// {
// if (_leaveOpen)
// _stream.Flush();
// else
// _stream.Dispose();
// }
// _isDisposed = true;
// }
// }

public void Dispose() => Dispose(true);
#endregion
}
}
// public void Dispose() => Dispose(true);
// #endregion
// }
//}

+ 447
- 447
src/Discord.Net/ETF/ETFWriter.cs View File

@@ -1,482 +1,482 @@
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
//using Newtonsoft.Json;
//using System;
//using System.Collections.Concurrent;
//using System.Collections.Generic;
//using System.IO;
//using System.Linq;
//using System.Reflection;
//using System.Reflection.Emit;
//using System.Text;

namespace Discord.ETF
{
public unsafe class ETFWriter : IDisposable
{
private static readonly ConcurrentDictionary<Type, Delegate> _serializers = new ConcurrentDictionary<Type, Delegate>();
private static readonly ConcurrentDictionary<Type, Delegate> _indirectSerializers = new ConcurrentDictionary<Type, Delegate>();
//namespace Discord.ETF
//{
// public unsafe class ETFWriter : IDisposable
// {
// private static readonly ConcurrentDictionary<Type, Delegate> _serializers = new ConcurrentDictionary<Type, Delegate>();
// private static readonly ConcurrentDictionary<Type, Delegate> _indirectSerializers = new ConcurrentDictionary<Type, Delegate>();

private static readonly byte[] _nilBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 3, (byte)'n', (byte)'i', (byte)'l' };
private static readonly byte[] _falseBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 5, (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' };
private static readonly byte[] _trueBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 4, (byte)'t', (byte)'r', (byte)'u', (byte)'e' };
// private static readonly byte[] _nilBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 3, (byte)'n', (byte)'i', (byte)'l' };
// private static readonly byte[] _falseBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 5, (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' };
// private static readonly byte[] _trueBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 4, (byte)'t', (byte)'r', (byte)'u', (byte)'e' };

private static readonly MethodInfo _writeTMethod = GetGenericWriteMethod(null);
private static readonly MethodInfo _writeNullableTMethod = GetGenericWriteMethod(typeof(Nullable<>));
private static readonly MethodInfo _writeDictionaryTMethod = GetGenericWriteMethod(typeof(IDictionary<,>));
private static readonly MethodInfo _writeEnumerableTMethod = GetGenericWriteMethod(typeof(IEnumerable<>));
private static readonly DateTime _epochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// private static readonly MethodInfo _writeTMethod = GetGenericWriteMethod(null);
// private static readonly MethodInfo _writeNullableTMethod = GetGenericWriteMethod(typeof(Nullable<>));
// private static readonly MethodInfo _writeDictionaryTMethod = GetGenericWriteMethod(typeof(IDictionary<,>));
// private static readonly MethodInfo _writeEnumerableTMethod = GetGenericWriteMethod(typeof(IEnumerable<>));
// private static readonly DateTime _epochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

private readonly Stream _stream;
private readonly byte[] _buffer;
private readonly bool _leaveOpen;
private readonly Encoding _encoding;
// private readonly Stream _stream;
// private readonly byte[] _buffer;
// private readonly bool _leaveOpen;
// private readonly Encoding _encoding;

public virtual Stream BaseStream
{
get
{
Flush();
return _stream;
}
}
// public virtual Stream BaseStream
// {
// get
// {
// Flush();
// return _stream;
// }
// }
public ETFWriter(Stream stream, bool leaveOpen = false)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
// public ETFWriter(Stream stream, bool leaveOpen = false)
// {
// if (stream == null) throw new ArgumentNullException(nameof(stream));

_stream = stream;
_leaveOpen = leaveOpen;
_buffer = new byte[11];
_encoding = Encoding.UTF8;
}
// _stream = stream;
// _leaveOpen = leaveOpen;
// _buffer = new byte[11];
// _encoding = Encoding.UTF8;
// }
public void Write(bool value)
{
if (value)
_stream.Write(_trueBytes, 0, _trueBytes.Length);
else
_stream.Write(_falseBytes, 0, _falseBytes.Length);
}
public void Write(sbyte value) => Write((long)value);
public void Write(byte value) => Write((ulong)value);
public void Write(short value) => Write((long)value);
public void Write(ushort value) => Write((ulong)value);
public void Write(int value) => Write((long)value);
public void Write(uint value) => Write((ulong)value);
public void Write(long value)
{
if (value >= byte.MinValue && value <= byte.MaxValue)
{
_buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT;
_buffer[1] = (byte)value;
_stream.Write(_buffer, 0, 2);
}
else if (value >= int.MinValue && value <= int.MaxValue)
{
//TODO: Does this encode negatives correctly?
_buffer[0] = (byte)ETFType.INTEGER_EXT;
_buffer[1] = (byte)(value >> 24);
_buffer[2] = (byte)(value >> 16);
_buffer[3] = (byte)(value >> 8);
_buffer[4] = (byte)value;
_stream.Write(_buffer, 0, 5);
}
else
{
_buffer[0] = (byte)ETFType.SMALL_BIG_EXT;
if (value < 0)
{
_buffer[2] = 1; //Is negative
value = -value;
}
// public void Write(bool value)
// {
// if (value)
// _stream.Write(_trueBytes, 0, _trueBytes.Length);
// else
// _stream.Write(_falseBytes, 0, _falseBytes.Length);
// }
// public void Write(sbyte value) => Write((long)value);
// public void Write(byte value) => Write((ulong)value);
// public void Write(short value) => Write((long)value);
// public void Write(ushort value) => Write((ulong)value);
// public void Write(int value) => Write((long)value);
// public void Write(uint value) => Write((ulong)value);
// public void Write(long value)
// {
// if (value >= byte.MinValue && value <= byte.MaxValue)
// {
// _buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT;
// _buffer[1] = (byte)value;
// _stream.Write(_buffer, 0, 2);
// }
// else if (value >= int.MinValue && value <= int.MaxValue)
// {
// //TODO: Does this encode negatives correctly?
// _buffer[0] = (byte)ETFType.INTEGER_EXT;
// _buffer[1] = (byte)(value >> 24);
// _buffer[2] = (byte)(value >> 16);
// _buffer[3] = (byte)(value >> 8);
// _buffer[4] = (byte)value;
// _stream.Write(_buffer, 0, 5);
// }
// else
// {
// _buffer[0] = (byte)ETFType.SMALL_BIG_EXT;
// if (value < 0)
// {
// _buffer[2] = 1; //Is negative
// value = -value;
// }

byte bytes = 0;
while (value > 0)
_buffer[3 + bytes++] = (byte)(value >>= 8);
_buffer[1] = bytes; //Encoded bytes
// byte bytes = 0;
// while (value > 0)
// _buffer[3 + bytes++] = (byte)(value >>= 8);
// _buffer[1] = bytes; //Encoded bytes

_stream.Write(_buffer, 0, 3 + bytes);
}
}
public void Write(ulong value)
{
if (value <= byte.MaxValue)
{
_buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT;
_buffer[1] = (byte)value;
_stream.Write(_buffer, 0, 2);
}
else if (value <= int.MaxValue)
{
_buffer[0] = (byte)ETFType.INTEGER_EXT;
_buffer[1] = (byte)(value >> 24);
_buffer[2] = (byte)(value >> 16);
_buffer[3] = (byte)(value >> 8);
_buffer[4] = (byte)value;
_stream.Write(_buffer, 0, 5);
}
else
{
_buffer[0] = (byte)ETFType.SMALL_BIG_EXT;
_buffer[2] = 0; //Always positive
// _stream.Write(_buffer, 0, 3 + bytes);
// }
// }
// public void Write(ulong value)
// {
// if (value <= byte.MaxValue)
// {
// _buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT;
// _buffer[1] = (byte)value;
// _stream.Write(_buffer, 0, 2);
// }
// else if (value <= int.MaxValue)
// {
// _buffer[0] = (byte)ETFType.INTEGER_EXT;
// _buffer[1] = (byte)(value >> 24);
// _buffer[2] = (byte)(value >> 16);
// _buffer[3] = (byte)(value >> 8);
// _buffer[4] = (byte)value;
// _stream.Write(_buffer, 0, 5);
// }
// else
// {
// _buffer[0] = (byte)ETFType.SMALL_BIG_EXT;
// _buffer[2] = 0; //Always positive

byte bytes = 0;
while (value > 0)
_buffer[3 + bytes++] = (byte)(value >>= 8);
_buffer[1] = bytes; //Encoded bytes
// byte bytes = 0;
// while (value > 0)
// _buffer[3 + bytes++] = (byte)(value >>= 8);
// _buffer[1] = bytes; //Encoded bytes

_stream.Write(_buffer, 0, 3 + bytes);
}
}
// _stream.Write(_buffer, 0, 3 + bytes);
// }
// }

public void Write(float value) => Write((double)value);
public void Write(double value)
{
ulong value2 = *(ulong*)&value;
_buffer[0] = (byte)ETFType.NEW_FLOAT_EXT;
_buffer[1] = (byte)(value2 >> 56);
_buffer[2] = (byte)(value2 >> 48);
_buffer[3] = (byte)(value2 >> 40);
_buffer[4] = (byte)(value2 >> 32);
_buffer[5] = (byte)(value2 >> 24);
_buffer[6] = (byte)(value2 >> 16);
_buffer[7] = (byte)(value2 >> 8);
_buffer[8] = (byte)value2;
_stream.Write(_buffer, 0, 9);
}
// public void Write(float value) => Write((double)value);
// public void Write(double value)
// {
// ulong value2 = *(ulong*)&value;
// _buffer[0] = (byte)ETFType.NEW_FLOAT_EXT;
// _buffer[1] = (byte)(value2 >> 56);
// _buffer[2] = (byte)(value2 >> 48);
// _buffer[3] = (byte)(value2 >> 40);
// _buffer[4] = (byte)(value2 >> 32);
// _buffer[5] = (byte)(value2 >> 24);
// _buffer[6] = (byte)(value2 >> 16);
// _buffer[7] = (byte)(value2 >> 8);
// _buffer[8] = (byte)value2;
// _stream.Write(_buffer, 0, 9);
// }

public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerSecond));
// public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerSecond));

public void Write(bool? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
public void Write(sbyte? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); }
public void Write(byte? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); }
public void Write(short? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); }
public void Write(ushort? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); }
public void Write(int? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
public void Write(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); }
public void Write(long? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
public void Write(ulong? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
public void Write(double? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
public void Write(float? value) { if (value.HasValue) Write((double)value.Value); else WriteNil(); }
public void Write(DateTime? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
// public void Write(bool? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
// public void Write(sbyte? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); }
// public void Write(byte? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); }
// public void Write(short? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); }
// public void Write(ushort? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); }
// public void Write(int? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
// public void Write(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); }
// public void Write(long? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
// public void Write(ulong? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
// public void Write(double? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }
// public void Write(float? value) { if (value.HasValue) Write((double)value.Value); else WriteNil(); }
// public void Write(DateTime? value) { if (value.HasValue) Write(value.Value); else WriteNil(); }

public void Write(string value)
{
if (value != null)
{
var bytes = _encoding.GetBytes(value);
int count = bytes.Length;
_buffer[0] = (byte)ETFType.BINARY_EXT;
_buffer[1] = (byte)(count >> 24);
_buffer[2] = (byte)(count >> 16);
_buffer[3] = (byte)(count >> 8);
_buffer[4] = (byte)count;
_stream.Write(_buffer, 0, 5);
_stream.Write(bytes, 0, bytes.Length);
}
else
WriteNil();
}
public void Write(byte[] value)
{
if (value != null)
{
int count = value.Length;
_buffer[0] = (byte)ETFType.BINARY_EXT;
_buffer[1] = (byte)(count >> 24);
_buffer[2] = (byte)(count >> 16);
_buffer[3] = (byte)(count >> 8);
_buffer[4] = (byte)count;
_stream.Write(_buffer, 0, 5);
_stream.Write(value, 0, value.Length);
}
else
WriteNil();
}
// public void Write(string value)
// {
// if (value != null)
// {
// var bytes = _encoding.GetBytes(value);
// int count = bytes.Length;
// _buffer[0] = (byte)ETFType.BINARY_EXT;
// _buffer[1] = (byte)(count >> 24);
// _buffer[2] = (byte)(count >> 16);
// _buffer[3] = (byte)(count >> 8);
// _buffer[4] = (byte)count;
// _stream.Write(_buffer, 0, 5);
// _stream.Write(bytes, 0, bytes.Length);
// }
// else
// WriteNil();
// }
// public void Write(byte[] value)
// {
// if (value != null)
// {
// int count = value.Length;
// _buffer[0] = (byte)ETFType.BINARY_EXT;
// _buffer[1] = (byte)(count >> 24);
// _buffer[2] = (byte)(count >> 16);
// _buffer[3] = (byte)(count >> 8);
// _buffer[4] = (byte)count;
// _stream.Write(_buffer, 0, 5);
// _stream.Write(value, 0, value.Length);
// }
// else
// WriteNil();
// }

public void Write<T>(T obj)
{
var type = typeof(T);
var typeInfo = type.GetTypeInfo();
var action = _serializers.GetOrAdd(type, _ => CreateSerializer<T>(type, typeInfo, false)) as Action<ETFWriter, T>;
action(this, obj);
}
public void Write<T>(T? obj)
where T : struct
{
if (obj != null)
Write(obj.Value);
else
WriteNil();
}
public void Write<T>(IEnumerable<T> obj)
{
if (obj != null)
{
var array = obj.ToArray();
int length = array.Length;
_buffer[0] = (byte)ETFType.LIST_EXT;
_buffer[1] = (byte)(length >> 24);
_buffer[2] = (byte)(length >> 16);
_buffer[3] = (byte)(length >> 8);
_buffer[4] = (byte)length;
_stream.Write(_buffer, 0, 5);
// public void Write<T>(T obj)
// {
// var type = typeof(T);
// var typeInfo = type.GetTypeInfo();
// var action = _serializers.GetOrAdd(type, _ => CreateSerializer<T>(type, typeInfo, false)) as Action<ETFWriter, T>;
// action(this, obj);
// }
// public void Write<T>(T? obj)
// where T : struct
// {
// if (obj != null)
// Write(obj.Value);
// else
// WriteNil();
// }
// public void Write<T>(IEnumerable<T> obj)
// {
// if (obj != null)
// {
// var array = obj.ToArray();
// int length = array.Length;
// _buffer[0] = (byte)ETFType.LIST_EXT;
// _buffer[1] = (byte)(length >> 24);
// _buffer[2] = (byte)(length >> 16);
// _buffer[3] = (byte)(length >> 8);
// _buffer[4] = (byte)length;
// _stream.Write(_buffer, 0, 5);

for (int i = 0; i < array.Length; i++)
Write(array[i]);
// for (int i = 0; i < array.Length; i++)
// Write(array[i]);

_buffer[0] = (byte)ETFType.NIL_EXT;
_stream.Write(_buffer, 0, 1);
}
else
WriteNil();
}
public void Write<TKey, TValue>(IDictionary<TKey, TValue> obj)
{
if (obj != null)
{
int length = obj.Count;
_buffer[0] = (byte)ETFType.MAP_EXT;
_buffer[1] = (byte)(length >> 24);
_buffer[2] = (byte)(length >> 16);
_buffer[3] = (byte)(length >> 8);
_buffer[4] = (byte)length;
_stream.Write(_buffer, 0, 5);
// _buffer[0] = (byte)ETFType.NIL_EXT;
// _stream.Write(_buffer, 0, 1);
// }
// else
// WriteNil();
// }
// public void Write<TKey, TValue>(IDictionary<TKey, TValue> obj)
// {
// if (obj != null)
// {
// int length = obj.Count;
// _buffer[0] = (byte)ETFType.MAP_EXT;
// _buffer[1] = (byte)(length >> 24);
// _buffer[2] = (byte)(length >> 16);
// _buffer[3] = (byte)(length >> 8);
// _buffer[4] = (byte)length;
// _stream.Write(_buffer, 0, 5);

foreach (var pair in obj)
{
Write(pair.Key);
Write(pair.Value);
}
}
else
WriteNil();
}
public void Write(object obj)
{
if (obj != null)
{
var type = obj.GetType();
var typeInfo = type.GetTypeInfo();
var action = _indirectSerializers.GetOrAdd(type, _ => CreateSerializer<object>(type, typeInfo, true)) as Action<ETFWriter, object>;
action(this, obj);
}
else
WriteNil();
}
// foreach (var pair in obj)
// {
// Write(pair.Key);
// Write(pair.Value);
// }
// }
// else
// WriteNil();
// }
// public void Write(object obj)
// {
// if (obj != null)
// {
// var type = obj.GetType();
// var typeInfo = type.GetTypeInfo();
// var action = _indirectSerializers.GetOrAdd(type, _ => CreateSerializer<object>(type, typeInfo, true)) as Action<ETFWriter, object>;
// action(this, obj);
// }
// else
// WriteNil();
// }

private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length);
// private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length);

public virtual void Flush() => _stream.Flush();
public virtual long Seek(int offset, SeekOrigin origin) => _stream.Seek(offset, origin);
// public virtual void Flush() => _stream.Flush();
// public virtual long Seek(int offset, SeekOrigin origin) => _stream.Seek(offset, origin);

#region Emit
private static Action<ETFWriter, T> CreateSerializer<T>(Type type, TypeInfo typeInfo, bool isDirect)
{
var method = new DynamicMethod(isDirect ? "SerializeETF" : "SerializeIndirectETF",
null, new[] { typeof(ETFWriter), isDirect ? type : typeof(object) }, true);
var generator = method.GetILGenerator();
// #region Emit
// private static Action<ETFWriter, T> CreateSerializer<T>(Type type, TypeInfo typeInfo, bool isDirect)
// {
// var method = new DynamicMethod(isDirect ? "SerializeETF" : "SerializeIndirectETF",
// null, new[] { typeof(ETFWriter), isDirect ? type : typeof(object) }, true);
// var generator = method.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value
if (!isDirect)
{
if (typeInfo.IsValueType) //Unbox value types
generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), real_value
else //Cast reference types
generator.Emit(OpCodes.Castclass, type); //ETFWriter(this), real_value
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); //Call generic version
}
else
EmitWriteValue(generator, type, typeInfo, true);
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
// generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value
// if (!isDirect)
// {
// if (typeInfo.IsValueType) //Unbox value types
// generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), real_value
// else //Cast reference types
// generator.Emit(OpCodes.Castclass, type); //ETFWriter(this), real_value
// generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); //Call generic version
// }
// else
// EmitWriteValue(generator, type, typeInfo, true);

generator.Emit(OpCodes.Ret);
return method.CreateDelegate(typeof(Action<ETFWriter, T>)) as Action<ETFWriter, T>;
}
private static void EmitWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop)
{
//Convert enum types to their base type
if (typeInfo.IsEnum)
{
type = Enum.GetUnderlyingType(type);
typeInfo = type.GetTypeInfo();
}
// generator.Emit(OpCodes.Ret);
// return method.CreateDelegate(typeof(Action<ETFWriter, T>)) as Action<ETFWriter, T>;
// }
// private static void EmitWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop)
// {
// //Convert enum types to their base type
// if (typeInfo.IsEnum)
// {
// type = Enum.GetUnderlyingType(type);
// typeInfo = type.GetTypeInfo();
// }
//Primitives/Enums
Type targetType = null;
if (!typeInfo.IsEnum && IsType(type, typeof(long), typeof(ulong), typeof(double), typeof(bool), typeof(string),
typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?),
typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?),
typeof(bool?), typeof(float?), typeof(double?),
typeof(object), typeof(DateTime)))
{
//No conversion needed
targetType = type;
}
else if (IsType(type, typeof(sbyte), typeof(short), typeof(int)))
{
//Convert to long
generator.Emit(OpCodes.Conv_I8);
targetType = typeof(long);
}
else if (IsType(type, typeof(byte), typeof(ushort), typeof(uint)))
{
//Convert to ulong
generator.Emit(OpCodes.Conv_U8);
targetType = typeof(ulong);
}
else if (IsType(type, typeof(float)))
{
//Convert to double
generator.Emit(OpCodes.Conv_R8);
targetType = typeof(double);
}
if (targetType != null)
generator.EmitCall(OpCodes.Call, GetWriteMethod(targetType), null);
// //Primitives/Enums
// Type targetType = null;
// if (!typeInfo.IsEnum && IsType(type, typeof(long), typeof(ulong), typeof(double), typeof(bool), typeof(string),
// typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?),
// typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?),
// typeof(bool?), typeof(float?), typeof(double?),
// typeof(object), typeof(DateTime)))
// {
// //No conversion needed
// targetType = type;
// }
// else if (IsType(type, typeof(sbyte), typeof(short), typeof(int)))
// {
// //Convert to long
// generator.Emit(OpCodes.Conv_I8);
// targetType = typeof(long);
// }
// else if (IsType(type, typeof(byte), typeof(ushort), typeof(uint)))
// {
// //Convert to ulong
// generator.Emit(OpCodes.Conv_U8);
// targetType = typeof(ulong);
// }
// else if (IsType(type, typeof(float)))
// {
// //Convert to double
// generator.Emit(OpCodes.Conv_R8);
// targetType = typeof(double);
// }
// if (targetType != null)
// generator.EmitCall(OpCodes.Call, GetWriteMethod(targetType), null);

//Dictionaries
else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
{
generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
}
//Enumerable
else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
}
//Nullable Structs
else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) &&
typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType)
{
generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
}
//Structs/Classes
else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive))
{
if (isTop)
{
typeInfo.ForEachField(f =>
{
string name;
if (!f.IsPublic || !IsETFProperty(f, out name)) return;
// //Dictionaries
// else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces
// .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
// {
// generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
// }
// //Enumerable
// else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces
// .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
// {
// generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
// }
// //Nullable Structs
// else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) &&
// typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType)
// {
// generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
// }
// //Structs/Classes
// else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive))
// {
// if (isTop)
// {
// typeInfo.ForEachField(f =>
// {
// string name;
// if (!f.IsPublic || !IsETFProperty(f, out name)) return;

generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null);
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj
generator.Emit(OpCodes.Ldfld, f); //ETFWriter(this), obj.fieldValue
EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false);
});
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
// generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name
// generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null);
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
// generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj
// generator.Emit(OpCodes.Ldfld, f); //ETFWriter(this), obj.fieldValue
// EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false);
// });

typeInfo.ForEachProperty(p =>
{
string name;
if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return;
// typeInfo.ForEachProperty(p =>
// {
// string name;
// if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return;

generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null);
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj
generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFWriter(this), obj.propValue
EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false);
});
}
else
{
//While we could drill deeper and make a large serializer that also serializes all subclasses,
//it's more efficient to serialize on a per-type basis via another Write<T> call.
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
}
}
//Unsupported (decimal, char)
else
throw new InvalidOperationException($"Serializing {type.Name} is not supported.");
}
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
// generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name
// generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null);
// generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this)
// generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj
// generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFWriter(this), obj.propValue
// EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false);
// });
// }
// else
// {
// //While we could drill deeper and make a large serializer that also serializes all subclasses,
// //it's more efficient to serialize on a per-type basis via another Write<T> call.
// generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null);
// }
// }
// //Unsupported (decimal, char)
// else
// throw new InvalidOperationException($"Serializing {type.Name} is not supported.");
// }

private static bool IsType(Type type, params Type[] types)
{
for (int i = 0; i < types.Length; i++)
{
if (type == types[i])
return true;
}
return false;
}
private static bool IsETFProperty(FieldInfo f, out string name)
{
var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault();
if (attrib != null)
{
name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name;
return true;
}
name = null;
return false;
}
private static bool IsETFProperty(PropertyInfo p, out string name)
{
var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault();
if (attrib != null)
{
name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name;
return true;
}
name = null;
return false;
}
// private static bool IsType(Type type, params Type[] types)
// {
// for (int i = 0; i < types.Length; i++)
// {
// if (type == types[i])
// return true;
// }
// return false;
// }
// private static bool IsETFProperty(FieldInfo f, out string name)
// {
// var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault();
// if (attrib != null)
// {
// name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name;
// return true;
// }
// name = null;
// return false;
// }
// private static bool IsETFProperty(PropertyInfo p, out string name)
// {
// var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault();
// if (attrib != null)
// {
// name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name;
// return true;
// }
// name = null;
// return false;
// }

private static MethodInfo GetWriteMethod(Type paramType)
{
return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write))
.Where(x => x.GetParameters()[0].ParameterType == paramType)
.Single();
}
private static MethodInfo GetGenericWriteMethod(Type genericType)
{
if (genericType == null)
{
return typeof(ETFWriter).GetTypeInfo()
.GetDeclaredMethods(nameof(Write))
.Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0])
.Single();
}
else
{
return typeof(ETFWriter).GetTypeInfo()
.GetDeclaredMethods(nameof(Write))
.Where(x =>
{
if (!x.IsGenericMethodDefinition) return false;
var p = x.GetParameters()[0].ParameterType.GetTypeInfo();
return p.IsGenericType && p.GetGenericTypeDefinition() == genericType;
})
.Single();
}
}
#endregion
// private static MethodInfo GetWriteMethod(Type paramType)
// {
// return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write))
// .Where(x => x.GetParameters()[0].ParameterType == paramType)
// .Single();
// }
// private static MethodInfo GetGenericWriteMethod(Type genericType)
// {
// if (genericType == null)
// {
// return typeof(ETFWriter).GetTypeInfo()
// .GetDeclaredMethods(nameof(Write))
// .Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0])
// .Single();
// }
// else
// {
// return typeof(ETFWriter).GetTypeInfo()
// .GetDeclaredMethods(nameof(Write))
// .Where(x =>
// {
// if (!x.IsGenericMethodDefinition) return false;
// var p = x.GetParameters()[0].ParameterType.GetTypeInfo();
// return p.IsGenericType && p.GetGenericTypeDefinition() == genericType;
// })
// .Single();
// }
// }
// #endregion

#region IDisposable
private bool _isDisposed = false;
// #region IDisposable
// private bool _isDisposed = false;

protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
if (_leaveOpen)
_stream.Flush();
else
_stream.Dispose();
}
_isDisposed = true;
}
}
// protected virtual void Dispose(bool disposing)
// {
// if (!_isDisposed)
// {
// if (disposing)
// {
// if (_leaveOpen)
// _stream.Flush();
// else
// _stream.Dispose();
// }
// _isDisposed = true;
// }
// }
public void Dispose() => Dispose(true);
#endregion
}
}
// public void Dispose() => Dispose(true);
// #endregion
// }
//}

+ 8
- 0
src/Discord.Net/Enums/GameType.cs View File

@@ -0,0 +1,8 @@
namespace Discord
{
public enum GameType : int
{
Default = 0, // "NotStreaming", pretty much
Twitch
}
}

+ 6
- 2
src/Discord.Net/Enums/PermissionBits.cs View File

@@ -6,7 +6,7 @@
CreateInstantInvite = 0,
KickMembers = 1,
BanMembers = 2,
ManageRolesOrPermissions = 3,
Administrator = 3,
ManageChannel = 4,
ManageServer = 5,

@@ -26,6 +26,10 @@
MuteMembers = 22,
DeafenMembers = 23,
MoveMembers = 24,
UseVoiceActivation = 25
UseVoiceActivation = 25,

ChangeNickname = 26,
ManageNicknames = 27,
ManageRolesOrPermissions = 28,
}
}

+ 1
- 1
src/Discord.Net/Enums/Relative.cs View File

@@ -2,6 +2,6 @@
{
public enum Relative
{
Before, After
Before, After, Around
}
}

+ 8
- 0
src/Discord.Net/Enums/TokenType.cs View File

@@ -0,0 +1,8 @@
namespace Discord
{
public enum TokenType
{
User,
Bot,
}
}

+ 8
- 0
src/Discord.Net/Enums/UserStatus.cs View File

@@ -8,6 +8,10 @@
public static UserStatus Idle { get; } = new UserStatus("idle");
/// <summary> User is offline. </summary>
public static UserStatus Offline { get; } = new UserStatus("offline");
/// <summary> User is busy. </summary>
public static UserStatus DoNotDisturb { get; } = new UserStatus("dnd");
/// <summary> User is invisible. </summary>
public static UserStatus Invisible { get; } = new UserStatus("invisible");

private UserStatus(string value)
: base(value) { }
@@ -24,6 +28,10 @@
return Idle;
case "offline":
return Offline;
case "dnd":
return DoNotDisturb;
case "invisible":
return Invisible;
default:
return new UserStatus(value);
}


+ 19
- 14
src/Discord.Net/InternalExtensions.cs View File

@@ -10,24 +10,19 @@ namespace Discord
{
internal static class InternalExtensions
{
internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture;
public static ulong ToId(this string value)
=> ulong.Parse(value, NumberStyles.None, _format);
public static ulong? ToNullableId(this string value)
=> value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format);
public static bool TryToId(this string value, out ulong result)
=> ulong.TryParse(value, NumberStyles.None, _format, out result);
internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture;
public static ulong ToId(this string value) => ulong.Parse(value, NumberStyles.None, _format);
public static ulong? ToNullableId(this string value) => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format);
public static bool TryToId(this string value, out ulong result) => ulong.TryParse(value, NumberStyles.None, _format, out result);
public static string ToIdString(this ulong value) => value.ToString(_format);
public static string ToIdString(this ulong? value) => value?.ToString(_format);

public static string ToIdString(this ulong value)
=> value.ToString(_format);
public static string ToIdString(this ulong? value)
=> value?.ToString(_format);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasBit(this uint rawValue, byte bit) => ((rawValue >> bit) & 1U) == 1;

public static bool TryGetOrAdd<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> d,
public static bool TryGetOrAdd<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> d,
TKey key, Func<TKey, TValue> factory, out TValue result)
{
bool created = false;
@@ -103,6 +98,16 @@ namespace Discord

if (!exactMatch)
{
if (name.Length >= 3 && name[0] == '<' && name[1] == '@' && name[2] == '!' && name[name.Length - 1] == '>') //Search by nickname'd mention
{
ulong id;
if (name.Substring(3, name.Length - 4).TryToId(out id))
{
var user = users.Where(x => x.Id == id).FirstOrDefault();
if (user != null)
query = query.Concat(new User[] { user });
}
}
if (name.Length >= 2 && name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Search by raw mention
{
ulong id;


+ 0
- 5
src/Discord.Net/Legacy.cs View File

@@ -28,11 +28,6 @@ namespace Discord.Legacy
{
client.ExecuteAndWait(asyncAction);
}
[Obsolete("Use DiscordClient.Wait")]
public static void Run(this DiscordClient client)
{
client.Wait();
}

[Obsolete("Use Server.FindChannels")]
public static IEnumerable<Channel> FindChannels(this DiscordClient client, Server server, string name, ChannelType type = null, bool exactMatch = false)


+ 6
- 6
src/Discord.Net/Logging/ILogger.cs View File

@@ -7,36 +7,36 @@ namespace Discord.Logging
LogSeverity Level { get; }

void Log(LogSeverity severity, string message, Exception exception = null);
#if DOTNET5_4
#if NETSTANDARD1_3
void Log(LogSeverity severity, FormattableString message, Exception exception = null);
#endif

void Error(string message, Exception exception = null);
#if DOTNET5_4
#if NETSTANDARD1_3
void Error(FormattableString message, Exception exception = null);
#endif
void Error(Exception exception);

void Warning(string message, Exception exception = null);
#if DOTNET5_4
#if NETSTANDARD1_3
void Warning(FormattableString message, Exception exception = null);
#endif
void Warning(Exception exception);

void Info(string message, Exception exception = null);
#if DOTNET5_4
#if NETSTANDARD1_3
void Info(FormattableString message, Exception exception = null);
#endif
void Info(Exception exception);

void Verbose(string message, Exception exception = null);
#if DOTNET5_4
#if NETSTANDARD1_3
void Verbose(FormattableString message, Exception exception = null);
#endif
void Verbose(Exception exception);

void Debug(string message, Exception exception = null);
#if DOTNET5_4
#if NETSTANDARD1_3
void Debug(FormattableString message, Exception exception = null);
#endif
void Debug(Exception exception);


+ 2
- 2
src/Discord.Net/Logging/LogManager.cs View File

@@ -25,7 +25,7 @@ namespace Discord.Logging
}
}

#if DOTNET5_4
#if NETSTANDARD1_3
public void Log(LogSeverity severity, string source, FormattableString message, Exception exception = null)
{
if (severity <= Level)
@@ -57,7 +57,7 @@ namespace Discord.Logging
public void Debug(string source, Exception ex)
=> Log(LogSeverity.Debug, source, (string)null, ex);

#if DOTNET5_4
#if NETSTANDARD1_3
public void Error(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Error, source, message, ex);
public void Warning(string source, FormattableString message, Exception ex = null)


+ 1
- 1
src/Discord.Net/Logging/Logger.cs View File

@@ -38,7 +38,7 @@ namespace Discord.Logging
public void Debug(Exception exception)
=> _manager.Debug(Name, exception);

#if DOTNET5_4
#if NETSTANDARD1_3
public void Log(LogSeverity severity, FormattableString message, Exception exception = null)
=> _manager.Log(severity, Name, message, exception);
public void Error(FormattableString message, Exception exception = null)


+ 36
- 17
src/Discord.Net/Models/Channel.cs View File

@@ -74,7 +74,7 @@ namespace Discord
public IEnumerable<Message> Messages => _messages?.Values ?? Enumerable.Empty<Message>();
/// <summary> Gets a collection of all custom permissions used for this channel. </summary>
public IEnumerable<PermissionOverwrite> PermissionOverwrites => _permissionOverwrites.Select(x => x.Value);
/// <summary> Gets a collection of all users with read access to this channel. </summary>
public IEnumerable<User> Users
{
@@ -131,7 +131,7 @@ namespace Discord
if (client.Config.MessageCacheSize > 0)
_messages = new ConcurrentDictionary<ulong, Message>(2, (int)(client.Config.MessageCacheSize * 1.05));
}
internal void Update(APIChannel model)
{
if (!IsPrivate && model.Name != null)
@@ -199,7 +199,21 @@ namespace Discord
await Server.ReorderChannels(channels.Skip(minPos), after).ConfigureAwait(false);
}
}

public async Task DeleteMessages(Message[] messages) => await DeleteMessages(messages.Select(m => m.Id).ToArray());

public async Task DeleteMessages(ulong[] messageIds)
{
if (messageIds.Count() > 100)
throw new ArgumentOutOfRangeException("messageIds",
"You must provide no more than 100 Messages or Message Ids");
if (messageIds.Count() == 1)
await Client.ClientAPI.Send(new DeleteMessageRequest(Id, messageIds.First()));
else if (messageIds.Any())
await Client.ClientAPI.Send(new BulkMessageDelete(Id, messageIds));
}

public async Task Delete()
{
try { await Client.ClientAPI.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); }
@@ -278,18 +292,25 @@ namespace Discord
return new Message(id, this, userId != null ? GetUserFast(userId.Value) : null);
}

public async Task<Message[]> DownloadMessages(int limit = 100, ulong? relativeMessageId = null,
public async Task<Message[]> DownloadMessages(int limit = 100, ulong? relativeMessageId = null,
Relative relativeDir = Relative.Before, bool useCache = true)
{
if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit));
if (limit == 0 || Type != ChannelType.Text) return new Message[0];
try
{
string dir;
switch (relativeDir)
{
case Relative.Before: default: dir = "before"; break;
case Relative.After: dir = "after"; break;
case Relative.Around: dir = "around"; break;
}
var request = new GetMessagesRequest(Id)
{
Limit = limit,
RelativeDir = relativeMessageId.HasValue ? relativeDir == Relative.Before ? "before" : "after" : null,
RelativeDir = relativeMessageId.HasValue ? dir : null,
RelativeId = relativeMessageId ?? 0
};
var msgs = await Client.ClientAPI.Send(request).ConfigureAwait(false);
@@ -331,12 +352,12 @@ namespace Discord
if (text == null) throw new ArgumentNullException(nameof(text));
if (text == "") throw new ArgumentException("Value cannot be blank", nameof(text));
if (text.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less.");
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less.");
return Task.FromResult(Client.MessageQueue.QueueSend(this, text, isTTS));
}

public async Task<Message> SendFile(string filePath)
{
{
using (var stream = File.OpenRead(filePath))
return await SendFile(System.IO.Path.GetFileName(filePath), stream).ConfigureAwait(false);
}
@@ -381,7 +402,7 @@ namespace Discord
if (_users.TryGetValue(user.Id, out member))
{
var perms = member.Permissions;
if (UpdatePermissions(member.User, ref perms))
if (UpdatePermissions(member.User, ref perms))
_users[user.Id] = new Member(member.User, perms);
}
}
@@ -398,8 +419,8 @@ namespace Discord
//Start with this user's server permissions
newPermissions = server.GetPermissions(user).RawValue;

if (IsPrivate || user == Server.Owner)
newPermissions = mask; //Owners always have all permissions
if (IsPrivate || user == Server.Owner || newPermissions.HasBit((byte)PermissionBits.Administrator))
newPermissions = mask; //Owners and Administrators always have all permissions
else
{
var channelOverwrites = PermissionOverwrites;
@@ -414,9 +435,7 @@ namespace Discord
foreach (var allowUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id && x.Permissions.AllowValue != 0))
newPermissions |= allowUser.Permissions.AllowValue;

if (newPermissions.HasBit((byte)PermissionBits.ManageRolesOrPermissions))
newPermissions = mask; //ManageRolesOrPermissions gives all permisions
else if (Type == ChannelType.Text && !newPermissions.HasBit((byte)PermissionBits.ReadMessages))
if (Type == ChannelType.Text && !newPermissions.HasBit((byte)PermissionBits.ReadMessages))
newPermissions = 0; //No read permission on a text channel removes all other permissions
else if (Type == ChannelType.Voice && !newPermissions.HasBit((byte)PermissionBits.Connect))
newPermissions = 0; //No connect permissions on a voice channel removes all other permissions
@@ -451,7 +470,7 @@ namespace Discord
return perms;
}
}
public ChannelPermissionOverrides GetPermissionsRule(User user)
{
if (user == null) throw new ArgumentNullException(nameof(user));
@@ -470,7 +489,7 @@ namespace Discord
.Select(x => x.Permissions)
.FirstOrDefault();
}
public Task AddPermissionsRule(User user, ChannelPermissions allow, ChannelPermissions deny)
{
if (user == null) throw new ArgumentNullException(nameof(user));
@@ -506,7 +525,7 @@ namespace Discord
};
return Client.ClientAPI.Send(request);
}
public Task RemovePermissionsRule(User user)
{
if (user == null) throw new ArgumentNullException(nameof(user));


+ 22
- 0
src/Discord.Net/Models/Game.cs View File

@@ -0,0 +1,22 @@
namespace Discord
{
public struct Game
{
public string Name { get; }
public string Url { get; }
public GameType Type { get; }

public Game(string name)
{
Name = name;
Url = null;
Type = GameType.Default;
}
public Game(string name, GameType type, string url)
{
Name = name;
Url = url;
Type = type;
}
}
}

+ 43
- 24
src/Discord.Net/Models/Message.cs View File

@@ -23,17 +23,32 @@ namespace Discord
{
private readonly static Action<Message, Message> _cloner = DynamicIL.CreateCopyMethod<Message>();

private static readonly Regex _userRegex = new Regex(@"<@[0-9]+>");
private static readonly Regex _channelRegex = new Regex(@"<#[0-9]+>");
private static readonly Regex _roleRegex = new Regex(@"@everyone");
private static readonly Regex _userRegex = new Regex(@"<@[0-9]+>", RegexOptions.Compiled);
private static readonly Regex _userNicknameRegex = new Regex(@"<@![0-9]+>", RegexOptions.Compiled);
private static readonly Regex _channelRegex = new Regex(@"<#[0-9]+>", RegexOptions.Compiled);
private static readonly Regex _roleRegex = new Regex(@"<@&[0-9]+>", RegexOptions.Compiled);
private static readonly Attachment[] _initialAttachments = new Attachment[0];
private static readonly Embed[] _initialEmbeds = new Embed[0];

internal static string CleanUserMentions(Channel channel, string text, List<User> users = null)
{
ulong id;
text = _userNicknameRegex.Replace(text, new MatchEvaluator(e =>
{
if (e.Value.Substring(3, e.Value.Length - 4).TryToId(out id))
{
var user = channel.GetUserFast(id);
if (user != null)
{
if (users != null)
users.Add(user);
return '@' + user.Nickname;
}
}
return e.Value; //User not found or parse failed
}));
return _userRegex.Replace(text, new MatchEvaluator(e =>
{
ulong id;
if (e.Value.Substring(2, e.Value.Length - 3).TryToId(out id))
{
var user = channel.GetUserFast(id);
@@ -43,7 +58,7 @@ namespace Discord
users.Add(user);
return '@' + user.Name;
}
}
}
return e.Value; //User not found or parse failed
}));
}
@@ -68,28 +83,36 @@ namespace Discord
return e.Value; //Channel not found or parse failed
}));
}
/*internal static string CleanRoleMentions(User user, Channel channel, string text, List<Role> roles = null)
internal static string CleanRoleMentions(Channel channel, string text, List<Role> roles = null)
{
var server = channel.Server;
if (server == null) return text;

return _roleRegex.Replace(text, new MatchEvaluator(e =>
return _roleRegex.Replace(text, new MatchEvaluator(e =>
{
if (roles != null && user.GetPermissions(channel).MentionEveryone)
roles.Add(server.EveryoneRole);
return e.Value;
ulong id;
if (e.Value.Substring(3, e.Value.Length - 4).TryToId(out id))
{
var role = server.GetRole(id);
if (role != null)
{
if (roles != null)
roles.Add(role);
return "@" + role.Name;
}
}
return e.Value; //Role not found or parse failed
}));
}*/
}
//TODO: Move this somewhere
private static string Resolve(Channel channel, string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));

var client = channel.Client;
text = CleanUserMentions(channel, text);
text = CleanChannelMentions(channel, text);
//text = CleanRoleMentions(Channel, text);
text = CleanRoleMentions(channel, text);
return text;
}

@@ -281,32 +304,28 @@ namespace Discord
.Where(x => x != null)
.ToArray();
}
if (model.IsMentioningEveryone != null)
{
if (model.IsMentioningEveryone.Value && User != null && User.GetPermissions(channel).MentionEveryone)
MentionedRoles = new Role[] { Server.EveryoneRole };
else
MentionedRoles = new Role[0];
}

if (model.Content != null)
{
string text = model.Content;
RawText = text;

//var mentionedUsers = new List<User>();
var mentionedChannels = new List<Channel>();
//var mentionedRoles = new List<Role>();
var mentionedRoles = new List<Role>();
text = CleanUserMentions(Channel, text/*, mentionedUsers*/);
if (server != null)
{
text = CleanChannelMentions(Channel, text, mentionedChannels);
//text = CleanRoleMentions(_client, User, channel, text, mentionedRoles);
text = CleanRoleMentions(Channel, text, mentionedRoles);
if (model.IsMentioningEveryone != null && model.IsMentioningEveryone.Value
&& User != null && User.GetPermissions(channel).MentionEveryone)
mentionedRoles.Add(Server.EveryoneRole);
}
Text = text;

//MentionedUsers = mentionedUsers;
MentionedChannels = mentionedChannels;
//MentionedRoles = mentionedRoles;
MentionedRoles = mentionedRoles;
}
}



+ 52
- 34
src/Discord.Net/Models/Permissions.cs View File

@@ -6,7 +6,7 @@ namespace Discord
public struct ServerPermissions
{
public static ServerPermissions None { get; } = new ServerPermissions();
public static ServerPermissions All { get; } = new ServerPermissions(Convert.ToUInt32("00000011111100111111110000111111", 2));
public static ServerPermissions All { get; } = new ServerPermissions(Convert.ToUInt32("00011111111100111111110000111111", 2));

public uint RawValue { get; }

@@ -16,8 +16,8 @@ namespace Discord
public bool BanMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.BanMembers);
/// <summary> If True, a user may kick users from the server. </summary>
public bool KickMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.KickMembers);
/// <summary> If True, a user may adjust roles. This also implictly grants all other permissions. </summary>
public bool ManageRoles => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions);
/// <summary> If True, a user has all permissions and cannot have them revoked. </summary>
public bool Administrator => PermissionsHelper.GetValue(RawValue, PermissionBits.Administrator);
/// <summary> If True, a user may create, delete and modify channels. </summary>
public bool ManageChannels => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageChannel);
/// <summary> If True, a user may adjust server properties. </summary>
@@ -53,30 +53,37 @@ namespace Discord
/// <summary> If True, a user may use voice activation rather than push-to-talk. </summary>
public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBits.UseVoiceActivation);

public ServerPermissions(bool? createInstantInvite = null, bool? manageRoles = null,
bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null,
/// <summary> If True, a user may change their own nickname. </summary>
public bool ChangeNickname => PermissionsHelper.GetValue(RawValue, PermissionBits.ChangeNickname);
/// <summary> If True, a user may change the nickname of other users. </summary>
public bool ManageNicknames => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageNicknames);
/// <summary> If True, a user may adjust roles. </summary>
public bool ManageRoles => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions);

public ServerPermissions(bool? createInstantInvite = null, bool? administrator = null,
bool? banMembers = null, bool? kickMembers = null, bool? manageChannel = null, bool? manageServer = null,
bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null,
bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null,
bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null,
bool? moveMembers = null, bool? useVoiceActivation = null)
: this(new ServerPermissions(), createInstantInvite, manageRoles, kickMembers, banMembers, manageChannel, manageServer, readMessages,
bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null)
: this(new ServerPermissions(), createInstantInvite, administrator, banMembers, kickMembers, manageChannel, manageServer, readMessages,
sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation)
moveMembers, useVoiceActivation, manageRoles)
{
}
public ServerPermissions(ServerPermissions basePerms, bool? createInstantInvite = null, bool? manageRoles = null,
bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null,
public ServerPermissions(ServerPermissions basePerms, bool? createInstantInvite = null, bool? administrator = null,
bool? banMembers = null, bool? kickMembers = null, bool? manageChannel = null, bool? manageServer = null,
bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null,
bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null,
bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null,
bool? moveMembers = null, bool? useVoiceActivation = null)
bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null)
{
uint value = basePerms.RawValue;

PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBits.CreateInstantInvite);
PermissionsHelper.SetValue(ref value, administrator, PermissionBits.Administrator);
PermissionsHelper.SetValue(ref value, banMembers, PermissionBits.BanMembers);
PermissionsHelper.SetValue(ref value, kickMembers, PermissionBits.KickMembers);
PermissionsHelper.SetValue(ref value, manageRoles, PermissionBits.ManageRolesOrPermissions);
PermissionsHelper.SetValue(ref value, manageChannel, PermissionBits.ManageChannel);
PermissionsHelper.SetValue(ref value, manageServer, PermissionBits.ManageServer);
PermissionsHelper.SetValue(ref value, readMessages, PermissionBits.ReadMessages);
@@ -93,6 +100,9 @@ namespace Discord
PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBits.DeafenMembers);
PermissionsHelper.SetValue(ref value, moveMembers, PermissionBits.MoveMembers);
PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBits.UseVoiceActivation);
PermissionsHelper.SetValue(ref value, changeNickname, PermissionBits.ChangeNickname);
PermissionsHelper.SetValue(ref value, manageNicknames, PermissionBits.ManageNicknames);
PermissionsHelper.SetValue(ref value, manageRoles, PermissionBits.ManageRolesOrPermissions);

RawValue = value;
}
@@ -102,9 +112,9 @@ namespace Discord
public struct ChannelPermissions
{
public static ChannelPermissions None { get; } = new ChannelPermissions();
public static ChannelPermissions TextOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000111111110000011001", 2));
public static ChannelPermissions TextOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00010000000000111111110000011001", 2));
public static ChannelPermissions PrivateOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000011100110000000000", 2));
public static ChannelPermissions VoiceOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000011111100000000000000011001", 2));
public static ChannelPermissions VoiceOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00010011111100000000000000011001", 2));
public static ChannelPermissions All(Channel channel) => All(channel.Type, channel.IsPrivate);
public static ChannelPermissions All(ChannelType channelType, bool isPrivate)
{
@@ -118,8 +128,6 @@ namespace Discord

/// <summary> If True, a user may create invites. </summary>
public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBits.CreateInstantInvite);
/// <summary> If True, a user may adjust permissions. This also implictly grants all other permissions. </summary>
public bool ManagePermissions => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions);
/// <summary> If True, a user may create, delete and modify this channel. </summary>
public bool ManageChannel => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageChannel);

@@ -153,25 +161,28 @@ namespace Discord
/// <summary> If True, a user may use voice activation rather than push-to-talk. </summary>
public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBits.UseVoiceActivation);

public ChannelPermissions(bool? createInstantInvite = null, bool? managePermissions = null,
/// <summary> If True, a user may adjust permissions. </summary>
public bool ManagePermissions => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions);

public ChannelPermissions(bool? createInstantInvite = null,
bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null,
bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null,
bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null,
bool? moveMembers = null, bool? useVoiceActivation = null)
: this(new ChannelPermissions(), createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages,
manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation)
bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null)
: this(new ChannelPermissions(), createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages,
manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation, managePermissions)
{
}
public ChannelPermissions(ChannelPermissions basePerms, bool? createInstantInvite = null, bool? managePermissions = null,
public ChannelPermissions(ChannelPermissions basePerms, bool? createInstantInvite = null,
bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null,
bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null,
bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null,
bool? moveMembers = null, bool? useVoiceActivation = null)
bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null)
{
uint value = basePerms.RawValue;

PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBits.CreateInstantInvite);
PermissionsHelper.SetValue(ref value, managePermissions, PermissionBits.ManageRolesOrPermissions);
PermissionsHelper.SetValue(ref value, manageChannel, PermissionBits.ManageChannel);
PermissionsHelper.SetValue(ref value, readMessages, PermissionBits.ReadMessages);
PermissionsHelper.SetValue(ref value, sendMessages, PermissionBits.SendMessages);
@@ -187,6 +198,7 @@ namespace Discord
PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBits.DeafenMembers);
PermissionsHelper.SetValue(ref value, moveMembers, PermissionBits.MoveMembers);
PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBits.UseVoiceActivation);
PermissionsHelper.SetValue(ref value, managePermissions, PermissionBits.ManageRolesOrPermissions);

RawValue = value;
}
@@ -202,8 +214,6 @@ namespace Discord

/// <summary> If True, a user may create invites. </summary>
public PermValue CreateInstantInvite => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.CreateInstantInvite);
/// <summary> If True, a user may adjust permissions. This also implictly grants all other permissions. </summary>
public PermValue ManagePermissions => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageRolesOrPermissions);
/// <summary> If True, a user may create, delete and modify this channel. </summary>
public PermValue ManageChannel => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageChannel);
/// <summary> If True, a user may join channels. </summary>
@@ -223,8 +233,8 @@ namespace Discord
/// <summary> If True, a user may mention @everyone. </summary>
public PermValue MentionEveryone => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.MentionEveryone);

/// <summary> If True, a user may connect to a voice channel. </summary>
public PermValue Connect => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Connect);
/// <summary> If True, a user may connect to a voice channel. </summary>
public PermValue Connect => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Connect);
/// <summary> If True, a user may speak in a voice channel. </summary>
public PermValue Speak => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Speak);
/// <summary> If True, a user may mute users. </summary>
@@ -236,25 +246,30 @@ namespace Discord
/// <summary> If True, a user may use voice activation rather than push-to-talk. </summary>
public PermValue UseVoiceActivation => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.UseVoiceActivation);

public ChannelPermissionOverrides(PermValue? createInstantInvite = null, PermValue? managePermissions = null,
/// <summary> If True, a user may adjust permissions. </summary>
public PermValue ManagePermissions => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageRolesOrPermissions);

public ChannelPermissionOverrides(PermValue? createInstantInvite = null,
PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null,
PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null,
PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null,
PermValue? moveMembers = null, PermValue? useVoiceActivation = null)
: this(new ChannelPermissionOverrides(), createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages,
manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation)
PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? changeNickname = null, PermValue? manageNicknames = null,
PermValue? managePermissions = null)
: this(new ChannelPermissionOverrides(), createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages,
manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation, managePermissions)
{
}
public ChannelPermissionOverrides(ChannelPermissionOverrides basePerms, PermValue? createInstantInvite = null, PermValue? managePermissions = null,
public ChannelPermissionOverrides(ChannelPermissionOverrides basePerms, PermValue? createInstantInvite = null,
PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null,
PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null,
PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null,
PermValue? moveMembers = null, PermValue? useVoiceActivation = null)
PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? changeNickname = null, PermValue? manageNicknames = null,
PermValue? managePermissions = null)
{
uint allow = basePerms.AllowValue, deny = basePerms.DenyValue;

PermissionsHelper.SetValue(ref allow, ref deny, createInstantInvite, PermissionBits.CreateInstantInvite);
PermissionsHelper.SetValue(ref allow, ref deny, managePermissions, PermissionBits.ManageRolesOrPermissions);
PermissionsHelper.SetValue(ref allow, ref deny, manageChannel, PermissionBits.ManageChannel);
PermissionsHelper.SetValue(ref allow, ref deny, readMessages, PermissionBits.ReadMessages);
PermissionsHelper.SetValue(ref allow, ref deny, sendMessages, PermissionBits.SendMessages);
@@ -270,6 +285,9 @@ namespace Discord
PermissionsHelper.SetValue(ref allow, ref deny, deafenMembers, PermissionBits.DeafenMembers);
PermissionsHelper.SetValue(ref allow, ref deny, moveMembers, PermissionBits.MoveMembers);
PermissionsHelper.SetValue(ref allow, ref deny, useVoiceActivation, PermissionBits.UseVoiceActivation);
PermissionsHelper.SetValue(ref allow, ref deny, changeNickname, PermissionBits.ChangeNickname);
PermissionsHelper.SetValue(ref allow, ref deny, manageNicknames, PermissionBits.ManageNicknames);
PermissionsHelper.SetValue(ref allow, ref deny, managePermissions, PermissionBits.ManageRolesOrPermissions);

AllowValue = allow;
DenyValue = deny;


+ 4
- 2
src/Discord.Net/Models/Profile.cs View File

@@ -23,12 +23,14 @@ namespace Discord
/// <summary> Gets an id uniquely identifying from others with the same name. </summary>
public ushort Discriminator => Client.PrivateUser.Discriminator;
/// <summary> Gets the name of the game this user is currently playing. </summary>
public string CurrentGame => Client.PrivateUser.CurrentGame;
public Game? CurrentGame => Client.PrivateUser.CurrentGame;
/// <summary> Gets the current status for this user. </summary>
public UserStatus Status => Client.PrivateUser.Status;
/// <summary> Returns the string used to mention this user. </summary>
public string Mention => $"<@{Id}>";
/// <summary> Returns the string used to mention this user by nickname. </summary>
public string NicknameMention => $"<@!{Id}>";

/// <summary> Gets the email for this user. </summary>
public string Email { get; private set; }
/// <summary> Gets if the email for this user has been verified. </summary>


+ 9
- 4
src/Discord.Net/Models/Role.cs View File

@@ -9,7 +9,7 @@ using APIRole = Discord.API.Client.Role;

namespace Discord
{
public class Role : IMentionable
public class Role : IMentionable
{
private readonly static Action<Role, Role> _cloner = DynamicIL.CreateCopyMethod<Role>();

@@ -28,6 +28,8 @@ namespace Discord
public int Position { get; private set; }
/// <summary> Gets whether this role is managed by server (e.g. for Twitch integration) </summary>
public bool IsManaged { get; private set; }
/// <summary> Gets whether this role is mentionable by anyone. </summary>
public bool IsMentionable { get; private set; }
/// <summary> Gets the the permissions given to this role. </summary>
public ServerPermissions Permissions { get; private set; }
/// <summary> Gets the color of this role. </summary>
@@ -41,7 +43,7 @@ namespace Discord
public IEnumerable<User> Members => IsEveryone ? Server.Users : Server.Users.Where(x => x.HasRole(this));

/// <summary> Gets the string used to mention this role. </summary>
public string Mention => IsEveryone ? "@everyone" : "";
public string Mention => IsEveryone ? "@everyone" : IsMentionable ? $"<@&{Id}>" : "";

internal Role(ulong id, Server server)
{
@@ -60,6 +62,8 @@ namespace Discord
IsHoisted = model.Hoist.Value;
if (model.Managed != null)
IsManaged = model.Managed.Value;
if (model.Mentionable != null)
IsMentionable = model.Mentionable.Value;
if (model.Position != null && !IsEveryone)
Position = model.Position.Value;
if (model.Color != null)
@@ -75,14 +79,15 @@ namespace Discord
}
}
public async Task Edit(string name = null, ServerPermissions? permissions = null, Color color = null, bool? isHoisted = null, int? position = null)
public async Task Edit(string name = null, ServerPermissions? permissions = null, Color color = null, bool? isHoisted = null, int? position = null, bool? isMentionable = null)
{
var updateRequest = new UpdateRoleRequest(Server.Id, Id)
{
Name = name ?? Name,
Permissions = (permissions ?? Permissions).RawValue,
Color = (color ?? Color).RawValue,
IsHoisted = isHoisted ?? IsHoisted
IsHoisted = isHoisted ?? IsHoisted,
IsMentionable = isMentionable ?? IsMentionable
};

var updateResponse = await Client.ClientAPI.Send(updateRequest).ConfigureAwait(false);


+ 41
- 42
src/Discord.Net/Models/Server.cs View File

@@ -70,10 +70,6 @@ namespace Discord
public int AFKTimeout { get; private set; }
/// <summary> Gets the date and time you joined this server. </summary>
public DateTime JoinedAt { get; private set; }
/// <summary> Gets the default channel for this server. </summary>
public Channel DefaultChannel { get; private set; }
/// <summary> Gets the the role representing all users in a server. </summary>
public Role EveryoneRole { get; private set; }
/// <summary> Gets all extra features added to this server. </summary>
public IEnumerable<string> Features { get; private set; }
/// <summary> Gets all custom emojis on this server. </summary>
@@ -93,6 +89,10 @@ namespace Discord
public string IconUrl => GetIconUrl(Id, IconId);
/// <summary> Gets the URL to this servers's splash image. </summary>
public string SplashUrl => GetSplashUrl(Id, SplashId);
/// <summary> Gets the default channel for this server. </summary>
public Channel DefaultChannel => GetChannel(Id);
/// <summary> Gets the the role representing all users in a server. </summary>
public Role EveryoneRole => GetRole(Id);

/// <summary> Gets a collection of all channels in this server. </summary>
public IEnumerable<Channel> AllChannels => _channels.Select(x => x.Value);
@@ -119,7 +119,7 @@ namespace Discord
Client = client;
Id = id;
}
internal void Update(Guild model)
{
if (model.Name != null)
@@ -144,7 +144,6 @@ namespace Discord
var role = AddRole(x.Id);
role.Update(x, false);
}
EveryoneRole = _roles[Id];
}
if (model.Emojis != null) //Needs Roles
{
@@ -171,7 +170,6 @@ namespace Discord
_channels = new ConcurrentDictionary<ulong, Channel>(2, (int)(model.Channels.Length * 1.05));
foreach (var subModel in model.Channels)
AddChannel(subModel.Id, false).Update(subModel);
DefaultChannel = _channels[Id];
}
if (model.MemberCount != null)
{
@@ -198,7 +196,7 @@ namespace Discord
}
}
}
/// <summary> Edits this server, changing only non-null attributes. </summary>
public Task Edit(string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png)
{
@@ -366,9 +364,9 @@ namespace Discord
if (name == null) throw new ArgumentNullException(nameof(name));
return _roles.Select(x => x.Value).Find(name, exactMatch);
}
/// <summary> Creates a new role. </summary>
public async Task<Role> CreateRole(string name, ServerPermissions? permissions = null, Color color = null, bool isHoisted = false)
public async Task<Role> CreateRole(string name, ServerPermissions? permissions = null, Color color = null, bool isHoisted = false, bool isMentionable = false)
{
if (name == null) throw new ArgumentNullException(nameof(name));

@@ -382,7 +380,8 @@ namespace Discord
Name = name,
Permissions = (permissions ?? role.Permissions).RawValue,
Color = (color ?? Color.Default).RawValue,
IsHoisted = isHoisted
IsHoisted = isHoisted,
IsMentionable = isMentionable
};
var editResponse = await Client.ClientAPI.Send(editRequest).ConfigureAwait(false);
role.Update(editResponse, true);
@@ -405,16 +404,16 @@ namespace Discord

#region Permissions
internal ServerPermissions GetPermissions(User user)
{
Member member;
if (_users.TryGetValue(user.Id, out member))
return member.Permissions;
else
return ServerPermissions.None;
}
internal void UpdatePermissions(User user)
{
{
Member member;
if (_users.TryGetValue(user.Id, out member))
return member.Permissions;
else
return ServerPermissions.None;
}
internal void UpdatePermissions(User user)
{
Member member;
if (_users.TryGetValue(user.Id, out member))
{
@@ -426,30 +425,30 @@ namespace Discord
channel.Value.UpdatePermissions(user);
}
}
}
}

private bool UpdatePermissions(User user, ref ServerPermissions permissions)
{
uint newPermissions = 0;
if (user.Id == _ownerId)
newPermissions = ServerPermissions.All.RawValue;
else
{
foreach (var serverRole in user.Roles)
newPermissions |= serverRole.Permissions.RawValue;
}
if (newPermissions.HasBit((byte)PermissionBits.ManageRolesOrPermissions))
newPermissions = ServerPermissions.All.RawValue;
if (newPermissions != permissions.RawValue)
{
permissions = new ServerPermissions(newPermissions);
{
uint newPermissions = 0;
if (user.Id == _ownerId)
newPermissions = ServerPermissions.All.RawValue;
else
{
foreach (var serverRole in user.Roles)
newPermissions |= serverRole.Permissions.RawValue;
}
if (newPermissions.HasBit((byte)PermissionBits.Administrator))
newPermissions = ServerPermissions.All.RawValue;
if (newPermissions != permissions.RawValue)
{
permissions = new ServerPermissions(newPermissions);
return true;
}
}
return false;
}
}
#endregion

#region Users
@@ -540,5 +539,5 @@ namespace Discord
private Server() { } //Used for cloning

public override string ToString() => Name ?? Id.ToIdString();
}
}
}

+ 51
- 13
src/Discord.Net/Models/User.cs View File

@@ -58,10 +58,14 @@ namespace Discord
public string Name { get; private set; }
/// <summary> Gets an id uniquely identifying from others with the same name. </summary>
public ushort Discriminator { get; private set; }
/// <summary> Gets a user's nickname in a server </summary>
public string Nickname { get; internal set; }
/// <summary> Gets the unique identifier for this user's current avatar. </summary>
public string AvatarId { get; private set; }
/// <summary> Gets the name of the game this user is currently playing. </summary>
public string CurrentGame { get; internal set; }
public Game? CurrentGame { get; internal set; }
/// <summary> Determines whether this user is a Bot account. </summary>
public bool IsBot { get; internal set; }
/// <summary> Gets the current status for this user. </summary>
public UserStatus Status { get; internal set; }
/// <summary> Gets the datetime that this user joined this server. </summary>
@@ -79,6 +83,8 @@ namespace Discord
public Channel PrivateChannel => Client.GetPrivateChannel(Id);
/// <summary> Returns the string used to mention this user. </summary>
public string Mention => $"<@{Id}>";
/// <summary> Returns the string used to mention this user by nickname. </summary>
public string NicknameMention => $"<@!{Id}>";
/// <summary> Returns true if this user has marked themselves as muted. </summary>
public bool IsSelfMuted => (_voiceState & VoiceState.SelfMuted) != 0;
/// <summary> Returns true if this user has marked themselves as deafened. </summary>
@@ -160,6 +166,9 @@ namespace Discord
Discriminator = model.Discriminator.Value;
if (model.Avatar != null)
AvatarId = model.Avatar;
if (model.Bot != null)
IsBot = model.Bot.Value;

}
internal void Update(APIMember model)
{
@@ -170,6 +179,8 @@ namespace Discord
JoinedAt = model.JoinedAt.Value;
if (model.Roles != null)
UpdateRoles(model.Roles.Select(x => Server.GetRole(x)));
if (model.Nick != "")
Nickname = model.Nick;
}
internal void Update(ExtendedMember model)
{
@@ -198,8 +209,11 @@ namespace Discord
if (Status == UserStatus.Offline)
_lastOnline = DateTime.UtcNow;
}
CurrentGame = model.Game?.Name; //Allows null

if (model.Game != null)
CurrentGame = new Game(model.Game.Name, model.Game.Type ?? GameType.Default, model.Game.Url);
else
CurrentGame = null;
}
internal void Update(MemberVoiceState model)
{
@@ -238,7 +252,7 @@ namespace Discord
LastActivityAt = activity ?? DateTime.UtcNow;
}

public Task Edit(bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null)
public async Task Edit(bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null, string nickname = "")
{
if (Server == null) throw new InvalidOperationException("Unable to edit users in a private channel");

@@ -249,14 +263,29 @@ namespace Discord
.Distinct()
.ToArray();

var request = new UpdateMemberRequest(Server.Id, Id)
bool isCurrentUser = Id == Server.CurrentUser.Id;
if (isCurrentUser && nickname != "")
{
IsMuted = isMuted ?? IsServerMuted,
IsDeafened = isDeafened ?? IsServerDeafened,
VoiceChannelId = voiceChannel?.Id,
RoleIds = roleIds
};
return Client.ClientAPI.Send(request);
var request = new UpdateOwnNick(Server.Id, nickname ?? "");
await Client.ClientAPI.Send(request).ConfigureAwait(false);
nickname = "";
}
if (!isCurrentUser || isMuted != null || isDeafened != null || voiceChannel != null || roles != null)
{
//Swap "" and null. Our libs meanings and the API's are flipped.
if (nickname == null) nickname = "";
else if (nickname == "") nickname = null;

var request = new UpdateMemberRequest(Server.Id, Id)
{
IsMuted = isMuted ?? IsServerMuted,
IsDeafened = isDeafened ?? IsServerDeafened,
VoiceChannelId = voiceChannel?.Id,
RoleIds = roleIds,
Nickname = nickname
};
await Client.ClientAPI.Send(request).ConfigureAwait(false);
}
}
public Task Kick()
@@ -268,7 +297,16 @@ namespace Discord
}

#region Permissions
public ServerPermissions ServerPermissions => Server.GetPermissions(this);
public ServerPermissions ServerPermissions
{
get
{
if (Server == null) throw new InvalidOperationException("Unable to get server permissions from a private channel");

return Server.GetPermissions(this);
}
}

public ChannelPermissions GetPermissions(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
@@ -362,6 +400,6 @@ namespace Discord
}
private User() { } //Used for cloning

public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString();
public override string ToString() => Name != null ? $"{Name}#{Discriminator.ToString("D4")}" : Id.ToIdString();
}
}

+ 3
- 4
src/Discord.Net/Net/Rest/BuiltInEngine.cs View File

@@ -1,4 +1,4 @@
#if DOTNET5_4
#if NETSTANDARD1_3
using Discord.Logging;
using System;
using System.IO;
@@ -37,8 +37,7 @@ namespace Discord.Net.Rest
{
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
UseCookies = false,
UseProxy = false,
PreAuthenticate = false //We do auth ourselves
UseProxy = false
});
_client.DefaultRequestHeaders.Add("accept", "*/*");
_client.DefaultRequestHeaders.Add("accept-encoding", "gzip,deflate");
@@ -66,7 +65,7 @@ namespace Discord.Net.Rest
using (var request = new HttpRequestMessage(GetMethod(method), _baseUrl + path))
{
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
content.Add(new StreamContent(File.OpenRead(path)), "file", filename);
content.Add(new StreamContent(stream), "file", filename);
request.Content = content;
return await Send(request, cancelToken).ConfigureAwait(false);
}


+ 0
- 27
src/Discord.Net/Net/Rest/ETFRestClient.cs View File

@@ -1,27 +0,0 @@
using Discord.ETF;
using System.IO;
using System;
using Discord.Logging;

namespace Discord.Net.Rest
{
public class ETFRestClient : RestClient
{
private readonly ETFWriter _serializer;

public ETFRestClient(DiscordConfig config, string baseUrl, ILogger logger = null)
: base(config, baseUrl, logger)
{
_serializer = new ETFWriter(new MemoryStream());
}

protected override string Serialize<T>(T obj)
{
throw new NotImplementedException();
}
protected override T Deserialize<T>(string json)
{
throw new NotImplementedException();
}
}
}

+ 4
- 2
src/Discord.Net/Net/Rest/RestClient.cs View File

@@ -36,7 +36,6 @@ namespace Discord.Net.Rest

private readonly DiscordConfig _config;
private readonly IRestEngine _engine;
private readonly ETFWriter _serializer;
private readonly ILogger _logger;
private string _token;

@@ -52,12 +51,15 @@ namespace Discord.Net.Rest
}
}

public int ShardId => _config.ShardId;
public int TotalShards => _config.TotalShards;

protected RestClient(DiscordConfig config, string baseUrl, ILogger logger = null)
{
_config = config;
_logger = logger;

#if !DOTNET5_4
#if !NETSTANDARD1_3
_engine = new RestSharpEngine(config, baseUrl, logger);
#else
_engine = new BuiltInEngine(config, baseUrl, logger);


+ 1
- 1
src/Discord.Net/Net/Rest/SharpRestEngine.cs View File

@@ -1,4 +1,4 @@
#if !DOTNET5_4
#if !NETSTANDARD1_3
using Discord.Logging;
using Nito.AsyncEx;
using RestSharp;


+ 3
- 4
src/Discord.Net/Net/WebSockets/BuiltInEngine.cs View File

@@ -1,4 +1,4 @@
#if DOTNET5_4
#if NETSTANDARD1_3
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -51,8 +51,7 @@ namespace Discord.Net.WebSockets
{
string ignored;
while (_sendQueue.TryDequeue(out ignored)) { }

var socket = _webSocket;
_webSocket = null;

return TaskHelper.CompletedTask;
@@ -89,7 +88,7 @@ namespace Discord.Net.WebSockets
if (result.MessageType == WebSocketMessageType.Close)
throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription);
else
stream.Write(buffer.Array, buffer.Offset, buffer.Count);
stream.Write(buffer.Array, 0, result.Count);

}
while (result == null || !result.EndOfMessage);


+ 18
- 18
src/Discord.Net/Net/WebSockets/GatewaySocket.cs View File

@@ -11,6 +11,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using APIGame = Discord.API.Client.Game;

namespace Discord.Net.WebSockets
{
@@ -43,12 +44,13 @@ namespace Discord.Net.WebSockets
//Token = rest.Token;

var gatewayResponse = await rest.Send(new GatewayRequest()).ConfigureAwait(false);
Logger.Verbose($"Login successful, gateway: {gatewayResponse.Url}");
string url = $"{gatewayResponse.Url}/?encoding=json&v=4";
Logger.Verbose($"Login successful, gateway: {url}");

Host = gatewayResponse.Url;
Host = url;
await BeginConnect(parentCancelToken).ConfigureAwait(false);
if (SessionId == null)
SendIdentify(_rest.Token);
SendIdentify(_rest.Token, _rest.ShardId, _rest.TotalShards);
else
SendResume();
}
@@ -131,15 +133,10 @@ namespace Discord.Net.WebSockets
}
}
break;
case OpCodes.Redirect:
case OpCodes.Reconnect:
{
var payload = (msg.Payload as JToken).ToObject<RedirectEvent>(_serializer);
if (payload.Url != null)
{
Host = payload.Url;
Logger.Info("Redirected to " + payload.Url);
await Reconnect().ConfigureAwait(false);
}
var payload = (msg.Payload as JToken).ToObject<ReconnectEvent>(_serializer);
await Reconnect().ConfigureAwait(false);
}
break;
default:
@@ -151,7 +148,7 @@ namespace Discord.Net.WebSockets
}
}

public void SendIdentify(string token)
public void SendIdentify(string token, int shardId = 0, int totalShards = 1)
{
var props = new Dictionary<string, string>
{
@@ -159,24 +156,27 @@ namespace Discord.Net.WebSockets
};
var msg = new IdentifyCommand()
{
Version = 3,
Token = token,
Properties = props,
Properties = props,
LargeThreshold = _config.LargeThreshold,
UseCompression = true
UseCompression = true,
ShardingParams = token.StartsWith("Bot ") ? new int[] { shardId, totalShards } : null
};
QueueMessage(msg);

QueueMessage(msg);
}

public void SendResume()
=> QueueMessage(new ResumeCommand { SessionId = SessionId, Sequence = _lastSequence });
public override void SendHeartbeat()
=> QueueMessage(new HeartbeatCommand());
public void SendUpdateStatus(long? idleSince, string gameName)
public void SendUpdateStatus(long? idleSince, Game? game, bool? afk, UserStatus status)
=> QueueMessage(new UpdateStatusCommand
{
IdleSince = idleSince,
Game = gameName != null ? new UpdateStatusCommand.GameInfo { Name = gameName } : null
Game = game != null ? new APIGame { Name = game.Value.Name, Type = game.Value.Type, Url = game.Value.Url } : null,
Afk = afk,
Status = status.Value
});
public void SendUpdateVoice(ulong? serverId, ulong? channelId, bool isSelfMuted, bool isSelfDeafened)
=> QueueMessage(new UpdateVoiceCommand { GuildId = serverId, ChannelId = channelId, IsSelfMuted = isSelfMuted, IsSelfDeafened = isSelfDeafened });


+ 1
- 1
src/Discord.Net/Net/WebSockets/WS4NetEngine.cs View File

@@ -1,4 +1,4 @@
#if !DOTNET5_4
#if !NETSTANDARD1_3
using SuperSocket.ClientEngine;
using System;
using System.Collections.Concurrent;


+ 71
- 71
src/Discord.Net/Net/WebSockets/WebSocket.cs View File

@@ -10,36 +10,36 @@ using System.Threading.Tasks;

namespace Discord.Net.WebSockets
{
public abstract partial class WebSocket
public abstract partial class WebSocket
{
private readonly AsyncLock _lock;
protected readonly IWebSocketEngine _engine;
protected readonly DiscordConfig _config;
protected readonly ManualResetEventSlim _connectedEvent;
protected readonly DiscordConfig _config;
protected readonly ManualResetEventSlim _connectedEvent;
protected readonly TaskManager _taskManager;
protected readonly JsonSerializer _serializer;
protected CancellationTokenSource _cancelSource;
protected CancellationToken _parentCancelToken;
protected int _heartbeatInterval;
private DateTime _lastHeartbeat;
private DateTime _lastHeartbeat;

/// <summary> Gets the logger used for this client. </summary>
protected internal Logger Logger { get; }
public CancellationToken CancelToken { get; private set; }

public string Host { get; set; }
public string Host { get; set; }
/// <summary> Gets the current connection state of this client. </summary>
public ConnectionState State { get; private set; }

public event EventHandler Connected = delegate { };
private void OnConnected()
=> Connected(this, EventArgs.Empty);
private void OnConnected()
=> Connected(this, EventArgs.Empty);
public event EventHandler<DisconnectedEventArgs> Disconnected = delegate { };
private void OnDisconnected(bool wasUnexpected, Exception error)
private void OnDisconnected(bool wasUnexpected, Exception error)
=> Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error));

public WebSocket(DiscordConfig config, JsonSerializer serializer, Logger logger)
{
public WebSocket(DiscordConfig config, JsonSerializer serializer, Logger logger)
{
_config = config;
_serializer = serializer;
Logger = logger;
@@ -47,30 +47,30 @@ namespace Discord.Net.WebSockets
_lock = new AsyncLock();
_taskManager = new TaskManager(Cleanup);
CancelToken = new CancellationToken(true);
_connectedEvent = new ManualResetEventSlim(false);
_connectedEvent = new ManualResetEventSlim(false);

#if !DOTNET5_4
_engine = new WS4NetEngine(config, _taskManager);
#if !NETSTANDARD1_3
_engine = new WS4NetEngine(config, _taskManager);
#else
_engine = new BuiltInEngine(config);
#endif
_engine.BinaryMessage += (s, e) =>
{
using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2))
using (var decompressed = new MemoryStream())
{
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress))
zlib.CopyTo(decompressed);
decompressed.Position = 0;
using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 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))
ProcessMessage(reader.ReadToEnd()).GetAwaiter().GetResult();
}
ProcessMessage(reader.ReadToEnd()).GetAwaiter().GetResult();
}
};
_engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait();
}
_engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait();
}

protected async Task BeginConnect(CancellationToken parentCancelToken)
{
protected async Task BeginConnect(CancellationToken parentCancelToken)
{
try
{
using (await _lock.LockAsync().ConfigureAwait(false))
@@ -80,7 +80,7 @@ namespace Discord.Net.WebSockets
await _taskManager.Stop().ConfigureAwait(false);
_taskManager.ClearException();
State = ConnectionState.Connecting;
_cancelSource = new CancellationTokenSource();
CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelSource.Token, parentCancelToken).Token;
_lastHeartbeat = DateTime.UtcNow;
@@ -88,39 +88,39 @@ namespace Discord.Net.WebSockets
await _engine.Connect(Host, CancelToken).ConfigureAwait(false);
await Run().ConfigureAwait(false);
}
}
catch (Exception ex)
{
}
catch (Exception ex)
{
//TODO: Should this be inside the lock?
await _taskManager.SignalError(ex).ConfigureAwait(false);
throw;
}
}
protected async Task EndConnect()
{
try
}
}
protected async Task EndConnect()
{
try
{
State = ConnectionState.Connected;
Logger.Info($"Connected");

OnConnected();
_connectedEvent.Set();
}
catch (Exception ex)
}
catch (Exception ex)
{
await _taskManager.SignalError(ex).ConfigureAwait(false);
}
}
}

protected abstract Task Run();
protected virtual async Task Cleanup()
{
protected abstract Task Run();
protected virtual async Task Cleanup()
{
var oldState = State;
State = ConnectionState.Disconnecting;

await _engine.Disconnect().ConfigureAwait(false);
_cancelSource = null;
_connectedEvent.Reset();
_cancelSource = null;
_connectedEvent.Reset();

if (oldState == ConnectionState.Connecting || oldState == ConnectionState.Connected)
{
@@ -136,35 +136,35 @@ namespace Discord.Net.WebSockets
State = ConnectionState.Disconnected;
}

protected virtual Task ProcessMessage(string json)
{
return TaskHelper.CompletedTask;
}
protected void QueueMessage(IWebSocketMessage message)
{
string json = JsonConvert.SerializeObject(new WebSocketMessage(message));
_engine.QueueMessage(json);
}
protected Task HeartbeatAsync(CancellationToken cancelToken)
{
return Task.Run(async () =>
{
try
{
while (!cancelToken.IsCancellationRequested)
{
if (this.State == ConnectionState.Connected && _heartbeatInterval > 0)
{
protected virtual Task ProcessMessage(string json)
{
return TaskHelper.CompletedTask;
}
protected void QueueMessage(IWebSocketMessage message)
{
string json = JsonConvert.SerializeObject(new WebSocketMessage(message));
_engine.QueueMessage(json);
}
protected Task HeartbeatAsync(CancellationToken cancelToken)
{
return Task.Run(async () =>
{
try
{
while (!cancelToken.IsCancellationRequested)
{
if (this.State == ConnectionState.Connected && _heartbeatInterval > 0)
{
SendHeartbeat();
await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false);
}
else
await Task.Delay(1000, cancelToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
});
await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false);
}
else
await Task.Delay(1000, cancelToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
});
}
public abstract void SendHeartbeat();

@@ -184,5 +184,5 @@ namespace Discord.Net.WebSockets
throw;
}
}
}
}
}

+ 1
- 0
src/Discord.Net/TaskManager.cs View File

@@ -20,6 +20,7 @@ namespace Discord

public bool StopOnCompletion { get; }
public bool WasStopExpected { get; private set; }
public CancellationToken CancelToken => _cancelSource.Token;

public Exception Exception => _stopReason?.SourceException;



+ 31
- 48
src/Discord.Net/project.json View File

@@ -1,65 +1,48 @@
{
"version": "0.9.0-rc3-3",
"version": "0.9.6",
"description": "An unofficial .Net API wrapper for the Discord client.",
"authors": [
"RogueException"
],
"tags": [
"discord",
"discordapp"
],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
"authors": [ "RogueException" ],

"packOptions": {
"tags": [
"discord",
"discordapp"
],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
}
},
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],

"compilationOptions": {
"buildOptions": {
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
"allowUnsafe": true,
"warningsAsErrors": true
},

"configurations": {
"TestResponses": {
"compilationOptions": {
"define": [
"DEBUG",
"TRACE",
"TEST_RESPONSES"
]
}
}
},

"dependencies": {
"Newtonsoft.Json": "8.0.1",
"Newtonsoft.Json": "8.0.3",
"Nito.AsyncEx": "3.0.1"
},

"frameworks": {
"dotnet5.4": {
"netstandard1.3": {
"dependencies": {
"System.Collections": "4.0.11-beta-23516",
"System.Collections.Concurrent": "4.0.11-beta-23516",
"System.Dynamic.Runtime": "4.0.11-beta-23516",
"System.IO.FileSystem": "4.0.1-beta-23516",
"System.IO.Compression": "4.1.0-beta-23516",
"System.Linq": "4.0.1-beta-23516",
"System.Net.Http": "4.0.1-beta-23516",
"System.Net.NameResolution": "4.0.0-beta-23516",
"System.Net.Sockets": "4.1.0-beta-23409",
"System.Net.Requests": "4.0.11-beta-23516",
"System.Net.WebSockets.Client": "4.0.0-beta-23516",
"System.Reflection": "4.1.0-beta-23516",
"System.Reflection.Emit.Lightweight": "4.0.1-beta-23516",
"System.Runtime.InteropServices": "4.0.21-beta-23516",
"System.Runtime.Serialization.Primitives": "4.1.0-beta-23516",
"System.Security.Cryptography.Algorithms": "4.0.0-beta-23516",
"System.Text.RegularExpressions": "4.0.11-beta-23516",
"System.Threading": "4.0.11-beta-23516"
}
"NETStandard.Library": "1.6.0",
"System.Net.Requests": "4.0.11",
"System.Net.Websockets.Client": "4.0.0",
"System.Reflection.Emit.Lightweight": "4.0.1",
"System.Runtime.Serialization.Primitives": "4.1.1",
"System.Security.Cryptography.Algorithms": "4.2.0",
"System.Net.NameResolution": "4.0.0"
},
"imports": [
"dotnet5.4",
"dnxcore50",
"portable-net45+win8"
]
},
"net45": {
"frameworkAssemblies": {


+ 4
- 5
test/Discord.Net.Tests/Discord.Net.Tests.csproj View File

@@ -37,7 +37,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\..\DiscordBot\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<HintPath>..\..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
@@ -56,18 +56,17 @@
</Choose>
<ItemGroup>
<Compile Include="Tests.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.Net45\Discord.Net.csproj">
<Project>{8d71a857-879a-4a10-859e-5ff824ed6688}</Project>
<Name>Discord.Net</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>


+ 0
- 32
test/Discord.Net.Tests/Settings.cs View File

@@ -1,32 +0,0 @@
using Newtonsoft.Json;
using System.IO;

namespace Discord.Tests
{
internal class Settings
{
private const string path = "../../config.json";
public static readonly Settings Instance;
static Settings()
{
if (!File.Exists(path))
throw new FileNotFoundException("config.json is missing, rename config.json.example and add credentials for three separate unused accounts for testing.");
Instance = JsonConvert.DeserializeObject<Settings>(File.ReadAllText(path));
}

public class Account
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}

[JsonProperty("user1")]
public Account User1 { get; set; }
[JsonProperty("user2")]
public Account User2 { get; set; }
[JsonProperty("user3")]
public Account User3 { get; set; }
}
}

+ 477
- 158
test/Discord.Net.Tests/Tests.cs View File

@@ -7,165 +7,484 @@ using System.Threading.Tasks;

namespace Discord.Tests
{
//TODO: Tests are massively incomplete and out of date, needing a full rewrite
//TODO: Tests are massively incomplete and out of date, needing a full rewrite

[TestClass]
public class Tests
{
private const int EventTimeout = 10000; //Max time in milliseconds to wait for an event response from our test actions

[TestClass]
public class Tests
{
private const int EventTimeout = 5000; //Max time in milliseconds to wait for an event response from our test actions
private static DiscordClient _hostClient, _targetBot, _observerBot;
private static Server _testServer;
private static Channel _testServerChannel;
private static Random _random;

[ClassInitialize]
public static void Initialize(TestContext testContext)
{
var settings = Settings.Instance;
_random = new Random();

_hostClient = new DiscordClient();
_targetBot = new DiscordClient();
_observerBot = new DiscordClient();

_hostClient.Connect(settings.User1.Email, settings.User1.Password).Wait();
_targetBot.Connect(settings.User2.Email, settings.User2.Password).Wait();
_observerBot.Connect(settings.User3.Email, settings.User3.Password).Wait();

//Cleanup existing servers
WaitMany(
_hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()),
_targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()),
_observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()));

//Create new server and invite the other bots to it
_testServer = _hostClient.CreateServer("Discord.Net Testing", _hostClient.Regions.First()).Result;
_testServerChannel = _testServer.DefaultChannel;
Invite invite = _testServer.CreateInvite(60, 1, false, false).Result;
WaitAll(
_targetBot.GetInvite(invite.Code).Result.Accept(),
_observerBot.GetInvite(invite.Code).Result.Accept());
}

//Channels
[TestMethod]
public void TestCreateTextChannel()
=> TestCreateChannel(ChannelType.Text);
[TestMethod]
public void TestCreateVoiceChannel()
=> TestCreateChannel(ChannelType.Voice);
private void TestCreateChannel(ChannelType type)
{
Channel channel = null;
string name = $"#test_{_random.Next()}";
AssertEvent<ChannelEventArgs>(
"ChannelCreated event never received",
async () => channel = await _testServer.CreateChannel(name.Substring(1), type),
x => _targetBot.ChannelCreated += x,
x => _targetBot.ChannelCreated -= x,
(s, e) => e.Channel.Name == name);

AssertEvent<ChannelEventArgs>(
"ChannelDestroyed event never received",
async () => await channel.Delete(),
x => _targetBot.ChannelDestroyed += x,
x => _targetBot.ChannelDestroyed -= x,
(s, e) => e.Channel.Name == name);
}

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public async Task TestCreateChannel_NoName()
{
await _testServer.CreateChannel($"", ChannelType.Text);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public async Task TestCreateChannel_NoType()
{
string name = $"#test_{_random.Next()}";
await _testServer.CreateChannel($"", ChannelType.FromString(""));
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public async Task TestCreateChannel_BadType()
{
string name = $"#test_{_random.Next()}";
await _testServer.CreateChannel($"", ChannelType.FromString("badtype"));
}

//Messages
[TestMethod]
public void TestSendMessage()
{
string text = $"test_{_random.Next()}";
AssertEvent<MessageEventArgs>(
"MessageCreated event never received",
() => _testServerChannel.SendMessage(text),
x => _targetBot.MessageReceived += x,
x => _targetBot.MessageReceived -= x,
(s, e) => e.Message.Text == text);
}

[ClassCleanup]
public static void Cleanup()
{
WaitMany(
_hostClient.State == ConnectionState.Connected ? _hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null,
_targetBot.State == ConnectionState.Connected ? _targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null,
_observerBot.State == ConnectionState.Connected ? _observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null);

WaitAll(
_hostClient.Disconnect(),
_targetBot.Disconnect(),
_observerBot.Disconnect());
}

private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null)
{
AssertEvent(msg, action, addEvent, removeEvent, test, true);
}
private static void AssertNoEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null)
{
AssertEvent(msg, action, addEvent, removeEvent, test, false);
}
private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test, bool assertTrue)
{
ManualResetEventSlim trigger = new ManualResetEventSlim(false);
bool result = false;

EventHandler<TArgs> handler = (s, e) =>
{
if (test != null)
{
result |= test(s, e);
trigger.Set();
private static Server _testServer;
private static Channel _testServerChannel;
private static Random _random;
private static Invite _testServerInvite;

private static TestContext _context;

private static string HostBotToken;
private static string ObserverBotToken;
private static string TargetEmail;
private static string TargetPassword;

public static string RandomText => $"test_{_random.Next()}";

#region Initialization

[ClassInitialize]
public static void Initialize(TestContext testContext)
{
_context = testContext;

HostBotToken = Environment.GetEnvironmentVariable("discord-unit-host_token");
ObserverBotToken = Environment.GetEnvironmentVariable("discord-unit-observer_token");
TargetEmail = Environment.GetEnvironmentVariable("discord-unit-target_email");
TargetPassword = Environment.GetEnvironmentVariable("discord-unit-target_pass");
}

[TestMethod]
[Priority(1)]
public async Task TestInitialize()
{
_context.WriteLine("Initializing.");

_random = new Random();

_hostClient = new DiscordClient();
_targetBot = new DiscordClient();
_observerBot = new DiscordClient();

await _hostClient.Connect(HostBotToken, TokenType.Bot);

await Task.Delay(3000);

//Cleanup existing servers
_hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave());
//Create new server and invite the other bots to it

_testServer = await _hostClient.CreateServer("Discord.Net Testing", _hostClient.Regions.First());

await Task.Delay(1000);

Invite invite = await _testServer.CreateInvite(60, 3, false, false);
_testServerInvite = invite;

_context.WriteLine($"Host: {_hostClient.CurrentUser.Name} in {_hostClient.Servers.Count()}");
}

[TestMethod]
[Priority(2)]
public async Task TestTokenLogin_Ready()
{
AssertEvent(
"READY never received",
async () => await _observerBot.Connect(ObserverBotToken, TokenType.Bot),
x => _observerBot.Ready += x,
x => _observerBot.Ready -= x,
null,
true);
_observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave());
await (await _observerBot.GetInvite(_testServerInvite.Code)).Accept();
}

[TestMethod]
[Priority(2)]
public void TestReady()
{
AssertEvent(
"READY never received",
async () => await _targetBot.Connect(TargetEmail, TargetPassword),
x => _targetBot.Ready += x,
x => _targetBot.Ready -= x,
null,
true);

_targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave());
_testServerChannel = _testServer.DefaultChannel;
}

#endregion

// Servers

#region Server Tests

[TestMethod]
[Priority(3)]
public void TestJoinedServer()
{
AssertEvent<ServerEventArgs>(
"Never Got JoinedServer",
async () => await (await _targetBot.GetInvite(_testServerInvite.Code)).Accept(),
x => _targetBot.JoinedServer += x,
x => _targetBot.JoinedServer -= x);
}

#endregion

#region Channel Tests

//Channels
[TestMethod]
public void TestCreateTextChannel()
=> TestCreateChannel(ChannelType.Text);
[TestMethod]
public void TestCreateVoiceChannel()
=> TestCreateChannel(ChannelType.Voice);
private void TestCreateChannel(ChannelType type)
{
_context.WriteLine($"Host: {_hostClient.CurrentUser.Name} in {_hostClient.Servers.Count()}");
_context.WriteLine($"Target: {_targetBot.CurrentUser.Name} in {_targetBot.Servers.Count()}");
_context.WriteLine($"Observer: {_observerBot.CurrentUser.Name} in {_observerBot.Servers.Count()}");
Channel channel = null;
string name = $"test_{_random.Next()}";
AssertEvent<ChannelEventArgs>(
"ChannelCreated event never received",
async () => channel = await _testServer.CreateChannel(name, type),
x => _targetBot.ChannelCreated += x,
x => _targetBot.ChannelCreated -= x,
(s, e) => e.Channel.Name == name);

AssertEvent<ChannelEventArgs>(
"ChannelDestroyed event never received",
async () => await channel.Delete(),
x => _targetBot.ChannelDestroyed += x,
x => _targetBot.ChannelDestroyed -= x,
(s, e) => e.Channel.Name == name);
}

[TestMethod]
[ExpectedException(typeof(Net.HttpException))]
public async Task TestCreateChannel_NoName()
{
await _testServer.CreateChannel($"", ChannelType.Text);
}
[TestMethod]
[ExpectedException(typeof(Net.HttpException))]
public async Task TestCreateChannel_NoType()
{
string name = $"#test_{_random.Next()}";
await _testServer.CreateChannel($"", ChannelType.FromString(""));
}
[TestMethod]
[ExpectedException(typeof(Net.HttpException))]
public async Task TestCreateChannel_BadType()
{
string name = $"#test_{_random.Next()}";
await _testServer.CreateChannel($"", ChannelType.FromString("badtype"));
}
[TestMethod]
public async Task Test_CreateGetChannel()
{
var name = $"test_{_random.Next()}";
var channel = await _testServer.CreateChannel(name, ChannelType.Text);
var get_channel = _testServer.GetChannel(channel.Id);
Assert.AreEqual(channel.Id, get_channel.Id, "ID of Channel and GetChannel were not equal.");
}
[TestMethod]
public void TestSendTyping()
{
var channel = _testServerChannel;
AssertEvent<ChannelUserEventArgs>(
"UserUpdated event never fired.",
async () => await channel.SendIsTyping(),
x => _targetBot.UserIsTyping += x,
x => _targetBot.UserIsTyping -= x);
}
[TestMethod]
public void TestEditChannel()
{
var channel = _testServerChannel;
AssertEvent<ChannelUpdatedEventArgs>(
"ChannelUpdated Never Received",
async () => await channel.Edit(RandomText, $"topic - {RandomText}", 26),
x => _targetBot.ChannelUpdated += x,
x => _targetBot.ChannelUpdated -= x);
}
[TestMethod]
public void TestChannelMention()
{
var channel = _testServerChannel;
Assert.AreEqual($"<#{channel.Id}>", channel.Mention, "Generated channel mention was not the expected channel mention.");
}
[TestMethod]
public void TestChannelUserCount()
{
Assert.AreEqual(3, _testServerChannel.Users.Count(), "Read an incorrect number of users in a channel");
}

#endregion

#region Message Tests

//Messages
[TestMethod]
public void TestMessageEvents()
{
string name = $"test_{_random.Next()}";
var channel = _testServer.CreateChannel(name, ChannelType.Text).Result;
_context.WriteLine($"Channel Name: {channel.Name} / {channel.Server.Name}");
string text = $"test_{_random.Next()}";
Message message = null;
AssertEvent<MessageEventArgs>(
"MessageCreated event never received",
async () => message = await channel.SendMessage(text),
x => _targetBot.MessageReceived += x,
x => _targetBot.MessageReceived -= x,
(s, e) => e.Message.Text == text);

AssertEvent<MessageUpdatedEventArgs>(
"MessageUpdated event never received",
async () => await message.Edit(text + " updated"),
x => _targetBot.MessageUpdated += x,
x => _targetBot.MessageUpdated -= x,
(s, e) => e.Before.Text == text && e.After.Text == text + " updated");

AssertEvent<MessageEventArgs>(
"MessageDeleted event never received",
async () => await message.Delete(),
x => _targetBot.MessageDeleted += x,
x => _targetBot.MessageDeleted -= x,
(s, e) => e.Message.Id == message.Id);
}
[TestMethod]
public async Task TestDownloadMessages_WithCache()
{
string name = $"test_{_random.Next()}";
var channel = await _testServer.CreateChannel(name, ChannelType.Text);
for (var i = 0; i < 10; i++) await channel.SendMessage(RandomText);
while (channel.Client.MessageQueue.Count > 0) await Task.Delay(100);
var messages = await channel.DownloadMessages(10);
Assert.AreEqual(10, messages.Count(), "Expected 10 messages in downloaded array, did not see 10.");
}
[TestMethod]
public async Task TestDownloadMessages_WithoutCache()
{
string name = $"test_{_random.Next()}";
var channel = await _testServer.CreateChannel(name, ChannelType.Text);
for (var i = 0; i < 10; i++) await channel.SendMessage(RandomText);
while (channel.Client.MessageQueue.Count > 0) await Task.Delay(100);
var messages = await channel.DownloadMessages(10, useCache: false);
Assert.AreEqual(10, messages.Count(), "Expected 10 messages in downloaded array, did not see 10.");
}
[TestMethod]
public async Task TestSendTTSMessage()
{
var channel = await _testServer.CreateChannel(RandomText, ChannelType.Text);
AssertEvent<MessageEventArgs>(
"MessageCreated event never fired",
async () => await channel.SendTTSMessage(RandomText),
x => _targetBot.MessageReceived += x,
x => _targetBot.MessageReceived -= x,
(s, e) => e.Message.IsTTS);
}

#endregion

#region User Tests

[TestMethod]
public void TestUserMentions()
{
var user = _targetBot.GetServer(_testServer.Id).CurrentUser;
Assert.AreEqual($"<@{user.Id}>", user.Mention);
}
[TestMethod]
public void TestUserEdit()
{
var user = _testServer.GetUser(_targetBot.CurrentUser.Id);
AssertEvent<UserUpdatedEventArgs>(
"UserUpdated never fired",
async () => await user.Edit(true, true, null, null),
x => _targetBot.UserUpdated += x,
x => _targetBot.UserUpdated -= x);
}
[TestMethod]
public void TestEditSelf()
{
var name = $"test_{_random.Next()}";
AssertEvent<UserUpdatedEventArgs>(
"UserUpdated never fired",
async () => await _targetBot.CurrentUser.Edit(TargetPassword, name),
x => _observerBot.UserUpdated += x,
x => _observerBot.UserUpdated -= x,
(s, e) => e.After.Name == name);
}
[TestMethod]
public void TestSetStatus()
{
AssertEvent<UserUpdatedEventArgs>(
"UserUpdated never fired",
async () => await SetStatus(_targetBot, UserStatus.Idle),
x => _observerBot.UserUpdated += x,
x => _observerBot.UserUpdated -= x,
(s, e) => e.After.Status == UserStatus.Idle);
}
private async Task SetStatus(DiscordClient _client, UserStatus status)
{
_client.SetStatus(status);
await Task.Delay(50);
}
[TestMethod]
public void TestSetGame()
{
AssertEvent<UserUpdatedEventArgs>(
"UserUpdated never fired",
async () => await SetGame(_targetBot, "test game"),
x => _observerBot.UserUpdated += x,
x => _observerBot.UserUpdated -= x,
(s, e) => _targetBot.CurrentGame.Name == "test game");

}
private async Task SetGame(DiscordClient _client, string game)
{
_client.SetGame(game);
await Task.Delay(5);
}

#endregion

#region Permission Tests

// Permissions
[TestMethod]
public async Task Test_AddGet_PermissionsRule()
{
var channel = await _testServer.CreateChannel($"test_{_random.Next()}", ChannelType.Text);
var user = _testServer.GetUser(_targetBot.CurrentUser.Id);
var perms = new ChannelPermissionOverrides(sendMessages: PermValue.Deny);
await channel.AddPermissionsRule(user, perms);
var resultPerms = channel.GetPermissionsRule(user);
Assert.IsNotNull(resultPerms, "Perms retrieved from server were null.");
}
[TestMethod]
public async Task Test_AddRemove_PermissionsRule()
{
var channel = await _testServer.CreateChannel($"test_{_random.Next()}", ChannelType.Text);
var user = _testServer.GetUser(_targetBot.CurrentUser.Id);
var perms = new ChannelPermissionOverrides(sendMessages: PermValue.Deny);
await channel.AddPermissionsRule(user, perms);
await channel.RemovePermissionsRule(user);
await Task.Delay(200);
Assert.AreEqual(PermValue.Inherit, channel.GetPermissionsRule(user).SendMessages);
}
[TestMethod]
public async Task Test_Permissions_Event()
{
var channel = await _testServer.CreateChannel($"test_{_random.Next()}", ChannelType.Text);
var user = _testServer.GetUser(_targetBot.CurrentUser.Id);
var perms = new ChannelPermissionOverrides(sendMessages: PermValue.Deny);
AssertEvent<ChannelUpdatedEventArgs>
("ChannelUpdatedEvent never fired.",
async () => await channel.AddPermissionsRule(user, perms),
x => _targetBot.ChannelUpdated += x,
x => _targetBot.ChannelUpdated -= x,
(s, e) => e.After.PermissionOverwrites.Count() != e.Before.PermissionOverwrites.Count());
}
[TestMethod]
[ExpectedException(typeof(Net.HttpException))]
public async Task Test_Affect_Permissions_Invalid_Channel()
{
var channel = await _testServer.CreateChannel($"test_{_random.Next()}", ChannelType.Text);
var user = _testServer.GetUser(_targetBot.CurrentUser.Id);
var perms = new ChannelPermissionOverrides(sendMessages: PermValue.Deny);
await channel.Delete();
await channel.AddPermissionsRule(user, perms);
}

#endregion


[ClassCleanup]
public static void Cleanup()
{
WaitMany(
_hostClient.State == ConnectionState.Connected ? _hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null,
_targetBot.State == ConnectionState.Connected ? _targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null,
_observerBot.State == ConnectionState.Connected ? _observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null);

WaitAll(
_hostClient.Disconnect(),
_targetBot.Disconnect(),
_observerBot.Disconnect());
}

#region Helpers

// Task Helpers

private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null)
{
AssertEvent(msg, action, addEvent, removeEvent, test, true);
}
private static void AssertNoEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null)
{
AssertEvent(msg, action, addEvent, removeEvent, test, false);
}
private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test, bool assertTrue)
{
ManualResetEventSlim trigger = new ManualResetEventSlim(false);
bool result = false;

EventHandler<TArgs> handler = (s, e) =>
{
if (test != null)
{
result |= test(s, e);
trigger.Set();
}
else
result = true;
};

addEvent(handler);
var task = action();
trigger.Wait(EventTimeout);
task.Wait();
removeEvent(handler);
Assert.AreEqual(assertTrue, result, msg);
}

private static void WaitAll(params Task[] tasks)
{
Task.WaitAll(tasks);
}
private static void WaitAll(IEnumerable<Task> tasks)
{
Task.WaitAll(tasks.ToArray());
}
private static void WaitMany(params IEnumerable<Task>[] tasks)
{
Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray());
}
}
else
result = true;
};

addEvent(handler);
var task = action();
trigger.Wait(EventTimeout);
task.Wait();
removeEvent(handler);

Assert.AreEqual(assertTrue, result, msg);
}

private static void AssertEvent(string msg, Func<Task> action, Action<EventHandler> addEvent, Action<EventHandler> removeEvent, Func<object, bool> test, bool assertTrue)
{
ManualResetEventSlim trigger = new ManualResetEventSlim(false);
bool result = false;

EventHandler handler = (s, e) =>
{
if (test != null)
{
result |= test(s);
trigger.Set();
}
else
result = true;
};

addEvent(handler);
var task = action();
trigger.Wait(EventTimeout);
task.Wait();
removeEvent(handler);

Assert.AreEqual(assertTrue, result, msg);
}

private static void WaitAll(params Task[] tasks)
{
Task.WaitAll(tasks);
}
private static void WaitAll(IEnumerable<Task> tasks)
{
Task.WaitAll(tasks.ToArray());
}
private static void WaitMany(params IEnumerable<Task>[] tasks)
{
Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray());
}

#endregion
}
}

+ 1
- 1
test/Discord.Net.Tests/packages.config View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="8.0.1" targetFramework="net45" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net45" />
</packages>

Loading…
Cancel
Save