Browse Source

Refactor all the things!

tags/docs-0.9
RogueException 10 years ago
parent
commit
4529ad3d4a
70 changed files with 3070 additions and 3059 deletions
  1. +0
    -31
      Discord.Net.sln
  2. +1
    -1
      src/Discord.Net.Commands/DiscordBotClient.cs
  3. +2
    -2
      src/Discord.Net.Commands/project.json
  4. +96
    -50
      src/Discord.Net.Net45/Discord.Net.csproj
  5. BIN
      src/Discord.Net.Net45/lib/libopus.so
  6. BIN
      src/Discord.Net.Net45/lib/opus.dll
  7. +1
    -0
      src/Discord.Net.Net45/packages.config
  8. +0
    -152
      src/Discord.Net/API/DiscordAPI.cs
  9. +0
    -56
      src/Discord.Net/API/Endpoints.cs
  10. +0
    -101
      src/Discord.Net/API/Models/APIResponses.cs
  11. +71
    -0
      src/Discord.Net/Audio/Opus.cs
  12. +17
    -15
      src/Discord.Net/Audio/OpusEncoder.cs
  13. +118
    -0
      src/Discord.Net/Collections/AsyncCollection.cs
  14. +61
    -0
      src/Discord.Net/Collections/Channels.cs
  15. +67
    -0
      src/Discord.Net/Collections/Members.cs
  16. +33
    -0
      src/Discord.Net/Collections/Messages.cs
  17. +44
    -0
      src/Discord.Net/Collections/Roles.cs
  18. +40
    -0
      src/Discord.Net/Collections/Servers.cs
  19. +54
    -0
      src/Discord.Net/Collections/Users.cs
  20. +87
    -89
      src/Discord.Net/DiscordClient.API.cs
  21. +34
    -447
      src/Discord.Net/DiscordClient.Cache.cs
  22. +24
    -77
      src/Discord.Net/DiscordClient.Events.cs
  23. +57
    -0
      src/Discord.Net/DiscordClient.Voice.cs
  24. +252
    -584
      src/Discord.Net/DiscordClient.cs
  25. +40
    -20
      src/Discord.Net/DiscordClientConfig.cs
  26. +0
    -25
      src/Discord.Net/DiscordDataSocket.Events.cs
  27. +0
    -140
      src/Discord.Net/DiscordDataSocket.cs
  28. +0
    -35
      src/Discord.Net/DiscordWebSocket.Events.cs
  29. +0
    -286
      src/Discord.Net/DiscordWebSocket.cs
  30. +4
    -4
      src/Discord.Net/Enums/Regions.cs
  31. +1
    -1
      src/Discord.Net/Enums/UserStatus.cs
  32. +4
    -18
      src/Discord.Net/Format.cs
  33. +0
    -107
      src/Discord.Net/Helpers/AsyncCache.cs
  34. +3
    -3
      src/Discord.Net/Helpers/Extensions.cs
  35. +0
    -14
      src/Discord.Net/Helpers/JsonHttpClient.Events.cs
  36. +0
    -207
      src/Discord.Net/Helpers/JsonHttpClient.cs
  37. +43
    -0
      src/Discord.Net/Helpers/MessageCleaner.cs
  38. +2
    -2
      src/Discord.Net/Helpers/TaskHelper.cs
  39. +24
    -0
      src/Discord.Net/Mention.cs
  40. +54
    -14
      src/Discord.Net/Models/Channel.cs
  41. +30
    -9
      src/Discord.Net/Models/Invite.cs
  42. +53
    -11
      src/Discord.Net/Models/Member.cs
  43. +83
    -15
      src/Discord.Net/Models/Message.cs
  44. +34
    -27
      src/Discord.Net/Models/PackedPermissions.cs
  45. +10
    -6
      src/Discord.Net/Models/Role.cs
  46. +138
    -85
      src/Discord.Net/Models/Server.cs
  47. +38
    -14
      src/Discord.Net/Models/User.cs
  48. +61
    -22
      src/Discord.Net/Net/API/Common.cs
  49. +158
    -0
      src/Discord.Net/Net/API/DiscordAPIClient.cs
  50. +42
    -0
      src/Discord.Net/Net/API/Endpoints.cs
  51. +41
    -23
      src/Discord.Net/Net/API/Requests.cs
  52. +85
    -0
      src/Discord.Net/Net/API/Responses.cs
  53. +3
    -3
      src/Discord.Net/Net/HttpException.cs
  54. +65
    -0
      src/Discord.Net/Net/RestClient.BuiltIn.cs
  55. +30
    -0
      src/Discord.Net/Net/RestClient.Events.cs
  56. +68
    -0
      src/Discord.Net/Net/RestClient.SharpRest.cs
  57. +184
    -0
      src/Discord.Net/Net/RestClient.cs
  58. +5
    -28
      src/Discord.Net/Net/WebSockets/Commands.cs
  59. +102
    -0
      src/Discord.Net/Net/WebSockets/DataWebSocket.cs
  60. +23
    -35
      src/Discord.Net/Net/WebSockets/Events.cs
  61. +6
    -29
      src/Discord.Net/Net/WebSockets/VoiceCommands.cs
  62. +2
    -2
      src/Discord.Net/Net/WebSockets/VoiceEvents.cs
  63. +160
    -182
      src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs
  64. +153
    -0
      src/Discord.Net/Net/WebSockets/WebSocket.BuiltIn.cs
  65. +34
    -0
      src/Discord.Net/Net/WebSockets/WebSocket.Events.cs
  66. +192
    -0
      src/Discord.Net/Net/WebSockets/WebSocket.cs
  67. +31
    -0
      src/Discord.Net/Net/WebSockets/WebSocketMessage.cs
  68. +0
    -78
      src/Discord.Net/lib/Opus/API.cs
  69. +5
    -3
      src/Discord.Net/project.json
  70. +0
    -6
      test/Discord.Net.Tests/Discord.Net.Tests.csproj

+ 0
- 31
Discord.Net.sln View File

@@ -12,18 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
global.json = global.json
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{855D6B1D-847B-42DA-BE6A-23683EA89511}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{ACFB060B-EC8A-4926-B293-04C01E17EE23}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.xproj", "{19793545-EF89-48F4-8100-3EBAAD0A9141}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net45", "net45", "{B47C4063-C4EB-46AA-886D-B868DA1BF0A0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net", "src\Discord.Net.Net45\Discord.Net.csproj", "{8D71A857-879A-4A10-859E-5FF824ED6688}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Commands", "src\Discord.Net.Commands.Net45\Discord.Net.Commands.csproj", "{1B5603B4-6F8F-4289-B945-7BAAE523D740}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet", "dotnet", "{EA68EBE2-51C8-4440-9EF7-D633C90A5D35}"
EndProject
Global
@@ -32,37 +22,16 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.Build.0 = Debug|Any CPU
{855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.ActiveCfg = Release|Any CPU
{855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.Build.0 = Release|Any CPU
{ACFB060B-EC8A-4926-B293-04C01E17EE23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACFB060B-EC8A-4926-B293-04C01E17EE23}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ACFB060B-EC8A-4926-B293-04C01E17EE23}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACFB060B-EC8A-4926-B293-04C01E17EE23}.Release|Any CPU.Build.0 = Release|Any CPU
{19793545-EF89-48F4-8100-3EBAAD0A9141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19793545-EF89-48F4-8100-3EBAAD0A9141}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19793545-EF89-48F4-8100-3EBAAD0A9141}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19793545-EF89-48F4-8100-3EBAAD0A9141}.Release|Any CPU.Build.0 = Release|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D71A857-879A-4A10-859E-5FF824ED6688}.Release|Any CPU.Build.0 = Release|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B5603B4-6F8F-4289-B945-7BAAE523D740}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{855D6B1D-847B-42DA-BE6A-23683EA89511} = {6317A2E6-8E36-4C3E-949B-3F10EC888AB9}
{ACFB060B-EC8A-4926-B293-04C01E17EE23} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35}
{19793545-EF89-48F4-8100-3EBAAD0A9141} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35}
{B47C4063-C4EB-46AA-886D-B868DA1BF0A0} = {8D7989F0-66CE-4DBB-8230-D8C811E9B1D7}
{8D71A857-879A-4A10-859E-5FF824ED6688} = {B47C4063-C4EB-46AA-886D-B868DA1BF0A0}
{1B5603B4-6F8F-4289-B945-7BAAE523D740} = {B47C4063-C4EB-46AA-886D-B868DA1BF0A0}
{EA68EBE2-51C8-4440-9EF7-D633C90A5D35} = {8D7989F0-66CE-4DBB-8230-D8C811E9B1D7}
EndGlobalSection
EndGlobal

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

@@ -33,7 +33,7 @@ namespace Discord
return;

//Ignore messages from ourselves
if (e.Message.UserId == _myId)
if (e.Message.UserId == CurrentUserId)
return;

//Check for the command character


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

@@ -1,5 +1,5 @@
{
"version": "0.6.1-beta2",
"version": "0.7.0-beta1",
"description": "A small Discord.Net extension to make bot creation easier.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
@@ -13,7 +13,7 @@
"warningsAsErrors": true
},
"dependencies": {
"Discord.Net": "0.6.1-beta2"
"Discord.Net": "0.7.0-beta1"
},
"frameworks": {
"net45": { },


+ 96
- 50
src/Discord.Net.Net45/Discord.Net.csproj View File

@@ -37,42 +37,52 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=7.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<HintPath>..\..\..\DiscordBot\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="RestSharp, Version=105.2.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\..\DiscordBot\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Content Include="lib\libopus.so">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="lib\opus.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Discord.Net\API\DiscordAPI.cs">
<Link>API\DiscordAPI.cs</Link>
<Compile Include="..\Discord.Net\Audio\Opus.cs">
<Link>Audio\Opus.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Endpoints.cs">
<Link>API\Endpoints.cs</Link>
<Compile Include="..\Discord.Net\Audio\OpusEncoder.cs">
<Link>Audio\OpusEncoder.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\APIRequests.cs">
<Link>API\Models\APIRequests.cs</Link>
<Compile Include="..\Discord.Net\Collections\AsyncCollection.cs">
<Link>Collections\AsyncCollection.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\APIResponses.cs">
<Link>API\Models\APIResponses.cs</Link>
<Compile Include="..\Discord.Net\Collections\Channels.cs">
<Link>Collections\Channels.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\Common.cs">
<Link>API\Models\Common.cs</Link>
<Compile Include="..\Discord.Net\Collections\Members.cs">
<Link>Collections\Members.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\TextWebSocketCommands.cs">
<Link>API\Models\TextWebSocketCommands.cs</Link>
<Compile Include="..\Discord.Net\Collections\Messages.cs">
<Link>Collections\Messages.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\TextWebSocketEvents.cs">
<Link>API\Models\TextWebSocketEvents.cs</Link>
<Compile Include="..\Discord.Net\Collections\Roles.cs">
<Link>Collections\Roles.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\VoiceWebSocketCommands.cs">
<Link>API\Models\VoiceWebSocketCommands.cs</Link>
<Compile Include="..\Discord.Net\Collections\Servers.cs">
<Link>Collections\Servers.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\VoiceWebSocketEvents.cs">
<Link>API\Models\VoiceWebSocketEvents.cs</Link>
<Compile Include="..\Discord.Net\Collections\Users.cs">
<Link>Collections\Users.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordClient.API.cs">
<Link>DiscordClient.API.cs</Link>
@@ -86,24 +96,12 @@
<Compile Include="..\Discord.Net\DiscordClient.Events.cs">
<Link>DiscordClient.Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordClient.Voice.cs">
<Link>DiscordClient.Voice.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordClientConfig.cs">
<Link>DiscordClientConfig.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordDataSocket.cs">
<Link>DiscordDataSocket.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordDataSocket.Events.cs">
<Link>DiscordDataSocket.Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordVoiceSocket.cs">
<Link>DiscordVoiceSocket.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordWebSocket.cs">
<Link>DiscordWebSocket.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordWebSocket.Events.cs">
<Link>DiscordWebSocket.Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Enums\ChannelTypes.cs">
<Link>Enums\ChannelTypes.cs</Link>
</Compile>
@@ -116,29 +114,17 @@
<Compile Include="..\Discord.Net\Format.cs">
<Link>Format.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\AsyncCache.cs">
<Link>Helpers\AsyncCache.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\Extensions.cs">
<Link>Helpers\Extensions.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\JsonHttpClient.cs">
<Link>Helpers\JsonHttpClient.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\JsonHttpClient.Events.cs">
<Link>Helpers\JsonHttpClient.Events.cs</Link>
<Compile Include="..\Discord.Net\Helpers\MessageCleaner.cs">
<Link>Helpers\MessageCleaner.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\TaskHelper.cs">
<Link>Helpers\TaskHelper.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\HttpException.cs">
<Link>HttpException.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\lib\Opus\API.cs">
<Link>lib\Opus\API.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\lib\Opus\OpusEncoder.cs">
<Link>lib\Opus\OpusEncoder.cs</Link>
<Compile Include="..\Discord.Net\Mention.cs">
<Link>Mention.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Models\Channel.cs">
<Link>Models\Channel.cs</Link>
@@ -164,6 +150,66 @@
<Compile Include="..\Discord.Net\Models\User.cs">
<Link>Models\User.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\API\Common.cs">
<Link>Net\API\Common.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\API\DiscordAPIClient.cs">
<Link>Net\API\DiscordAPIClient.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\API\Endpoints.cs">
<Link>Net\API\Endpoints.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\API\Requests.cs">
<Link>Net\API\Requests.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\API\Responses.cs">
<Link>Net\API\Responses.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\HttpException.cs">
<Link>Net\HttpException.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\RestClient.BuiltIn.cs">
<Link>Net\RestClient.BuiltIn.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\RestClient.cs">
<Link>Net\RestClient.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\RestClient.Events.cs">
<Link>Net\RestClient.Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\RestClient.SharpRest.cs">
<Link>Net\RestClient.SharpRest.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\Commands.cs">
<Link>Net\WebSockets\Commands.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\DataWebSocket.cs">
<Link>Net\WebSockets\DataWebSocket.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\Events.cs">
<Link>Net\WebSockets\Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\VoiceCommands.cs">
<Link>Net\WebSockets\VoiceCommands.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\VoiceEvents.cs">
<Link>Net\WebSockets\VoiceEvents.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\VoiceWebSocket.cs">
<Link>Net\WebSockets\VoiceWebSocket.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\WebSocket.BuiltIn.cs">
<Link>Net\WebSockets\WebSocket.BuiltIn.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\WebSocket.cs">
<Link>Net\WebSockets\WebSocket.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\WebSocket.Events.cs">
<Link>Net\WebSockets\WebSocket.Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\WebSocketMessage.cs">
<Link>Net\WebSockets\WebSocketMessage.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup />


BIN
src/Discord.Net.Net45/lib/libopus.so View File


BIN
src/Discord.Net.Net45/lib/opus.dll View File


+ 1
- 0
src/Discord.Net.Net45/packages.config View File

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

+ 0
- 152
src/Discord.Net/API/DiscordAPI.cs View File

@@ -1,152 +0,0 @@
using Discord.API.Models;
using Discord.Helpers;
using System;
using System.IO;
using System.Threading.Tasks;

namespace Discord.API
{
internal class DiscordAPI
{
public const int MaxMessageSize = 2000;
private readonly JsonHttpClient _http;

public DiscordAPI(JsonHttpClient http)
{
_http = http;
}

//Auth
public Task<APIResponses.Gateway> GetWebSocketEndpoint()
=> _http.Get<APIResponses.Gateway>(Endpoints.Gateway);
public async Task<APIResponses.AuthRegister> LoginAnonymous(string username)
{
var fingerprintResponse = await _http.Post<APIResponses.AuthFingerprint>(Endpoints.AuthFingerprint).ConfigureAwait(false);
var registerRequest = new APIRequests.AuthRegisterRequest { Fingerprint = fingerprintResponse.Fingerprint, Username = username };
var registerResponse = await _http.Post<APIResponses.AuthRegister>(Endpoints.AuthRegister, registerRequest).ConfigureAwait(false);
return registerResponse;
}
public async Task<APIResponses.AuthLogin> Login(string email, string password)
{
var request = new APIRequests.AuthLogin { Email = email, Password = password };
var response = await _http.Post<APIResponses.AuthLogin>(Endpoints.AuthLogin, request).ConfigureAwait(false);
return response;
}
public Task Logout()
=> _http.Post(Endpoints.AuthLogout);

//Servers
public Task<APIResponses.CreateServer> CreateServer(string name, string region)
{
var request = new APIRequests.CreateServer { Name = name, Region = region };
return _http.Post<APIResponses.CreateServer>(Endpoints.Servers, request);
}
public Task LeaveServer(string id)
=> _http.Delete<APIResponses.DeleteServer>(Endpoints.Server(id));

//Channels
public Task<APIResponses.CreateChannel> CreateChannel(string serverId, string name, string channelType)
{
var request = new APIRequests.CreateChannel { Name = name, Type = channelType };
return _http.Post<APIResponses.CreateChannel>(Endpoints.ServerChannels(serverId), request);
}
public Task<APIResponses.CreateChannel> CreatePMChannel(string myId, string recipientId)
{
var request = new APIRequests.CreatePMChannel { RecipientId = recipientId };
return _http.Post<APIResponses.CreateChannel>(Endpoints.UserChannels(myId), request);
}
public Task<APIResponses.DestroyChannel> DestroyChannel(string channelId)
=> _http.Delete<APIResponses.DestroyChannel>(Endpoints.Channel(channelId));
public Task<APIResponses.GetMessages[]> GetMessages(string channelId, int count)
=> _http.Get<APIResponses.GetMessages[]>(Endpoints.ChannelMessages(channelId, count));

//Members
public Task Kick(string serverId, string memberId)
=> _http.Delete(Endpoints.ServerMember(serverId, memberId));
public Task Ban(string serverId, string memberId)
=> _http.Put(Endpoints.ServerBan(serverId, memberId));
public Task Unban(string serverId, string memberId)
=> _http.Delete(Endpoints.ServerBan(serverId, memberId));

//Invites
public Task<APIResponses.CreateInvite> CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
{
var request = new APIRequests.CreateInvite { MaxAge = maxAge, MaxUses = maxUses, IsTemporary = isTemporary, HasXkcdPass = hasXkcdPass };
return _http.Post<APIResponses.CreateInvite>(Endpoints.ChannelInvites(channelId), request);
}
public Task<APIResponses.GetInvite> GetInvite(string id)
=> _http.Get<APIResponses.GetInvite>(Endpoints.Invite(id));
public Task AcceptInvite(string id)
=> _http.Post<APIResponses.AcceptInvite>(Endpoints.Invite(id));
public Task DeleteInvite(string id)
=> _http.Delete(Endpoints.Invite(id));
//Chat
public Task<APIResponses.SendMessage> SendMessage(string channelId, string message, string[] mentions, string nonce)
{
var request = new APIRequests.SendMessage { Content = message, Mentions = mentions, Nonce = nonce };
return _http.Post<APIResponses.SendMessage>(Endpoints.ChannelMessages(channelId), request);
}
public Task<APIResponses.EditMessage> EditMessage(string channelId, string messageId, string message, string[] mentions)
{
var request = new APIRequests.EditMessage { Content = message, Mentions = mentions };
return _http.Patch<APIResponses.EditMessage>(Endpoints.ChannelMessage(channelId, messageId), request);
}
public Task SendIsTyping(string channelId)
=> _http.Post(Endpoints.ChannelTyping(channelId));
public Task DeleteMessage(string channelId, string msgId)
=> _http.Delete(Endpoints.ChannelMessage(channelId, msgId));
public Task SendFile(string channelId, Stream stream, string filename = null)
=> _http.File<APIResponses.SendMessage>(Endpoints.ChannelMessages(channelId), stream, filename);

//Voice
public Task<APIResponses.GetRegions[]> GetVoiceRegions()
=> _http.Get<APIResponses.GetRegions[]>(Endpoints.VoiceRegions);
public Task<APIResponses.GetIce> GetVoiceIce()
=> _http.Get<APIResponses.GetIce>(Endpoints.VoiceIce);
public Task Mute(string serverId, string memberId)
{
var request = new APIRequests.SetMemberMute { Mute = true };
return _http.Patch(Endpoints.ServerMember(serverId, memberId));
}
public Task Unmute(string serverId, string memberId)
{
var request = new APIRequests.SetMemberMute { Mute = false };
return _http.Patch(Endpoints.ServerMember(serverId, memberId));
}
public Task Deafen(string serverId, string memberId)
{
var request = new APIRequests.SetMemberDeaf { Deaf = true };
return _http.Patch(Endpoints.ServerMember(serverId, memberId));
}
public Task Undeafen(string serverId, string memberId)
{
var request = new APIRequests.SetMemberDeaf { Deaf = false };
return _http.Patch(Endpoints.ServerMember(serverId, memberId));
}

//Profile
public Task<SelfUserInfo> ChangeUsername(string newUsername, string currentEmail, string currentPassword)
{
var request = new APIRequests.ChangeUsername { Username = newUsername, CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request);
}
public Task<SelfUserInfo> ChangeEmail(string newEmail, string currentPassword)
{
var request = new APIRequests.ChangeEmail { NewEmail = newEmail, CurrentPassword = currentPassword };
return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request);
}
public Task<SelfUserInfo> ChangePassword(string newPassword, string currentEmail, string currentPassword)
{
var request = new APIRequests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request);
}
public Task<SelfUserInfo> ChangeAvatar(AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
{
string base64 = Convert.ToBase64String(bytes);
string type = imageType == AvatarImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64";
var request = new APIRequests.ChangeAvatar { Avatar = $"data:{type},/9j/{base64}", CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request);
}
}
}

+ 0
- 56
src/Discord.Net/API/Endpoints.cs View File

@@ -1,56 +0,0 @@
namespace Discord.API
{
internal static class Endpoints
{
public static readonly string BaseUrl = "discordapp.com";
public static readonly string BaseShortUrl = "discord.gg";
public static readonly string BaseHttps = $"https://{BaseUrl}";
public static readonly string BaseShortHttps = $"https://{BaseShortUrl}";

// /api
public static readonly string BaseApi = $"{BaseHttps}/api";
//public static readonly string Track = $"{BaseApi}/track";
public static readonly string Gateway = $"{BaseApi}/gateway";

// /api/auth
public static readonly string Auth = $"{BaseApi}/auth";
public static readonly string AuthFingerprint = $"{Auth}fingerprint";
public static readonly string AuthRegister = $"{Auth}/register";
public static readonly string AuthLogin = $"{Auth}/login";
public static readonly string AuthLogout = $"{Auth}/logout";

// /api/channels
public static readonly string Channels = $"{BaseApi}/channels";
public static string Channel(string channelId) => $"{Channels}/{channelId}";
public static string ChannelTyping(string channelId) => $"{Channels}/{channelId}/typing";
public static string ChannelMessages(string channelId) => $"{Channels}/{channelId}/messages";
public static string ChannelMessages(string channelId, int limit) => $"{Channels}/{channelId}/messages?limit={limit}";
public static string ChannelMessage(string channelId, string msgId) => $"{Channels}/{channelId}/messages/{msgId}";
public static string ChannelInvites(string channelId) => $"{Channels}/{channelId}/invites";

// /api/guilds
public static readonly string Servers = $"{BaseApi}/guilds";
public static string Server(string serverId) => $"{Servers}/{serverId}";
public static string ServerChannels(string serverId) => $"{Servers}/{serverId}/channels";
public static string ServerMember(string serverId, string userId) => $"{Servers}/{serverId}/members/{userId}";
public static string ServerBan(string serverId, string userId) => $"{Servers}/{serverId}/bans/{userId}";

// /api/invites
public static readonly string Invites = $"{BaseApi}/invite";
public static string Invite(string id) => $"{Invites}/{id}";

// /api/users
public static readonly string Users = $"{BaseApi}/users";
public static string UserMe => $"{Users}/@me";
public static string UserChannels(string userId) => $"{Users}/{userId}/channels";
public static string UserAvatar(string userId, string avatarId) => $"{Users}/{userId}/avatars/{avatarId}.jpg";

// /api/voice
public static readonly string Voice = $"{BaseApi}/voice";
public static readonly string VoiceRegions = $"{Voice}/regions";
public static readonly string VoiceIce = $"{Voice}/ice";

//Website
public static string InviteUrl(string code) => $"{BaseShortHttps}/{code}";
}
}

+ 0
- 101
src/Discord.Net/API/Models/APIResponses.cs View File

@@ -1,101 +0,0 @@
//Ignore unused/unassigned variable warnings
#pragma warning disable CS0649
#pragma warning disable CS0169

using Newtonsoft.Json;
using System;

namespace Discord.API.Models
{
internal static class APIResponses
{
public class Gateway
{
[JsonProperty(PropertyName = "url")]
public string Url;
}

public class AuthFingerprint
{
[JsonProperty(PropertyName = "fingerprint")]
public string Fingerprint;
}
public class AuthRegister : AuthLogin { }
public class AuthLogin
{
[JsonProperty(PropertyName = "token")]
public string Token;
}

public class CreateServer : ServerInfo { }
public class DeleteServer : ServerInfo { }

public class CreateChannel : ChannelInfo { }
public class DestroyChannel : ChannelInfo { }

public class CreateInvite : GetInvite
{
[JsonProperty(PropertyName = "max_age")]
public int MaxAge;
[JsonProperty(PropertyName = "max_uses")]
public int MaxUses;
[JsonProperty(PropertyName = "revoked")]
public bool IsRevoked;
[JsonProperty(PropertyName = "temporary")]
public bool IsTemporary;
[JsonProperty(PropertyName = "uses")]
public int Uses;
[JsonProperty(PropertyName = "created_at")]
public DateTime CreatedAt;
}

public class GetInvite
{
[JsonProperty(PropertyName = "inviter")]
public UserReference Inviter;
[JsonProperty(PropertyName = "guild")]
public ServerReference Server;
[JsonProperty(PropertyName = "channel")]
public ChannelReference Channel;
[JsonProperty(PropertyName = "code")]
public string Code;
[JsonProperty(PropertyName = "xkcdpass")]
public string XkcdPass;
}
public class AcceptInvite : GetInvite { }

public class SendMessage : Message { }
public class EditMessage : Message { }
public class GetMessages : Message { }

public class GetRegions
{
[JsonProperty(PropertyName = "sample_hostname")]
public string Hostname;
[JsonProperty(PropertyName = "sample_port")]
public int Port;
[JsonProperty(PropertyName = "id")]
public string Id;
[JsonProperty(PropertyName = "name")]
public string Name;
}

public class GetIce
{
[JsonProperty(PropertyName = "ttl")]
public string TTL;
[JsonProperty(PropertyName = "servers")]
public Server[] Servers;

public class Server
{
[JsonProperty(PropertyName = "url")]
public string URL;
[JsonProperty(PropertyName = "username")]
public string Username;
[JsonProperty(PropertyName = "credential")]
public string Credential;
}
}
}
}

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

@@ -0,0 +1,71 @@
using System;
using System.Runtime.InteropServices;

namespace Discord.Audio
{
internal unsafe class Opus
{
[DllImport("lib/opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out Error error);
[DllImport("lib/opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyEncoder(IntPtr encoder);
[DllImport("lib/opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)]
public static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes);

/*[DllImport("lib/opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateDecoder(int Fs, int channels, out Errors error);
[DllImport("lib/opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyDecoder(IntPtr decoder);
[DllImport("lib/opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)]
public static extern int Decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec);*/

[DllImport("lib/opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)]
public static extern int EncoderCtl(IntPtr st, Ctl request, int value);

public enum Ctl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}

/// <summary>Supported coding modes.</summary>
public enum Application : int
{
/// <summary>
/// Gives best quality at a given bitrate for voice signals. It enhances the input signal by high-pass filtering and emphasizing formants and harmonics.
/// Optionally it includes in-band forward error correction to protect against packet loss. Use this mode for typical VoIP applications.
/// Because of the enhancement, even at high bitrates the output may sound different from the input.
/// </summary>
Voip = 2048,
/// <summary>
/// Gives best quality at a given bitrate for most non-voice signals like music.
/// Use this mode for music and mixed (music/voice) content, broadcast, and applications requiring less than 15 ms of coding delay.
/// </summary>
Audio = 2049,
/// <summary> Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay. </summary>
Restricted_LowLatency = 2051
}

public enum Error : int
{
/// <summary> No error. </summary>
OK = 0,
/// <summary> One or more invalid/out of range arguments. </summary>
BadArg = -1,
/// <summary> The mode struct passed is invalid. </summary>
BufferToSmall = -2,
/// <summary> An internal error was detected. </summary>
InternalError = -3,
/// <summary> The compressed data passed is corrupted. </summary>
InvalidPacket = -4,
/// <summary> Invalid/unsupported request number. </summary>
Unimplemented = -5,
/// <summary> An encoder or decoder structure is invalid or already freed. </summary>
InvalidState = -6,
/// <summary> Memory allocation has failed. </summary>
AllocFail = -7
}
}
}

src/Discord.Net/lib/Opus/OpusEncoder.cs → src/Discord.Net/Audio/OpusEncoder.cs View File

@@ -1,11 +1,11 @@
using System;

namespace Discord.Opus
namespace Discord.Audio
{
/// <summary> Opus codec wrapper. </summary>
public class OpusEncoder : IDisposable
internal class OpusEncoder : IDisposable
{
private readonly IntPtr _encoder;
private readonly IntPtr _encoderPtr;

/// <summary> Gets the bit rate of the encoder. </summary>
public const int BitRate = 16;
@@ -22,7 +22,7 @@ namespace Discord.Opus
/// <summary> Gets the bytes per frame. </summary>
public int FrameSize { get; private set; }
/// <summary> Gets the coding mode of the encoder. </summary>
public Application Application { get; private set; }
public Opus.Application Application { get; private set; }

/// <summary> Creates a new Opus encoder. </summary>
/// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.</param>
@@ -30,7 +30,7 @@ namespace Discord.Opus
/// <param name="frameLength">Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60</param>
/// <param name="application">Coding mode.</param>
/// <returns>A new <c>OpusEncoder</c></returns>
public OpusEncoder(int samplingRate, int channels, int frameLength, Application application)
public OpusEncoder(int samplingRate, int channels, int frameLength, Opus.Application application)
{
if (samplingRate != 8000 && samplingRate != 12000 &&
samplingRate != 16000 && samplingRate != 24000 &&
@@ -47,9 +47,9 @@ namespace Discord.Opus
SamplesPerFrame = samplingRate / 1000 * FrameLength;
FrameSize = SamplesPerFrame * SampleSize;

Error error;
_encoder = API.opus_encoder_create(samplingRate, channels, (int)application, out error);
if (error != Error.OK)
Opus.Error error;
_encoderPtr = Opus.CreateEncoder(samplingRate, channels, (int)application, out error);
if (error != Opus.Error.OK)
throw new InvalidOperationException($"Error occured while creating encoder: {error}");

SetForwardErrorCorrection(true);
@@ -68,24 +68,25 @@ namespace Discord.Opus
int result = 0;
fixed (byte* inPtr = pcmSamples)
fixed (byte* outPtr = outputBuffer)
result = API.opus_encode(_encoder, inPtr + offset, SamplesPerFrame, outPtr, outputBuffer.Length);
result = Opus.Encode(_encoderPtr, inPtr + offset, SamplesPerFrame, outPtr, outputBuffer.Length);

if (result < 0)
throw new Exception("Encoding failed: " + ((Error)result).ToString());
throw new Exception("Encoding failed: " + ((Opus.Error)result).ToString());
return result;
}

/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetForwardErrorCorrection(bool value)
{
if (_encoder == IntPtr.Zero)
if (disposed)
throw new ObjectDisposedException("OpusEncoder");

var result = API.opus_encoder_ctl(_encoder, Ctl.SetInbandFECRequest, value ? 1 : 0);
var result = Opus.EncoderCtl(_encoderPtr, Opus.Ctl.SetInbandFECRequest, value ? 1 : 0);
if (result < 0)
throw new Exception("Encoder error: " + ((Error)result).ToString());
throw new Exception("Encoder error: " + ((Opus.Error)result).ToString());
}

#region IDisposable
private bool disposed;
public void Dispose()
{
@@ -94,8 +95,8 @@ namespace Discord.Opus

GC.SuppressFinalize(this);

if (_encoder != IntPtr.Zero)
API.opus_encoder_destroy(_encoder);
if (_encoderPtr != IntPtr.Zero)
Opus.DestroyEncoder(_encoderPtr);

disposed = true;
}
@@ -103,5 +104,6 @@ namespace Discord.Opus
{
Dispose();
}
#endregion
}
}

+ 118
- 0
src/Discord.Net/Collections/AsyncCollection.cs View File

@@ -0,0 +1,118 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Collections
{
public abstract class AsyncCollection<TValue> : IEnumerable<TValue>
where TValue : class
{
private static readonly object _writerLock = new object();

internal class CollectionItemEventArgs : EventArgs
{
public TValue Item { get; }
public CollectionItemEventArgs(TValue item) { Item = item; }
}

internal EventHandler<CollectionItemEventArgs> ItemCreated;
private void RaiseItemCreated(TValue item)
{
if (ItemCreated != null)
ItemCreated(this, new CollectionItemEventArgs(item));
}
internal EventHandler<CollectionItemEventArgs> ItemUpdated;
protected void RaiseItemUpdated(TValue item)
{
if (ItemUpdated != null)
ItemUpdated(this, new CollectionItemEventArgs(item));
}
internal EventHandler<CollectionItemEventArgs> ItemDestroyed;
private void RaiseItemDestroyed(TValue item)
{
if (ItemDestroyed != null)
ItemDestroyed(this, new CollectionItemEventArgs(item));
}

protected readonly DiscordClient _client;
protected readonly ConcurrentDictionary<string, TValue> _dictionary;

protected AsyncCollection(DiscordClient client)
{
_client = client;
_dictionary = new ConcurrentDictionary<string, TValue>();
}

protected TValue Get(string key)
{
TValue result;
if (!_dictionary.TryGetValue(key, out result))
return null;
return result;
}
protected TValue GetOrAdd(string key, Func<TValue> createFunc)
{
TValue result;
if (_dictionary.TryGetValue(key, out result))
return result;

lock (_writerLock)
{
TValue newItem = createFunc();
result = _dictionary.GetOrAdd(key, newItem);
if (result == newItem)
{
OnCreated(newItem);
RaiseItemCreated(result);
}
}
return result;
}
protected TValue TryRemove(string key)
{
if (_dictionary.ContainsKey(key))
{
lock (_writerLock)
{
TValue result;
if (_dictionary.TryRemove(key, out result))
return result;
}
}
return null;
}
protected TValue Remap(string oldKey, string newKey)
{
if (_dictionary.ContainsKey(oldKey))
{
lock (_writerLock)
{
TValue result;
if (_dictionary.TryRemove(oldKey, out result))
_dictionary[newKey] = result;
return result;
}
}
return null;
}
protected internal void Clear()
{
lock (_writerLock)
_dictionary.Clear();
}

protected abstract void OnCreated(TValue item);
protected abstract void OnRemoved(TValue item);

public IEnumerator<TValue> GetEnumerator()
{
return _dictionary.Select(x => x.Value).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

+ 61
- 0
src/Discord.Net/Collections/Channels.cs View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Collections
{
public sealed class Channels : AsyncCollection<Channel>
{
internal Channels(DiscordClient client)
: base(client) { }

internal Channel GetOrAdd(string id, string serverId, string recipientId = null) => GetOrAdd(id, () => new Channel(_client, id, serverId, recipientId));
internal new Channel TryRemove(string id) => base.TryRemove(id);

protected override void OnCreated(Channel item)
{
item.Server.AddChannel(item.Id);
if (item.RecipientId != null)
{
var user = item.Recipient;
if (user.PrivateChannelId != null)
throw new Exception("User already has a private channel.");
user.PrivateChannelId = item.Id;
item.Recipient.AddRef();
}
}
protected override void OnRemoved(Channel item)
{
item.Server.RemoveChannel(item.Id);

if (item.RecipientId != null)
{
var user = item.Recipient;
if (user.PrivateChannelId != item.Id)
throw new Exception("User has a different private channel.");
user.PrivateChannelId = null;
user.RemoveRef();
}
}

internal Channel this[string id] => Get(id);
internal IEnumerable<Channel> Find(string serverId, string name)
{
if (serverId == null) throw new ArgumentNullException(nameof(serverId));

if (name.StartsWith("#"))
{
string name2 = name.Substring(1);
return this.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return this.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}
}
}

+ 67
- 0
src/Discord.Net/Collections/Members.cs View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Collections
{
public sealed class Members : AsyncCollection<Member>
{
internal Members(DiscordClient client)
: base(client) { }

private string GetKey(string userId, string serverId) => serverId + '_' + userId;

internal Member GetOrAdd(string userId, string serverId) => GetOrAdd(GetKey(userId, serverId), () => new Member(_client, userId, serverId));
internal Member TryRemove(string userId, string serverId) => base.TryRemove(GetKey(userId, serverId));

protected override void OnCreated(Member item)
{
item.Server.AddMember(item.UserId);
item.User.AddRef();
}
protected override void OnRemoved(Member item)
{
item.Server.RemoveMember(item.UserId);
item.User.RemoveRef();
}
internal Member this[string userId, string serverId]
{
get
{
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));

return Get(GetKey(userId, serverId));
}
}
internal IEnumerable<Member> Find(Server server, string name)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return server.Members.Where(x =>
{
var user = x.User;
if (user == null)
return false;
return string.Equals(user.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(user.Name, name2, StringComparison.OrdinalIgnoreCase);
});
}
else
{
return server.Members.Where(x =>
{
var user = x.User;
if (user == null)
return false;
return string.Equals(x.User.Name, name, StringComparison.OrdinalIgnoreCase);
});
}
}
}
}

+ 33
- 0
src/Discord.Net/Collections/Messages.cs View File

@@ -0,0 +1,33 @@
using Discord.Helpers;

namespace Discord.Collections
{
public sealed class Messages : AsyncCollection<Message>
{
private readonly MessageCleaner _msgCleaner;
internal Messages(DiscordClient client)
: base(client)
{
_msgCleaner = new MessageCleaner(client);
}

internal Message GetOrAdd(string id, string channelId) => GetOrAdd(id, () => new Message(_client, id, channelId));
internal new Message TryRemove(string id) => base.TryRemove(id);
internal new Message Remap(string oldKey, string newKey) => base.Remap(oldKey, newKey);

protected override void OnCreated(Message item)
{
item.Channel.AddMessage(item.UserId);
item.User.AddRef();
}
protected override void OnRemoved(Message item)
{
item.Channel.RemoveMessage(item.UserId);
item.User.RemoveRef();
}

internal Message this[string id] => Get(id);

internal string CleanText(string text) => _msgCleaner.Clean(text);
}
}

+ 44
- 0
src/Discord.Net/Collections/Roles.cs View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Collections
{
public sealed class Roles : AsyncCollection<Role>
{
internal Roles(DiscordClient client)
: base(client) { }

internal Role GetOrAdd(string id, string serverId) => GetOrAdd(id, () => new Role(_client, id, serverId));
internal new Role TryRemove(string id) => base.TryRemove(id);

protected override void OnCreated(Role item)
{
item.Server.AddRole(item.Id);
}
protected override void OnRemoved(Role item)
{
item.Server.RemoveRole(item.Id);
}

internal Role this[string id] => Get(id);
internal IEnumerable<Role> Find(string serverId, string name)
{
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (name == null) throw new ArgumentNullException(nameof(name));

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return this.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return this.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}
}
}

+ 40
- 0
src/Discord.Net/Collections/Servers.cs View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Collections
{
public sealed class Servers : AsyncCollection<Server>
{
internal Servers(DiscordClient client)
: base(client) { }

internal Server GetOrAdd(string id) => base.GetOrAdd(id, () => new Server(_client, id));
internal new Server TryRemove(string id) => base.TryRemove(id);

protected override void OnCreated(Server item) { }
protected override void OnRemoved(Server item)
{
var channels = _client.Channels;
foreach (var channelId in item.ChannelIds)
channels.TryRemove(channelId);

var members = _client.Members;
foreach (var userId in item.UserIds)
members.TryRemove(userId, item.Id);

var roles = _client.Roles;
foreach (var roleId in item.RoleIds)
roles.TryRemove(roleId);
}

internal Server this[string id] => Get(id);

internal IEnumerable<Server> Find(string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

return this.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}
}

+ 54
- 0
src/Discord.Net/Collections/Users.cs View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Collections
{
public sealed class Users : AsyncCollection<User>
{
internal Users(DiscordClient client)
: base(client) { }

internal User GetOrAdd(string id) => GetOrAdd(id, () => new User(_client, id));
internal new User TryRemove(string id) => base.TryRemove(id);

protected override void OnCreated(User item) { }
protected override void OnRemoved(User item) { }

public User this[string id] => Get(id);
public User this[string name, string discriminator]
{
get
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (discriminator == null) throw new ArgumentNullException(nameof(discriminator));

if (name.StartsWith("@"))
name = name.Substring(1);

return this.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) &&
x.Discriminator == discriminator
)
.FirstOrDefault();
}
}

public IEnumerable<User> Find(string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return this.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return this.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}
}
}

+ 87
- 89
src/Discord.Net/DiscordClient.API.cs View File

@@ -1,8 +1,7 @@
using Discord.API;
using Discord.API.Models;
using Discord.Net;
using Discord.Net.API;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@@ -14,8 +13,11 @@ namespace Discord
Jpeg,
Png
}

public partial class DiscordClient
{
//TODO: Move all these functions into their respective collections object

//Servers
/// <summary> Creates a new server with the provided name and region (see Regions). </summary>
public async Task<Server> CreateServer(string name, string region)
@@ -25,7 +27,9 @@ namespace Discord
if (region == null) throw new ArgumentNullException(nameof(region));

var response = await _api.CreateServer(name, region).ConfigureAwait(false);
return _servers.Update(response.Id, response);
var server = _servers.GetOrAdd(response.Id);
server.Update(response);
return server;
}

/// <summary> Leaves the provided server, destroying it if you are the owner. </summary>
@@ -39,7 +43,7 @@ namespace Discord

try { await _api.LeaveServer(serverId).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
return _servers.Remove(serverId);
return _servers.TryRemove(serverId);
}

//Channels
@@ -55,7 +59,29 @@ namespace Discord
if (type == null) throw new ArgumentNullException(nameof(type));

var response = await _api.CreateChannel(serverId, name, type).ConfigureAwait(false);
return _channels.Update(response.Id, serverId, response);
var channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id);
channel.Update(response);
return channel;
}
/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public Task<Channel> CreatePMChannel(string userId) => CreatePMChannel(_users[userId], userId);
/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public Task<Channel> CreatePMChannel(User user) => CreatePMChannel(user, user?.Id);
private async Task<Channel> CreatePMChannel(User user, string userId)
{
CheckReady();
if (userId == null) throw new ArgumentNullException(nameof(userId));

Channel channel = null;
if (user != null)
channel = user.PrivateChannel;
if (channel == null)
{
var response = await _api.CreatePMChannel(_currentUserId, userId).ConfigureAwait(false);
channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id);
channel.Update(response);
}
return channel;
}

/// <summary> Destroys the provided channel. </summary>
@@ -69,7 +95,7 @@ namespace Discord

try { await _api.DestroyChannel(channelId).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
return _channels.Remove(channelId);
return _channels.TryRemove(channelId);
}

//Bans
@@ -140,36 +166,22 @@ namespace Discord
if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses));

var response = await _api.CreateInvite(channelId, maxAge, maxUses, isTemporary, hasXkcdPass).ConfigureAwait(false);
_channels.Update(response.Channel.Id, response.Server.Id, response.Channel);
_servers.Update(response.Server.Id, response.Server);
_users.Update(response.Inviter.Id, response.Inviter);
return new Invite(response.Code, response.XkcdPass, this)
{
ChannelId = response.Channel.Id,
InviterId = response.Inviter.Id,
ServerId = response.Server.Id,
IsRevoked = response.IsRevoked,
IsTemporary = response.IsTemporary,
MaxAge = response.MaxAge,
MaxUses = response.MaxUses,
Uses = response.Uses
};
var invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id);
invite.Update(response);
return invite;
}

/// <summary> Gets more info about the provided invite. </summary>
/// <summary> Gets more info about the provided invite code. </summary>
/// <remarks> Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode </remarks>
public async Task<Invite> GetInvite(string id)
{
CheckReady();
if (id == null) throw new ArgumentNullException(nameof(id));
var response = await _api.GetInvite(id).ConfigureAwait(false);
return new Invite(response.Code, response.XkcdPass, this)
{
ChannelId = response.Channel.Id,
InviterId = response.Inviter.Id,
ServerId = response.Server.Id
};
var invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id);
invite.Update(response);
return invite;
}

/// <summary> Accepts the provided invite. </summary>
@@ -181,34 +193,33 @@ namespace Discord
return _api.AcceptInvite(invite.Id);
}
/// <summary> Accepts the provided invite. </summary>
public async Task AcceptInvite(string id)
public async Task AcceptInvite(string code)
{
CheckReady();
if (id == null) throw new ArgumentNullException(nameof(id));
if (code == null) throw new ArgumentNullException(nameof(code));

//Remove Url Parts
if (id.StartsWith(Endpoints.BaseShortHttps))
id = id.Substring(Endpoints.BaseShortHttps.Length);
if (id.Length > 0 && id[0] == '/')
id = id.Substring(1);
if (id.Length > 0 && id[id.Length - 1] == '/')
id = id.Substring(0, id.Length - 1);
//Remove trailing slash and any non-code url parts
if (code.Length > 0 && code[code.Length - 1] == '/')
code = code.Substring(0, code.Length - 1);
int index = code.LastIndexOf('/');
if (index >= 0)
code = code.Substring(index + 1);

//Check if this is a human-readable link and get its ID
var response = await _api.GetInvite(id).ConfigureAwait(false);
await _api.AcceptInvite(response.Code).ConfigureAwait(false);
var invite = await GetInvite(code).ConfigureAwait(false);
await _api.AcceptInvite(invite.Id).ConfigureAwait(false);
}

/// <summary> Deletes the provided invite. </summary>
public async Task DeleteInvite(string id)
public async Task DeleteInvite(string code)
{
CheckReady();
if (id == null) throw new ArgumentNullException(nameof(id));
if (code == null) throw new ArgumentNullException(nameof(code));

try
{
//Check if this is a human-readable link and get its ID
var response = await _api.GetInvite(id).ConfigureAwait(false);
var response = await _api.GetInvite(code).ConfigureAwait(false);
await _api.DeleteInvite(response.Code).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
@@ -234,20 +245,21 @@ namespace Discord
if (text == null) throw new ArgumentNullException(nameof(text));
if (mentions == null) throw new ArgumentNullException(nameof(mentions));

int blockCount = (int)Math.Ceiling(text.Length / (double)DiscordAPI.MaxMessageSize);
int blockCount = (int)Math.Ceiling(text.Length / (double)DiscordAPIClient.MaxMessageSize);
Message[] result = new Message[blockCount];
for (int i = 0; i < blockCount; i++)
{
int index = i * DiscordAPI.MaxMessageSize;
int index = i * DiscordAPIClient.MaxMessageSize;
string blockText = text.Substring(index, Math.Min(2000, text.Length - index));
var nonce = GenerateNonce();
if (_config.UseMessageQueue)
{
var msg = _messages.Update("nonce_" + nonce, channelId, new API.Models.Message
var msg = _messages.GetOrAdd("nonce_" + nonce, channelId);
msg.Update(new Net.API.Message
{
Content = blockText,
Timestamp = DateTime.UtcNow,
Author = new UserReference { Avatar = User.AvatarId, Discriminator = User.Discriminator, Id = User.Id, Username = User.Name },
Author = new UserReference { Avatar = _currentUser.AvatarId, Discriminator = _currentUser.Discriminator, Id = _currentUser.Id, Username = _currentUser.Name },
ChannelId = channelId
});
msg.IsQueued = true;
@@ -257,9 +269,9 @@ namespace Discord
}
else
{
var msg = await _api.SendMessage(channelId, blockText, mentions, nonce).ConfigureAwait(false);
result[i] = _messages.Update(msg.Id, channelId, msg);
result[i].Nonce = nonce;
var response = await _api.SendMessage(channelId, blockText, mentions, nonce).ConfigureAwait(false);
var msg = _messages.GetOrAdd(response.Id, channelId);
msg.Update(response);
try { RaiseMessageSent(result[i]); } catch { }
}
await Task.Delay(1000).ConfigureAwait(false);
@@ -268,15 +280,12 @@ namespace Discord
}

/// <summary> Sends a private message to the provided channel. </summary>
public async Task<Message[]> SendPrivateMessage(User user, string text)
{
var channel = await GetPMChannel(user).ConfigureAwait(false);
return await SendMessage(channel, text, new string[0]).ConfigureAwait(false);
}
public Task<Message[]> SendPrivateMessage(User user, string text)
=> SendPrivateMessage(user?.Id, text);
/// <summary> Sends a private message to the provided channel. </summary>
public async Task<Message[]> SendPrivateMessage(string userId, string text)
{
var channel = await GetPMChannel(userId).ConfigureAwait(false);
var channel = await CreatePMChannel(userId).ConfigureAwait(false);
return await SendMessage(channel, text, new string[0]).ConfigureAwait(false);
}

@@ -307,11 +316,12 @@ namespace Discord
if (text == null) throw new ArgumentNullException(nameof(text));
if (mentions == null) throw new ArgumentNullException(nameof(mentions));

if (text.Length > DiscordAPI.MaxMessageSize)
text = text.Substring(0, DiscordAPI.MaxMessageSize);
if (text.Length > DiscordAPIClient.MaxMessageSize)
text = text.Substring(0, DiscordAPIClient.MaxMessageSize);

var msg = await _api.EditMessage(channelId, messageId, text, mentions).ConfigureAwait(false);
_messages.Update(msg.Id, channelId, msg);
var response = await _api.EditMessage(channelId, messageId, text, mentions).ConfigureAwait(false);
var msg = _messages.GetOrAdd(messageId, channelId);
msg.Update(response);
}

/// <summary> Deletes the provided message. </summary>
@@ -327,7 +337,7 @@ namespace Discord
try
{
await _api.DeleteMessage(channelId, msgId).ConfigureAwait(false);
_messages.Remove(msgId);
_messages.TryRemove(msgId);
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
@@ -338,7 +348,7 @@ namespace Discord

foreach (var msg in msgs)
{
try
try
{
await _api.DeleteMessage(msg.ChannelId, msg.Id).ConfigureAwait(false);
}
@@ -352,37 +362,25 @@ namespace Discord

foreach (var msgId in msgIds)
{
try
try
{
await _api.DeleteMessage(channelId, msgId).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
}
/// <summary> Sends a file to the provided channel. </summary>
public Task SendFile(Channel channel, string path)
=> SendFile(channel?.Id, path);
public Task SendFile(Channel channel, string filePath)
=> SendFile(channel?.Id, filePath);
/// <summary> Sends a file to the provided channel. </summary>
public Task SendFile(string channelId, string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
return SendFile(channelId, File.OpenRead(path), Path.GetFileName(path));
}
/// <summary> Reads a stream and sends it to the provided channel as a file. </summary>
/// <remarks> It is highly recommended that this stream be cached in memory or on disk, or the request may time out. </remarks>
public Task SendFile(Channel channel, Stream stream, string filename = null)
=> SendFile(channel?.Id, stream, filename);
/// <summary> Reads a stream and sends it to the provided channel as a file. </summary>
/// <remarks> It is highly recommended that this stream be cached in memory or on disk, or the request may time out. </remarks>
public Task SendFile(string channelId, Stream stream, string filename = null)
public Task SendFile(string channelId, string filePath)
{
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (stream == null) throw new ArgumentNullException(nameof(stream));
if (filename == null) throw new ArgumentNullException(nameof(filename));
if (filePath == null) throw new ArgumentNullException(nameof(filePath));

return _api.SendFile(channelId, stream, filename);
return _api.SendFile(channelId, filePath);
}

/// <summary> Downloads last count messages from the server, starting at beforeMessageId if it's provided. </summary>
@@ -396,16 +394,16 @@ namespace Discord
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
if (count == 0) return new Message[0];

Channel channel = GetChannel(channelId);
Channel channel = _channels[channelId];
if (channel != null && channel.Type == ChannelTypes.Text)
{
try
{
var msgs = await _api.GetMessages(channel.Id, count).ConfigureAwait(false);
return msgs.OrderBy(x => x.Timestamp)
.Select(x =>
return msgs.Select(x =>
{
var msg = _messages.Update(x.Id, x.ChannelId, x);
var msg = _messages.GetOrAdd(x.Id, x.ChannelId);
msg.Update(x);
var user = msg.User;
if (user != null)
user.UpdateActivity(x.Timestamp);
@@ -501,21 +499,21 @@ namespace Discord
{
CheckReady();
var response = await _api.ChangeUsername(newName, currentEmail, currentPassword).ConfigureAwait(false);
_users.Update(response.Id, response);
_currentUser.Update(response);
}
/// <summary> Changes your email to newEmail. </summary>
public async Task ChangeEmail(string newEmail, string currentPassword)
{
CheckReady();
var response = await _api.ChangeEmail(newEmail, currentPassword).ConfigureAwait(false);
_users.Update(response.Id, response);
_currentUser.Update(response);
}
/// <summary> Changes your password to newPassword. </summary>
public async Task ChangePassword(string newPassword, string currentEmail, string currentPassword)
{
CheckReady();
var response = await _api.ChangePassword(newPassword, currentEmail, currentPassword).ConfigureAwait(false);
_users.Update(response.Id, response);
_currentUser.Update(response);
}

/// <summary> Changes your avatar. </summary>
@@ -524,7 +522,7 @@ namespace Discord
{
CheckReady();
var response = await _api.ChangeAvatar(imageType, bytes, currentEmail, currentPassword).ConfigureAwait(false);
_users.Update(response.Id, response);
_currentUser.Update(response);
}
}
}

+ 34
- 447
src/Discord.Net/DiscordClient.Cache.cs View File

@@ -1,472 +1,59 @@
using Discord.API.Models;
using Discord.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace Discord
{
public partial class DiscordClient
{
/// <summary> Returns a collection of all users the client can see across all servers. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<User> Users => _users;
internal AsyncCache<User, API.Models.UserReference> _users;

/// <summary> Returns a collection of all servers the client is a member of. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<Server> Servers => _servers;
internal AsyncCache<Server, API.Models.ServerReference> _servers;

/// <summary> Returns a collection of all channels the client can see across all servers. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<Channel> Channels => _channels;
internal AsyncCache<Channel, API.Models.ChannelReference> _channels;

/// <summary> Returns a collection of all messages the client has in cache. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<Message> Messages => _messages;
internal AsyncCache<Message, API.Models.MessageReference> _messages;

/// <summary> Returns a collection of all roles the client can see across all servers. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<Role> Roles => _roles;
internal AsyncCache<Role, API.Models.Role> _roles;

private void CreateCaches()
{
_servers = new AsyncCache<Server, API.Models.ServerReference>(
(key, parentKey) =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created server {key}.");
return new Server(key, this);
},
(server, model) =>
{
server.Name = model.Name;
_channels.Update(server.DefaultChannelId, server.Id, null);
if (model is ExtendedServerInfo)
{
var extendedModel = model as ExtendedServerInfo;
server.AFKChannelId = extendedModel.AFKChannelId;
server.AFKTimeout = extendedModel.AFKTimeout;
server.JoinedAt = extendedModel.JoinedAt ?? DateTime.MinValue;
server.OwnerId = extendedModel.OwnerId;
server.Region = extendedModel.Region;

foreach (var role in extendedModel.Roles)
_roles.Update(role.Id, model.Id, role);
foreach (var channel in extendedModel.Channels)
_channels.Update(channel.Id, model.Id, channel);
foreach (var membership in extendedModel.Members)
{
_users.Update(membership.User.Id, membership.User);
server.UpdateMember(membership);
}
foreach (var membership in extendedModel.VoiceStates)
server.UpdateMember(membership);
foreach (var membership in extendedModel.Presences)
server.UpdateMember(membership);
}
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated server {server.Name} ({server.Id}).");
},
server =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed server {server.Name} ({server.Id}).");
}
);

_channels = new AsyncCache<Channel, API.Models.ChannelReference>(
(key, parentKey) =>
{
if (_isDebugMode)
{
if (parentKey != null)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created channel {key} in server {parentKey}.");
else
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created private channel {key}.");
}
return new Channel(key, parentKey, this);
},
(channel, model) =>
{
channel.Name = model.Name;
channel.Type = model.Type;
if (model is ChannelInfo)
{
var extendedModel = model as ChannelInfo;
channel.Position = extendedModel.Position;

if (extendedModel.IsPrivate)
{
var user = _users.Update(extendedModel.Recipient.Id, extendedModel.Recipient);
channel.RecipientId = user.Id;
user.PrivateChannelId = channel.Id;
}

if (extendedModel.PermissionOverwrites != null)
{
channel.PermissionOverwrites = extendedModel.PermissionOverwrites.Select(x => new Channel.PermissionOverwrite
{
Type = x.Type,
Id = x.Id,
Deny = new PackedPermissions(x.Deny),
Allow = new PackedPermissions(x.Allow)
}).ToArray();
}
else
channel.PermissionOverwrites = null;
}
if (_isDebugMode)
{
if (channel.IsPrivate)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated private channel {channel.Name} ({channel.Id}).");
else
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated channel {channel.Name} ({channel.Id}) in server {channel.Server?.Name} ({channel.ServerId}).");
}
},
channel =>
{
if (channel.IsPrivate)
{
var user = channel.Recipient;
if (user.PrivateChannelId == channel.Id)
user.PrivateChannelId = null;
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed private channel {channel.Name} ({channel.Id}).");
}
else
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed channel {channel.Name} ({channel.Id}) in server {channel.Server?.Name} ({channel.ServerId}).");
}
});

_messages = new AsyncCache<Message, API.Models.MessageReference>(
(key, parentKey) =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created message {key} in channel {parentKey}.");
return new Message(key, parentKey, this);
},
(message, model) =>
{
if (model is API.Models.Message)
{
var extendedModel = model as API.Models.Message;
if (extendedModel.Attachments != null)
{
message.Attachments = extendedModel.Attachments.Select(x => new Message.Attachment
{
Id = x.Id,
Url = x.Url,
ProxyUrl = x.ProxyUrl,
Size = x.Size,
Filename = x.Filename,
Width = x.Width,
Height = x.Height
}).ToArray();
}
else
message.Attachments = new Message.Attachment[0];
if (extendedModel.Embeds != null)
{
message.Embeds = extendedModel.Embeds.Select(x =>
{
var embed = new Message.Embed
{
Url = x.Url,
Type = x.Type,
Description = x.Description,
Title = x.Title
};
if (x.Provider != null)
{
embed.Provider = new Message.EmbedReference
{
Url = x.Provider.Url,
Name = x.Provider.Name
};
}
if (x.Author != null)
{
embed.Author = new Message.EmbedReference
{
Url = x.Author.Url,
Name = x.Author.Name
};
}
if (x.Thumbnail != null)
{
embed.Thumbnail = new Message.File
{
Url = x.Thumbnail.Url,
ProxyUrl = x.Thumbnail.ProxyUrl,
Width = x.Thumbnail.Width,
Height = x.Thumbnail.Height
};
}
return embed;
}).ToArray();
}
else
message.Embeds = new Message.Embed[0];
message.IsMentioningEveryone = extendedModel.IsMentioningEveryone;
message.IsTTS = extendedModel.IsTextToSpeech;
message.MentionIds = extendedModel.Mentions?.Select(x => x.Id)?.ToArray() ?? new string[0];
message.IsMentioningMe = message.MentionIds.Contains(_myId);
message.RawText = extendedModel.Content;
message.Timestamp = extendedModel.Timestamp;
message.EditedTimestamp = extendedModel.EditedTimestamp;
if (extendedModel.Author != null)
message.UserId = extendedModel.Author.Id;
}
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated message {message.Id} in channel {message.Channel?.Name} ({message.ChannelId}).");
},
message =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed message {message.Id} in channel {message.Channel?.Name} ({message.ChannelId}).");
}
);

_roles = new AsyncCache<Role, API.Models.Role>(
(key, parentKey) =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created role {key} in server {parentKey}.");
return new Role(key, parentKey, this);
},
(role, model) =>
{
role.Name = model.Name;
role.Permissions.RawValue = (uint)model.Permissions;
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated role {role.Name} ({role.Id}) in server {role.Server?.Name} ({role.ServerId}).");
},
role =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed role {role.Name} ({role.Id}) in server {role.Server?.Name} ({role.ServerId}).");
}
);

_users = new AsyncCache<User, API.Models.UserReference>(
(key, parentKey) =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key}.");
return new User(key, this);
},
(user, model) =>
{
user.AvatarId = model.Avatar;
user.Discriminator = model.Discriminator;
user.Name = model.Username;
if (model is SelfUserInfo)
{
var extendedModel = model as SelfUserInfo;
user.Email = extendedModel.Email;
user.IsVerified = extendedModel.IsVerified;
}
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {user?.Name} ({user.Id}).");
},
user =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {user?.Name} ({user.Id}).");
}
);
}

/// <summary> Returns the user with the specified id, or null if none was found. </summary>
public User GetUser(string id)
=> _users[id];
/// <summary> Returns the user with the specified name and discriminator, or null if none was found. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public User GetUser(string name, string discriminator)
{
if (name == null || discriminator == null)
return null;

if (name.StartsWith("@"))
name = name.Substring(1);

return _users
.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) &&
x.Discriminator == discriminator
)
.FirstOrDefault();
}
/// <summary> Returns all users with the specified name across all servers. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<User> FindUsers(string name)
{
if (name == null)
return new User[0];

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return _users.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return _users.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary> Returns the channel with the specified id, or null if none was found. </summary>
public Channel GetChannel(string id) => _channels[id];
/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks>
public IEnumerable<Channel> FindChannels(Server server, string name) => _channels.Find(server?.Id, name);
/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks>
public IEnumerable<Channel> FindChannels(string serverId, string name) => _channels.Find(serverId, name);

/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Member GetMember(string serverId, User user)
=> GetMember(_servers[serverId], user?.Id);
public Member GetMember(string serverId, User user) => _members[user?.Id, serverId];
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Member GetMember(string serverId, string userId)
=> GetMember(_servers[serverId], userId);
public Member GetMember(Server server, User user) => _members[user?.Id, server?.Id];
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Member GetMember(Server server, User user)
=> GetMember(server, user?.Id);
public Member GetMember(Server server, string userId) => _members[userId, server?.Id];
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Member GetMember(Server server, string userId)
{
if (server == null || userId == null)
return null;
return server.GetMember(userId);
}

public Member GetMember(string serverId, string userId) => _members[userId, serverId];
/// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks>
public IEnumerable<Member> FindMembers(string serverId, string name)
=> FindMembers(GetServer(serverId), name);
public IEnumerable<Member> FindMembers(Server server, string name) => _members.Find(server, name);
/// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks>
public IEnumerable<Member> FindMembers(Server server, string name)
{
if (server == null || name == null)
return new Member[0];

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return server.Members.Where(x =>
{
var user = x.User;
if (user == null)
return false;
return string.Equals(user.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(user.Name, name2, StringComparison.OrdinalIgnoreCase);
});
}
else
{
return server.Members.Where(x =>
{
var user = x.User;
if (user == null)
return false;
return string.Equals(x.User.Name, name, StringComparison.OrdinalIgnoreCase);
});
}
}

/// <summary> Returns the server with the specified id, or null if none was found. </summary>
public Server GetServer(string id)
=> _servers[id];
/// <summary> Returns all servers with the specified name. </summary>
/// <remarks> Search is case-insensitive. </remarks>
public IEnumerable<Server> FindServers(string name)
{
if (name == null)
return new Server[0];
return _servers.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}

/// <summary> Returns the channel with the specified id, or null if none was found. </summary>
public Channel GetChannel(string id) => _channels[id];
/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public Task<Channel> GetPMChannel(string userId)
=> GetPMChannel(_users[userId]);
/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public async Task<Channel> GetPMChannel(User user)
{
CheckReady();
if (user == null) throw new ArgumentNullException(nameof(user));

var channel = user.PrivateChannel;
if (channel != null)
return channel;
return await CreatePMChannel(user?.Id).ConfigureAwait(false);
}
private async Task<Channel> CreatePMChannel(string userId)
{
CheckReady();
if (userId == null) throw new ArgumentNullException(nameof(userId));

var response = await _api.CreatePMChannel(_myId, userId).ConfigureAwait(false);
return _channels.Update(response.Id, response);
}
public IEnumerable<Member> FindMembers(string serverId, string name) => _members.Find(_servers[serverId], name);

/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks>
public IEnumerable<Channel> FindChannels(Server server, string name)
=> FindChannels(server?.Id, name);
/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks>
public IEnumerable<Channel> FindChannels(string serverId, string name)
{
if (serverId == null || name == null)
return new Channel[0];

if (name.StartsWith("#"))
{
string name2 = name.Substring(1);
return _channels.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return _channels.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary> Returns the message with the specified id, or null if none was found. </summary>
public Message GetMessage(string id) => _messages[id];

/// <summary> Returns the role with the specified id, or null if none was found. </summary>
public Role GetRole(string id)
=> _roles[id];
public Role GetRole(string id) => _roles[id];
/// <summary> Returns all roles with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<Role> FindRoles(Server server, string name)
=> FindRoles(server?.Id, name);
public IEnumerable<Role> FindRoles(Server server, string name) => _roles.Find(server?.Id, name);
/// <summary> Returns all roles with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<Role> FindRoles(string serverId, string name)
{
if (serverId == null || name == null)
return new Role[0];
public IEnumerable<Role> FindRoles(string serverId, string name) => _roles.Find(serverId, name);

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return _roles.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return _roles.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary> Returns the server with the specified id, or null if none was found. </summary>
public Server GetServer(string id) => _servers[id];
/// <summary> Returns all servers with the specified name. </summary>
/// <remarks> Search is case-insensitive. </remarks>
public IEnumerable<Server> FindServers(string name) => _servers.Find(name);

/// <summary> Returns the user with the specified id, or null if none was found. </summary>
public User GetUser(string id) => _users[id];
/// <summary> Returns the user with the specified name and discriminator, or null if none was found. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public User GetUser(string name, string discriminator) => _users[name, discriminator];
/// <summary> Returns all users with the specified name across all servers. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<User> FindUsers(string name) => _users.Find(name);

/// <summary> Returns the message with the specified id, or null if none was found. </summary>
public Message GetMessage(string id)
=> _messages[id];
}
}

+ 24
- 77
src/Discord.Net/DiscordClient.Events.cs View File

@@ -2,24 +2,30 @@

namespace Discord
{
public enum DebugMessageType : byte
public enum LogMessageSeverity : byte
{
Connection,
Event,
Error,
Warning,
Info,
Verbose,
Debug
}
public enum LogMessageSource : byte
{
Unknown,
Authentication,
Cache,
WebSocketRawInput, //TODO: Make Http instanced and add a rawoutput event
WebSocketUnknownOpCode,
WebSocketUnknownEvent,
XHRRawOutput,
XHRTiming,
VoiceInput,
VoiceOutput,
DataWebSocket,
MessageQueue,
VoiceWebSocket,
}

public sealed class LogMessageEventArgs : EventArgs
{
public readonly DebugMessageType Type;
public readonly LogMessageSeverity Severity;
public readonly LogMessageSource Source;
public readonly string Message;
internal LogMessageEventArgs(DebugMessageType type, string msg) { Type = type; Message = msg; }
internal LogMessageEventArgs(LogMessageSeverity severity, LogMessageSource source, string msg) { Severity = severity; Message = msg; }
}
public sealed class ServerEventArgs : EventArgs
{
@@ -82,57 +88,44 @@ namespace Discord
}
}


public partial class DiscordClient
{
//Debug
public event EventHandler<LogMessageEventArgs> DebugMessage;
internal void RaiseOnDebugMessage(DebugMessageType type, string message)
{
if (DebugMessage != null)
DebugMessage(this, new LogMessageEventArgs(type, message));
}

//General
public event EventHandler Connected;
private void RaiseConnected()
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"Connected");
if (Connected != null)
Connected(this, EventArgs.Empty);
}
public event EventHandler Disconnected;
private void RaiseDisconnected()
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"Disconnected");
if (Disconnected != null)
Disconnected(this, EventArgs.Empty);
}
public event EventHandler<LogMessageEventArgs> LogMessage;
internal void RaiseOnLog(LogMessageSeverity severity, LogMessageSource source, string message)
{
if (LogMessage != null)
LogMessage(this, new LogMessageEventArgs(severity, source, message));
}

//Server
public event EventHandler<ServerEventArgs> ServerCreated;
private void RaiseServerCreated(Server server)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"ServerCreated {server.Name} ({server.Id})");
if (ServerCreated != null)
ServerCreated(this, new ServerEventArgs(server));
}
public event EventHandler<ServerEventArgs> ServerDestroyed;
private void RaiseServerDestroyed(Server server)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"ServerDestroyed {server.Name} ({server.Id})");
if (ServerDestroyed != null)
ServerDestroyed(this, new ServerEventArgs(server));
}
public event EventHandler<ServerEventArgs> ServerUpdated;
private void RaiseServerUpdated(Server server)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"ServerUpdated {server.Name} ({server.Id})");
if (ServerUpdated != null)
ServerUpdated(this, new ServerEventArgs(server));
}
@@ -141,8 +134,6 @@ namespace Discord
public event EventHandler<UserEventArgs> UserUpdated;
private void RaiseUserUpdated(User user)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"UserUpdated {user.Name} ({user.Id})");
if (UserUpdated != null)
UserUpdated(this, new UserEventArgs(user));
}
@@ -151,24 +142,18 @@ namespace Discord
public event EventHandler<ChannelEventArgs> ChannelCreated;
private void RaiseChannelCreated(Channel channel)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"ChannelCreated {channel.Name} ({channel.Id}) in {channel.Server?.Name} ({channel.ServerId})");
if (ChannelCreated != null)
ChannelCreated(this, new ChannelEventArgs(channel));
}
public event EventHandler<ChannelEventArgs> ChannelDestroyed;
private void RaiseChannelDestroyed(Channel channel)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"ChannelDestroyed {channel.Name} ({channel.Id}) in {channel.Server?.Name} ({channel.ServerId})");
if (ChannelDestroyed != null)
ChannelDestroyed(this, new ChannelEventArgs(channel));
}
public event EventHandler<ChannelEventArgs> ChannelUpdated;
private void RaiseChannelUpdated(Channel channel)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"ChannelUpdated {channel.Name} ({channel.Id}) in {channel.Server?.Name} ({channel.ServerId})");
if (ChannelUpdated != null)
ChannelUpdated(this, new ChannelEventArgs(channel));
}
@@ -177,40 +162,30 @@ namespace Discord
public event EventHandler<MessageEventArgs> MessageCreated;
private void RaiseMessageCreated(Message msg)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"MessageCreated {msg.Id} in {msg.Channel?.Name} ({msg.ChannelId})");
if (MessageCreated != null)
MessageCreated(this, new MessageEventArgs(msg));
}
public event EventHandler<MessageEventArgs> MessageDeleted;
private void RaiseMessageDeleted(Message msg)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"MessageDeleted {msg.Id} in {msg.Channel?.Name} ({msg.ChannelId})");
if (MessageDeleted != null)
MessageDeleted(this, new MessageEventArgs(msg));
}
public event EventHandler<MessageEventArgs> MessageUpdated;
private void RaiseMessageUpdated(Message msg)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"MessageUpdated {msg.Id} in {msg.Channel?.Name} ({msg.ChannelId})");
if (MessageUpdated != null)
MessageUpdated(this, new MessageEventArgs(msg));
}
public event EventHandler<MessageEventArgs> MessageRead;
private void RaiseMessageRead(Message msg)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"MessageRead {msg.Id} in {msg.Channel?.Name} ({msg.ChannelId})");
if (MessageRead != null)
MessageRead(this, new MessageEventArgs(msg));
}
public event EventHandler<MessageEventArgs> MessageSent;
private void RaiseMessageSent(Message msg)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"MessageSent {msg.Id} in {msg.Channel?.Name} ({msg.ChannelId})");
if (MessageSent != null)
MessageSent(this, new MessageEventArgs(msg));
}
@@ -219,24 +194,18 @@ namespace Discord
public event EventHandler<RoleEventArgs> RoleCreated;
private void RaiseRoleCreated(Role role)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"RoleCreated {role.Name} ({role.Id}) in {role.Server?.Name} ({role.ServerId})");
if (RoleCreated != null)
RoleCreated(this, new RoleEventArgs(role));
}
public event EventHandler<RoleEventArgs> RoleUpdated;
private void RaiseRoleDeleted(Role role)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"RoleDeleted {role.Name} ({role.Id}) in {role.Server?.Name} ({role.ServerId})");
if (RoleDeleted != null)
RoleDeleted(this, new RoleEventArgs(role));
}
public event EventHandler<RoleEventArgs> RoleDeleted;
private void RaiseRoleUpdated(Role role)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"RoleUpdated {role.Name} ({role.Id}) in {role.Server?.Name} ({role.ServerId})");
if (RoleUpdated != null)
RoleUpdated(this, new RoleEventArgs(role));
}
@@ -245,16 +214,12 @@ namespace Discord
public event EventHandler<BanEventArgs> BanAdded;
private void RaiseBanAdded(User user, Server server)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"BanAdded {user.Name} ({user.Id}) in {server.Name} ({server.Id})");
if (BanAdded != null)
BanAdded(this, new BanEventArgs(user, server));
}
public event EventHandler<BanEventArgs> BanRemoved;
private void RaiseBanRemoved(User user, Server server)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"BanRemoved {user.Name} ({user.Id}) in {server.Name} ({server.Id})");
if (BanRemoved != null)
BanRemoved(this, new BanEventArgs(user, server));
}
@@ -263,24 +228,18 @@ namespace Discord
public event EventHandler<MemberEventArgs> MemberAdded;
private void RaiseMemberAdded(Member member)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"MemberAdded {member.User?.Name} ({member.UserId}) in {member.Server?.Name} ({member.ServerId})");
if (MemberAdded != null)
MemberAdded(this, new MemberEventArgs(member));
}
public event EventHandler<MemberEventArgs> MemberRemoved;
private void RaiseMemberRemoved(Member member)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"MemberRemoved {member.User?.Name} ({member.UserId}) in {member.Server?.Name} ({member.ServerId})");
if (MemberRemoved != null)
MemberRemoved(this, new MemberEventArgs(member));
}
public event EventHandler<MemberEventArgs> MemberUpdated;
private void RaiseMemberUpdated(Member member)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"MemberUpdated {member.User?.Name} ({member.UserId}) in {member.Server?.Name} ({member.ServerId})");
if (MemberUpdated != null)
MemberUpdated(this, new MemberEventArgs(member));
}
@@ -289,24 +248,18 @@ namespace Discord
public event EventHandler<MemberEventArgs> PresenceUpdated;
private void RaisePresenceUpdated(Member member)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"PresenceUpdated {member.User?.Name} ({member.UserId}) in {member.Server?.Name} ({member.ServerId})");
if (PresenceUpdated != null)
PresenceUpdated(this, new MemberEventArgs(member));
}
public event EventHandler<MemberEventArgs> VoiceStateUpdated;
private void RaiseVoiceStateUpdated(Member member)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"VoiceStateUpdated {member.User?.Name} ({member.UserId}) in {member.Server?.Name} ({member.ServerId})");
if (VoiceStateUpdated != null)
VoiceStateUpdated(this, new MemberEventArgs(member));
}
public event EventHandler<UserTypingEventArgs> UserTyping;
private void RaiseUserTyping(User user, Channel channel)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"VoiceStateUpdated {user.Name} ({user.Id}) in {channel.Name} ({channel.Id})");
if (UserTyping != null)
UserTyping(this, new UserTypingEventArgs(user, channel));
}
@@ -315,24 +268,18 @@ namespace Discord
public event EventHandler VoiceConnected;
private void RaiseVoiceConnected()
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"VoiceConnected");
if (VoiceConnected != null)
VoiceConnected(this, EventArgs.Empty);
}
public event EventHandler VoiceDisconnected;
private void RaiseVoiceDisconnected()
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"VoiceDisconnected");
if (VoiceDisconnected != null)
VoiceDisconnected(this, EventArgs.Empty);
}
public event EventHandler<VoiceServerUpdatedEventArgs> VoiceServerUpdated;
private void RaiseVoiceServerUpdated(Server server, string endpoint)
{
if (_config.EnableDebug)
RaiseOnDebugMessage(DebugMessageType.Event, $"VoiceServerUpdated {server.Name} ({server.Id})");
if (VoiceServerUpdated != null)
VoiceServerUpdated(this, new VoiceServerUpdatedEventArgs(server, endpoint));
}


+ 57
- 0
src/Discord.Net/DiscordClient.Voice.cs View File

@@ -0,0 +1,57 @@
using Discord.Helpers;
using System;
using System.Threading.Tasks;

namespace Discord
{
public partial class DiscordClient
{
public Task JoinVoiceServer(string channelId)
=> JoinVoiceServer(_channels[channelId]);
public async Task JoinVoiceServer(Channel channel)
{
CheckReady(checkVoice: true);
if (channel == null) throw new ArgumentNullException(nameof(channel));

await LeaveVoiceServer().ConfigureAwait(false);
_dataSocket.SendJoinVoice(channel);
//await _voiceSocket.WaitForConnection().ConfigureAwait(false);
//TODO: Add another ManualResetSlim to wait on here, base it off of DiscordClient's setup
}

public async Task LeaveVoiceServer()
{
CheckReady(checkVoice: true);
await _voiceSocket.Disconnect().ConfigureAwait(false);
await TaskHelper.CompletedTask.ConfigureAwait(false);
_dataSocket.SendLeaveVoice();
}

/// <summary> Sends a PCM frame to the voice server. </summary>
/// <param name="data">PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. </param>
/// <param name="count">Number of bytes in this frame. </param>
/// <remarks>Will block until</remarks>
public void SendVoicePCM(byte[] data, int count)
{
CheckReady(checkVoice: true);
if (count == 0) return;
_voiceSocket.SendPCMFrames(data, count);
}

/// <summary> Clears the PCM buffer. </summary>
public void ClearVoicePCM()
{
CheckReady(checkVoice: true);
_voiceSocket.ClearPCMFrames();
}

/// <summary> Returns a task that completes once the voice output buffer is empty. </summary>
public async Task WaitVoice()
{
CheckReady(checkVoice: true);
_voiceSocket.Wait();
await TaskHelper.CompletedTask.ConfigureAwait(false);
}
}
}

+ 252
- 584
src/Discord.Net/DiscordClient.cs View File

@@ -1,498 +1,137 @@
using Discord.API;
using Discord.API.Models;
using Discord.Collections;
using Discord.Helpers;
using Newtonsoft.Json;
using Discord.Net;
using Discord.Net.API;
using Discord.Net.WebSockets;
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Text.RegularExpressions;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;

namespace Discord
{
public enum DiscordClientState : byte
{
Disconnected,
Connecting,
Connected,
Disconnecting
}

/// <summary> Provides a connection to the DiscordApp service. </summary>
public partial class DiscordClient
{
private readonly JsonHttpClient _http;
private readonly DiscordAPI _api;
private readonly DiscordDataSocket _webSocket;
#if !DNXCORE50
private readonly DiscordVoiceSocket _voiceWebSocket;
#endif
private readonly JsonSerializer _serializer;
private readonly Regex _userRegex, _channelRegex;
private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator;
private readonly ManualResetEvent _disconnectEvent;
private readonly Random _rand;
private readonly DiscordAPIClient _api;
private readonly DataWebSocket _dataSocket;
private readonly VoiceWebSocket _voiceSocket;
private readonly ConcurrentQueue<Message> _pendingMessages;
private readonly ManualResetEvent _disconnectedEvent;
private readonly ManualResetEventSlim _connectedEvent;
private Task _runTask;
protected ExceptionDispatchInfo _disconnectReason;
private bool _wasDisconnectUnexpected;

/// <summary> Returns the id of the current logged-in user. </summary>
public string CurrentUserId => _currentUserId;
private string _currentUserId;
/// <summary> Returns the current logged-in user. </summary>
public User CurrentUser => _currentUser;
private User _currentUser;

public DiscordClientState State => (DiscordClientState)_state;
private int _state;

public DiscordClientConfig Config => _config;
private readonly DiscordClientConfig _config;
public Channels Channels => _channels;
private readonly Channels _channels;
public Members Members => _members;
private readonly Members _members;
public Messages Messages => _messages;
private readonly Messages _messages;
public Roles Roles => _roles;
private readonly Roles _roles;
public Servers Servers => _servers;
private readonly Servers _servers;
public Users Users => _users;
private readonly Users _users;

public CancellationToken CancelToken => _cancelToken.Token;
private CancellationTokenSource _cancelToken;

private volatile Task _runTask;
protected volatile string _myId, _sessionId;

/// <summary> Returns the User object for the current logged in user. </summary>
public User User => _user;
private User _user;

/// <summary> Returns true if the user has successfully logged in and the websocket connection has been established. </summary>
public bool IsConnected => _isConnected;
private bool _isConnected, _isDisconnecting;

/// <summary> Returns true if this client was requested to disconnect. </summary>
public bool IsClosing => _disconnectToken.IsCancellationRequested;
/// <summary> Returns a cancel token that is triggered when a disconnect is requested. </summary>
public CancellationToken CloseToken => _disconnectToken.Token;
private volatile CancellationTokenSource _disconnectToken;

internal bool IsDebugMode => _isDebugMode;
private bool _isDebugMode;

#if !DNXCORE50
public Server CurrentVoiceServer => GetServer(_voiceWebSocket.CurrentVoiceServerId);
#endif

//Constructor
/// <summary> Initializes a new instance of the DiscordClient class. </summary>
public DiscordClient(DiscordClientConfig config = null)
{
_disconnectEvent = new ManualResetEvent(true);
_config = config ?? new DiscordClientConfig();
_isDebugMode = _config.EnableDebug;
_rand = new Random();
_serializer = new JsonSerializer();
#if TEST_RESPONSES
_serializer.CheckAdditionalContent = true;
_serializer.MissingMemberHandling = MissingMemberHandling.Error;
#endif
_config.Lock();

_userRegex = new Regex(@"<@\d+?>", RegexOptions.Compiled);
_channelRegex = new Regex(@"<#\d+?>", RegexOptions.Compiled);
_userRegexEvaluator = new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var user = _users[id];
if (user != null)
return '@' + user.Name;
else //User not found
return e.Value;
});
_channelRegexEvaluator = new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var channel = _channels[id];
if (channel != null)
return channel.Name;
else //Channel not found
return e.Value;
});

if (_config.UseMessageQueue)
_pendingMessages = new ConcurrentQueue<Message>();

_http = new JsonHttpClient(_config.EnableDebug);
_api = new DiscordAPI(_http);
if (_isDebugMode)
_http.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Type, e.Message);

CreateCaches();
_state = (int)DiscordClientState.Disconnected;
_disconnectedEvent = new ManualResetEvent(true);
_connectedEvent = new ManualResetEventSlim(false);
_rand = new Random();

_webSocket = new DiscordDataSocket(this, _config.ConnectionTimeout, _config.WebSocketInterval, _config.EnableDebug);
_webSocket.Connected += (s, e) => RaiseConnected();
_webSocket.Disconnected += async (s, e) =>
_api = new DiscordAPIClient(_config.LogLevel);
_dataSocket = new DataWebSocket(this);
_dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); };
_voiceSocket = new VoiceWebSocket(this);
_channels = new Channels(this);
_members = new Members(this);
_messages = new Messages(this);
_roles = new Roles(this);
_servers = new Servers(this);
_users = new Users(this);

_dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message);
_voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message);
if (_config.LogLevel >= LogMessageSeverity.Info)
{
RaiseDisconnected();

//Reconnect if we didn't cause the disconnect
if (e.WasUnexpected)
{
await Task.Delay(_config.ReconnectDelay).ConfigureAwait(false);
while (!_disconnectToken.IsCancellationRequested)
{
try
{
await _webSocket.ReconnectAsync().ConfigureAwait(false);
if (_http.Token != null)
await _webSocket.Login(_http.Token).ConfigureAwait(false);
break;
}
catch (Exception ex)
{
RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket reconnect failed: {ex.Message}");
//Net is down? We can keep trying to reconnect until the user runs Disconnect()
await Task.Delay(_config.FailedReconnectDelay).ConfigureAwait(false);
}
}
}
};
if (_isDebugMode)
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Type, $"DataSocket: {e.Message}");

#if !DNXCORE50
if (_config.EnableVoice)
_dataSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected");
_dataSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected");
_voiceSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Connected");
_voiceSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected");
}
if (_config.LogLevel >= LogMessageSeverity.Verbose)
{
_voiceWebSocket = new DiscordVoiceSocket(this, _config.VoiceConnectionTimeout, _config.WebSocketInterval, _config.VoiceBufferLength, _config.EnableDebug);
_voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected();
_voiceWebSocket.Disconnected += async (s, e) =>
_channels.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Channel {e.Item.ServerId}/{e.Item.Id}");
_channels.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Channel {e.Item.ServerId}/{e.Item.Id}");
_channels.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Channel {e.Item.ServerId}/{e.Item.Id}");
_members.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Member {e.Item.ServerId}/{e.Item.UserId}");
_members.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Member {e.Item.ServerId}/{e.Item.UserId}");
_members.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Member {e.Item.ServerId}/{e.Item.UserId}");
_messages.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Message {e.Item.ServerId}/{e.Item.ChannelId}/{e.Item.Id}");
_messages.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Message {e.Item.ServerId}/{e.Item.ChannelId}/{e.Item.Id}");
_messages.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Message {e.Item.ServerId}/{e.Item.ChannelId}/{e.Item.Id}");
_roles.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Role {e.Item.ServerId}/{e.Item.Id}");
_roles.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Role {e.Item.ServerId}/{e.Item.Id}");
_roles.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Role {e.Item.ServerId}/{e.Item.Id}");
_servers.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Server {e.Item.Id}");
_servers.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Server {e.Item.Id}");
_servers.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Server {e.Item.Id}");
_users.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created User {e.Item.Id}");
_users.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated User {e.Item.Id}");
_users.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed User {e.Item.Id}");

_api.RestClient.OnRequest += (s, e) =>
{
RaiseVoiceDisconnected();

//Reconnect if we didn't cause the disconnect
if (e.WasUnexpected)
{
await Task.Delay(_config.ReconnectDelay).ConfigureAwait(false);
while (!_disconnectToken.IsCancellationRequested)
{
try
{
await _voiceWebSocket.ReconnectAsync().ConfigureAwait(false);
break;
}
catch (Exception ex)
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Connection, $"VoiceSocket reconnect failed: {ex.Message}");
//Net is down? We can keep trying to reconnect until the user runs Disconnect()
await Task.Delay(_config.FailedReconnectDelay).ConfigureAwait(false);
}
}
}
if (e.Payload != null)
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ({e.Payload})");
else
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)}");
};
if (_isDebugMode)
_voiceWebSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Type, $"VoiceSocket: {e.Message}");
}
#endif

#if !DNXCORE50
_webSocket.GotEvent += async (s, e) =>
#else
_webSocket.GotEvent += (s, e) =>
#endif
{
switch (e.Type)
{
//Global
case "READY": //Resync
{
var data = e.Event.ToObject<TextWebSocketEvents.Ready>(_serializer);

_servers.Clear();
_channels.Clear();
_users.Clear();

_myId = data.User.Id;
#if !DNXCORE50
_sessionId = data.SessionId;
#endif
_user = _users.Update(data.User.Id, data.User);
foreach (var server in data.Guilds)
_servers.Update(server.Id, server);
foreach (var channel in data.PrivateChannels)
_channels.Update(channel.Id, null, channel);
}
break;

//Servers
case "GUILD_CREATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildCreate>(_serializer);
var server = _servers.Update(data.Id, data);
try { RaiseServerCreated(server); } catch { }
}
break;
case "GUILD_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildUpdate>(_serializer);
var server = _servers.Update(data.Id, data);
try { RaiseServerUpdated(server); } catch { }
}
break;
case "GUILD_DELETE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildDelete>(_serializer);
var server = _servers.Remove(data.Id);
if (server != null)
try { RaiseServerDestroyed(server); } catch { }
}
break;

//Channels
case "CHANNEL_CREATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.ChannelCreate>(_serializer);
var channel = _channels.Update(data.Id, data.GuildId, data);
try { RaiseChannelCreated(channel); } catch { }
}
break;
case "CHANNEL_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.ChannelUpdate>(_serializer);
var channel = _channels.Update(data.Id, data.GuildId, data);
try { RaiseChannelUpdated(channel); } catch { }
}
break;
case "CHANNEL_DELETE":
{
var data = e.Event.ToObject<TextWebSocketEvents.ChannelDelete>(_serializer);
var channel = _channels.Remove(data.Id);
if (channel != null)
try { RaiseChannelDestroyed(channel); } catch { }
}
break;

//Members
case "GUILD_MEMBER_ADD":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildMemberAdd>(_serializer);
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.ServerId];
if (server != null)
{
var member = server.UpdateMember(data);
try { RaiseMemberAdded(member); } catch { }
}
}
break;
case "GUILD_MEMBER_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildMemberUpdate>(_serializer);
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.ServerId];
if (server != null)
{
var member = server.UpdateMember(data);
try { RaiseMemberUpdated(member); } catch { }
}
}
break;
case "GUILD_MEMBER_REMOVE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildMemberRemove>(_serializer);
var server = _servers[data.ServerId];
if (server != null)
{
var member = server.RemoveMember(data.User.Id);
if (member != null)
try { RaiseMemberRemoved(member); } catch { }
}
}
break;

//Roles
case "GUILD_ROLE_CREATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildRoleCreateUpdate>(_serializer);
var role = _roles.Update(data.Role.Id, data.ServerId, data.Role);
try { RaiseRoleCreated(role); } catch { }
}
break;
case "GUILD_ROLE_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildRoleCreateUpdate>(_serializer);
var role = _roles.Update(data.Role.Id, data.ServerId, data.Role);
try { RaiseRoleUpdated(role); } catch { }
}
break;
case "GUILD_ROLE_DELETE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildRoleDelete>(_serializer);
var role = _roles.Remove(data.RoleId);
if (role != null)
try { RaiseRoleDeleted(role); } catch { }
}
break;

//Bans
case "GUILD_BAN_ADD":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildBanAddRemove>(_serializer);
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.ServerId];
try { RaiseBanAdded(user, server); } catch { }
}
break;
case "GUILD_BAN_REMOVE":
{
var data = e.Event.ToObject<TextWebSocketEvents.GuildBanAddRemove>(_serializer);
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.ServerId];
if (server != null && server.RemoveBan(user.Id))
{
try { RaiseBanRemoved(user, server); } catch { }
}
}
break;

//Messages
case "MESSAGE_CREATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.MessageCreate>(_serializer);
Message msg = null;
bool wasLocal = _config.UseMessageQueue && data.Author.Id == _myId && data.Nonce != null;
if (wasLocal)
{
msg = _messages.Remap("nonce" + data.Nonce, data.Id);
if (msg != null)
{
msg.IsQueued = false;
msg.Id = data.Id;
}
}
msg = _messages.Update(data.Id, data.ChannelId, data);
msg.User.UpdateActivity(data.Timestamp);
if (wasLocal)
{
try { RaiseMessageSent(msg); } catch { }
}
try { RaiseMessageCreated(msg); } catch { }
}
break;
case "MESSAGE_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.MessageUpdate>(_serializer);
var msg = _messages.Update(data.Id, data.ChannelId, data);
try { RaiseMessageUpdated(msg); } catch { }
}
break;
case "MESSAGE_DELETE":
{
var data = e.Event.ToObject<TextWebSocketEvents.MessageDelete>(_serializer);
var msg = GetMessage(data.MessageId);
if (msg != null)
{
_messages.Remove(msg.Id);
try { RaiseMessageDeleted(msg); } catch { }
}
}
break;
case "MESSAGE_ACK":
{
var data = e.Event.ToObject<TextWebSocketEvents.MessageAck>(_serializer);
var msg = GetMessage(data.MessageId);
if (msg != null)
try { RaiseMessageRead(msg); } catch { }
}
break;

//Statuses
case "PRESENCE_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.PresenceUpdate>(_serializer);
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.ServerId];
if (server != null)
{
var member = server.UpdateMember(data);
try { RaisePresenceUpdated(member); } catch { }
}
}
break;
case "VOICE_STATE_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.VoiceStateUpdate>(_serializer);
var server = _servers[data.ServerId];
if (server != null)
{
var member = server.UpdateMember(data);
if (member != null)
try { RaiseVoiceStateUpdated(member); } catch { }
}
}
break;
case "TYPING_START":
{
var data = e.Event.ToObject<TextWebSocketEvents.TypingStart>(_serializer);
var channel = _channels[data.ChannelId];
var user = _users[data.UserId];
if (user != null)
{
user.UpdateActivity(DateTime.UtcNow);
if (channel != null)
try { RaiseUserTyping(user, channel); } catch { }
}
}
break;

//Voice
case "VOICE_SERVER_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.VoiceServerUpdate>(_serializer);
var server = _servers[data.ServerId];
server.VoiceServer = data.Endpoint;
try { RaiseVoiceServerUpdated(server, data.Endpoint); } catch { }

#if !DNXCORE50
if (_config.EnableVoice)
{
_voiceWebSocket.SetSessionData(data.ServerId, _myId, _sessionId, data.Token);
await _voiceWebSocket.ConnectAsync("wss://" + data.Endpoint.Split(':')[0]).ConfigureAwait(false);
}
#endif
}
break;

//Settings
case "USER_UPDATE":
{
var data = e.Event.ToObject<TextWebSocketEvents.UserUpdate>(_serializer);
var user = _users.Update(data.Id, data);
try { RaiseUserUpdated(user); } catch { }
}
break;
case "USER_SETTINGS_UPDATE":
{
//TODO: Process this
}
break;

//Others
default:
RaiseOnDebugMessage(DebugMessageType.WebSocketUnknownEvent, "Unknown WebSocket message type: " + e.Type);
break;
}
};
}

//Async
private async Task MessageQueueLoop()
{
var cancelToken = _disconnectToken.Token;
try
{
Message msg;
while (!cancelToken.IsCancellationRequested)
{
while (_pendingMessages.TryDequeue(out msg))
{
bool hasFailed = false;
APIResponses.SendMessage apiMsg = null;
try
{
apiMsg = await _api.SendMessage(msg.ChannelId, msg.RawText, msg.MentionIds, msg.Nonce).ConfigureAwait(false);
}
catch (WebException) { break; }
catch (HttpException) { hasFailed = true; }
if (_config.UseMessageQueue)
_pendingMessages = new ConcurrentQueue<Message>();
}

if (!hasFailed)
{
_messages.Remap("nonce_", apiMsg.Id);
_messages.Update(msg.Id, msg.ChannelId, apiMsg);
}
msg.IsQueued = false;
msg.HasFailed = hasFailed;
try { RaiseMessageSent(msg); } catch { }
}
await Task.Delay(_config.MessageQueueInterval).ConfigureAwait(false);
}
}
catch { }
finally { _disconnectToken.Cancel(); }
}
private string GenerateNonce()
private void _dataSocket_Connected(object sender, EventArgs e)
{
lock (_rand)
return _rand.Next().ToString();
throw new NotImplementedException();
}

//Connection
@@ -501,184 +140,213 @@ namespace Discord
{
await Disconnect().ConfigureAwait(false);

if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket is using cached token.");
if (_config.LogLevel >= LogMessageSeverity.Verbose)
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Using cached token.");

await ConnectInternal(token).ConfigureAwait(false);
}
await ConnectInternal(token).ConfigureAwait(false);
}
/// <summary> Connects to the Discord server with the provided email and password. </summary>
/// <returns> Returns a token for future connections. </returns>
public async Task<string> Connect(string email, string password)
{
await Disconnect().ConfigureAwait(false);
var response = await _api.Login(email, password).ConfigureAwait(false);
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket got token.");
if (_config.LogLevel >= LogMessageSeverity.Verbose)
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, "Login successful, got token.");
return await ConnectInternal(response.Token).ConfigureAwait(false);
}

private async Task<string> ConnectInternal(string token)
{
_http.Token = token;
string url = (await _api.GetWebSocketEndpoint().ConfigureAwait(false)).Url;
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket got endpoint.");
if (_state != (int)DiscordClientState.Disconnected)
throw new InvalidOperationException("Client is already connected or connecting to the server.");

await _webSocket.ConnectAsync(url).ConfigureAwait(false);
await _webSocket.Login(token).ConfigureAwait(false);
try
{
_disconnectedEvent.Reset();
_cancelToken = new CancellationTokenSource();
_state = (int)DiscordClientState.Connecting;

_api.Token = token;
string url = (await _api.GetWebSocketEndpoint().ConfigureAwait(false)).Url;
if (_config.LogLevel >= LogMessageSeverity.Verbose)
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Websocket endpoint: {url}");
await _dataSocket.Login(url, token).ConfigureAwait(false);

_runTask = RunTasks();

try
{
if (!_connectedEvent.Wait(_config.ConnectionTimeout, CancellationTokenSource.CreateLinkedTokenSource(_cancelToken.Token, _dataSocket.CancelToken).Token))
throw new Exception("Operation timed out.");
}
catch (OperationCanceledException)
{
_dataSocket.ThrowError();
throw;
}

_runTask = Run();
return token;
//_state = (int)DiscordClientState.Connected;
return token;
}
catch
{
await Disconnect().ConfigureAwait(false);
throw;
}
}
protected void CompleteConnect()
{
_state = (int)WebSocketState.Connected;
_connectedEvent.Set();
}

/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary>
public async Task Disconnect()
public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."));
protected async Task DisconnectInternal(Exception ex, bool isUnexpected = true)
{
Task task = _runTask;
if (task != null)
int oldState;
bool hasWriterLock;

//If in either connecting or connected state, get a lock by being the first to switch to disconnecting
oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connecting);
if (oldState == (int)DiscordClientState.Disconnected) return; //Already disconnected
hasWriterLock = oldState == (int)DiscordClientState.Connecting; //Caused state change
if (!hasWriterLock)
{
try { _disconnectToken.Cancel(); } catch (NullReferenceException) { }
try { await task.ConfigureAwait(false); } catch (NullReferenceException) { }
oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connected);
if (oldState == (int)DiscordClientState.Disconnected) return; //Already disconnected
hasWriterLock = oldState == (int)DiscordClientState.Connected; //Caused state change
}

if (hasWriterLock)
{
_wasDisconnectUnexpected = isUnexpected;
_disconnectReason = ExceptionDispatchInfo.Capture(ex);
_cancelToken.Cancel();
}

Task task = _runTask;
if (task != null)
await task.ConfigureAwait(false);

if (hasWriterLock)
{
_state = (int)DiscordClientState.Disconnected;
_disconnectedEvent.Set();
_connectedEvent.Reset();
}
}

private async Task Run()
private async Task RunTasks()
{
_disconnectEvent.Reset();
_disconnectToken = new CancellationTokenSource();

//Run Loops
Task task;
if (_config.UseMessageQueue)
task = MessageQueueLoop();
else
task = _disconnectToken.Wait();
task = _cancelToken.Wait();

_isConnected = true;
try
{
await task.ConfigureAwait(false);
}
catch (Exception ex) { await DisconnectInternal(ex).ConfigureAwait(false); }

await task.ConfigureAwait(false);
await Cleanup();
await Cleanup().ConfigureAwait(false);
_runTask = null;
}
//TODO: What happens if a reconnect occurs and caches havent been cleared yet? Compare to DiscordWebSocket.Cleanup()
private async Task Cleanup()
{
_disconnectEvent.Set();
_disconnectedEvent.Set();

await _webSocket.DisconnectAsync().ConfigureAwait(false);
await _dataSocket.Disconnect().ConfigureAwait(false);
#if !DNXCORE50
if (_config.EnableVoice)
await _voiceWebSocket.DisconnectAsync().ConfigureAwait(false);
await _voiceSocket.Disconnect().ConfigureAwait(false);
#endif

Message ignored;
while (_pendingMessages.TryDequeue(out ignored)) { }
_channels.Clear();
_members.Clear();
_messages.Clear();
_roles.Clear();
_servers.Clear();
_users.Clear();

_runTask = null;
_isConnected = false;
_isDisconnecting = false;
}

//Voice
public Task JoinVoiceServer(string channelId)
=> JoinVoiceServer(_channels[channelId]);
public async Task JoinVoiceServer(Channel channel)
{
CheckReady(checkVoice: true);
if (channel == null) throw new ArgumentNullException(nameof(channel));

await LeaveVoiceServer().ConfigureAwait(false);
_webSocket.JoinVoice(channel);
#if !DNXCORE50
await _voiceWebSocket.BeginConnect().ConfigureAwait(false);
#else
await Task.CompletedTask.ConfigureAwait(false);
#endif
_currentUser = null;
_currentUserId = null;
}

public async Task LeaveVoiceServer()
//Helpers
private void CheckReady(bool checkVoice = false)
{
CheckReady(checkVoice: true);
switch (_state)
{
case (int)DiscordClientState.Disconnecting:
throw new InvalidOperationException("The client is disconnecting.");
case (int)DiscordClientState.Disconnected:
throw new InvalidOperationException("The client is not connected to Discord");
case (int)DiscordClientState.Connecting:
throw new InvalidOperationException("The client is connecting.");
}

#if !DNXCORE50
await _voiceWebSocket.DisconnectAsync().ConfigureAwait(false);
if (checkVoice && !_config.EnableVoice)
#else
await Task.CompletedTask.ConfigureAwait(false);
if (checkVoice) //Always fail on DNXCORE50
#endif
_webSocket.LeaveVoice();
throw new InvalidOperationException("Voice is not enabled for this client.");
}

/// <summary> Sends a PCM frame to the voice server. </summary>
/// <param name="data">PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. </param>
/// <param name="count">Number of bytes in this frame. </param>
/// <remarks>Will block until</remarks>
public void SendVoicePCM(byte[] data, int count)
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
public void Block()
{
CheckReady(checkVoice: true);
if (count == 0) return;

if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Queued {count} bytes for voice output.");
#if !DNXCORE50
_voiceWebSocket.SendPCMFrames(data, count);
#endif
_disconnectedEvent.WaitOne();
}

/// <summary> Clears the PCM buffer. </summary>
public void ClearVoicePCM()
//Experimental
private Task MessageQueueLoop()
{
CheckReady(checkVoice: true);

if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Cleared the voice buffer.");
#if !DNXCORE50
_voiceWebSocket.ClearPCMFrames();
#endif
}
var cancelToken = _cancelToken.Token;
int interval = _config.MessageQueueInterval;

/// <summary> Returns a task that completes once the voice output buffer is empty. </summary>
public async Task WaitVoice()
{
CheckReady(checkVoice: true);
return Task.Run(async () =>
{
Message msg;
while (!cancelToken.IsCancellationRequested)
{
while (_pendingMessages.TryDequeue(out msg))
{
bool hasFailed = false;
Responses.SendMessage response = null;
try
{
response = await _api.SendMessage(msg.ChannelId, msg.RawText, msg.MentionIds, msg.Nonce).ConfigureAwait(false);
}
catch (WebException) { break; }
catch (HttpException) { hasFailed = true; }

#if !DNXCORE50
_voiceWebSocket.Wait();
#endif
await TaskHelper.CompletedTask.ConfigureAwait(false);
if (!hasFailed)
{
_messages.Remap(msg.Id, response.Id);
msg.Id = response.Id;
msg.Update(response);
}
msg.IsQueued = false;
msg.HasFailed = hasFailed;
try { RaiseMessageSent(msg); } catch { }
}
await Task.Delay(interval).ConfigureAwait(false);
}
});
}

//Helpers
private void CheckReady(bool checkVoice = false)
private string GenerateNonce()
{
if (_isDisconnecting)
throw new InvalidOperationException("The client is currently disconnecting.");
else if (!_isConnected)
throw new InvalidOperationException("The client is not currently connected to Discord");
#if !DNXCORE50
else if (checkVoice && !_config.EnableVoice)
#else
else if (checkVoice) //Always fail on DNXCORE50
#endif
throw new InvalidOperationException("Voice is not enabled for this client.");
lock (_rand)
return _rand.Next().ToString();
}
internal string CleanMessageText(string text)
{
text = _userRegex.Replace(text, _userRegexEvaluator);
text = _channelRegex.Replace(text, _channelRegexEvaluator);
return text;
}

/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
public void Block()
{
if (_isConnected)
_disconnectEvent.WaitOne();
}
}
}

+ 40
- 20
src/Discord.Net/DiscordClientConfig.cs View File

@@ -1,31 +1,51 @@
namespace Discord
using System;

namespace Discord
{
public class DiscordClientConfig
public class DiscordClientConfig
{
#if !DNXCORE50
/// <summary> Enables the voice websocket and UDP client (Experimental!). This option requires the opus .dll or .so be in the local lib/ folder. </remarks>
public bool EnableVoice { get; set; } = false;
#endif
/// <summary> Enables the verbose DebugMessage event handler. May hinder performance but should help debug any issues. </summary>
public bool EnableDebug { get; set; } = false;
/// <summary> Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to verbose will hinder performance but should help investigate any internal issues. </summary>
public LogMessageSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } }
private LogMessageSeverity _logLevel = LogMessageSeverity.Info;

/// <summary> Max time in milliseconds to wait for the web socket to connect. </summary>
public int ConnectionTimeout { get; set; } = 10000;
/// <summary> Max time in milliseconds to wait for the voice web socket to connect. </summary>
public int VoiceConnectionTimeout { get; set; } = 10000;
/// <summary> Max time in milliseconds to wait for DiscordClient to connect and initialize. </summary>
public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } }
private int _connectionTimeout = 10000;
/// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary>
public int ReconnectDelay { get; set; } = 1000;
public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } }
private int _reconnectDelay = 1000;
/// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary>
public int FailedReconnectDelay { get; set; } = 10000;
public int FailedReconnectDelay { get { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } }
private int _failedReconnectDelay = 10000;

/// <summary> Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again. </summary>
public int WebSocketInterval { get; set; } = 100;
/// <summary> Enables or disables the internal message queue. This will allow SendMessage to return immediately and handle messages internally. Messages will set the IsQueued and HasFailed properties to show their progress. </summary>
public bool UseMessageQueue { get; set; } = false;
public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } }
private int _webSocketInterval = 100;
/// <summary> Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. </summary>
public int MessageQueueInterval { get; set; } = 100;
public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } }
private int _messageQueueInterval = 100;
/// <summary> Gets or sets the max buffer length (in milliseconds) for outgoing voice packets. This value is the target maximum but is not guaranteed, the buffer will often go slightly above this value. </remarks>
public int VoiceBufferLength { get; set; } = 3000;
public int VoiceBufferLength { get { return _voiceBufferLength; } set { SetValue(ref _voiceBufferLength, value); } }
private int _voiceBufferLength = 3000;

//Experimental Features
#if !DNXCORE50
/// <summary> (Experimental) Enables the voice websocket and UDP client (Experimental!). This option requires the opus .dll or .so be in the local lib/ folder. </remarks>
public bool EnableVoice { get { return _enableVoice; } set { SetValue(ref _enableVoice, value); } }
private bool _enableVoice = false;
#endif
/// <summary> (Experimental) Enables or disables the internal message queue. This will allow SendMessage to return immediately and handle messages internally. Messages will set the IsQueued and HasFailed properties to show their progress. </summary>
public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } }
private bool _useMessageQueue = false;

public DiscordClientConfig() { }
//Lock
private bool _isLocked;
internal void Lock() { _isLocked = true; }
private void SetValue<T>(ref T storage, T value)
{
if (_isLocked)
throw new InvalidOperationException("Unable to modify a discord client's configuration after it has been created.");
storage = value;
}
}
}

+ 0
- 25
src/Discord.Net/DiscordDataSocket.Events.cs View File

@@ -1,25 +0,0 @@
using Newtonsoft.Json.Linq;
using System;

namespace Discord
{
internal partial class DiscordDataSocket
{
public event EventHandler<MessageEventArgs> GotEvent;
public sealed class MessageEventArgs : EventArgs
{
public readonly string Type;
public readonly JToken Event;
internal MessageEventArgs(string type, JToken data)
{
Type = type;
Event = data;
}
}
private void RaiseGotEvent(string type, JToken payload)
{
if (GotEvent != null)
GotEvent(this, new MessageEventArgs(type, payload));
}
}
}

+ 0
- 140
src/Discord.Net/DiscordDataSocket.cs View File

@@ -1,140 +0,0 @@
using Discord.API.Models;
using Discord.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Threading;
using System.Threading.Tasks;
using WebSocketMessage = Discord.API.Models.TextWebSocketCommands.WebSocketMessage;

namespace Discord
{
internal sealed partial class DiscordDataSocket : DiscordWebSocket
{
private readonly ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2;
private string _lastSession, _redirectServer;
private int _lastSeq;

public DiscordDataSocket(DiscordClient client, int timeout, int interval, bool isDebug)
: base(client, timeout, interval, isDebug)
{
_connectWaitOnLogin = new ManualResetEventSlim(false);
_connectWaitOnLogin2 = new ManualResetEventSlim(false);
}

public override async Task ConnectAsync(string url)
{
_lastSeq = 0;
_lastSession = null;
_redirectServer = null;
await BeginConnect().ConfigureAwait(false);
await base.ConnectAsync(url).ConfigureAwait(false);
}
public async Task Login(string token)
{
var cancelToken = _disconnectToken.Token;

_connectWaitOnLogin.Reset();
_connectWaitOnLogin2.Reset();

TextWebSocketCommands.Login msg = new TextWebSocketCommands.Login();
msg.Payload.Token = token;
msg.Payload.Properties["$os"] = "";
msg.Payload.Properties["$browser"] = "";
msg.Payload.Properties["$device"] = "Discord.Net";
msg.Payload.Properties["$referrer"] = "";
msg.Payload.Properties["$referring_domain"] = "";
await SendMessage(msg, cancelToken).ConfigureAwait(false);

try
{
if (!_connectWaitOnLogin.Wait(_timeout, cancelToken)) //Waiting on READY message
throw new Exception("No reply from Discord server");
}
catch (OperationCanceledException)
{
if (_disconnectReason == null)
throw new Exception("An unknown websocket error occurred.");
else
_disconnectReason.Throw();
}
try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler
catch (OperationCanceledException) { return; }
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.Connection, $"Logged in.");

SetConnected();
}

protected override Task ProcessMessage(string json)
{
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json);
if (msg.Sequence.HasValue)
_lastSeq = msg.Sequence.Value;
switch (msg.Operation)
{
case 0:
{
if (msg.Type == "READY")
{
var payload = (msg.Payload as JToken).ToObject<TextWebSocketEvents.Ready>();
_lastSession = payload.SessionId;
_heartbeatInterval = payload.HeartbeatInterval;
QueueMessage(new TextWebSocketCommands.UpdateStatus());
//QueueMessage(GetKeepAlive());
_connectWaitOnLogin.Set(); //Pre-Event
}
RaiseGotEvent(msg.Type, msg.Payload as JToken);
if (msg.Type == "READY")
_connectWaitOnLogin2.Set(); //Post-Event
}
break;
case 7:
{
var payload = (msg.Payload as JToken).ToObject<TextWebSocketEvents.Redirect>();
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.Connection, $"Redirected to {payload.Url}.");
_host = payload.Url;
DisconnectInternal(new Exception("Server is redirecting."), true);
}
break;
default:
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.WebSocketUnknownOpCode, "Unknown Opcode: " + msg.Operation);
break;
}
return TaskHelper.CompletedTask;
}

protected override object GetKeepAlive()
{
return new TextWebSocketCommands.KeepAlive();
}

public void JoinVoice(Channel channel)
{
var joinVoice = new TextWebSocketCommands.JoinVoice();
joinVoice.Payload.ServerId = channel.ServerId;
joinVoice.Payload.ChannelId = channel.Id;
QueueMessage(joinVoice);
}
public void LeaveVoice()
{
var joinVoice = new TextWebSocketCommands.JoinVoice();
QueueMessage(joinVoice);
}

protected override void OnConnect()
{
if (_redirectServer != null)
{
var resumeMsg = new TextWebSocketCommands.Resume();
resumeMsg.Payload.SessionId = _lastSession;
resumeMsg.Payload.Sequence = _lastSeq;
SendMessage(resumeMsg, _disconnectToken.Token);
}
_redirectServer = null;
}
}
}

+ 0
- 35
src/Discord.Net/DiscordWebSocket.Events.cs View File

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

namespace Discord
{
public class DisconnectedEventArgs : EventArgs
{
public readonly bool WasUnexpected;
internal DisconnectedEventArgs(bool wasUnexpected) { WasUnexpected = wasUnexpected; }
}

internal abstract partial class DiscordWebSocket
{
//Debug
public event EventHandler<LogMessageEventArgs> OnDebugMessage;
protected void RaiseOnDebugMessage(DebugMessageType type, string message)
{
if (OnDebugMessage != null)
OnDebugMessage(this, new LogMessageEventArgs(type, message));
}

//Connection
public event EventHandler Connected;
private void RaiseConnected()
{
if (Connected != null)
Connected(this, EventArgs.Empty);
}
public event EventHandler<DisconnectedEventArgs> Disconnected;
private void RaiseDisconnected(bool wasUnexpected)
{
if (Disconnected != null)
Disconnected(this, new DisconnectedEventArgs(wasUnexpected));
}
}
}

+ 0
- 286
src/Discord.Net/DiscordWebSocket.cs View File

@@ -1,286 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Net.WebSockets;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Discord
{
internal abstract partial class DiscordWebSocket : IDisposable
{
private const int ReceiveChunkSize = 4096;
private const int SendChunkSize = 4096;
private const int HR_TIMEOUT = -2147012894;

protected readonly DiscordClient _client;
protected readonly int _sendInterval;
protected readonly bool _isDebug;
private readonly ConcurrentQueue<byte[]> _sendQueue;

protected CancellationTokenSource _disconnectToken;
protected string _host;
protected int _timeout, _heartbeatInterval;
protected ExceptionDispatchInfo _disconnectReason;
private ClientWebSocket _webSocket;
private DateTime _lastHeartbeat;
private Task _runTask;
private bool _isConnected, _wasDisconnectUnexpected;

public DiscordWebSocket(DiscordClient client, int timeout, int interval, bool isDebug)
{
_client = client;
_timeout = timeout;
_sendInterval = interval;
_isDebug = isDebug;

_sendQueue = new ConcurrentQueue<byte[]>();
}

protected virtual async Task BeginConnect()
{
await DisconnectAsync().ConfigureAwait(false);
_disconnectToken = new CancellationTokenSource();
_disconnectReason = null;
}
public virtual async Task ConnectAsync(string url)
{
var cancelToken = _disconnectToken.Token;

_webSocket = new ClientWebSocket();
_webSocket.Options.KeepAliveInterval = TimeSpan.Zero;
await _webSocket.ConnectAsync(new Uri(url), cancelToken).ConfigureAwait(false);
_host = url;

if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.Connection, $"Connected.");

OnConnect();

_runTask = Run();
}
public Task ReconnectAsync()
=> ConnectAsync(_host);
public async Task DisconnectAsync()
{
Task task = _runTask;
if (task != null)
{
try { DisconnectInternal(new Exception("Disconnect requested by user."), false); } catch (NullReferenceException) { }
try { await task.ConfigureAwait(false); } catch (NullReferenceException) { }
}
}
protected void DisconnectInternal(Exception ex, bool isUnexpected = true)
{
if (_disconnectReason == null)
{
_wasDisconnectUnexpected = isUnexpected;
_disconnectReason = ExceptionDispatchInfo.Capture(ex);
_disconnectToken.Cancel();
}
}

protected virtual void OnConnect() { }
protected virtual void OnDisconnect() { }

protected void SetConnected()
{
_isConnected = true;
RaiseConnected();
}

private async Task Run()
{
_lastHeartbeat = DateTime.UtcNow;

await Task.WhenAll(CreateTasks());
Cleanup();
}
private void Cleanup()
{
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.Connection, $"Disconnected.");
OnDisconnect();

bool wasUnexpected = _wasDisconnectUnexpected;
_disconnectToken.Dispose();
_disconnectToken = null;
_wasDisconnectUnexpected = false;
_heartbeatInterval = 0;
_lastHeartbeat = DateTime.MinValue;
_webSocket.Dispose();
_webSocket = null;
byte[] ignored;
while (_sendQueue.TryDequeue(out ignored)) { }

_runTask = null;
if (_isConnected)
{
_isConnected = false;
RaiseDisconnected(wasUnexpected);
}
}

protected virtual Task[] CreateTasks()
{
return new Task[]
{
ReceiveAsync(),
SendAsync()
};
}

private Task ReceiveAsync()
{
var cancelSource = _disconnectToken;
var cancelToken = cancelSource.Token;

return Task.Run(async () =>
{
var buffer = new byte[ReceiveChunkSize];
var builder = new StringBuilder();

try
{
while (_webSocket.State == WebSocketState.Open && !cancelToken.IsCancellationRequested)
{
WebSocketReceiveResult result = null;
do
{
if (_webSocket.State != WebSocketState.Open || cancelToken.IsCancellationRequested)
return;

try
{
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancelToken).ConfigureAwait(false);
}
catch (Win32Exception ex)
when (ex.HResult == HR_TIMEOUT)
{
string msg = $"Connection timed out.";
RaiseOnDebugMessage(DebugMessageType.Connection, msg);
DisconnectInternal(new Exception(msg));
return;
}

if (result.MessageType == WebSocketMessageType.Close)
{
string msg = $"Got Close Message ({result.CloseStatus?.ToString() ?? "Unexpected"}, {result.CloseStatusDescription ?? "No Reason"})";
RaiseOnDebugMessage(DebugMessageType.Connection, msg);
DisconnectInternal(new Exception(msg));
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false);
return;
}
else
builder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));

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

#if DEBUG
System.Diagnostics.Debug.WriteLine(">>> " + builder.ToString());
#endif
await ProcessMessage(builder.ToString()).ConfigureAwait(false);

builder.Clear();
}
}
catch (OperationCanceledException) { }
catch (Exception ex) { DisconnectInternal(ex); }
});
}
private Task SendAsync()
{
var cancelSource = _disconnectToken;
var cancelToken = cancelSource.Token;

return Task.Run(async () =>
{
try
{
byte[] bytes;
while (_webSocket.State == WebSocketState.Open && !cancelToken.IsCancellationRequested)
{
if (_heartbeatInterval > 0)
{
DateTime now = DateTime.UtcNow;
if ((now - _lastHeartbeat).TotalMilliseconds > _heartbeatInterval)
{
await SendMessage(GetKeepAlive(), cancelToken).ConfigureAwait(false);
_lastHeartbeat = now;
}
}
while (_sendQueue.TryDequeue(out bytes))
await SendMessage(bytes, cancelToken).ConfigureAwait(false);
await Task.Delay(_sendInterval, cancelToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
catch (Exception ex) { DisconnectInternal(ex); }
});
}

protected abstract Task ProcessMessage(string json);
protected abstract object GetKeepAlive();

protected void QueueMessage(object message)
{
#if DEBUG
System.Diagnostics.Debug.WriteLine("<<< " + JsonConvert.SerializeObject(message));
#endif
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message));
_sendQueue.Enqueue(bytes);
}
protected Task SendMessage(object message, CancellationToken cancelToken)
{
#if DEBUG
System.Diagnostics.Debug.WriteLine("<<< " + JsonConvert.SerializeObject(message));
#endif
return SendMessage(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), cancelToken);
}
protected async Task SendMessage(byte[] message, CancellationToken cancelToken)
{
var frameCount = (int)Math.Ceiling((double)message.Length / SendChunkSize);

int offset = 0;
for (var i = 0; i < frameCount; i++, offset += SendChunkSize)
{
bool isLast = i == (frameCount - 1);

int count;
if (isLast)
count = message.Length - (i * SendChunkSize);
else
count = SendChunkSize;
try
{
await _webSocket.SendAsync(new ArraySegment<byte>(message, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false);
}
catch (Win32Exception ex)
when (ex.HResult == HR_TIMEOUT)
{
return;
}
}
}

#region IDisposable Support
private bool _isDisposed = false;

public void Dispose()
{
if (!_isDisposed)
{
DisconnectAsync().Wait();
_isDisposed = true;
}
}
#endregion
}
}

+ 4
- 4
src/Discord.Net/Enums/Regions.cs View File

@@ -4,9 +4,9 @@
{
public const string US_West = "us-west";
public const string US_East = "us-east";
public const string Singapore = "singapore";
public const string London = "london";
public const string Sydney = "sydney";
public const string Singapore = "singapore";
public const string London = "london";
public const string Sydney = "sydney";
public const string Amsterdam = "amsterdam";
}
}
}

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

@@ -1,6 +1,6 @@
namespace Discord
{
public static class UserStatus
public static class UserStatus
{
/// <summary> User is currently online and active. </summary>
public const string Online = "online";


+ 4
- 18
src/Discord.Net/Format.cs View File

@@ -4,7 +4,7 @@ namespace Discord
{
public static class Format
{
private static char[] specialChars = new char[] {'_', '*', '~', '\\' }; //Backslash must always be last!
private static char[] specialChars = new char[] { '_', '*', '~', '\\' }; //Backslash must always be last!

/// <summary> Removes all special formatting characters from the provided text. </summary>
private static string Escape(string text)
@@ -21,27 +21,13 @@ namespace Discord
{
builder.Insert(i, '\\');
length++;
}
}
}
}
}
return builder.ToString();
}
return text;
}

/// <summary> Returns the string used to create a user mention. </summary>
public static string User(User user)
=> $"<@{user.Id}>";
/// <summary> Returns the string used to create a user mention. </summary>
public static string User(string userId)
=> $"<@{userId}>";

/// <summary> Returns the string used to create a channel mention. </summary>
public static string Channel(Channel channel)
=> $"<#{channel.Id}>";
/// <summary> Returns the string used to create a channel mention. </summary>
public static string Channel(string channelId)
=> $"<#{channelId}>";
}

/// <summary> Returns a markdown-formatted string with no formatting, optionally escaping the contents. </summary>
public static string Normal(string text, bool escape = true)


+ 0
- 107
src/Discord.Net/Helpers/AsyncCache.cs View File

@@ -1,107 +0,0 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Discord.Helpers
{
public class AsyncCache<TValue, TModel> : IEnumerable<TValue>
where TValue : class
where TModel : class
{
protected readonly ConcurrentDictionary<string, TValue> _dictionary;
private readonly Func<string, string, TValue> _onCreate;
private readonly Action<TValue, TModel> _onUpdate;
private readonly Action<TValue> _onRemove;

public AsyncCache(Func<string, string, TValue> onCreate, Action<TValue, TModel> onUpdate, Action<TValue> onRemove = null)
{
_dictionary = new ConcurrentDictionary<string, TValue>();
_onCreate = onCreate;
_onUpdate = onUpdate;
_onRemove = onRemove;
}

public TValue this[string key]
{
get
{
if (key == null)
return null;
TValue value = null;
_dictionary.TryGetValue(key, out value);
return value;
}
}

public TValue Add(string key, TValue obj)
{
_dictionary[key] = obj;
return obj;
}
public TValue Remap(string oldKey, string newKey)
{
var obj = Remove(oldKey);
if (obj != null)
Add(newKey, obj);
return obj;
}
public TValue Update(string key, TModel model)
{
return Update(key, null, model);
}
public TValue Update(string key, string parentKey, TModel model)
{
if (key == null)
return null;
while (true)
{
bool isNew;
TValue value;
isNew = !_dictionary.TryGetValue(key, out value);
if (isNew)
value = _onCreate(key, parentKey);
if (model != null)
_onUpdate(value, model);
if (isNew)
{
//If this fails, repeat as an update instead of an add
if (_dictionary.TryAdd(key, value))
return value;
}
else
{
_dictionary[key] = value;
return value;
}
}
}
public TValue Remove(string key)
{
TValue value = null;
if (_dictionary.TryRemove(key, out value))
{
if (_onRemove != null)
_onRemove(value);
return value;
}
else
return null;
}

public void Clear()
{
_dictionary.Clear();
}

public IEnumerator<TValue> GetEnumerator()
{
return _dictionary.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _dictionary.Values.GetEnumerator();
}
}
}

+ 3
- 3
src/Discord.Net/Helpers/Extensions.cs View File

@@ -5,17 +5,17 @@ using System.Threading.Tasks;
namespace Discord.Helpers
{
internal static class Extensions
{
{
public static async Task Wait(this CancellationTokenSource tokenSource)
{
var token = tokenSource.Token;
try { await Task.Delay(-1, token).ConfigureAwait(false); }
catch (OperationCanceledException) { }
catch (OperationCanceledException) { } //Expected
}
public static async Task Wait(this CancellationToken token)
{
try { await Task.Delay(-1, token).ConfigureAwait(false); }
catch (OperationCanceledException) { }
catch (OperationCanceledException) { } //Expected
}
}
}

+ 0
- 14
src/Discord.Net/Helpers/JsonHttpClient.Events.cs View File

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

namespace Discord.Helpers
{
internal partial class JsonHttpClient
{
public event EventHandler<LogMessageEventArgs> OnDebugMessage;
protected void RaiseOnDebugMessage(DebugMessageType type, string message)
{
if (OnDebugMessage != null)
OnDebugMessage(this, new LogMessageEventArgs(type, message));
}
}
}

+ 0
- 207
src/Discord.Net/Helpers/JsonHttpClient.cs View File

@@ -1,207 +0,0 @@
using Discord.API;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Globalization;
using System.Net.Http;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

namespace Discord.Helpers
{
internal partial class JsonHttpClient
{
private bool _isDebug;
private readonly HttpClient _client;
private readonly HttpMethod _patch;
#if TEST_RESPONSES
private readonly JsonSerializerSettings _settings;
#endif

public JsonHttpClient(bool isDebug)
{
_isDebug = isDebug;
_patch = new HttpMethod("PATCH"); //Not sure why this isn't a default...

_client = new HttpClient(new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
UseCookies = false,
PreAuthenticate = false //We do auth ourselves
});
_client.DefaultRequestHeaders.Add("accept", "*/*");
_client.DefaultRequestHeaders.Add("accept-encoding", "gzip, deflate");

string version = typeof(JsonHttpClient).GetTypeInfo().Assembly.GetName().Version.ToString(2);
_client.DefaultRequestHeaders.Add("user-agent", $"Discord.Net/{version} (https://github.com/RogueException/Discord.Net)");

#if TEST_RESPONSES
_settings = new JsonSerializerSettings();
_settings.CheckAdditionalContent = true;
_settings.MissingMemberHandling = MissingMemberHandling.Error;
#endif
}

private string _token;
public string Token
{
get { return _token; }
set
{
_token = value;
_client.DefaultRequestHeaders.Remove("authorization");
if (_token != null)
_client.DefaultRequestHeaders.Add("authorization", _token);
}
}

internal Task<ResponseT> Get<ResponseT>(string path)
where ResponseT : class
=> Send<ResponseT>(HttpMethod.Get, path, null);
internal Task<string> Get(string path)
=> Send(HttpMethod.Get, path, null);
internal Task<ResponseT> Post<ResponseT>(string path, object data)
where ResponseT : class
=> Send<ResponseT>(HttpMethod.Post, path, AsJson(data));
internal Task<string> Post(string path, object data)
=> Send(HttpMethod.Post, path, AsJson(data));
internal Task<ResponseT> Post<ResponseT>(string path)
where ResponseT : class
=> Send<ResponseT>(HttpMethod.Post, path, null);
internal Task<string> Post(string path)
=> Send(HttpMethod.Post, path, null);
internal Task<ResponseT> Put<ResponseT>(string path, object data)
where ResponseT : class
=> Send<ResponseT>(HttpMethod.Put, path, AsJson(data));
internal Task<string> Put(string path, object data)
=> Send(HttpMethod.Put, path, AsJson(data));
internal Task<ResponseT> Put<ResponseT>(string path)
where ResponseT : class
=> Send<ResponseT>(HttpMethod.Put, path, null);
internal Task<string> Put(string path)
=> Send(HttpMethod.Put, path, null);

internal Task<ResponseT> Patch<ResponseT>(string path, object data)
where ResponseT : class
=> Send<ResponseT>(_patch, path, AsJson(data));
internal Task<string> Patch(string path, object data)
=> Send(_patch, path, AsJson(data));
internal Task<ResponseT> Patch<ResponseT>(string path)
where ResponseT : class
=> Send<ResponseT>(_patch, path, null);
internal Task<string> Patch(string path)
=> Send(_patch, path, null);

internal Task<ResponseT> Delete<ResponseT>(string path, object data)
where ResponseT : class
=> Send<ResponseT>(HttpMethod.Delete, path, AsJson(data));
internal Task<string> Delete(string path, object data)
=> Send(HttpMethod.Delete, path, AsJson(data));
internal Task<ResponseT> Delete<ResponseT>(string path)
where ResponseT : class
=> Send<ResponseT>(HttpMethod.Delete, path, null);
internal Task<string> Delete(string path)
=> Send(HttpMethod.Delete, path, null);

internal Task<ResponseT> File<ResponseT>(string path, Stream stream, string filename = null)
where ResponseT : class
=> Send<ResponseT>(HttpMethod.Post, path, AsFormData(stream, filename));
internal Task<string> File(string path, Stream stream, string filename = null)
=> Send(HttpMethod.Post, path, AsFormData(stream, filename));

private async Task<ResponseT> Send<ResponseT>(HttpMethod method, string path, HttpContent content)
where ResponseT : class
{
string responseJson = await SendRequest(method, path, content, true).ConfigureAwait(false);
#if TEST_RESPONSES
if (path.StartsWith(Endpoints.BaseApi))
return JsonConvert.DeserializeObject<ResponseT>(responseJson, _settings);
#endif
return JsonConvert.DeserializeObject<ResponseT>(responseJson);
}
#if TEST_RESPONSES
private async Task<string> Send(HttpMethod method, string path, HttpContent content)
{
string responseJson = await SendRequest(method, path, content, true).ConfigureAwait(false);
if (path.StartsWith(Endpoints.BaseApi) && !string.IsNullOrEmpty(responseJson))
throw new Exception("API check failed: Response is not empty.");
return responseJson;
}
#else
private Task<string> Send(HttpMethod method, string path, HttpContent content)
=> SendRequest(method, path, content, false);
#endif

private async Task<string> SendRequest(HttpMethod method, string path, HttpContent content, bool hasResponse)
{
Stopwatch stopwatch = null;
if (_isDebug)
{
if (content != null)
{
if (content is StringContent)
{
string json = await (content as StringContent).ReadAsStringAsync().ConfigureAwait(false);
RaiseOnDebugMessage(DebugMessageType.XHRRawOutput, $"{method} {path}: {json}");
}
else
{
byte[] bytes = await content.ReadAsByteArrayAsync().ConfigureAwait(false);
RaiseOnDebugMessage(DebugMessageType.XHRRawOutput, $"{method} {path}: {bytes.Length} bytes");
}
}
stopwatch = Stopwatch.StartNew();
}

string result;
using (HttpRequestMessage msg = new HttpRequestMessage(method, path))
{
if (content != null)
msg.Content = content;

HttpResponseMessage response;
if (hasResponse)
{
response = await _client.SendAsync(msg, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
throw new HttpException(response.StatusCode);
result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
else
{
#if !NET45
response = await _client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
#else
response = await _client.SendAsync(msg, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false);
#endif
if (!response.IsSuccessStatusCode)
throw new HttpException(response.StatusCode);
result = null;
}
}

if (_isDebug)
{
stopwatch.Stop();
RaiseOnDebugMessage(DebugMessageType.XHRTiming, $"{method} {path}: {Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2)}ms");
}
return result;
}

private StringContent AsJson(object obj)
{
return new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json");
}
private MultipartFormDataContent AsFormData(Stream stream, string filename)
{
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
content.Add(new StreamContent(stream), "file", filename);
return content;
}
}
}

+ 43
- 0
src/Discord.Net/Helpers/MessageCleaner.cs View File

@@ -0,0 +1,43 @@
using System.Text.RegularExpressions;

namespace Discord.Helpers
{
//TODO: Better name please?
internal class MessageCleaner
{
private readonly Regex _userRegex, _channelRegex;
private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator;

public MessageCleaner(DiscordClient client)
{
_userRegex = new Regex(@"<@\d+?>", RegexOptions.Compiled);
_userRegexEvaluator = new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var user = client.Users[id];
if (user != null)
return '@' + user.Name;
else //User not found
return e.Value;
});

_channelRegex = new Regex(@"<#\d+?>", RegexOptions.Compiled);
_channelRegexEvaluator = new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var channel = client.Channels[id];
if (channel != null)
return channel.Name;
else //Channel not found
return e.Value;
});
}

public string Clean(string text)
{
text = _userRegex.Replace(text, _userRegexEvaluator);
text = _channelRegex.Replace(text, _channelRegexEvaluator);
return text;
}
}
}

+ 2
- 2
src/Discord.Net/Helpers/TaskHelper.cs View File

@@ -2,8 +2,8 @@

namespace Discord.Helpers
{
internal static class TaskHelper
{
internal static class TaskHelper
{
public static Task CompletedTask { get; }
static TaskHelper()
{


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

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Discord
{
public static class Mention
{
/// <summary> Returns the string used to create a user mention. </summary>
public static string User(User user)
=> $"<@{user.Id}>";
/// <summary> Returns the string used to create a user mention. </summary>
public static string User(string userId)
=> $"<@{userId}>";

/// <summary> Returns the string used to create a channel mention. </summary>
public static string Channel(Channel channel)
=> $"<#{channel.Id}>";
/// <summary> Returns the string used to create a channel mention. </summary>
public static string Channel(string channelId)
=> $"<#{channelId}>";
}
}

+ 54
- 14
src/Discord.Net/Models/Channel.cs View File

@@ -1,10 +1,12 @@
using Newtonsoft.Json;
using Discord.Net.API;
using Newtonsoft.Json;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Discord
{
public sealed class Channel
public sealed class Channel
{
public sealed class PermissionOverwrite
{
@@ -15,18 +17,19 @@ namespace Discord
}

private readonly DiscordClient _client;
private ConcurrentDictionary<string, bool> _messages;

/// <summary> Returns the unique identifier for this channel. </summary>
public string Id { get; }

private string _name;
/// <summary> Returns the name of this channel. </summary>
public string Name { get { return !IsPrivate ? $"{_name}" : $"@{Recipient.Name}"; } internal set { _name = value; } }
public string Name { get { return !IsPrivate ? $"{_name}" : $"@{Recipient.Name}"; } internal set { _name = value; } }

/// <summary> Returns the position of this channel in the channel list for this server. </summary>
public int Position { get; internal set; }
/// <summary> Returns false is this is a public chat and true if this is a private chat with another user (see Recipient). </summary>
public bool IsPrivate { get; }
public bool IsPrivate => ServerId == null;
/// <summary> Returns the type of this channel (see ChannelTypes). </summary>
public string Type { get; internal set; }

@@ -34,33 +37,70 @@ namespace Discord
public string ServerId { get; }
/// <summary> Returns the server containing this channel. </summary>
[JsonIgnore]
public Server Server => ServerId != null ? _client.GetServer(ServerId) : null;
public Server Server => _client.Servers[ServerId];

/// For private chats, returns the Id of the target user, otherwise null.
[JsonIgnore]
public string RecipientId { get; internal set; }
/// For private chats, returns the target user, otherwise null.
public User Recipient => _client.GetUser(RecipientId);
[JsonIgnore]
public User Recipient => _client.Users[RecipientId];

/// <summary> Returns a collection of all messages the client has in cache. </summary>
/// <summary> Returns a collection of the ids of all messages the client has seen posted in this channel. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
[JsonIgnore]
public IEnumerable<string> MessageIds => _messages.Select(x => x.Key);
/// <summary> Returns a collection of all messages the client has seen posted in this channel. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
[JsonIgnore]
public IEnumerable<Message> Messages => _client.Messages.Where(x => x.ChannelId == Id);
public IEnumerable<Message> Messages => _messages.Select(x => _client.Messages[x.Key]);

/// <summary> Returns a collection of all custom permissions used for this channel. </summary>
public PermissionOverwrite[] PermissionOverwrites { get; internal set; }

internal Channel(string id, string serverId, DiscordClient client)
internal Channel(DiscordClient client, string id, string serverId, string recipientId)
{
_client = client;
Id = id;
ServerId = serverId;
IsPrivate = serverId == null;
_client = client;
RecipientId = recipientId;
_messages = new ConcurrentDictionary<string, bool>();
}

public override string ToString()
internal void Update(ChannelReference model)
{
Name = model.Name;
Type = model.Type;
}
internal void Update(ChannelInfo model)
{
Update(model as ChannelReference);
Position = model.Position;

if (model.PermissionOverwrites != null)
{
PermissionOverwrites = model.PermissionOverwrites.Select(x => new PermissionOverwrite
{
Type = x.Type,
Id = x.Id,
Deny = new PackedPermissions(true, x.Deny),
Allow = new PackedPermissions(true, x.Allow)
}).ToArray();
}
else
PermissionOverwrites = null;
}

public override string ToString() => Name;

internal void AddMessage(string messageId)
{
_messages.TryAdd(messageId, true);
}
internal bool RemoveMessage(string messageId)
{
return Name;
bool ignored;
return _messages.TryRemove(messageId, out ignored);
}
}
}

+ 30
- 9
src/Discord.Net/Models/Invite.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Discord.Net.API;
using Newtonsoft.Json;

namespace Discord
{
@@ -8,7 +9,7 @@ namespace Discord

/// <summary> Returns the unique identifier for this invite. </summary>
public string Id { get; }
/// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary>
public int MaxAge { get; internal set; }
/// <summary> The amount of times this invite has been used. </summary>
@@ -23,31 +24,51 @@ namespace Discord
public string XkcdPass { get; }

/// <summary> Returns a URL for this invite using XkcdPass if available or Id if not. </summary>
public string Url => API.Endpoints.InviteUrl(XkcdPass ?? Id);
public string Url => Endpoints.InviteUrl(XkcdPass ?? Id);

/// <summary> Returns the id of the user that created this invite. </summary>
public string InviterId { get; internal set; }
/// <summary> Returns the user that created this invite. </summary>
[JsonIgnore]
public User Inviter => _client.GetUser(InviterId);
public User Inviter => _client.Users[InviterId];

/// <summary> Returns the id of the server this invite is to. </summary>
public string ServerId { get; internal set; }
/// <summary> Returns the server this invite is to. </summary>
[JsonIgnore]
public Server Server => _client.GetServer(ServerId);
public Server Server => _client.Servers[ServerId];

/// <summary> Returns the id of the channel this invite is to. </summary>
public string ChannelId { get; internal set; }
/// <summary> Returns the channel this invite is to. </summary>
[JsonIgnore]
public Channel Channel => _client.GetChannel(ChannelId);
public Channel Channel => _client.Channels[ChannelId];

internal Invite(string code, string xkcdPass, DiscordClient client)
internal Invite(DiscordClient client, string code, string xkcdPass, string serverId)
{
_client = client;
Id = code;
XkcdPass = xkcdPass;
_client = client;
}
ServerId = serverId;
}

public override string ToString() => XkcdPass ?? Id;

internal void Update(Net.API.Invite model)
{
ChannelId = model.Channel.Id;
InviterId = model.Inviter.Id;
ServerId = model.Guild.Id;
}

internal void Update(Net.API.ExtendedInvite model)
{
Update(model as Net.API.Invite);
IsRevoked = model.IsRevoked;
IsTemporary = model.IsTemporary;
MaxAge = model.MaxAge;
MaxUses = model.MaxUses;
Uses = model.Uses;
}
}
}

+ 53
- 11
src/Discord.Net/Models/Member.cs View File

@@ -1,4 +1,4 @@
using Discord.API.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -24,23 +24,65 @@ namespace Discord
/// <summary> Returns the current status for this user. </summary>
public string Status { get; internal set; }

public string ServerId { get; }
public Server Server => _client.GetServer(ServerId);

public string UserId { get; }
public User User => _client.GetUser(UserId);
[JsonIgnore]
public User User => _client.Users[UserId];

public string ServerId { get; }
[JsonIgnore]
public Server Server => _client.Servers[ServerId];

public string VoiceChannelId { get; internal set; }
public Channel VoiceChannel => _client.GetChannel(VoiceChannelId);
[JsonIgnore]
public Channel VoiceChannel => _client.Channels[VoiceChannelId];

public string[] RoleIds { get; internal set; }
public IEnumerable<Role> Roles => RoleIds.Select(x => _client.GetRole(x));
[JsonIgnore]
public IEnumerable<Role> Roles => RoleIds.Select(x => _client.Roles[x]);

/// <summary> Returns a collection of all messages this user has sent on this server that are still in cache. </summary>
public IEnumerable<Message> Messages => _client.Messages.Where(x => x.UserId == UserId && x.ServerId == ServerId);

public Member(string serverId, string userId, DiscordClient client)
internal Member(DiscordClient client, string userId, string serverId)
{
ServerId = serverId;
UserId = userId;
_client = client;
UserId = userId;
ServerId = serverId;
}

public override string ToString() => UserId;

internal void Update(Net.API.MemberInfo model)
{
RoleIds = model.Roles;
if (model.JoinedAt.HasValue)
JoinedAt = model.JoinedAt.Value;
}
internal void Update(Net.API.ExtendedMemberInfo model)
{
Update(model as Net.API.MemberInfo);
IsDeafened = model.IsDeafened;
IsMuted = model.IsMuted;
}
internal void Update(Net.API.PresenceMemberInfo model)
{
Status = model.Status;
GameId = model.GameId;
}
internal void Update(Net.API.VoiceMemberInfo model)
{
IsDeafened = model.IsDeafened;
IsMuted = model.IsMuted;
SessionId = model.SessionId;
Token = model.Token;

VoiceChannelId = model.ChannelId;
if (model.IsSelfDeafened.HasValue)
IsSelfDeafened = model.IsSelfDeafened.Value;
if (model.IsSelfMuted.HasValue)
IsSelfMuted = model.IsSelfMuted.Value;
if (model.IsSuppressed.HasValue)
IsSuppressed = model.IsSuppressed.Value;
}
}
}
}

+ 83
- 15
src/Discord.Net/Models/Message.cs View File

@@ -54,7 +54,7 @@ namespace Discord

private readonly DiscordClient _client;
private string _cleanText;
/// <summary> Returns the global unique identifier for this message. </summary>
public string Id { get; internal set; }
/// <summary> Returns the local unique identifier for this message. </summary>
@@ -75,7 +75,7 @@ namespace Discord
public string RawText { get; internal set; }
/// <summary> Returns the content of this message with any special references such as mentions converted. </summary>
/// <remarks> This value is lazy loaded and only processed on first request. Each subsequent request will pull from cache. </remarks>
public string Text => _cleanText != null ? _cleanText : (_cleanText = _client.CleanMessageText(RawText));
public string Text => _cleanText != null ? _cleanText : (_cleanText = _client.Messages.CleanText(RawText));
/// <summary> Returns the timestamp for when this message was sent. </summary>
public DateTime Timestamp { get; internal set; }
/// <summary> Returns the timestamp for when this message was last edited. </summary>
@@ -89,40 +89,108 @@ namespace Discord
public string[] MentionIds { get; internal set; }
/// <summary> Returns a collection of all users mentioned in this message. </summary>
[JsonIgnore]
public IEnumerable<User> Mentions => MentionIds.Select(x => _client.GetUser(x)).Where(x => x != null);
public IEnumerable<User> Mentions => MentionIds.Select(x => _client.Users[x]).Where(x => x != null);

/// <summary> Returns the id of the server containing the channel this message was sent to. </summary>
public string ServerId => Channel.ServerId;
/// <summary> Returns the server containing the channel this message was sent to. </summary>
[JsonIgnore]
public Server Server => _client.GetServer(Channel.ServerId);
public Server Server => _client.Servers[Channel.ServerId];

/// <summary> Returns the id of the channel this message was sent to. </summary>
public string ChannelId { get; }
/// <summary> Returns the channel this message was sent to. </summary>
[JsonIgnore]
public Channel Channel => _client.GetChannel(ChannelId);
public Channel Channel => _client.Channels[ChannelId];

/// <summary> Returns true if the current user created this message. </summary>
public bool IsAuthor => _client.CurrentUserId == UserId;
/// <summary> Returns the id of the author of this message. </summary>
public string UserId { get; internal set; }
/// <summary> Returns the author of this message. </summary>
[JsonIgnore]
public User User => _client.GetUser(UserId);
public User User => _client.Users[UserId];
/// <summary> Returns the author of this message. </summary>
[JsonIgnore]
public Member Member => _client.GetMember(ServerId, UserId);
/// <summary> Returns true if the current user created this message. </summary>
public bool IsAuthor => _client.User?.Id == UserId;
public Member Member => _client.Members[ServerId, UserId];

internal Message(string id, string channelId, DiscordClient client)
internal Message(DiscordClient client, string id, string channelId)
{
_client = client;
Id = id;
ChannelId = channelId;
_client = client;
}
}

public override string ToString()
internal void Update(Net.API.Message model)
{
return User.ToString() + ": " + RawText;
if (model.Attachments != null)
{
Attachments = model.Attachments.Select(x => new Attachment
{
Id = x.Id,
Url = x.Url,
ProxyUrl = x.ProxyUrl,
Size = x.Size,
Filename = x.Filename,
Width = x.Width,
Height = x.Height
}).ToArray();
}
else
Attachments = new Attachment[0];
if (model.Embeds != null)
{
Embeds = model.Embeds.Select(x =>
{
var embed = new Embed
{
Url = x.Url,
Type = x.Type,
Description = x.Description,
Title = x.Title
};
if (x.Provider != null)
{
embed.Provider = new EmbedReference
{
Url = x.Provider.Url,
Name = x.Provider.Name
};
}
if (x.Author != null)
{
embed.Author = new EmbedReference
{
Url = x.Author.Url,
Name = x.Author.Name
};
}
if (x.Thumbnail != null)
{
embed.Thumbnail = new File
{
Url = x.Thumbnail.Url,
ProxyUrl = x.Thumbnail.ProxyUrl,
Width = x.Thumbnail.Width,
Height = x.Thumbnail.Height
};
}
return embed;
}).ToArray();
}
else
Embeds = new Embed[0];
IsMentioningEveryone = model.IsMentioningEveryone;
IsTTS = model.IsTextToSpeech;
MentionIds = model.Mentions?.Select(x => x.Id)?.ToArray() ?? new string[0];
IsMentioningMe = MentionIds.Contains(_client.CurrentUserId);
RawText = model.Content;
Timestamp = model.Timestamp;
EditedTimestamp = model.EditedTimestamp;
if (model.Author != null)
UserId = model.Author.Id;
}
}

public override string ToString() => User.ToString() + ": " + RawText;
}
}

+ 34
- 27
src/Discord.Net/Models/PackedPermissions.cs View File

@@ -2,65 +2,72 @@
{
public sealed class PackedPermissions
{
private bool _isLocked;
private uint _rawValue;
internal uint RawValue { get { return _rawValue; } set { _rawValue = value; } }
public uint RawValue { get { return _rawValue; } internal set { _rawValue = value; } } //Internal set bypasses isLocked for API changes.

internal PackedPermissions() { }
internal PackedPermissions(uint rawValue) { _rawValue = rawValue; }
internal PackedPermissions(bool isLocked) { _isLocked = isLocked; }
internal PackedPermissions(bool isLocked, uint rawValue) { _isLocked = isLocked; _rawValue = rawValue; }

/// <summary> If True, a user may create invites. </summary>
public bool General_CreateInstantInvite => ((_rawValue >> 0) & 0x1) == 1;
public bool General_CreateInstantInvite { get { return GetBit(1); } set { SetBit(1, value); } }
/// <summary> If True, a user may ban users from the server. </summary>
public bool General_BanMembers => ((_rawValue >> 1) & 0x1) == 1;
public bool General_BanMembers { get { return GetBit(2); } set { SetBit(2, value); } }
/// <summary> If True, a user may kick users from the server. </summary>
public bool General_KickMembers => ((_rawValue >> 2) & 0x1) == 1;
/// <summary> If True, a user may adjust roles. This also bypasses all other permissions, granting all the others. </summary>
public bool General_ManageRoles => ((_rawValue >> 3) & 0x1) == 1;
public bool General_KickMembers { get { return GetBit(3); } set { SetBit(3, value); } }
/// <summary> If True, a user may adjust roles. This also implictly grants all other permissions. </summary>
public bool General_ManageRoles { get { return GetBit(4); } set { SetBit(4, value); } }
/// <summary> If True, a user may create, delete and modify channels. </summary>
public bool General_ManageChannels => ((_rawValue >> 4) & 0x1) == 1;
public bool General_ManageChannels { get { return GetBit(5); } set { SetBit(5, value); } }
/// <summary> If True, a user may adjust server properties. </summary>
public bool General_ManageServer => ((_rawValue >> 5) & 0x1) == 1;
public bool General_ManageServer { get { return GetBit(6); } set { SetBit(6, value); } }

//4 Unused

/// <summary> If True, a user may join channels. </summary>
/// <remarks> Note that without this permission, a channel is not sent by the server. </remarks>
public bool Text_ReadMessages => ((_rawValue >> 10) & 0x1) == 1;
public bool Text_ReadMessages { get { return GetBit(11); } set { SetBit(11, value); } }
/// <summary> If True, a user may send messages. </summary>
public bool Text_SendMessages => ((_rawValue >> 11) & 0x1) == 1;
public bool Text_SendMessages { get { return GetBit(12); } set { SetBit(12, value); } }
/// <summary> If True, a user may send text-to-speech messages. </summary>
public bool Text_SendTTSMessages => ((_rawValue >> 12) & 0x1) == 1;
public bool Text_SendTTSMessages { get { return GetBit(13); } set { SetBit(13, value); } }
/// <summary> If True, a user may delete messages. </summary>
public bool Text_ManageMessages => ((_rawValue >> 13) & 0x1) == 1;
public bool Text_ManageMessages { get { return GetBit(14); } set { SetBit(14, value); } }
/// <summary> If True, Discord will auto-embed links sent by this user. </summary>
public bool Text_EmbedLinks => ((_rawValue >> 14) & 0x1) == 1;
public bool Text_EmbedLinks { get { return GetBit(15); } set { SetBit(15, value); } }
/// <summary> If True, a user may send files. </summary>
public bool Text_AttachFiles => ((_rawValue >> 15) & 0x1) == 1;
public bool Text_AttachFiles { get { return GetBit(16); } set { SetBit(16, value); } }
/// <summary> If True, a user may read previous messages. </summary>
public bool Text_ReadMessageHistory => ((_rawValue >> 16) & 0x1) == 1;
public bool Text_ReadMessageHistory { get { return GetBit(17); } set { SetBit(17, value); } }
/// <summary> If True, a user may mention @everyone. </summary>
public bool Text_MentionEveryone => ((_rawValue >> 17) & 0x1) == 1;
public bool Text_MentionEveryone { get { return GetBit(18); } set { SetBit(18, value); } }

//2 Unused

/// <summary> If True, a user may connect to a voice channel. </summary>
public bool Voice_Connect => ((_rawValue >> 20) & 0x1) == 1;
public bool Voice_Connect { get { return GetBit(21); } set { SetBit(21, value); } }
/// <summary> If True, a user may speak in a voice channel. </summary>
public bool Voice_Speak => ((_rawValue >> 21) & 0x1) == 1;
public bool Voice_Speak { get { return GetBit(22); } set { SetBit(22, value); } }
/// <summary> If True, a user may mute users. </summary>
public bool Voice_MuteMembers => ((_rawValue >> 22) & 0x1) == 1;
public bool Voice_MuteMembers { get { return GetBit(23); } set { SetBit(23, value); } }
/// <summary> If True, a user may deafen users. </summary>
public bool Voice_DeafenMembers => ((_rawValue >> 23) & 0x1) == 1;
public bool Voice_DeafenMembers { get { return GetBit(24); } set { SetBit(24, value); } }
/// <summary> If True, a user may move other users between voice channels. </summary>
public bool Voice_MoveMembers => ((_rawValue >> 24) & 0x1) == 1;
public bool Voice_MoveMembers { get { return GetBit(25); } set { SetBit(25, value); } }
/// <summary> If True, a user may use voice activation rather than push-to-talk. </summary>
public bool Voice_UseVoiceActivation => ((_rawValue >> 25) & 0x1) == 1;
public bool Voice_UseVoiceActivation { get { return GetBit(26); } set { SetBit(26, value); } }

//6 Unused

public static implicit operator uint (PackedPermissions perms)
private bool GetBit(int pos) => ((_rawValue >> (pos - 1)) & 1U) == 1;
private void SetBit(int pos, bool value)
{
return perms._rawValue;
if (value)
_rawValue &= (1U << (pos - 1));
else
_rawValue |= ~(1U << (pos - 1));
}

public static implicit operator uint (PackedPermissions perms) => perms._rawValue;
public PackedPermissions Edit() => new PackedPermissions(false, _rawValue);
}
}

+ 10
- 6
src/Discord.Net/Models/Role.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System.Threading;

namespace Discord
{
@@ -18,19 +19,22 @@ namespace Discord
public string ServerId { get; }
/// <summary> Returns the server this role is a member of. </summary>
[JsonIgnore]
public Server Server => _client.GetServer(ServerId);
public Server Server => _client.Servers[ServerId];

internal Role(string id, string serverId, DiscordClient client)
internal Role(DiscordClient client, string id, string serverId)
{
Permissions = new PackedPermissions();
_client = client;
Id = id;
ServerId = serverId;
_client = client;
Permissions = new PackedPermissions(true);
}

public override string ToString()
internal void Update(Net.API.RoleInfo model)
{
return Name;
Name = model.Name;
Permissions.RawValue = (uint)model.Permissions;
}

public override string ToString() => Name;
}
}

+ 138
- 85
src/Discord.Net/Models/Server.cs View File

@@ -1,4 +1,5 @@
using Discord.Helpers;
using Discord.Net.API;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -9,6 +10,7 @@ namespace Discord
public sealed class Server
{
private readonly DiscordClient _client;
private ConcurrentDictionary<string, bool> _bans, _channels, _invites, _members, _roles;

/// <summary> Returns the unique identifier for this server. </summary>
public string Id { get; }
@@ -24,116 +26,128 @@ namespace Discord
/// <summary> Returns the endpoint for this server's voice server. </summary>
internal string VoiceServer { get; set; }

/// <summary> Returns true if the current user created this server. </summary>
public bool IsOwner => _client.CurrentUserId == OwnerId;
/// <summary> Returns the id of the user that first created this server. </summary>
public string OwnerId { get; internal set; }
/// <summary> Returns the user that first created this server. </summary>
public User Owner => _client.GetUser(OwnerId);
/// <summary> Returns true if the current user created this server. </summary>
public bool IsOwner => _client.User?.Id == OwnerId;
[JsonIgnore]
public User Owner => _client.Users[OwnerId];

/// <summary> Returns the id of the AFK voice channel for this server (see AFKTimeout). </summary>
public string AFKChannelId { get; internal set; }
/// <summary> Returns the AFK voice channel for this server (see AFKTimeout). </summary>
public Channel AFKChannel => _client.GetChannel(AFKChannelId);
[JsonIgnore]
public Channel AFKChannel => _client.Channels[AFKChannelId];

/// <summary> Returns the id of the default channel for this server. </summary>
public string DefaultChannelId => Id;
/// <summary> Returns the default channel for this server. </summary>
public Channel DefaultChannel =>_client.GetChannel(DefaultChannelId);

internal AsyncCache<Member, API.Models.MemberInfo> _members;
/// <summary> Returns a collection of all channels within this server. </summary>
public IEnumerable<Member> Members => _members;

internal ConcurrentDictionary<string, bool> _bans;
/// <summary> Returns a collection of all users banned on this server. </summary>
/// <remarks> Only users seen in other servers will be returned. To get a list of all users, use BanIds. </remarks>
public IEnumerable<User> Bans => _bans.Keys.Select(x => _client.GetUser(x));
[JsonIgnore]
public Channel DefaultChannel => _client.Channels[DefaultChannelId];
/// <summary> Returns a collection of the ids of all users banned on this server. </summary>
public IEnumerable<string> BanIds => _bans.Keys;
[JsonIgnore]
public IEnumerable<string> BanIds => _bans.Select(x => x.Key);

/// <summary> Returns a collection of the ids of all channels within this server. </summary>
[JsonIgnore]
public IEnumerable<string> ChannelIds => _channels.Select(x => x.Key);
/// <summary> Returns a collection of all channels within this server. </summary>
public IEnumerable<Channel> Channels => _client.Channels.Where(x => x.ServerId == Id);
[JsonIgnore]
public IEnumerable<Channel> Channels => _channels.Select(x => _client.Channels[x.Key]);
/// <summary> Returns a collection of all channels within this server. </summary>
public IEnumerable<Channel> TextChannels => _client.Channels.Where(x => x.ServerId == Id && x.Type == ChannelTypes.Text);
[JsonIgnore]
public IEnumerable<Channel> TextChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelTypes.Text);
/// <summary> Returns a collection of all channels within this server. </summary>
public IEnumerable<Channel> VoiceChannels => _client.Channels.Where(x => x.ServerId == Id && x.Type == ChannelTypes.Voice);
[JsonIgnore]
public IEnumerable<Channel> VoiceChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelTypes.Voice);
/// <summary> Returns a collection of all invite codes to this server. </summary>
[JsonIgnore]
public IEnumerable<string> InviteCodes => _invites.Select(x => x.Key);
/*/// <summary> Returns a collection of all invites to this server. </summary>
[JsonIgnore]
public IEnumerable<Invite> Invites => _invites.Select(x => _client.Invites[x.Key]);*/

/// <summary> Returns a collection of all users within this server with their server-specific data. </summary>
[JsonIgnore]
public IEnumerable<Member> Members => _members.Select(x => _client.Members[x.Key, Id]);
/// <summary> Returns a collection of the ids of all users within this server. </summary>
[JsonIgnore]
public IEnumerable<string> UserIds => _members.Select(x => x.Key);
/// <summary> Returns a collection of all users within this server. </summary>
[JsonIgnore]
public IEnumerable<User> Users => _members.Select(x => _client.Users[x.Key]);

/// <summary> Returns a collection of the ids of all roles within this server. </summary>
[JsonIgnore]
public IEnumerable<string> RoleIds => _roles.Select(x => x.Key);
/// <summary> Returns a collection of all roles within this server. </summary>
public IEnumerable<Role> Roles => _client.Roles.Where(x => x.ServerId == Id);
[JsonIgnore]
public IEnumerable<Role> Roles => _roles.Select(x => _client.Roles[x.Key]);

internal Server(string id, DiscordClient client)
internal Server(DiscordClient client, string id)
{
Id = id;
_client = client;
Id = id;
_bans = new ConcurrentDictionary<string, bool>();
_members = new AsyncCache<Member, API.Models.MemberInfo>(
(key, parentKey) =>
{
if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key} in server {parentKey}.");
return new Member(parentKey, key, _client);
},
(member, model) =>
{
if (model is API.Models.PresenceMemberInfo)
{
var extendedModel = model as API.Models.PresenceMemberInfo;
member.Status = extendedModel.Status;
member.GameId = extendedModel.GameId;
}
if (model is API.Models.VoiceMemberInfo)
{
var extendedModel = model as API.Models.VoiceMemberInfo;
member.VoiceChannelId = extendedModel.ChannelId;
member.IsDeafened = extendedModel.IsDeafened;
member.IsMuted = extendedModel.IsMuted;
if (extendedModel.IsSelfDeafened.HasValue)
member.IsSelfDeafened = extendedModel.IsSelfDeafened.Value;
if (extendedModel.IsSelfMuted.HasValue)
member.IsSelfMuted = extendedModel.IsSelfMuted.Value;
member.IsSuppressed = extendedModel.IsSuppressed;
member.SessionId = extendedModel.SessionId;
member.Token = extendedModel.Token;
}
if (model is API.Models.RoleMemberInfo)
{
var extendedModel = model as API.Models.RoleMemberInfo;
member.RoleIds = extendedModel.Roles;
if (extendedModel.JoinedAt.HasValue)
member.JoinedAt = extendedModel.JoinedAt.Value;
}
if (model is API.Models.InitialMemberInfo)
{
var extendedModel = model as API.Models.InitialMemberInfo;
member.IsDeafened = extendedModel.IsDeafened;
member.IsMuted = extendedModel.IsMuted;
}
if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId}).");
},
(member) =>
{
if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId}).");
}
);
_channels = new ConcurrentDictionary<string, bool>();
_invites = new ConcurrentDictionary<string, bool>();
_members = new ConcurrentDictionary<string, bool>();
_roles = new ConcurrentDictionary<string, bool>();
}

internal Member UpdateMember(API.Models.MemberInfo membership)
internal void Update(GuildInfo model)
{
return _members.Update(membership.User?.Id ?? membership.UserId, Id, membership);
}
internal Member RemoveMember(string userId)
{
return _members.Remove(userId);
AFKChannelId = model.AFKChannelId;
AFKTimeout = model.AFKTimeout;
if (model.JoinedAt.HasValue)
JoinedAt = model.JoinedAt.Value;
OwnerId = model.OwnerId;
Region = model.Region;

var roles = _client.Roles;
foreach (var subModel in model.Roles)
{
var role = roles.GetOrAdd(subModel.Id, Id);
role.Update(subModel);
}
}
public Member GetMembership(User user)
=> GetMember(user.Id);
public Member GetMember(string userId)
internal void Update(ExtendedGuildInfo model)
{
return _members[userId];
Update(model as GuildInfo);

var channels = _client.Channels;
foreach (var subModel in model.Channels)
{
var channel = channels.GetOrAdd(subModel.Id, Id);
channel.Update(subModel);
}

var users = _client.Users;
var members = _client.Members;
foreach (var subModel in model.Members)
{
var user = users.GetOrAdd(subModel.UserId);
var member = members.GetOrAdd(subModel.UserId, Id);
user.Update(subModel.User);
member.Update(subModel);
}
foreach (var subModel in model.VoiceStates)
{
var member = members.GetOrAdd(subModel.UserId, Id);
member.Update(subModel);
}
foreach (var subModel in model.Presences)
{
var member = members.GetOrAdd(subModel.UserId, Id);
member.Update(subModel);
}
}

public override string ToString() => Name;

internal void AddBan(string banId)
{
_bans.TryAdd(banId, true);
@@ -144,9 +158,48 @@ namespace Discord
return _bans.TryRemove(banId, out ignored);
}

public override string ToString()
internal void AddChannel(string channelId)
{
_channels.TryAdd(channelId, true);
}
internal bool RemoveChannel(string channelId)
{
bool ignored;
return _channels.TryRemove(channelId, out ignored);
}

internal void AddInvite(string inviteId)
{
_invites.TryAdd(inviteId, true);
}
internal bool RemoveInvite(string inviteId)
{
bool ignored;
return _invites.TryRemove(inviteId, out ignored);
}

internal void AddMember(string userId)
{
_members.TryAdd(userId, true);
}
internal bool RemoveMember(string userId)
{
return Name;
bool ignored;
return _members.TryRemove(userId, out ignored);
}
internal bool HasMember(string userId)
{
return _members.ContainsKey(userId);
}

internal void AddRole(string roleId)
{
_roles.TryAdd(roleId, true);
}
internal bool RemoveRole(string roleId)
{
bool ignored;
return _roles.TryRemove(roleId, out ignored);
}
}
}

+ 38
- 14
src/Discord.Net/Models/User.cs View File

@@ -1,14 +1,16 @@
using Discord.API;
using Discord.Net.API;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace Discord
{
public sealed class User
{
private readonly DiscordClient _client;
private int _refs;

/// <summary> Returns the unique identifier for this user. </summary>
public string Id { get; }
@@ -21,37 +23,52 @@ namespace Discord
public string AvatarUrl => Endpoints.UserAvatar(Id, AvatarId);
/// <summary> Returns a by-name unique identifier separating this user from others with the same name. </summary>
public string Discriminator { get; internal set; }
[JsonIgnore]
/// <summary> Returns the email for this user. </summary>
/// <remarks> This field is only ever populated for the current logged in user. </remarks>
public string Email { get; internal set; }
[JsonIgnore]
public string Email { get; internal set; }
/// <summary> Returns if the email for this user has been verified. </summary>
/// <remarks> This field is only ever populated for the current logged in user. </remarks>
public bool IsVerified { get; internal set; }
[JsonIgnore]
public bool? IsVerified { get; internal set; }

/// <summary> Returns the Id of the private messaging channel with this user, if one exists. </summary>
public string PrivateChannelId { get; set; }
/// <summary> Returns the private messaging channel with this user, if one exists. </summary>
public Channel PrivateChannel => _client.GetChannel(PrivateChannelId);
[JsonIgnore]
public Channel PrivateChannel => _client.Channels[PrivateChannelId];

/// <summary> Returns a collection of all server-specific data for every server this user is a member of. </summary>
public IEnumerable<Member> Memberships => _client._servers.Select(x => x._members[Id]).Where(x => x != null);
public IEnumerable<Member> Memberships => _client.Servers.Where(x => x.HasMember(Id)).Select(x => _client.Members[Id, x?.Id]);
/// <summary> Returns a collection of all servers this user is a member of. </summary>
public IEnumerable<Server> Servers => _client._servers.Where(x => x._members[Id] != null);
public IEnumerable<Server> Servers => _client.Servers.Where(x => x.HasMember(Id));
/// <summary> Returns a collection of all messages this user has sent that are still in cache. </summary>
public IEnumerable<Message> Messages => _client.Messages.Where(x => x.UserId == Id);

//TODO: Add voice triggering lastactivity
//TODO: Add voice triggering LastActivity
/// <summary> Returns the time this user last sent a message. </summary>
/// <remarks> Is not currently affected by voice activity </remarks>
public DateTime LastActivity { get; private set; }

internal User(string id, DiscordClient client)
internal User(DiscordClient client, string id)
{
Id = id;
_client = client;
Id = id;
LastActivity = DateTime.UtcNow;
IsVerified = true;
}
}

internal void Update(UserReference model)
{
AvatarId = model.Avatar;
Discriminator = model.Discriminator;
Name = model.Username;
}
internal void Update(SelfUserInfo model)
{
Update(model as UserReference);
Email = model.Email;
IsVerified = model.IsVerified;
}

internal void UpdateActivity(DateTime activity)
{
@@ -59,9 +76,16 @@ namespace Discord
LastActivity = activity;
}

public override string ToString()
public override string ToString() => Name;
public void AddRef()
{
Interlocked.Increment(ref _refs);
}
public void RemoveRef()
{
return Name;
if (Interlocked.Decrement(ref _refs) == 0)
_client.Users.TryRemove(Id);
}
}
}

src/Discord.Net/API/Models/Common.cs → src/Discord.Net/Net/API/Common.cs View File

@@ -5,9 +5,9 @@
using Newtonsoft.Json;
using System;

namespace Discord.API.Models
namespace Discord.Net.API
{
//Users
//User
internal class UserReference
{
[JsonProperty(PropertyName = "username")]
@@ -26,35 +26,44 @@ namespace Discord.API.Models
[JsonProperty(PropertyName = "verified")]
public bool IsVerified;
}
internal class MemberInfo

//Members
internal class MemberReference
{
[JsonProperty(PropertyName = "user_id")]
public string UserId;
[JsonProperty(PropertyName = "user")]
public UserReference User;
[JsonProperty(PropertyName = "guild_id")]
public string ServerId;
public string GuildId;
}
internal class MemberInfo : MemberReference
{
[JsonProperty(PropertyName = "joined_at")]
public DateTime? JoinedAt;
[JsonProperty(PropertyName = "roles")]
public string[] Roles;
}
internal class InitialMemberInfo : RoleMemberInfo
internal class ExtendedMemberInfo : MemberInfo
{
[JsonProperty(PropertyName = "mute")]
public bool IsMuted;
[JsonProperty(PropertyName = "deaf")]
public bool IsDeafened;
}
internal class PresenceMemberInfo : MemberInfo
internal class PresenceMemberInfo : MemberReference
{
[JsonProperty(PropertyName = "game_id")]
public string GameId;
[JsonProperty(PropertyName = "status")]
public string Status;
}
internal class VoiceMemberInfo : MemberInfo
internal class VoiceMemberInfo : MemberReference
{
[JsonProperty(PropertyName = "channel_id")]
public string ChannelId;
[JsonProperty(PropertyName = "suppress")]
public bool IsSuppressed;
public bool? IsSuppressed;
[JsonProperty(PropertyName = "session_id")]
public string SessionId;
[JsonProperty(PropertyName = "self_mute")]
@@ -68,13 +77,6 @@ namespace Discord.API.Models
[JsonProperty(PropertyName = "token")]
public string Token;
}
internal class RoleMemberInfo : MemberInfo
{
[JsonProperty(PropertyName = "joined_at")]
public DateTime? JoinedAt;
[JsonProperty(PropertyName = "roles")]
public string[] Roles;
}

//Channels
internal class ChannelReference
@@ -114,15 +116,15 @@ namespace Discord.API.Models
public UserReference Recipient;
}

//Servers
internal class ServerReference
//Guilds (Servers)
internal class GuildReference
{
[JsonProperty(PropertyName = "id")]
public string Id;
[JsonProperty(PropertyName = "name")]
public string Name;
}
internal class ServerInfo : ServerReference
internal class GuildInfo : GuildReference
{
[JsonProperty(PropertyName = "afk_channel_id")]
public string AFKChannelId;
@@ -141,14 +143,14 @@ namespace Discord.API.Models
[JsonProperty(PropertyName = "region")]
public string Region;
[JsonProperty(PropertyName = "roles")]
public Role[] Roles;
public RoleInfo[] Roles;
}
internal class ExtendedServerInfo : ServerInfo
internal class ExtendedGuildInfo : GuildInfo
{
[JsonProperty(PropertyName = "channels")]
public ChannelInfo[] Channels;
[JsonProperty(PropertyName = "members")]
public InitialMemberInfo[] Members;
public ExtendedMemberInfo[] Members;
[JsonProperty(PropertyName = "presences")]
public PresenceMemberInfo[] Presences;
[JsonProperty(PropertyName = "voice_states")]
@@ -244,7 +246,14 @@ namespace Discord.API.Models
}

//Roles
internal class Role
internal class RoleReference
{
[JsonProperty(PropertyName = "guild_id")]
public string GuildId;
[JsonProperty(PropertyName = "role_id")]
public string RoleId;
}
internal class RoleInfo
{
[JsonProperty(PropertyName = "permissions")]
public int Permissions;
@@ -253,4 +262,34 @@ namespace Discord.API.Models
[JsonProperty(PropertyName = "id")]
public string Id;
}

//Invites
internal class Invite
{
[JsonProperty(PropertyName = "inviter")]
public UserReference Inviter;
[JsonProperty(PropertyName = "guild")]
public GuildReference Guild;
[JsonProperty(PropertyName = "channel")]
public ChannelReference Channel;
[JsonProperty(PropertyName = "code")]
public string Code;
[JsonProperty(PropertyName = "xkcdpass")]
public string XkcdPass;
}
internal class ExtendedInvite : Invite
{
[JsonProperty(PropertyName = "max_age")]
public int MaxAge;
[JsonProperty(PropertyName = "max_uses")]
public int MaxUses;
[JsonProperty(PropertyName = "revoked")]
public bool IsRevoked;
[JsonProperty(PropertyName = "temporary")]
public bool IsTemporary;
[JsonProperty(PropertyName = "uses")]
public int Uses;
[JsonProperty(PropertyName = "created_at")]
public DateTime CreatedAt;
}
}

+ 158
- 0
src/Discord.Net/Net/API/DiscordAPIClient.cs View File

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

namespace Discord.Net.API
{
internal class DiscordAPIClient
{
public const int MaxMessageSize = 2000;

public RestClient RestClient => _rest;
private readonly RestClient _rest;

public DiscordAPIClient(LogMessageSeverity logLevel)
{
_rest = new RestClient(logLevel);
}

private string _token;
public string Token
{
get { return _token; }
set { _token = value; _rest.SetToken(value); }
}

//Auth
public Task<Responses.Gateway> GetWebSocketEndpoint()
=> _rest.Get<Responses.Gateway>(Endpoints.Gateway);
public async Task<Responses.AuthRegister> LoginAnonymous(string username)
{
var fingerprintResponse = await _rest.Post<Responses.AuthFingerprint>(Endpoints.AuthFingerprint).ConfigureAwait(false);
var registerRequest = new Requests.AuthRegister { Fingerprint = fingerprintResponse.Fingerprint, Username = username };
var registerResponse = await _rest.Post<Responses.AuthRegister>(Endpoints.AuthRegister, registerRequest).ConfigureAwait(false);
return registerResponse;
}
public async Task<Responses.AuthLogin> Login(string email, string password)
{
var request = new Requests.AuthLogin { Email = email, Password = password };
var response = await _rest.Post<Responses.AuthLogin>(Endpoints.AuthLogin, request).ConfigureAwait(false);
return response;
}
public Task Logout()
=> _rest.Post(Endpoints.AuthLogout);

//Servers
public Task<Responses.CreateServer> CreateServer(string name, string region)
{
var request = new Requests.CreateServer { Name = name, Region = region };
return _rest.Post<Responses.CreateServer>(Endpoints.Servers, request);
}
public Task LeaveServer(string id)
=> _rest.Delete<Responses.DeleteServer>(Endpoints.Server(id));

//Channels
public Task<Responses.CreateChannel> CreateChannel(string serverId, string name, string channelType)
{
var request = new Requests.CreateChannel { Name = name, Type = channelType };
return _rest.Post<Responses.CreateChannel>(Endpoints.ServerChannels(serverId), request);
}
public Task<Responses.CreateChannel> CreatePMChannel(string myId, string recipientId)
{
var request = new Requests.CreatePMChannel { RecipientId = recipientId };
return _rest.Post<Responses.CreateChannel>(Endpoints.UserChannels(myId), request);
}
public Task<Responses.DestroyChannel> DestroyChannel(string channelId)
=> _rest.Delete<Responses.DestroyChannel>(Endpoints.Channel(channelId));
public Task<Responses.GetMessages[]> GetMessages(string channelId, int count)
=> _rest.Get<Responses.GetMessages[]>(Endpoints.ChannelMessages(channelId, count));

//Members
public Task Kick(string serverId, string memberId)
=> _rest.Delete(Endpoints.ServerMember(serverId, memberId));
public Task Ban(string serverId, string memberId)
=> _rest.Put(Endpoints.ServerBan(serverId, memberId));
public Task Unban(string serverId, string memberId)
=> _rest.Delete(Endpoints.ServerBan(serverId, memberId));

//Invites
public Task<Responses.CreateInvite> CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool withXkcdPass)
{
var request = new Requests.CreateInvite { MaxAge = maxAge, MaxUses = maxUses, IsTemporary = isTemporary, WithXkcdPass = withXkcdPass };
return _rest.Post<Responses.CreateInvite>(Endpoints.ChannelInvites(channelId), request);
}
public Task<Responses.GetInvite> GetInvite(string id)
=> _rest.Get<Responses.GetInvite>(Endpoints.Invite(id));
public Task AcceptInvite(string id)
=> _rest.Post<Responses.AcceptInvite>(Endpoints.Invite(id));
public Task DeleteInvite(string id)
=> _rest.Delete(Endpoints.Invite(id));
//Chat
public Task<Responses.SendMessage> SendMessage(string channelId, string message, string[] mentions, string nonce)
{
var request = new Requests.SendMessage { Content = message, Mentions = mentions, Nonce = nonce };
return _rest.Post<Responses.SendMessage>(Endpoints.ChannelMessages(channelId), request);
}
public Task<Responses.EditMessage> EditMessage(string messageId, string channelId, string message, string[] mentions)
{
var request = new Requests.EditMessage { Content = message, Mentions = mentions };
return _rest.Patch<Responses.EditMessage>(Endpoints.ChannelMessage(channelId, messageId), request);
}
public Task SendIsTyping(string channelId)
=> _rest.Post(Endpoints.ChannelTyping(channelId));
public Task DeleteMessage(string channelId, string msgId)
=> _rest.Delete(Endpoints.ChannelMessage(channelId, msgId));
public Task SendFile(string channelId, string filePath)
=> _rest.PostFile<Responses.SendMessage>(Endpoints.ChannelMessages(channelId), filePath);

//Voice
public Task<Responses.GetRegions[]> GetVoiceRegions()
=> _rest.Get<Responses.GetRegions[]>(Endpoints.VoiceRegions);
public Task<Responses.GetIce> GetVoiceIce()
=> _rest.Get<Responses.GetIce>(Endpoints.VoiceIce);
public Task Mute(string serverId, string memberId)
{
var request = new Requests.SetMemberMute { Value = true };
return _rest.Patch(Endpoints.ServerMember(serverId, memberId));
}
public Task Unmute(string serverId, string memberId)
{
var request = new Requests.SetMemberMute { Value = false };
return _rest.Patch(Endpoints.ServerMember(serverId, memberId));
}
public Task Deafen(string serverId, string memberId)
{
var request = new Requests.SetMemberDeaf { Value = true };
return _rest.Patch(Endpoints.ServerMember(serverId, memberId));
}
public Task Undeafen(string serverId, string memberId)
{
var request = new Requests.SetMemberDeaf { Value = false };
return _rest.Patch(Endpoints.ServerMember(serverId, memberId));
}

//Profile
public Task<Responses.ChangeProfile> ChangeUsername(string newUsername, string currentEmail, string currentPassword)
{
var request = new Requests.ChangeUsername { Username = newUsername, CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _rest.Patch<Responses.ChangeProfile>(Endpoints.UserMe, request);
}
public Task<Responses.ChangeProfile> ChangeEmail(string newEmail, string currentPassword)
{
var request = new Requests.ChangeEmail { NewEmail = newEmail, CurrentPassword = currentPassword };
return _rest.Patch<Responses.ChangeProfile>(Endpoints.UserMe, request);
}
public Task<Responses.ChangeProfile> ChangePassword(string newPassword, string currentEmail, string currentPassword)
{
var request = new Requests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _rest.Patch<Responses.ChangeProfile>(Endpoints.UserMe, request);
}
public Task<Responses.ChangeProfile> ChangeAvatar(AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
{
string base64 = Convert.ToBase64String(bytes);
string type = imageType == AvatarImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64";
var request = new Requests.ChangeAvatar { Avatar = $"data:{type},/9j/{base64}", CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _rest.Patch<Responses.ChangeProfile>(Endpoints.UserMe, request);
}
}
}

+ 42
- 0
src/Discord.Net/Net/API/Endpoints.cs View File

@@ -0,0 +1,42 @@
namespace Discord.Net.API
{
internal static class Endpoints
{
public const string BaseApi = "https://discordapp.com/api/";
//public const string Track = "track";
public const string Gateway = "gateway";

public const string Auth = "auth";
public const string AuthFingerprint = "auth/fingerprint";
public const string AuthRegister = "auth/register";
public const string AuthLogin = "auth/login";
public const string AuthLogout = "auth/logout";
public const string Channels = "channels";
public static string Channel(string channelId) => $"channels/{channelId}";
public static string ChannelTyping(string channelId) => $"channels/{channelId}/typing";
public static string ChannelMessages(string channelId) => $"channels/{channelId}/messages";
public static string ChannelMessages(string channelId, int limit) => $"channels/{channelId}/messages?limit={limit}";
public static string ChannelMessage(string channelId, string msgId) => $"channels/{channelId}/messages/{msgId}";
public static string ChannelInvites(string channelId) => $"channels/{channelId}/invites";
public const string Servers = "guilds";
public static string Server(string serverId) => $"guilds/{serverId}";
public static string ServerChannels(string serverId) => $"guilds/{serverId}/channels";
public static string ServerMember(string serverId, string userId) => $"guilds/{serverId}/members/{userId}";
public static string ServerBan(string serverId, string userId) => $"guilds/{serverId}/bans/{userId}";
public const string Invites = "invite";
public static string Invite(string inviteId) => $"invite/{inviteId}";
public static string InviteUrl(string inviteId) => $"https://discord.gg/{inviteId}";

public const string Users = "users";
public static string UserMe => $"users/@me";
public static string UserChannels(string userId) => $"users/{userId}/channels";
public static string UserAvatar(string userId, string avatarId) => $"users/{userId}/avatars/{avatarId}.jpg";
public const string Voice = "voice";
public const string VoiceRegions = "voice/regions";
public const string VoiceIce = "voice/ice";
}
}

src/Discord.Net/API/Models/APIRequests.cs → src/Discord.Net/Net/API/Requests.cs View File

@@ -4,18 +4,19 @@

using Newtonsoft.Json;

namespace Discord.API.Models
namespace Discord.Net.API
{
internal static class APIRequests
internal static class Requests
{
public class AuthRegisterRequest
//Auth
public sealed class AuthRegister
{
[JsonProperty(PropertyName = "fingerprint")]
public string Fingerprint;
[JsonProperty(PropertyName = "username")]
public string Username;
}
public class AuthLogin
public sealed class AuthLogin
{
[JsonProperty(PropertyName = "email")]
public string Email;
@@ -23,7 +24,8 @@ namespace Discord.API.Models
public string Password;
}

public class CreateServer
//Servers
public sealed class CreateServer
{
[JsonProperty(PropertyName = "name")]
public string Name;
@@ -31,20 +33,22 @@ namespace Discord.API.Models
public string Region;
}

public class CreateChannel
//Channels
public sealed class CreateChannel
{
[JsonProperty(PropertyName = "name")]
public string Name;
[JsonProperty(PropertyName = "type")]
public string Type;
}
public class CreatePMChannel
public sealed class CreatePMChannel
{
[JsonProperty(PropertyName = "recipient_id")]
public string RecipientId;
}

public class CreateInvite
//Invites
public sealed class CreateInvite
{
[JsonProperty(PropertyName = "max_age")]
public int MaxAge;
@@ -53,10 +57,11 @@ namespace Discord.API.Models
[JsonProperty(PropertyName = "temporary")]
public bool IsTemporary;
[JsonProperty(PropertyName = "xkcdpass")]
public bool HasXkcdPass;
public bool WithXkcdPass;
}

public class SendMessage
//Messages
public sealed class SendMessage
{
[JsonProperty(PropertyName = "content")]
public string Content;
@@ -65,45 +70,58 @@ namespace Discord.API.Models
[JsonProperty(PropertyName = "nonce")]
public string Nonce;
}
public class EditMessage : SendMessage { }
public sealed class EditMessage
{
[JsonProperty(PropertyName = "content")]
public string Content;
[JsonProperty(PropertyName = "mentions")]
public string[] Mentions;
}

public class SetMemberMute
//Members
public sealed class SetMemberMute
{
[JsonProperty(PropertyName = "mute")]
public bool Mute;
public bool Value;
}
public class SetMemberDeaf
public sealed class SetMemberDeaf
{
[JsonProperty(PropertyName = "deaf")]
public bool Deaf;
public bool Value;
}

public abstract class ChangeProfile
//Profile
public sealed class ChangeUsername
{
[JsonProperty(PropertyName = "email")]
public string CurrentEmail;
[JsonProperty(PropertyName = "password")]
public string CurrentPassword;
}
public class ChangeUsername : ChangeProfile
{
[JsonProperty(PropertyName = "username")]
public string Username;
}
public class ChangeEmail
public sealed class ChangeEmail
{
[JsonProperty(PropertyName = "email")]
public string NewEmail;
[JsonProperty(PropertyName = "password")]
public string CurrentPassword;
}
public class ChangePassword : ChangeProfile
public sealed class ChangePassword
{
[JsonProperty(PropertyName = "email")]
public string CurrentEmail;
[JsonProperty(PropertyName = "password")]
public string CurrentPassword;
[JsonProperty(PropertyName = "new_password")]
public string NewPassword;
}
public class ChangeAvatar : ChangeProfile
public sealed class ChangeAvatar
{
[JsonProperty(PropertyName = "email")]
public string CurrentEmail;
[JsonProperty(PropertyName = "password")]
public string CurrentPassword;
[JsonProperty(PropertyName = "avatar")]
public string Avatar;
}

+ 85
- 0
src/Discord.Net/Net/API/Responses.cs View File

@@ -0,0 +1,85 @@
//Ignore unused/unassigned variable warnings
#pragma warning disable CS0649
#pragma warning disable CS0169

using Newtonsoft.Json;
using System;

namespace Discord.Net.API
{
internal static class Responses
{
//Auth
public sealed class Gateway
{
[JsonProperty(PropertyName = "url")]
public string Url;
}
public sealed class AuthFingerprint
{
[JsonProperty(PropertyName = "fingerprint")]
public string Fingerprint;
}
public sealed class AuthRegister
{
[JsonProperty(PropertyName = "token")]
public string Token;
}
public sealed class AuthLogin
{
[JsonProperty(PropertyName = "token")]
public string Token;
}

//Users
public sealed class ChangeProfile : SelfUserInfo { }

//Servers
public sealed class CreateServer : GuildInfo { }
public sealed class DeleteServer : GuildInfo { }

//Channels
public sealed class CreateChannel : ChannelInfo { }
public sealed class DestroyChannel : ChannelInfo { }

//Invites
public sealed class CreateInvite : ExtendedInvite { }
public sealed class GetInvite : Invite { }
public sealed class AcceptInvite : Invite { }

//Messages
public sealed class SendMessage : Message { }
public sealed class EditMessage : Message { }
public sealed class GetMessages : Message { }

//Voice
public sealed class GetRegions
{
[JsonProperty(PropertyName = "sample_hostname")]
public string Hostname;
[JsonProperty(PropertyName = "sample_port")]
public int Port;
[JsonProperty(PropertyName = "id")]
public string Id;
[JsonProperty(PropertyName = "name")]
public string Name;
}
public sealed class GetIce
{
[JsonProperty(PropertyName = "ttl")]
public string TTL;
[JsonProperty(PropertyName = "servers")]
public Server[] Servers;

public sealed class Server
{
[JsonProperty(PropertyName = "url")]
public string URL;
[JsonProperty(PropertyName = "username")]
public string Username;
[JsonProperty(PropertyName = "credential")]
public string Credential;
}
}
}
}

src/Discord.Net/HttpException.cs → src/Discord.Net/Net/HttpException.cs View File

@@ -1,7 +1,7 @@
using System;
using System.Net;

namespace Discord
namespace Discord.Net
{
public class HttpException : Exception
{
@@ -9,8 +9,8 @@ namespace Discord

public HttpException(HttpStatusCode statusCode)
: base($"The server responded with error {statusCode}")
{
{
StatusCode = statusCode;
}
}
}
}

+ 65
- 0
src/Discord.Net/Net/RestClient.BuiltIn.cs View File

@@ -0,0 +1,65 @@
#if DNXCORE50
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net
{
internal class BuiltInRestEngine : IRestEngine
{
private readonly HttpClient _client;

public BuiltInRestEngine(string userAgent)
{
_client = new HttpClient(new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
UseCookies = false,
PreAuthenticate = false //We do auth ourselves
});
_client.DefaultRequestHeaders.Add("accept", "*/*");
_client.DefaultRequestHeaders.Add("accept-encoding", "gzip,deflate");
_client.DefaultRequestHeaders.Add("user-agent", userAgent);
}

public void SetToken(string token)
{
_client.DefaultRequestHeaders.Remove("authorization");
if (token != null)
_client.DefaultRequestHeaders.Add("authorization", token);
}

public Task<string> Send(HttpMethod method, string path, string json, CancellationToken cancelToken)
{
using (var request = new HttpRequestMessage(method, path))
{
if (json != null)
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
return Send(request, cancelToken);
}
}
public Task<string> SendFile(HttpMethod method, string path, string filePath, CancellationToken cancelToken)
{
using (var request = new HttpRequestMessage(method, path))
{
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
content.Add(new StreamContent(File.OpenRead(filePath)), "file", Path.GetFileName(filePath));
request.Content = content;
return Send(request, cancelToken);
}
}
private async Task<string> Send(HttpRequestMessage request, CancellationToken cancelToken)
{
var response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
throw new HttpException(response.StatusCode);
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
}
}
#endif

+ 30
- 0
src/Discord.Net/Net/RestClient.Events.cs View File

@@ -0,0 +1,30 @@
using System;
using System.Net.Http;

namespace Discord.Net
{
internal partial class RestClient
{
public class RequestEventArgs : EventArgs
{
public HttpMethod Method { get; }
public string Path { get; }
public string Payload { get; }
public double ElapsedMilliseconds { get; }
public RequestEventArgs(HttpMethod method, string path, string payload, double milliseconds)
{
Method = method;
Path = path;
Payload = payload;
ElapsedMilliseconds = milliseconds;
}
}

public event EventHandler<RequestEventArgs> OnRequest;
protected void RaiseOnRequest(HttpMethod method, string path, string payload, double milliseconds)
{
if (OnRequest != null)
OnRequest(this, new RequestEventArgs(method, path, payload, milliseconds));
}
}
}

+ 68
- 0
src/Discord.Net/Net/RestClient.SharpRest.cs View File

@@ -0,0 +1,68 @@
#if !DNXCORE50
using RestSharp;
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net
{
internal class SharpRestEngine : IRestEngine
{
private readonly RestSharp.RestClient _client;

public SharpRestEngine(string userAgent)
{
_client = new RestSharp.RestClient()
{
PreAuthenticate = false
};
_client.AddDefaultHeader("accept", "*/*");
_client.AddDefaultHeader("accept-encoding", "gzip,deflate");
_client.UserAgent = userAgent;
}

public void SetToken(string token)
{
_client.RemoveDefaultParameter("authorization");
if (token != null)
_client.AddDefaultHeader("authorization", token);
}

public Task<string> Send(HttpMethod method, string path, string json, CancellationToken cancelToken)
{
var request = new RestRequest(path, GetMethod(method)) { RequestFormat = DataFormat.Json };
request.AddBody(json);
return Send(request, cancelToken);
}
public Task<string> SendFile(HttpMethod method, string path, string filePath, CancellationToken cancelToken)
{
var request = new RestRequest(path, Method.POST);
request.AddFile(Path.GetFileName(filePath), filePath);
return Send(request, cancelToken);
}
private async Task<string> Send(RestRequest request, CancellationToken cancelToken)
{
var response = await _client.ExecuteTaskAsync(request, cancelToken).ConfigureAwait(false);
int statusCode = (int)response.StatusCode;
if (statusCode < 200 || statusCode >= 300) //2xx = Success
throw new HttpException(response.StatusCode);
return response.Content;
}

private Method GetMethod(HttpMethod method)
{
switch (method.Method)
{
case "DELETE": return Method.DELETE;
case "GET": return Method.GET;
case "PATCH": return Method.PATCH;
case "POST": return Method.POST;
case "PUT": return Method.PUT;
default: throw new InvalidOperationException($"Unknown HttpMethod: {method}");
}
}
}
}
#endif

+ 184
- 0
src/Discord.Net/Net/RestClient.cs View File

@@ -0,0 +1,184 @@
using Discord.Net.API;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net
{
internal interface IRestEngine
{
void SetToken(string token);
Task<string> Send(HttpMethod method, string path, string json, CancellationToken cancelToken);
Task<string> SendFile(HttpMethod method, string path, string filePath, CancellationToken cancelToken);
}

internal partial class RestClient
{
private readonly IRestEngine _engine;
private readonly LogMessageSeverity _logLevel;
private CancellationToken _cancelToken;

public RestClient(LogMessageSeverity logLevel)
{
_logLevel = logLevel;

string version = typeof(RestClient).GetTypeInfo().Assembly.GetName().Version.ToString(2);
string userAgent = $"Discord.Net/{version} (https://github.com/RogueException/Discord.Net)";
#if DNXCORE50
_engine = new BuiltInRestEngine(userAgent);
#else
_engine = new SharpRestEngine(userAgent);
#endif
}

private static readonly HttpMethod _delete = HttpMethod.Delete;
internal Task<ResponseT> Delete<ResponseT>(string path, object data) where ResponseT : class
=> Send<ResponseT>(_delete, path, data);
internal Task<ResponseT> Delete<ResponseT>(string path) where ResponseT : class
=> Send<ResponseT>(_delete, path);
internal Task Delete(string path, object data)
=> Send(_delete, path, data);
internal Task Delete(string path)
=> Send(_delete, path);

private static readonly HttpMethod _get = HttpMethod.Get;
internal Task<ResponseT> Get<ResponseT>(string path) where ResponseT : class
=> Send<ResponseT>(_get, path);
internal Task Get(string path)
=> Send(_get, path);

private static readonly HttpMethod _patch = new HttpMethod("PATCH");
internal Task<ResponseT> Patch<ResponseT>(string path, object data) where ResponseT : class
=> Send<ResponseT>(_patch, path, data);
internal Task Patch(string path, object data)
=> Send(_patch, path, data);
internal Task<ResponseT> Patch<ResponseT>(string path) where ResponseT : class
=> Send<ResponseT>(_patch, path);
internal Task Patch(string path)
=> Send(_patch, path);

private static readonly HttpMethod _post = HttpMethod.Post;
internal Task<ResponseT> Post<ResponseT>(string path, object data) where ResponseT : class
=> Send<ResponseT>(_post, path, data);
internal Task<ResponseT> Post<ResponseT>(string path) where ResponseT : class
=> Send<ResponseT>(_post, path);
internal Task Post(string path, object data)
=> Send(_post, path, data);
internal Task Post(string path)
=> Send(_post, path);

private static readonly HttpMethod _put = HttpMethod.Put;
internal Task<ResponseT> Put<ResponseT>(string path, object data) where ResponseT : class
=> Send<ResponseT>(_put, path, data);
internal Task<ResponseT> Put<ResponseT>(string path) where ResponseT : class
=> Send<ResponseT>(_put, path);
internal Task Put(string path, object data)
=> Send(_put, path, data);
internal Task Put(string path)
=> Send(_put, path);

internal Task<ResponseT> PostFile<ResponseT>(string path, string filePath) where ResponseT : class
=> SendFile<ResponseT>(_post, path, filePath);
internal Task PostFile(string path, string filePath)
=> SendFile(_post, path, filePath);

internal Task<ResponseT> PutFile<ResponseT>(string path, string filePath) where ResponseT : class
=> SendFile<ResponseT>(_put, path, filePath);
internal Task PutFile(string path, string filePath)
=> SendFile(_put, path, filePath);

private async Task<ResponseT> Send<ResponseT>(HttpMethod method, string path, object content = null)
where ResponseT : class
{
string responseJson = await Send(method, path, content, true).ConfigureAwait(false);
return DeserializeResponse<ResponseT>(responseJson);
}
private Task Send(HttpMethod method, string path, object content = null)
=> Send(method, path, content, false);
private async Task<string> Send(HttpMethod method, string path, object content, bool hasResponse)
{
Stopwatch stopwatch = null;
string requestJson = null;
if (content != null)
requestJson = JsonConvert.SerializeObject(content);

if (_logLevel >= LogMessageSeverity.Verbose)
stopwatch = Stopwatch.StartNew();

string responseJson = await _engine.Send(method, path, requestJson, _cancelToken).ConfigureAwait(false);
#if TEST_RESPONSES
if (!hasResponse && !string.IsNullOrEmpty(responseJson))
throw new Exception("API check failed: Response is not empty.");
#endif

if (_logLevel >= LogMessageSeverity.Verbose)
{
stopwatch.Stop();
if (content != null && _logLevel >= LogMessageSeverity.Debug)
RaiseOnRequest(method, path, requestJson, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
else
RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
}

return responseJson;
}

private async Task<ResponseT> SendFile<ResponseT>(HttpMethod method, string path, string filePath)
where ResponseT : class
{
string responseJson = await SendFile(method, path, filePath, true).ConfigureAwait(false);
return DeserializeResponse<ResponseT>(responseJson);
}
private Task SendFile(HttpMethod method, string path, string filePath)
=> SendFile(method, path, filePath, false);
private async Task<string> SendFile(HttpMethod method, string path, string filePath, bool hasResponse)
{
Stopwatch stopwatch = null;

if (_logLevel >= LogMessageSeverity.Verbose)
stopwatch = Stopwatch.StartNew();
string responseJson = await _engine.SendFile(method, path, filePath, _cancelToken).ConfigureAwait(false);
#if TEST_RESPONSES
if (!hasResponse && !string.IsNullOrEmpty(responseJson))
throw new Exception("API check failed: Response is not empty.");
#endif

if (_logLevel >= LogMessageSeverity.Verbose)
{
stopwatch.Stop();
if (_logLevel >= LogMessageSeverity.Debug)
RaiseOnRequest(method, path, filePath, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
else
RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
}

return responseJson;
}

private JsonSerializerSettings _deserializeSettings = new JsonSerializerSettings();
private T DeserializeResponse<T>(string json)
{
#if TEST_RESPONSES
if (_deserializeSettings == null)
{
_deserializeSettings = new JsonSerializerSettings();
_deserializeSettings.CheckAdditionalContent = true;
_deserializeSettings.MissingMemberHandling = MissingMemberHandling.Error;
}
if (string.IsNullOrEmpty(json))
throw new Exception("API check failed: Response is empty.");
return JsonConvert.DeserializeObject<T>(json, _deserializeSettings);
#else
return JsonConvert.DeserializeObject<T>(json);
#endif
}

internal void SetToken(string token) => _engine.SetToken(token);
internal void SetCancelToken(CancellationToken token) => _cancelToken = token;
}
}

src/Discord.Net/API/Models/TextWebSocketCommands.cs → src/Discord.Net/Net/WebSockets/Commands.cs View File

@@ -7,39 +7,16 @@ using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;

namespace Discord.API.Models
namespace Discord.Net.WebSockets
{
internal static class TextWebSocketCommands
internal static class Commands
{
public class WebSocketMessage
{
[JsonProperty(PropertyName = "op")]
public int Operation;
[JsonProperty(PropertyName = "t", NullValueHandling = NullValueHandling.Ignore)]
public string Type;
[JsonProperty(PropertyName = "s", NullValueHandling = NullValueHandling.Ignore)]
public int? Sequence;
[JsonProperty(PropertyName = "d", NullValueHandling = NullValueHandling.Ignore)]
public object Payload;
}
internal abstract class WebSocketMessage<T> : WebSocketMessage
where T : new()
{
public WebSocketMessage() { Payload = new T(); }
public WebSocketMessage(int op) { Operation = op; Payload = new T(); }
public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; }

[JsonIgnore]
public new T Payload
{
get { if (base.Payload is JToken) { base.Payload = (base.Payload as JToken).ToObject<T>(); } return (T)base.Payload; }
set { base.Payload = value; }
}
}
public sealed class KeepAlive : WebSocketMessage<int>
{
public KeepAlive() : base(1, GetTimestamp()) { }
private static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public KeepAlive() : base(1, (int)(DateTime.UtcNow - epoch).TotalMilliseconds) { }
private static int GetTimestamp()
=> (int)(DateTime.UtcNow - epoch).TotalMilliseconds;
}
public sealed class Login : WebSocketMessage<Login.Data>
{

+ 102
- 0
src/Discord.Net/Net/WebSockets/DataWebSocket.cs View File

@@ -0,0 +1,102 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Threading.Tasks;

namespace Discord.Net.WebSockets
{
internal partial class DataWebSocket : WebSocket
{
private string _lastSession, _redirectServer;
private int _lastSeq;

public DataWebSocket(DiscordClient client)
: base(client)
{
}
public async Task Login(string host, string token)
{
await base.Connect(host);
Commands.Login msg = new Commands.Login();
msg.Payload.Token = token;
//msg.Payload.Properties["$os"] = "";
//msg.Payload.Properties["$browser"] = "";
msg.Payload.Properties["$device"] = "Discord.Net";
//msg.Payload.Properties["$referrer"] = "";
//msg.Payload.Properties["$referring_domain"] = "";
QueueMessage(msg);
}

protected override Task[] Run()
{
//Send resume session if we were transferred
if (_redirectServer != null)
{
var resumeMsg = new Commands.Resume();
resumeMsg.Payload.SessionId = _lastSession;
resumeMsg.Payload.Sequence = _lastSeq;
QueueMessage(resumeMsg);
_redirectServer = null;
}
return base.Run();
}

protected override async Task ProcessMessage(string json)
{
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json);
if (msg.Sequence.HasValue)
_lastSeq = msg.Sequence.Value;
switch (msg.Operation)
{
case 0:
{
if (msg.Type == "READY")
{
var payload = (msg.Payload as JToken).ToObject<Events.Ready>();
_lastSession = payload.SessionId;
_heartbeatInterval = payload.HeartbeatInterval;
QueueMessage(new Commands.UpdateStatus());
CompleteConnect();
}
if (_logLevel >= LogMessageSeverity.Info)
RaiseOnLog(LogMessageSeverity.Info, "Got Event: " + msg.Type);
}
break;
case 7: //Redirect
{
var payload = (msg.Payload as JToken).ToObject<Events.Redirect>();
_host = payload.Url;
if (_logLevel >= LogMessageSeverity.Info)
RaiseOnLog(LogMessageSeverity.Info, "Redirected to " + payload.Url);
await DisconnectInternal(new Exception("Server is redirecting."), true);
}
break;
default:
if (_logLevel >= LogMessageSeverity.Warning)
RaiseOnLog(LogMessageSeverity.Warning, $"Unknown Opcode: {msg.Operation}");
break;
}
}

protected override object GetKeepAlive()
{
return new Commands.KeepAlive();
}

public void SendJoinVoice(Channel channel)
{
var joinVoice = new Commands.JoinVoice();
joinVoice.Payload.ServerId = channel.ServerId;
joinVoice.Payload.ChannelId = channel.Id;
QueueMessage(joinVoice);
}
public void SendLeaveVoice()
{
var leaveVoice = new Commands.JoinVoice();
QueueMessage(leaveVoice);
}
}
}

src/Discord.Net/API/Models/TextWebSocketEvents.cs → src/Discord.Net/Net/WebSockets/Events.cs View File

@@ -2,11 +2,12 @@
#pragma warning disable CS0649
#pragma warning disable CS0169

using Discord.Net.API;
using Newtonsoft.Json;

namespace Discord.API.Models
namespace Discord.Net.WebSockets
{
internal static class TextWebSocketEvents
internal static class Events
{
public sealed class Ready
{
@@ -29,7 +30,7 @@ namespace Discord.API.Models
[JsonProperty(PropertyName = "read_state")]
public ReadStateInfo[] ReadState;
[JsonProperty(PropertyName = "guilds")]
public ExtendedServerInfo[] Guilds;
public ExtendedGuildInfo[] Guilds;
[JsonProperty(PropertyName = "private_channels")]
public ChannelInfo[] PrivateChannels;
[JsonProperty(PropertyName = "heartbeat_interval")]
@@ -43,9 +44,9 @@ namespace Discord.API.Models
}

//Servers
public sealed class GuildCreate : ExtendedServerInfo { }
public sealed class GuildUpdate : ServerInfo { }
public sealed class GuildDelete : ExtendedServerInfo { }
public sealed class GuildCreate : ExtendedGuildInfo { }
public sealed class GuildUpdate : GuildInfo { }
public sealed class GuildDelete : ExtendedGuildInfo { }

//Channels
public sealed class ChannelCreate : ChannelInfo { }
@@ -53,43 +54,30 @@ namespace Discord.API.Models
public sealed class ChannelUpdate : ChannelInfo { }

//Memberships
public sealed class GuildMemberAdd : RoleMemberInfo { }
public sealed class GuildMemberUpdate : RoleMemberInfo { }
public sealed class GuildMemberAdd : MemberInfo { }
public sealed class GuildMemberUpdate : MemberInfo { }
public sealed class GuildMemberRemove : MemberInfo { }

//Roles
public abstract class GuildRoleEvent
public sealed class GuildRoleCreate
{
[JsonProperty(PropertyName = "guild_id")]
public string ServerId;
}
public sealed class GuildRoleCreateUpdate : GuildRoleEvent
{
public string GuildId;
[JsonProperty(PropertyName = "role")]
public Role Role;
public RoleInfo Data;
}
public sealed class GuildRoleDelete : GuildRoleEvent
public sealed class GuildRoleUpdate
{
[JsonProperty(PropertyName = "role_id")]
public string RoleId;
[JsonProperty(PropertyName = "guild_id")]
public string GuildId;
[JsonProperty(PropertyName = "role")]
public RoleInfo Data;
}
public sealed class GuildRoleDelete : RoleReference { }

//Bans
public abstract class GuildBanEvent
{
[JsonProperty(PropertyName = "guild_id")]
public string ServerId;
}
public sealed class GuildBanAddRemove : GuildBanEvent
{
[JsonProperty(PropertyName = "user")]
public UserReference User;
}
public sealed class GuildBanRemove : GuildBanEvent
{
[JsonProperty(PropertyName = "user_id")]
public string UserId;
}
public sealed class GuildBanAdd : MemberReference { }
public sealed class GuildBanRemove : MemberReference { }

//User
public sealed class UserUpdate : SelfUserInfo { }
@@ -97,8 +85,8 @@ namespace Discord.API.Models
public sealed class VoiceStateUpdate : VoiceMemberInfo { }

//Chat
public sealed class MessageCreate : Message { }
public sealed class MessageUpdate : Message { }
public sealed class MessageCreate : API.Message { }
public sealed class MessageUpdate : API.Message { }
public sealed class MessageDelete : MessageReference { }
public sealed class MessageAck : MessageReference { }
public sealed class TypingStart
@@ -115,7 +103,7 @@ namespace Discord.API.Models
public sealed class VoiceServerUpdate
{
[JsonProperty(PropertyName = "guild_id")]
public string ServerId;
public string GuildId;
[JsonProperty(PropertyName = "endpoint")]
public string Endpoint;
[JsonProperty(PropertyName = "token")]

src/Discord.Net/API/Models/VoiceWebSocketCommands.cs → src/Discord.Net/Net/WebSockets/VoiceCommands.cs View File

@@ -3,38 +3,11 @@
#pragma warning disable CS0169

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Discord.API.Models
namespace Discord.Net.WebSockets
{
internal static class VoiceWebSocketCommands
internal static class VoiceCommands
{
public class WebSocketMessage
{
[JsonProperty(PropertyName = "op")]
public int Operation;
[JsonProperty(PropertyName = "d")]
public object Payload;
}
internal abstract class WebSocketMessage<T> : WebSocketMessage
where T : new()
{
public WebSocketMessage() { Payload = new T(); }
public WebSocketMessage(int op) { Operation = op; Payload = new T(); }
public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; }

[JsonIgnore]
public new T Payload
{
get { if (base.Payload is JToken) { base.Payload = (base.Payload as JToken).ToObject<T>(); } return (T)base.Payload; }
set { base.Payload = value; }
}
}

public sealed class KeepAlive : WebSocketMessage<object>
{
public KeepAlive() : base(3, null) { }
}
public sealed class Login : WebSocketMessage<Login.Data>
{
public Login() : base(0) { }
@@ -70,6 +43,10 @@ namespace Discord.API.Models
public SocketInfo SocketData = new SocketInfo();
}
}
public sealed class KeepAlive : WebSocketMessage<object>
{
public KeepAlive() : base(3, null) { }
}
public sealed class IsTalking : WebSocketMessage<IsTalking.Data>
{
public IsTalking() : base(5) { }

src/Discord.Net/API/Models/VoiceWebSocketEvents.cs → src/Discord.Net/Net/WebSockets/VoiceEvents.cs View File

@@ -4,9 +4,9 @@

using Newtonsoft.Json;

namespace Discord.API.Models
namespace Discord.Net.WebSockets
{
internal static class VoiceWebSocketEvents
internal static class VoiceEvents
{
public sealed class Ready
{

src/Discord.Net/DiscordVoiceSocket.cs → src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs View File

@@ -1,7 +1,5 @@
#define USE_THREAD
#if !DNXCORE50
using Discord.API.Models;
using Discord.Opus;
using Discord.Audio;
using Discord.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
@@ -10,21 +8,19 @@ using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage;
using Discord.Helpers;

namespace Discord
namespace Discord.Net.WebSockets
{
internal sealed partial class DiscordVoiceSocket : DiscordWebSocket
internal partial class VoiceWebSocket : WebSocket
{
private readonly int _targetAudioBufferLength;
private ManualResetEventSlim _connectWaitOnLogin;
private ManualResetEventSlim _connectWaitOnLogin;
private uint _ssrc;
private readonly Random _rand = new Random();
private readonly Random _rand = new Random();
private OpusEncoder _encoder;
private ConcurrentQueue<byte[]> _sendQueue;
private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait;
@@ -44,19 +40,19 @@ namespace Discord

public string CurrentVoiceServerId => _serverId;

public DiscordVoiceSocket(DiscordClient client, int timeout, int interval, int audioBufferLength, bool isDebug)
: base(client, timeout, interval, isDebug)
public VoiceWebSocket(DiscordClient client)
: base(client)
{
_connectWaitOnLogin = new ManualResetEventSlim(false);
_sendQueue = new ConcurrentQueue<byte[]>();
_sendQueueWait = new ManualResetEventSlim(true);
_sendQueueEmptyWait = new ManualResetEventSlim(true);
_encoder = new OpusEncoder(48000, 1, 20, Application.Audio);
_encoder = new OpusEncoder(48000, 1, 20, Opus.Application.Audio);
_encodingBuffer = new byte[4000];
_targetAudioBufferLength = audioBufferLength / 20;
}
protected override void OnConnect()
_targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames
}
protected override Task[] Run()
{
_udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
#if !DNX451
@@ -64,31 +60,32 @@ namespace Discord
#endif
_isReady = false;
_isClearing = false;
VoiceCommands.Login msg = new VoiceCommands.Login();
msg.Payload.ServerId = _serverId;
msg.Payload.SessionId = _sessionId;
msg.Payload.Token = _token;
msg.Payload.UserId = _userId;
QueueMessage(msg);

var cancelToken = _disconnectToken.Token;
Task.Run(async () =>
#if USE_THREAD
_sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_disconnectToken)));
_sendThread.Start();
#endif
return new Task[]
{
try
{
_connectWaitOnLogin.Reset();

VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login();
msg.Payload.ServerId = _serverId;
msg.Payload.SessionId = _sessionId;
msg.Payload.Token = _token;
msg.Payload.UserId = _userId;
await SendMessage(msg, cancelToken).ConfigureAwait(false);

if (!_connectWaitOnLogin.Wait(_timeout, cancelToken))
return;

SetConnected();
}
catch (OperationCanceledException) { }
}, _disconnectToken.Token);
ReceiveVoiceAsync(),
#if !USE_THREAD
SendVoiceAsync(),
#endif
#if !DNXCORE50
WatcherAsync()
#endif
}.Concat(base.Run()).ToArray();
}
protected override void OnDisconnect()
protected override Task Cleanup()
{
ClearPCMFrames();
_udp = null;
_serverId = null;
_userId = null;
@@ -98,22 +95,7 @@ namespace Discord
_sendThread.Join();
_sendThread = null;
#endif
}

protected override Task[] CreateTasks()
{
#if USE_THREAD
_sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_disconnectToken)));
_sendThread.Start();
#endif
return new Task[]
{
ReceiveVoiceAsync(),
#if !USE_THREAD
SendVoiceAsync(),
#endif
WatcherAsync()
}.Concat(base.CreateTasks()).ToArray();
return base.Cleanup();
}

public void SetSessionData(string serverId, string userId, string sessionId, string token)
@@ -124,31 +106,9 @@ namespace Discord
_token = token;
}

public new async Task BeginConnect()
{
await base.BeginConnect().ConfigureAwait(false);
var cancelToken = _disconnectToken.Token;

await Task.Run(() =>
{
try
{
if (!_connectWaitOnLogin.Wait(_timeout, cancelToken)) //Waiting on JoinServer message
throw new Exception("No reply from Discord server");
}
catch (OperationCanceledException)
{
if (_disconnectReason == null)
throw new Exception("An unknown websocket error occurred.");
else
_disconnectReason.Throw();
}
}).ConfigureAwait(false);
}
private async Task ReceiveVoiceAsync()
{
var cancelSource = _disconnectToken;
var cancelSource = _cancelToken;
var cancelToken = cancelSource.Token;

await Task.Run(async () =>
@@ -157,13 +117,22 @@ namespace Discord
{
while (!cancelToken.IsCancellationRequested)
{
var result = await _udp.ReceiveAsync().ConfigureAwait(false);
ProcessUdpMessage(result);
#if DNXCORE50
if (_udp.Available > 0)
{
#endif
var result = await _udp.ReceiveAsync().ConfigureAwait(false);
ProcessUdpMessage(result);
#if DNXCORE50
}
else
await Task.Delay(1).ConfigureAwait(false);
#endif
}
}
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } //Includes ObjectDisposedException
catch (Exception ex) { DisconnectInternal(ex); }
catch (Exception ex) { await DisconnectInternal(ex); }
}).ConfigureAwait(false);
}

@@ -174,101 +143,106 @@ namespace Discord
#else
private Task SendVoiceAsync()
{
var cancelSource = _disconnectToken;
var cancelSource = _cancelToken;
var cancelToken = cancelSource.Token;
return Task.Run(() =>
return Task.Run(async () =>
{
#endif

byte[] packet;
try
{
while (!cancelToken.IsCancellationRequested && !_isReady)
Thread.Sleep(1);

if (cancelToken.IsCancellationRequested)
return;

uint timestamp = 0;
double nextTicks = 0.0;
double ticksPerMillisecond = Stopwatch.Frequency / 1000.0;
double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength;
double spinLockThreshold = 1.5 * ticksPerMillisecond;
uint samplesPerFrame = (uint)_encoder.SamplesPerFrame;
Stopwatch sw = Stopwatch.StartNew();

byte[] rtpPacket = new byte[_encodingBuffer.Length + 12];
rtpPacket[0] = 0x80; //Flags;
rtpPacket[1] = 0x78; //Payload Type
rtpPacket[8] = (byte)((_ssrc >> 24) & 0xFF);
rtpPacket[9] = (byte)((_ssrc >> 16) & 0xFF);
rtpPacket[10] = (byte)((_ssrc >> 8) & 0xFF);
rtpPacket[11] = (byte)((_ssrc >> 0) & 0xFF);

while (!cancelToken.IsCancellationRequested)
byte[] packet;
try
{
double ticksToNextFrame = nextTicks - sw.ElapsedTicks;
if (ticksToNextFrame <= 0.0)
while (!cancelToken.IsCancellationRequested && !_isReady)
#if USE_THREAD
Thread.Sleep(1);
#else
await Task.Delay(1);
#endif

if (cancelToken.IsCancellationRequested)
return;

uint timestamp = 0;
double nextTicks = 0.0;
double ticksPerMillisecond = Stopwatch.Frequency / 1000.0;
double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength;
double spinLockThreshold = 1.5 * ticksPerMillisecond;
uint samplesPerFrame = (uint)_encoder.SamplesPerFrame;
Stopwatch sw = Stopwatch.StartNew();

byte[] rtpPacket = new byte[_encodingBuffer.Length + 12];
rtpPacket[0] = 0x80; //Flags;
rtpPacket[1] = 0x78; //Payload Type
rtpPacket[8] = (byte)((_ssrc >> 24) & 0xFF);
rtpPacket[9] = (byte)((_ssrc >> 16) & 0xFF);
rtpPacket[10] = (byte)((_ssrc >> 8) & 0xFF);
rtpPacket[11] = (byte)((_ssrc >> 0) & 0xFF);

while (!cancelToken.IsCancellationRequested)
{
while (sw.ElapsedTicks > nextTicks)
double ticksToNextFrame = nextTicks - sw.ElapsedTicks;
if (ticksToNextFrame <= 0.0)
{
if (!_isClearing)
while (sw.ElapsedTicks > nextTicks)
{
if (_sendQueue.TryDequeue(out packet))
if (!_isClearing)
{
ushort sequence = unchecked(_sequence++);
rtpPacket[2] = (byte)((sequence >> 8) & 0xFF);
rtpPacket[3] = (byte)((sequence >> 0) & 0xFF);
rtpPacket[4] = (byte)((timestamp >> 24) & 0xFF);
rtpPacket[5] = (byte)((timestamp >> 16) & 0xFF);
rtpPacket[6] = (byte)((timestamp >> 8) & 0xFF);
rtpPacket[7] = (byte)((timestamp >> 0) & 0xFF);
Buffer.BlockCopy(packet, 0, rtpPacket, 12, packet.Length);
if (_sendQueue.TryDequeue(out packet))
{
ushort sequence = unchecked(_sequence++);
rtpPacket[2] = (byte)((sequence >> 8) & 0xFF);
rtpPacket[3] = (byte)((sequence >> 0) & 0xFF);
rtpPacket[4] = (byte)((timestamp >> 24) & 0xFF);
rtpPacket[5] = (byte)((timestamp >> 16) & 0xFF);
rtpPacket[6] = (byte)((timestamp >> 8) & 0xFF);
rtpPacket[7] = (byte)((timestamp >> 0) & 0xFF);
Buffer.BlockCopy(packet, 0, rtpPacket, 12, packet.Length);
#if USE_THREAD
_udp.Send(rtpPacket, packet.Length + 12);
_udp.Send(rtpPacket, packet.Length + 12);
#else
await _udp.SendAsync(rtpPacket, packet.Length + 12).ConfigureAwait(false);
await _udp.SendAsync(rtpPacket, packet.Length + 12).ConfigureAwait(false);
#endif
}
timestamp = unchecked(timestamp + samplesPerFrame);
nextTicks += ticksPerFrame;

//If we have less than our target data buffered, request more
int count = _sendQueue.Count;
if (count == 0)
{
_sendQueueWait.Set();
_sendQueueEmptyWait.Set();
}
else if (count < _targetAudioBufferLength)
_sendQueueWait.Set();
}
timestamp = unchecked(timestamp + samplesPerFrame);
nextTicks += ticksPerFrame;

//If we have less than our target data buffered, request more
int count = _sendQueue.Count;
if (count == 0)
{
_sendQueueWait.Set();
_sendQueueEmptyWait.Set();
}
else if (count < _targetAudioBufferLength)
_sendQueueWait.Set();
}
}
}
//Dont sleep for 1 millisecond if we need to output audio in the next 1.5
else if (_sendQueue.Count == 0 || ticksToNextFrame >= spinLockThreshold)
//Dont sleep for 1 millisecond if we need to output audio in the next 1.5
else if (_sendQueue.Count == 0 || ticksToNextFrame >= spinLockThreshold)
#if USE_THREAD
Thread.Sleep(1);
#else
await Task.Delay(1).ConfigureAwait(false);
await Task.Delay(1).ConfigureAwait(false);
#endif
}
}
}
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } //Includes ObjectDisposedException
catch (Exception ex) { DisconnectInternal(ex); }
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } //Includes ObjectDisposedException
#if !USE_THREAD
}).ConfigureAwait(false);
});
#endif
}
#if !DNXCORE50
//Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken
private Task WatcherAsync()
{
var cancelToken = _disconnectToken.Token;
var cancelToken = _cancelToken.Token;
return cancelToken.Wait()
.ContinueWith(_ => _udp.Close());
}
}
#endif

protected override async Task ProcessMessage(string json)
{
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json);
@@ -278,7 +252,7 @@ namespace Discord
{
if (!_isReady)
{
var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.Ready>();
var payload = (msg.Payload as JToken).ToObject<VoiceEvents.Ready>();
_heartbeatInterval = payload.HeartbeatInterval;
_ssrc = payload.SSRC;
_endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(_host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port);
@@ -303,22 +277,22 @@ namespace Discord
break;
case 4: //SESSION_DESCRIPTION
{
var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.JoinServer>();
var payload = (msg.Payload as JToken).ToObject<VoiceEvents.JoinServer>();
_secretKey = payload.SecretKey;
SendIsTalking(true);
_connectWaitOnLogin.Set();
}
break;
default:
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.WebSocketUnknownOpCode, "Unknown Opcode: " + msg.Operation);
if (_logLevel >= LogMessageSeverity.Warning)
RaiseOnLog(LogMessageSeverity.Warning, $"Unknown Opcode: {msg.Operation}");
break;
}
}

private void ProcessUdpMessage(UdpReceiveResult msg)
{
if (msg.Buffer.Length > 0 && msg.RemoteEndPoint.Equals(_endpoint))
if (msg.Buffer.Length > 0 && msg.RemoteEndPoint.Equals(_endpoint))
{
byte[] buffer = msg.Buffer;
int length = msg.Buffer.Length;
@@ -327,8 +301,8 @@ namespace Discord
_isReady = true;
if (length != 70)
{
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Unexpected message length. Expected >= 70, got {length}.");
if (_logLevel >= LogMessageSeverity.Warning)
RaiseOnLog(LogMessageSeverity.Warning, $"Unexpected message length. Expected 70, got {length}.");
return;
}

@@ -337,7 +311,7 @@ namespace Discord
_myIp = Encoding.ASCII.GetString(buffer, 4, 70 - 6).TrimEnd('\0');

_isReady = true;
var login2 = new VoiceWebSocketCommands.Login2();
var login2 = new VoiceCommands.Login2();
login2.Payload.Protocol = "udp";
login2.Payload.SocketData.Address = _myIp;
login2.Payload.SocketData.Mode = _mode;
@@ -349,36 +323,36 @@ namespace Discord
//Parse RTP Data
if (length < 12)
{
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Unexpected message length. Expected >= 12, got {length}.");
if (_logLevel >= LogMessageSeverity.Warning)
RaiseOnLog(LogMessageSeverity.Warning, $"Unexpected message length. Expected >= 12, got {length}.");
return;
}

byte flags = buffer[0];
if (flags != 0x80)
{
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Unexpected Flags: {flags}");
if (_logLevel >= LogMessageSeverity.Warning)
RaiseOnLog(LogMessageSeverity.Warning, $"Unexpected Flags: {flags}");
return;
}

byte payloadType = buffer[1];
if (payloadType != 0x78)
{
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Unexpected Payload Type: {flags}");
if (_logLevel >= LogMessageSeverity.Warning)
RaiseOnLog(LogMessageSeverity.Warning, $"Unexpected Payload Type: {payloadType}");
return;
}

ushort sequenceNumber = (ushort)((buffer[2] << 8) |
ushort sequenceNumber = (ushort)((buffer[2] << 8) |
buffer[3] << 0);
uint timestamp = (uint)((buffer[4] << 24) |
uint timestamp = (uint)((buffer[4] << 24) |
(buffer[5] << 16) |
(buffer[6] << 8) |
(buffer[6] << 8) |
(buffer[7] << 0));
uint ssrc = (uint)((buffer[8] << 24) |
uint ssrc = (uint)((buffer[8] << 24) |
(buffer[9] << 16) |
(buffer[10] << 8) |
(buffer[10] << 8) |
(buffer[11] << 0));

//Decrypt
@@ -402,24 +376,24 @@ namespace Discord
buffer = newBuffer;
}*/

if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Received {buffer.Length - 12} bytes.");
if (_logLevel >= LogMessageSeverity.Verbose)
RaiseOnLog(LogMessageSeverity.Verbose, $"Received {buffer.Length - 12} bytes.");
//TODO: Use Voice Data
}
}
}
}

public void SendPCMFrames(byte[] data, int bytes)
{
var cancelToken = _disconnectToken.Token;
if (!_isReady || cancelToken == null)
var cancelToken = _cancelToken.Token;
if (!_isReady || cancelToken == null)
throw new InvalidOperationException("Not connected to a voice server.");
if (bytes == 0)
return;

int frameSize = _encoder.FrameSize;
int frames = bytes / frameSize;
int expectedBytes = frames * frameSize;
int expectedBytes = frames * frameSize;
int lastFrameSize = expectedBytes - bytes;

//If this only consists of a partial frame and the buffer is too small to pad the end, make a new one
@@ -432,7 +406,7 @@ namespace Discord

byte[] payload;
//Opus encoder requires packets be queued in the same order they were generated, so all of this must still be locked.
lock (_encoder)
lock (_encoder)
{
for (int i = 0, pos = 0; i <= frames; i++, pos += frameSize)
{
@@ -465,39 +439,43 @@ namespace Discord
Buffer.BlockCopy(_encodingBuffer, 0, payload, 0, encodedLength);

//Wait until the queue has a spot open
_sendQueueWait.Wait(_disconnectToken.Token);
_sendQueueWait.Wait(_cancelToken.Token);
_sendQueue.Enqueue(payload);
if (_sendQueue.Count >= _targetAudioBufferLength)
_sendQueueWait.Reset();
_sendQueueEmptyWait.Reset();
}
}

if (_logLevel >= LogMessageSeverity.Verbose)
RaiseOnLog(LogMessageSeverity.Verbose, $"Queued {bytes} bytes for voice output.");
}
public void ClearPCMFrames()
{
_isClearing = true;
byte[] ignored;
while (_sendQueue.TryDequeue(out ignored)) { }
while (_sendQueue.TryDequeue(out ignored)) { }
if (_logLevel >= LogMessageSeverity.Verbose)
RaiseOnLog(LogMessageSeverity.Verbose, "Cleared the voice buffer.");
_isClearing = false;
}

private void SendIsTalking(bool value)
{
var isTalking = new VoiceWebSocketCommands.IsTalking();
var isTalking = new VoiceCommands.IsTalking();
isTalking.Payload.IsSpeaking = value;
isTalking.Payload.Delay = 0;
QueueMessage(isTalking);
QueueMessage(isTalking);
}

protected override object GetKeepAlive()
{
return new VoiceWebSocketCommands.KeepAlive();
return new VoiceCommands.KeepAlive();
}

public void Wait()
{
_sendQueueEmptyWait.Wait();
}
}
}
}
#endif

+ 153
- 0
src/Discord.Net/Net/WebSockets/WebSocket.BuiltIn.cs View File

@@ -0,0 +1,153 @@
using Discord.Helpers;
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using State = System.Net.WebSockets.WebSocketState;

namespace Discord.Net.WebSockets
{
internal class BuiltInWebSocketEngine : IWebSocketEngine
{
private const int ReceiveChunkSize = 4096;
private const int SendChunkSize = 4096;
private const int HR_TIMEOUT = -2147012894;

private readonly ConcurrentQueue<byte[]> _sendQueue;
private readonly ClientWebSocket _webSocket;
private readonly int _sendInterval;
public event EventHandler<WebSocketMessageEventArgs> ProcessMessage;
private void RaiseProcessMessage(string msg)
{
if (ProcessMessage != null)
ProcessMessage(this, new WebSocketMessageEventArgs(msg));
}

public BuiltInWebSocketEngine(int sendInterval)
{
_sendInterval = sendInterval;
_sendQueue = new ConcurrentQueue<byte[]>();
_webSocket = new ClientWebSocket();
_webSocket.Options.KeepAliveInterval = TimeSpan.Zero;
}

public Task Connect(string host, CancellationToken cancelToken)
{
return _webSocket.ConnectAsync(new Uri(host), cancelToken);
}

public Task Disconnect()
{
byte[] ignored;
while (_sendQueue.TryDequeue(out ignored)) { }
return TaskHelper.CompletedTask;
}

public Task[] RunTasks(CancellationToken cancelToken)
{
return new Task[]
{
ReceiveAsync(cancelToken),
SendAsync(cancelToken)
};
}

private Task ReceiveAsync(CancellationToken cancelToken)
{
return Task.Run(async () =>
{
var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]);
var builder = new StringBuilder();

try
{
while (_webSocket.State == State.Open && !cancelToken.IsCancellationRequested)
{
WebSocketReceiveResult result = null;
do
{
if (_webSocket.State != State.Open || cancelToken.IsCancellationRequested)
return;

try
{
result = await _webSocket.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false);
}
catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT)
{
throw new Exception($"Connection timed out.");
}

if (result.MessageType == WebSocketMessageType.Close)
throw new Exception($"Got Close Message ({result.CloseStatus?.ToString() ?? "Unexpected"}, {result.CloseStatusDescription ?? "No Reason"})");
else
builder.Append(Encoding.UTF8.GetString(buffer.Array, buffer.Offset, result.Count));

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

#if DEBUG
System.Diagnostics.Debug.WriteLine(">>> " + builder.ToString());
#endif
RaiseProcessMessage(builder.ToString());

builder.Clear();
}
}
catch (OperationCanceledException) { }
});
}
private Task SendAsync(CancellationToken cancelToken)
{
return Task.Run(async () =>
{
try
{
byte[] bytes;
while (_webSocket.State == State.Open && !cancelToken.IsCancellationRequested)
{
while (_sendQueue.TryDequeue(out bytes))
QueueMessage(bytes);
await Task.Delay(_sendInterval, cancelToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
});
}
public void QueueMessage(byte[] message)
{
_sendQueue.Enqueue(message);
}

private async Task SendMessageInternal(byte[] message, CancellationToken cancelToken)
{
var frameCount = (int)Math.Ceiling((double)message.Length / SendChunkSize);

int offset = 0;
for (var i = 0; i < frameCount; i++, offset += SendChunkSize)
{
bool isLast = i == (frameCount - 1);

int count;
if (isLast)
count = message.Length - (i * SendChunkSize);
else
count = SendChunkSize;

try
{
await _webSocket.SendAsync(new ArraySegment<byte>(message, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false);
}
catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT)
{
return;
}
}
}
}
}

+ 34
- 0
src/Discord.Net/Net/WebSockets/WebSocket.Events.cs View File

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

namespace Discord.Net.WebSockets
{
public class DisconnectedEventArgs : EventArgs
{
public readonly bool WasUnexpected;
public readonly Exception Error;
internal DisconnectedEventArgs(bool wasUnexpected, Exception error) { WasUnexpected = wasUnexpected; Error = error; }
}

internal partial class WebSocket
{
public event EventHandler Connected;
private void RaiseConnected()
{
if (Connected != null)
Connected(this, EventArgs.Empty);
}
public event EventHandler<DisconnectedEventArgs> Disconnected;
private void RaiseDisconnected(bool wasUnexpected, Exception error)
{
if (Disconnected != null)
Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error));
}

public event EventHandler<LogMessageEventArgs> LogMessage;
internal void RaiseOnLog(LogMessageSeverity severity, string message)
{
if (LogMessage != null)
LogMessage(this, new LogMessageEventArgs(severity, LogMessageSource.Unknown, message));
}
}
}

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

@@ -0,0 +1,192 @@
using Discord.Helpers;
using Newtonsoft.Json;
using System;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net.WebSockets
{
public enum WebSocketState : byte
{
Disconnected,
Connecting,
Connected,
Disconnecting
}

public class WebSocketMessageEventArgs : EventArgs
{
public readonly string Message;
public WebSocketMessageEventArgs(string msg) { Message = msg; }
}
internal interface IWebSocketEngine
{
event EventHandler<WebSocketMessageEventArgs> ProcessMessage;

Task Connect(string host, CancellationToken cancelToken);
Task Disconnect();
void QueueMessage(byte[] message);
Task[] RunTasks(CancellationToken cancelToken);
}

internal abstract partial class WebSocket
{
protected readonly IWebSocketEngine _engine;
protected readonly DiscordClient _client;
protected readonly LogMessageSeverity _logLevel;

protected int _state;
protected string _host;
protected int _loginTimeout, _heartbeatInterval;
private DateTime _lastHeartbeat;
private Task _runTask;

protected ExceptionDispatchInfo _disconnectReason;
private bool _wasDisconnectUnexpected;

public CancellationToken CancelToken => _cancelToken.Token;
protected CancellationTokenSource _cancelToken;

public WebSocket(DiscordClient client)
{
_client = client;
_logLevel = client.Config.LogLevel;
_loginTimeout = client.Config.ConnectionTimeout;
_engine = new BuiltInWebSocketEngine(client.Config.WebSocketInterval);
_engine.ProcessMessage += (s, e) =>
{
if (_logLevel >= LogMessageSeverity.Debug)
RaiseOnLog(LogMessageSeverity.Debug, $"In: " + e.Message);
ProcessMessage(e.Message);
};
}

protected virtual async Task Connect(string host)
{
if (_state != (int)WebSocketState.Disconnected)
throw new InvalidOperationException("Client is already connected or connecting to the server.");

try
{
await Disconnect().ConfigureAwait(false);
_state = (int)WebSocketState.Connecting;

_cancelToken = new CancellationTokenSource();

await _engine.Connect(host, _cancelToken.Token).ConfigureAwait(false);
_host = host;
_lastHeartbeat = DateTime.UtcNow;

_runTask = RunTasks();
}
catch
{
await Disconnect().ConfigureAwait(false);
throw;
}
}
protected void CompleteConnect()
{
_state = (int)WebSocketState.Connected;
RaiseConnected();
}
public Task Reconnect()
=> Connect(_host);

public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."));
protected async Task DisconnectInternal(Exception ex, bool isUnexpected = true)
{
int oldState;
bool hasWriterLock;

//If in either connecting or connected state, get a lock by being the first to switch to disconnecting
oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connecting);
if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected
hasWriterLock = oldState == (int)WebSocketState.Connecting; //Caused state change
if (!hasWriterLock)
{
oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connected);
if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected
hasWriterLock = oldState == (int)WebSocketState.Connected; //Caused state change
}

if (hasWriterLock)
{
_wasDisconnectUnexpected = isUnexpected;
_disconnectReason = ExceptionDispatchInfo.Capture(ex);
_cancelToken.Cancel();
}

Task task = _runTask;
if (task != null)
await task.ConfigureAwait(false);

if (hasWriterLock)
{
_state = (int)WebSocketState.Disconnected;
RaiseDisconnected(isUnexpected, ex);
}
}

protected virtual async Task RunTasks()
{
Task[] tasks = Run();
try
{
await Task.WhenAll(tasks).ConfigureAwait(false);
}
catch (Exception ex) { await DisconnectInternal(ex).ConfigureAwait(false); }

bool wasUnexpected = _wasDisconnectUnexpected;
_wasDisconnectUnexpected = false;

await _engine.Disconnect().ConfigureAwait(false);
await Cleanup().ConfigureAwait(false);
_runTask = null;
}
protected virtual Task[] Run() { return _engine.RunTasks(_cancelToken.Token); }
protected virtual Task Cleanup() { return TaskHelper.CompletedTask; }

protected abstract Task ProcessMessage(string json);
protected abstract object GetKeepAlive();
protected void QueueMessage(object message)
{
string json = JsonConvert.SerializeObject(message);
if (_logLevel >= LogMessageSeverity.Debug)
RaiseOnLog(LogMessageSeverity.Debug, $"Out: " + json);
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message));
_engine.QueueMessage(bytes);
}

private Task HeartbeatAsync(CancellationToken cancelToken)
{
return Task.Run(async () =>
{
try
{
while (!cancelToken.IsCancellationRequested)
{
if (_heartbeatInterval > 0)
{
QueueMessage(GetKeepAlive());
await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false);
}
else
await Task.Delay(100, cancelToken);
}
}
catch (OperationCanceledException) { }
});
}

internal void ThrowError()
{
if (_wasDisconnectUnexpected)
_disconnectReason.Throw();
}
}
}

+ 31
- 0
src/Discord.Net/Net/WebSockets/WebSocketMessage.cs View File

@@ -0,0 +1,31 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Discord.Net.WebSockets
{
public class WebSocketMessage
{
[JsonProperty(PropertyName = "op")]
public int Operation;
[JsonProperty(PropertyName = "d")]
public object Payload;
[JsonProperty(PropertyName = "t", NullValueHandling = NullValueHandling.Ignore)]
public string Type;
[JsonProperty(PropertyName = "s", NullValueHandling = NullValueHandling.Ignore)]
public int? Sequence;
}
internal abstract class WebSocketMessage<T> : WebSocketMessage
where T : new()
{
public WebSocketMessage() { Payload = new T(); }
public WebSocketMessage(int op) { Operation = op; Payload = new T(); }
public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; }

[JsonIgnore]
public new T Payload
{
get { if (base.Payload is JToken) { base.Payload = (base.Payload as JToken).ToObject<T>(); } return (T)base.Payload; }
set { base.Payload = value; }
}
}
}

+ 0
- 78
src/Discord.Net/lib/Opus/API.cs View File

@@ -1,78 +0,0 @@
using System;
using System.Runtime.InteropServices;

namespace Discord.Opus
{
internal unsafe class API
{
[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr opus_encoder_create(int Fs, int channels, int application, out Error error);

[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
public static extern void opus_encoder_destroy(IntPtr encoder);

[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
public static extern int opus_encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes);

/*[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr opus_decoder_create(int Fs, int channels, out Errors error);

[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
public static extern void opus_decoder_destroy(IntPtr decoder);

[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
public static extern int opus_decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec);*/

[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
public static extern int opus_encoder_ctl(IntPtr st, Ctl request, int value);

[DllImport("lib/opus", CallingConvention = CallingConvention.Cdecl)]
public static extern int opus_encoder_ctl(IntPtr st, Ctl request, out int value);
}

public enum Ctl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}

/// <summary>Supported coding modes.</summary>
public enum Application : int
{
/// <summary>
/// Gives best quality at a given bitrate for voice signals. It enhances the input signal by high-pass filtering and emphasizing formants and harmonics.
/// Optionally it includes in-band forward error correction to protect against packet loss. Use this mode for typical VoIP applications.
/// Because of the enhancement, even at high bitrates the output may sound different from the input.
/// </summary>
Voip = 2048,
/// <summary>
/// Gives best quality at a given bitrate for most non-voice signals like music.
/// Use this mode for music and mixed (music/voice) content, broadcast, and applications requiring less than 15 ms of coding delay.
/// </summary>
Audio = 2049,
/// <summary> Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay. </summary>
Restricted_LowLatency = 2051
}

public enum Error : int
{
/// <summary> No error. </summary>
OK = 0,
/// <summary> One or more invalid/out of range arguments. </summary>
BadArg = -1,
/// <summary> The mode struct passed is invalid. </summary>
BufferToSmall = -2,
/// <summary> An internal error was detected. </summary>
InternalError = -3,
/// <summary> The compressed data passed is corrupted. </summary>
InvalidPacket = -4,
/// <summary> Invalid/unsupported request number. </summary>
Unimplemented = -5,
/// <summary> An encoder or decoder structure is invalid or already freed. </summary>
InvalidState = -6,
/// <summary> Memory allocation has failed. </summary>
AllocFail = -7
}
}

+ 5
- 3
src/Discord.Net/project.json View File

@@ -1,5 +1,5 @@
{
"version": "0.6.1-beta2",
"version": "0.7.0-beta1",
"description": "An unofficial .Net API wrapper for the Discord client.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
@@ -27,12 +27,14 @@
"frameworks": {
"net45": {
"dependencies": {
"Microsoft.Net.Http": "2.2.29"
"Microsoft.Net.Http": "2.2.29",
"RestSharp": "105.2.3"
}
},
"dnx451": {
"dependencies": {
"Microsoft.Net.Http": "2.2.29"
"Microsoft.Net.Http": "2.2.29",
"RestSharp": "105.2.3"
}
},
"dnxcore50": {


+ 0
- 6
test/Discord.Net.Tests/Discord.Net.Tests.csproj View File

@@ -59,12 +59,6 @@
<Compile Include="Settings.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</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>


Loading…
Cancel
Save