diff --git a/Discord.Net.sln b/Discord.Net.sln
index 1d7cbfd25..b424296b0 100644
--- a/Discord.Net.sln
+++ b/Discord.Net.sln
@@ -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
diff --git a/src/Discord.Net.Commands/DiscordBotClient.cs b/src/Discord.Net.Commands/DiscordBotClient.cs
index 403b002e7..2ccd70828 100644
--- a/src/Discord.Net.Commands/DiscordBotClient.cs
+++ b/src/Discord.Net.Commands/DiscordBotClient.cs
@@ -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
diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json
index 11d1ebda6..49c4ed2b4 100644
--- a/src/Discord.Net.Commands/project.json
+++ b/src/Discord.Net.Commands/project.json
@@ -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": { },
diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj
index 12f47e3b1..3bf5d75c9 100644
--- a/src/Discord.Net.Net45/Discord.Net.csproj
+++ b/src/Discord.Net.Net45/Discord.Net.csproj
@@ -37,42 +37,52 @@
- ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll
+ ..\..\..\DiscordBot\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+ ..\..\..\DiscordBot\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll
True
+
+ PreserveNewest
+
+
+ PreserveNewest
+
-
- API\DiscordAPI.cs
+
+ Audio\Opus.cs
-
- API\Endpoints.cs
+
+ Audio\OpusEncoder.cs
-
- API\Models\APIRequests.cs
+
+ Collections\AsyncCollection.cs
-
- API\Models\APIResponses.cs
+
+ Collections\Channels.cs
-
- API\Models\Common.cs
+
+ Collections\Members.cs
-
- API\Models\TextWebSocketCommands.cs
+
+ Collections\Messages.cs
-
- API\Models\TextWebSocketEvents.cs
+
+ Collections\Roles.cs
-
- API\Models\VoiceWebSocketCommands.cs
+
+ Collections\Servers.cs
-
- API\Models\VoiceWebSocketEvents.cs
+
+ Collections\Users.cs
DiscordClient.API.cs
@@ -86,24 +96,12 @@
DiscordClient.Events.cs
+
+ DiscordClient.Voice.cs
+
DiscordClientConfig.cs
-
- DiscordDataSocket.cs
-
-
- DiscordDataSocket.Events.cs
-
-
- DiscordVoiceSocket.cs
-
-
- DiscordWebSocket.cs
-
-
- DiscordWebSocket.Events.cs
-
Enums\ChannelTypes.cs
@@ -116,29 +114,17 @@
Format.cs
-
- Helpers\AsyncCache.cs
-
Helpers\Extensions.cs
-
- Helpers\JsonHttpClient.cs
-
-
- Helpers\JsonHttpClient.Events.cs
+
+ Helpers\MessageCleaner.cs
Helpers\TaskHelper.cs
-
- HttpException.cs
-
-
- lib\Opus\API.cs
-
-
- lib\Opus\OpusEncoder.cs
+
+ Mention.cs
Models\Channel.cs
@@ -164,6 +150,66 @@
Models\User.cs
+
+ Net\API\Common.cs
+
+
+ Net\API\DiscordAPIClient.cs
+
+
+ Net\API\Endpoints.cs
+
+
+ Net\API\Requests.cs
+
+
+ Net\API\Responses.cs
+
+
+ Net\HttpException.cs
+
+
+ Net\RestClient.BuiltIn.cs
+
+
+ Net\RestClient.cs
+
+
+ Net\RestClient.Events.cs
+
+
+ Net\RestClient.SharpRest.cs
+
+
+ Net\WebSockets\Commands.cs
+
+
+ Net\WebSockets\DataWebSocket.cs
+
+
+ Net\WebSockets\Events.cs
+
+
+ Net\WebSockets\VoiceCommands.cs
+
+
+ Net\WebSockets\VoiceEvents.cs
+
+
+ Net\WebSockets\VoiceWebSocket.cs
+
+
+ Net\WebSockets\WebSocket.BuiltIn.cs
+
+
+ Net\WebSockets\WebSocket.cs
+
+
+ Net\WebSockets\WebSocket.Events.cs
+
+
+ Net\WebSockets\WebSocketMessage.cs
+
diff --git a/src/Discord.Net.Net45/lib/libopus.so b/src/Discord.Net.Net45/lib/libopus.so
new file mode 100644
index 000000000..4d24cdc2b
Binary files /dev/null and b/src/Discord.Net.Net45/lib/libopus.so differ
diff --git a/src/Discord.Net.Net45/lib/opus.dll b/src/Discord.Net.Net45/lib/opus.dll
new file mode 100644
index 000000000..a9eec802c
Binary files /dev/null and b/src/Discord.Net.Net45/lib/opus.dll differ
diff --git a/src/Discord.Net.Net45/packages.config b/src/Discord.Net.Net45/packages.config
index 505e58836..e09901c26 100644
--- a/src/Discord.Net.Net45/packages.config
+++ b/src/Discord.Net.Net45/packages.config
@@ -1,4 +1,5 @@
+
\ No newline at end of file
diff --git a/src/Discord.Net/API/DiscordAPI.cs b/src/Discord.Net/API/DiscordAPI.cs
deleted file mode 100644
index 894c609dd..000000000
--- a/src/Discord.Net/API/DiscordAPI.cs
+++ /dev/null
@@ -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 GetWebSocketEndpoint()
- => _http.Get(Endpoints.Gateway);
- public async Task LoginAnonymous(string username)
- {
- var fingerprintResponse = await _http.Post(Endpoints.AuthFingerprint).ConfigureAwait(false);
- var registerRequest = new APIRequests.AuthRegisterRequest { Fingerprint = fingerprintResponse.Fingerprint, Username = username };
- var registerResponse = await _http.Post(Endpoints.AuthRegister, registerRequest).ConfigureAwait(false);
- return registerResponse;
- }
- public async Task Login(string email, string password)
- {
- var request = new APIRequests.AuthLogin { Email = email, Password = password };
- var response = await _http.Post(Endpoints.AuthLogin, request).ConfigureAwait(false);
- return response;
- }
- public Task Logout()
- => _http.Post(Endpoints.AuthLogout);
-
- //Servers
- public Task CreateServer(string name, string region)
- {
- var request = new APIRequests.CreateServer { Name = name, Region = region };
- return _http.Post(Endpoints.Servers, request);
- }
- public Task LeaveServer(string id)
- => _http.Delete(Endpoints.Server(id));
-
- //Channels
- public Task CreateChannel(string serverId, string name, string channelType)
- {
- var request = new APIRequests.CreateChannel { Name = name, Type = channelType };
- return _http.Post(Endpoints.ServerChannels(serverId), request);
- }
- public Task CreatePMChannel(string myId, string recipientId)
- {
- var request = new APIRequests.CreatePMChannel { RecipientId = recipientId };
- return _http.Post(Endpoints.UserChannels(myId), request);
- }
- public Task DestroyChannel(string channelId)
- => _http.Delete(Endpoints.Channel(channelId));
- public Task GetMessages(string channelId, int count)
- => _http.Get(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 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(Endpoints.ChannelInvites(channelId), request);
- }
- public Task GetInvite(string id)
- => _http.Get(Endpoints.Invite(id));
- public Task AcceptInvite(string id)
- => _http.Post(Endpoints.Invite(id));
- public Task DeleteInvite(string id)
- => _http.Delete(Endpoints.Invite(id));
-
- //Chat
- public Task SendMessage(string channelId, string message, string[] mentions, string nonce)
- {
- var request = new APIRequests.SendMessage { Content = message, Mentions = mentions, Nonce = nonce };
- return _http.Post(Endpoints.ChannelMessages(channelId), request);
- }
- public Task EditMessage(string channelId, string messageId, string message, string[] mentions)
- {
- var request = new APIRequests.EditMessage { Content = message, Mentions = mentions };
- return _http.Patch(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(Endpoints.ChannelMessages(channelId), stream, filename);
-
- //Voice
- public Task GetVoiceRegions()
- => _http.Get(Endpoints.VoiceRegions);
- public Task GetVoiceIce()
- => _http.Get(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 ChangeUsername(string newUsername, string currentEmail, string currentPassword)
- {
- var request = new APIRequests.ChangeUsername { Username = newUsername, CurrentEmail = currentEmail, CurrentPassword = currentPassword };
- return _http.Patch(Endpoints.UserMe, request);
- }
- public Task ChangeEmail(string newEmail, string currentPassword)
- {
- var request = new APIRequests.ChangeEmail { NewEmail = newEmail, CurrentPassword = currentPassword };
- return _http.Patch(Endpoints.UserMe, request);
- }
- public Task ChangePassword(string newPassword, string currentEmail, string currentPassword)
- {
- var request = new APIRequests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword };
- return _http.Patch(Endpoints.UserMe, request);
- }
- public Task 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(Endpoints.UserMe, request);
- }
- }
-}
diff --git a/src/Discord.Net/API/Endpoints.cs b/src/Discord.Net/API/Endpoints.cs
deleted file mode 100644
index ca681408e..000000000
--- a/src/Discord.Net/API/Endpoints.cs
+++ /dev/null
@@ -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}";
- }
-}
diff --git a/src/Discord.Net/API/Models/APIResponses.cs b/src/Discord.Net/API/Models/APIResponses.cs
deleted file mode 100644
index 3f2a1d3e2..000000000
--- a/src/Discord.Net/API/Models/APIResponses.cs
+++ /dev/null
@@ -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;
- }
- }
- }
-}
diff --git a/src/Discord.Net/Audio/Opus.cs b/src/Discord.Net/Audio/Opus.cs
new file mode 100644
index 000000000..b2c550e93
--- /dev/null
+++ b/src/Discord.Net/Audio/Opus.cs
@@ -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
+ }
+
+ /// Supported coding modes.
+ public enum Application : int
+ {
+ ///
+ /// 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.
+ ///
+ Voip = 2048,
+ ///
+ /// 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.
+ ///
+ Audio = 2049,
+ /// Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay.
+ Restricted_LowLatency = 2051
+ }
+
+ public enum Error : int
+ {
+ /// No error.
+ OK = 0,
+ /// One or more invalid/out of range arguments.
+ BadArg = -1,
+ /// The mode struct passed is invalid.
+ BufferToSmall = -2,
+ /// An internal error was detected.
+ InternalError = -3,
+ /// The compressed data passed is corrupted.
+ InvalidPacket = -4,
+ /// Invalid/unsupported request number.
+ Unimplemented = -5,
+ /// An encoder or decoder structure is invalid or already freed.
+ InvalidState = -6,
+ /// Memory allocation has failed.
+ AllocFail = -7
+ }
+ }
+}
diff --git a/src/Discord.Net/lib/Opus/OpusEncoder.cs b/src/Discord.Net/Audio/OpusEncoder.cs
similarity index 79%
rename from src/Discord.Net/lib/Opus/OpusEncoder.cs
rename to src/Discord.Net/Audio/OpusEncoder.cs
index 06db38e49..56f48e3f8 100644
--- a/src/Discord.Net/lib/Opus/OpusEncoder.cs
+++ b/src/Discord.Net/Audio/OpusEncoder.cs
@@ -1,11 +1,11 @@
using System;
-namespace Discord.Opus
+namespace Discord.Audio
{
/// Opus codec wrapper.
- public class OpusEncoder : IDisposable
+ internal class OpusEncoder : IDisposable
{
- private readonly IntPtr _encoder;
+ private readonly IntPtr _encoderPtr;
/// Gets the bit rate of the encoder.
public const int BitRate = 16;
@@ -22,7 +22,7 @@ namespace Discord.Opus
/// Gets the bytes per frame.
public int FrameSize { get; private set; }
/// Gets the coding mode of the encoder.
- public Application Application { get; private set; }
+ public Opus.Application Application { get; private set; }
/// Creates a new Opus encoder.
/// Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.
@@ -30,7 +30,7 @@ namespace Discord.Opus
/// Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60
/// Coding mode.
/// A new OpusEncoder
- 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;
}
/// Gets or sets whether Forward Error Correction is enabled.
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
}
}
\ No newline at end of file
diff --git a/src/Discord.Net/Collections/AsyncCollection.cs b/src/Discord.Net/Collections/AsyncCollection.cs
new file mode 100644
index 000000000..21c5be04d
--- /dev/null
+++ b/src/Discord.Net/Collections/AsyncCollection.cs
@@ -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 : IEnumerable
+ 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 ItemCreated;
+ private void RaiseItemCreated(TValue item)
+ {
+ if (ItemCreated != null)
+ ItemCreated(this, new CollectionItemEventArgs(item));
+ }
+ internal EventHandler ItemUpdated;
+ protected void RaiseItemUpdated(TValue item)
+ {
+ if (ItemUpdated != null)
+ ItemUpdated(this, new CollectionItemEventArgs(item));
+ }
+ internal EventHandler ItemDestroyed;
+ private void RaiseItemDestroyed(TValue item)
+ {
+ if (ItemDestroyed != null)
+ ItemDestroyed(this, new CollectionItemEventArgs(item));
+ }
+
+ protected readonly DiscordClient _client;
+ protected readonly ConcurrentDictionary _dictionary;
+
+ protected AsyncCollection(DiscordClient client)
+ {
+ _client = client;
+ _dictionary = new ConcurrentDictionary();
+ }
+
+ protected TValue Get(string key)
+ {
+ TValue result;
+ if (!_dictionary.TryGetValue(key, out result))
+ return null;
+ return result;
+ }
+ protected TValue GetOrAdd(string key, Func 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 GetEnumerator()
+ {
+ return _dictionary.Select(x => x.Value).GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/src/Discord.Net/Collections/Channels.cs b/src/Discord.Net/Collections/Channels.cs
new file mode 100644
index 000000000..959a12087
--- /dev/null
+++ b/src/Discord.Net/Collections/Channels.cs
@@ -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
+ {
+ 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 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));
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net/Collections/Members.cs b/src/Discord.Net/Collections/Members.cs
new file mode 100644
index 000000000..da9d81c0d
--- /dev/null
+++ b/src/Discord.Net/Collections/Members.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Collections
+{
+ public sealed class Members : AsyncCollection
+ {
+ 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 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);
+ });
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net/Collections/Messages.cs b/src/Discord.Net/Collections/Messages.cs
new file mode 100644
index 000000000..5d17cfd72
--- /dev/null
+++ b/src/Discord.Net/Collections/Messages.cs
@@ -0,0 +1,33 @@
+using Discord.Helpers;
+
+namespace Discord.Collections
+{
+ public sealed class Messages : AsyncCollection
+ {
+ 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);
+ }
+}
diff --git a/src/Discord.Net/Collections/Roles.cs b/src/Discord.Net/Collections/Roles.cs
new file mode 100644
index 000000000..8c63a2200
--- /dev/null
+++ b/src/Discord.Net/Collections/Roles.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Collections
+{
+ public sealed class Roles : AsyncCollection
+ {
+ 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 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));
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net/Collections/Servers.cs b/src/Discord.Net/Collections/Servers.cs
new file mode 100644
index 000000000..85e243c6e
--- /dev/null
+++ b/src/Discord.Net/Collections/Servers.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Collections
+{
+ public sealed class Servers : AsyncCollection
+ {
+ 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 Find(string name)
+ {
+ if (name == null) throw new ArgumentNullException(nameof(name));
+
+ return this.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/src/Discord.Net/Collections/Users.cs b/src/Discord.Net/Collections/Users.cs
new file mode 100644
index 000000000..14da56c6b
--- /dev/null
+++ b/src/Discord.Net/Collections/Users.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Collections
+{
+ public sealed class Users : AsyncCollection
+ {
+ 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 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));
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net/DiscordClient.API.cs b/src/Discord.Net/DiscordClient.API.cs
index e160d6215..9a3a5b8d4 100644
--- a/src/Discord.Net/DiscordClient.API.cs
+++ b/src/Discord.Net/DiscordClient.API.cs
@@ -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
/// Creates a new server with the provided name and region (see Regions).
public async Task 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;
}
/// Leaves the provided server, destroying it if you are the owner.
@@ -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;
+ }
+ /// Returns the private channel with the provided user, creating one if it does not currently exist.
+ public Task CreatePMChannel(string userId) => CreatePMChannel(_users[userId], userId);
+ /// Returns the private channel with the provided user, creating one if it does not currently exist.
+ public Task CreatePMChannel(User user) => CreatePMChannel(user, user?.Id);
+ private async Task 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;
}
/// Destroys the provided channel.
@@ -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;
}
- /// Gets more info about the provided invite.
+ /// Gets more info about the provided invite code.
/// Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode
public async Task 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;
}
/// Accepts the provided invite.
@@ -181,34 +193,33 @@ namespace Discord
return _api.AcceptInvite(invite.Id);
}
/// Accepts the provided invite.
- 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);
}
/// Deletes the provided invite.
- 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
}
/// Sends a private message to the provided channel.
- public async Task SendPrivateMessage(User user, string text)
- {
- var channel = await GetPMChannel(user).ConfigureAwait(false);
- return await SendMessage(channel, text, new string[0]).ConfigureAwait(false);
- }
+ public Task SendPrivateMessage(User user, string text)
+ => SendPrivateMessage(user?.Id, text);
/// Sends a private message to the provided channel.
public async Task 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);
}
/// Deletes the provided message.
@@ -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) { }
}
}
-
+
/// Sends a file to the provided channel.
- public Task SendFile(Channel channel, string path)
- => SendFile(channel?.Id, path);
+ public Task SendFile(Channel channel, string filePath)
+ => SendFile(channel?.Id, filePath);
/// Sends a file to the provided channel.
- public Task SendFile(string channelId, string path)
- {
- if (path == null) throw new ArgumentNullException(nameof(path));
- return SendFile(channelId, File.OpenRead(path), Path.GetFileName(path));
- }
- /// Reads a stream and sends it to the provided channel as a file.
- /// It is highly recommended that this stream be cached in memory or on disk, or the request may time out.
- public Task SendFile(Channel channel, Stream stream, string filename = null)
- => SendFile(channel?.Id, stream, filename);
- /// Reads a stream and sends it to the provided channel as a file.
- /// It is highly recommended that this stream be cached in memory or on disk, or the request may time out.
- 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);
}
/// Downloads last count messages from the server, starting at beforeMessageId if it's provided.
@@ -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);
}
/// Changes your email to newEmail.
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);
}
/// Changes your password to newPassword.
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);
}
/// Changes your avatar.
@@ -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);
}
}
}
diff --git a/src/Discord.Net/DiscordClient.Cache.cs b/src/Discord.Net/DiscordClient.Cache.cs
index 515eb18ed..1416de4cb 100644
--- a/src/Discord.Net/DiscordClient.Cache.cs
+++ b/src/Discord.Net/DiscordClient.Cache.cs
@@ -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
{
- /// Returns a collection of all users the client can see across all servers.
- /// This collection does not guarantee any ordering.
- public IEnumerable Users => _users;
- internal AsyncCache _users;
-
- /// Returns a collection of all servers the client is a member of.
- /// This collection does not guarantee any ordering.
- public IEnumerable Servers => _servers;
- internal AsyncCache _servers;
-
- /// Returns a collection of all channels the client can see across all servers.
- /// This collection does not guarantee any ordering.
- public IEnumerable Channels => _channels;
- internal AsyncCache _channels;
-
- /// Returns a collection of all messages the client has in cache.
- /// This collection does not guarantee any ordering.
- public IEnumerable Messages => _messages;
- internal AsyncCache _messages;
-
- /// Returns a collection of all roles the client can see across all servers.
- /// This collection does not guarantee any ordering.
- public IEnumerable Roles => _roles;
- internal AsyncCache _roles;
-
- private void CreateCaches()
- {
- _servers = new AsyncCache(
- (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(
- (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(
- (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(
- (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(
- (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}).");
- }
- );
- }
-
- /// Returns the user with the specified id, or null if none was found.
- public User GetUser(string id)
- => _users[id];
- /// Returns the user with the specified name and discriminator, or null if none was found.
- /// Name formats supported: Name and @Name. Search is case-insensitive.
- 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();
- }
- /// Returns all users with the specified name across all servers.
- /// Name formats supported: Name and @Name. Search is case-insensitive.
- public IEnumerable 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));
- }
- }
+ /// Returns the channel with the specified id, or null if none was found.
+ public Channel GetChannel(string id) => _channels[id];
+ /// Returns all channels with the specified server and name.
+ /// Name formats supported: Name and #Name. Search is case-insensitive.
+ public IEnumerable FindChannels(Server server, string name) => _channels.Find(server?.Id, name);
+ /// Returns all channels with the specified server and name.
+ /// Name formats supported: Name and #Name. Search is case-insensitive.
+ public IEnumerable FindChannels(string serverId, string name) => _channels.Find(serverId, name);
/// Returns the user with the specified id, along with their server-specific data, or null if none was found.
- public Member GetMember(string serverId, User user)
- => GetMember(_servers[serverId], user?.Id);
+ public Member GetMember(string serverId, User user) => _members[user?.Id, serverId];
/// Returns the user with the specified id, along with their server-specific data, or null if none was found.
- public Member GetMember(string serverId, string userId)
- => GetMember(_servers[serverId], userId);
+ public Member GetMember(Server server, User user) => _members[user?.Id, server?.Id];
/// Returns the user with the specified id, along with their server-specific data, or null if none was found.
- public Member GetMember(Server server, User user)
- => GetMember(server, user?.Id);
+ public Member GetMember(Server server, string userId) => _members[userId, server?.Id];
/// Returns the user with the specified id, along with their server-specific data, or null if none was found.
- 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];
/// Returns all users in with the specified server and name, along with their server-specific data.
/// Name formats supported: Name and @Name. Search is case-insensitive.
- public IEnumerable FindMembers(string serverId, string name)
- => FindMembers(GetServer(serverId), name);
+ public IEnumerable FindMembers(Server server, string name) => _members.Find(server, name);
/// Returns all users in with the specified server and name, along with their server-specific data.
/// Name formats supported: Name and @Name. Search is case-insensitive.
- public IEnumerable 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);
- });
- }
- }
-
- /// Returns the server with the specified id, or null if none was found.
- public Server GetServer(string id)
- => _servers[id];
- /// Returns all servers with the specified name.
- /// Search is case-insensitive.
- public IEnumerable FindServers(string name)
- {
- if (name == null)
- return new Server[0];
- return _servers.Where(x =>
- string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
- }
-
- /// Returns the channel with the specified id, or null if none was found.
- public Channel GetChannel(string id) => _channels[id];
- /// Returns the private channel with the provided user, creating one if it does not currently exist.
- public Task GetPMChannel(string userId)
- => GetPMChannel(_users[userId]);
- /// Returns the private channel with the provided user, creating one if it does not currently exist.
- public async Task 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 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 FindMembers(string serverId, string name) => _members.Find(_servers[serverId], name);
- /// Returns all channels with the specified server and name.
- /// Name formats supported: Name and #Name. Search is case-insensitive.
- public IEnumerable FindChannels(Server server, string name)
- => FindChannels(server?.Id, name);
- /// Returns all channels with the specified server and name.
- /// Name formats supported: Name and #Name. Search is case-insensitive.
- public IEnumerable 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));
- }
- }
+ /// Returns the message with the specified id, or null if none was found.
+ public Message GetMessage(string id) => _messages[id];
/// Returns the role with the specified id, or null if none was found.
- public Role GetRole(string id)
- => _roles[id];
+ public Role GetRole(string id) => _roles[id];
/// Returns all roles with the specified server and name.
/// Name formats supported: Name and @Name. Search is case-insensitive.
- public IEnumerable FindRoles(Server server, string name)
- => FindRoles(server?.Id, name);
+ public IEnumerable FindRoles(Server server, string name) => _roles.Find(server?.Id, name);
/// Returns all roles with the specified server and name.
/// Name formats supported: Name and @Name. Search is case-insensitive.
- public IEnumerable FindRoles(string serverId, string name)
- {
- if (serverId == null || name == null)
- return new Role[0];
+ public IEnumerable 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));
- }
- }
+ /// Returns the server with the specified id, or null if none was found.
+ public Server GetServer(string id) => _servers[id];
+ /// Returns all servers with the specified name.
+ /// Search is case-insensitive.
+ public IEnumerable FindServers(string name) => _servers.Find(name);
+
+ /// Returns the user with the specified id, or null if none was found.
+ public User GetUser(string id) => _users[id];
+ /// Returns the user with the specified name and discriminator, or null if none was found.
+ /// Name formats supported: Name and @Name. Search is case-insensitive.
+ public User GetUser(string name, string discriminator) => _users[name, discriminator];
+ /// Returns all users with the specified name across all servers.
+ /// Name formats supported: Name and @Name. Search is case-insensitive.
+ public IEnumerable FindUsers(string name) => _users.Find(name);
- /// Returns the message with the specified id, or null if none was found.
- public Message GetMessage(string id)
- => _messages[id];
}
}
diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs
index 18e586b21..c3b9fa965 100644
--- a/src/Discord.Net/DiscordClient.Events.cs
+++ b/src/Discord.Net/DiscordClient.Events.cs
@@ -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 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 LogMessage;
+ internal void RaiseOnLog(LogMessageSeverity severity, LogMessageSource source, string message)
+ {
+ if (LogMessage != null)
+ LogMessage(this, new LogMessageEventArgs(severity, source, message));
+ }
//Server
public event EventHandler 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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));
}
diff --git a/src/Discord.Net/DiscordClient.Voice.cs b/src/Discord.Net/DiscordClient.Voice.cs
new file mode 100644
index 000000000..62e59cb74
--- /dev/null
+++ b/src/Discord.Net/DiscordClient.Voice.cs
@@ -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();
+ }
+
+ /// Sends a PCM frame to the voice server.
+ /// PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames.
+ /// Number of bytes in this frame.
+ /// Will block until
+ public void SendVoicePCM(byte[] data, int count)
+ {
+ CheckReady(checkVoice: true);
+ if (count == 0) return;
+ _voiceSocket.SendPCMFrames(data, count);
+ }
+
+ /// Clears the PCM buffer.
+ public void ClearVoicePCM()
+ {
+ CheckReady(checkVoice: true);
+ _voiceSocket.ClearPCMFrames();
+ }
+
+ /// Returns a task that completes once the voice output buffer is empty.
+ public async Task WaitVoice()
+ {
+ CheckReady(checkVoice: true);
+ _voiceSocket.Wait();
+ await TaskHelper.CompletedTask.ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs
index 2229d35bc..0c1baf1a4 100644
--- a/src/Discord.Net/DiscordClient.cs
+++ b/src/Discord.Net/DiscordClient.cs
@@ -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
+ }
+
/// Provides a connection to the DiscordApp service.
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 _pendingMessages;
+ private readonly ManualResetEvent _disconnectedEvent;
+ private readonly ManualResetEventSlim _connectedEvent;
+ private Task _runTask;
+ protected ExceptionDispatchInfo _disconnectReason;
+ private bool _wasDisconnectUnexpected;
+
+ /// Returns the id of the current logged-in user.
+ public string CurrentUserId => _currentUserId;
+ private string _currentUserId;
+ /// Returns the current logged-in user.
+ 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;
-
- /// Returns the User object for the current logged in user.
- public User User => _user;
- private User _user;
-
- /// Returns true if the user has successfully logged in and the websocket connection has been established.
- public bool IsConnected => _isConnected;
- private bool _isConnected, _isDisconnecting;
-
- /// Returns true if this client was requested to disconnect.
- public bool IsClosing => _disconnectToken.IsCancellationRequested;
- /// Returns a cancel token that is triggered when a disconnect is requested.
- 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
/// Initializes a new instance of the DiscordClient class.
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();
-
- _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(_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(_serializer);
- var server = _servers.Update(data.Id, data);
- try { RaiseServerCreated(server); } catch { }
- }
- break;
- case "GUILD_UPDATE":
- {
- var data = e.Event.ToObject(_serializer);
- var server = _servers.Update(data.Id, data);
- try { RaiseServerUpdated(server); } catch { }
- }
- break;
- case "GUILD_DELETE":
- {
- var data = e.Event.ToObject(_serializer);
- var server = _servers.Remove(data.Id);
- if (server != null)
- try { RaiseServerDestroyed(server); } catch { }
- }
- break;
-
- //Channels
- case "CHANNEL_CREATE":
- {
- var data = e.Event.ToObject(_serializer);
- var channel = _channels.Update(data.Id, data.GuildId, data);
- try { RaiseChannelCreated(channel); } catch { }
- }
- break;
- case "CHANNEL_UPDATE":
- {
- var data = e.Event.ToObject(_serializer);
- var channel = _channels.Update(data.Id, data.GuildId, data);
- try { RaiseChannelUpdated(channel); } catch { }
- }
- break;
- case "CHANNEL_DELETE":
- {
- var data = e.Event.ToObject(_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(_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(_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(_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(_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(_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(_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(_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(_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(_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(_serializer);
- var msg = _messages.Update(data.Id, data.ChannelId, data);
- try { RaiseMessageUpdated(msg); } catch { }
- }
- break;
- case "MESSAGE_DELETE":
- {
- var data = e.Event.ToObject(_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(_serializer);
- var msg = GetMessage(data.MessageId);
- if (msg != null)
- try { RaiseMessageRead(msg); } catch { }
- }
- break;
-
- //Statuses
- case "PRESENCE_UPDATE":
- {
- var data = e.Event.ToObject(_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(_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(_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(_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(_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();
+ }
- 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);
+ }
/// Connects to the Discord server with the provided email and password.
/// Returns a token for future connections.
public async Task 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 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();
}
+
/// Disconnects from the Discord server, canceling any pending requests.
- 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.");
}
-
- /// Sends a PCM frame to the voice server.
- /// PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames.
- /// Number of bytes in this frame.
- /// Will block until
- public void SendVoicePCM(byte[] data, int count)
+ /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications.
+ 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();
}
- /// Clears the PCM buffer.
- 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;
- /// Returns a task that completes once the voice output buffer is empty.
- 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;
- }
-
- /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications.
- public void Block()
- {
- if (_isConnected)
- _disconnectEvent.WaitOne();
- }
}
}
diff --git a/src/Discord.Net/DiscordClientConfig.cs b/src/Discord.Net/DiscordClientConfig.cs
index bf6ff8715..900ee4b4a 100644
--- a/src/Discord.Net/DiscordClientConfig.cs
+++ b/src/Discord.Net/DiscordClientConfig.cs
@@ -1,31 +1,51 @@
-namespace Discord
+using System;
+
+namespace Discord
{
- public class DiscordClientConfig
+ public class DiscordClientConfig
{
-#if !DNXCORE50
- /// Enables the voice websocket and UDP client (Experimental!). This option requires the opus .dll or .so be in the local lib/ folder.
- public bool EnableVoice { get; set; } = false;
-#endif
- /// Enables the verbose DebugMessage event handler. May hinder performance but should help debug any issues.
- public bool EnableDebug { get; set; } = false;
+ /// 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.
+ public LogMessageSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } }
+ private LogMessageSeverity _logLevel = LogMessageSeverity.Info;
- /// Max time in milliseconds to wait for the web socket to connect.
- public int ConnectionTimeout { get; set; } = 10000;
- /// Max time in milliseconds to wait for the voice web socket to connect.
- public int VoiceConnectionTimeout { get; set; } = 10000;
+ /// Max time in milliseconds to wait for DiscordClient to connect and initialize.
+ public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } }
+ private int _connectionTimeout = 10000;
/// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting.
- public int ReconnectDelay { get; set; } = 1000;
+ public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } }
+ private int _reconnectDelay = 1000;
/// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying.
- public int FailedReconnectDelay { get; set; } = 10000;
+ public int FailedReconnectDelay { get { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } }
+ private int _failedReconnectDelay = 10000;
+
/// Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again.
- public int WebSocketInterval { get; set; } = 100;
- /// 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.
- public bool UseMessageQueue { get; set; } = false;
+ public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } }
+ private int _webSocketInterval = 100;
/// Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again.
- public int MessageQueueInterval { get; set; } = 100;
+ public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } }
+ private int _messageQueueInterval = 100;
/// 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.
- 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
+ /// (Experimental) Enables the voice websocket and UDP client (Experimental!). This option requires the opus .dll or .so be in the local lib/ folder.
+ public bool EnableVoice { get { return _enableVoice; } set { SetValue(ref _enableVoice, value); } }
+ private bool _enableVoice = false;
+#endif
+ /// (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.
+ 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(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;
+ }
}
}
diff --git a/src/Discord.Net/DiscordDataSocket.Events.cs b/src/Discord.Net/DiscordDataSocket.Events.cs
deleted file mode 100644
index f0d8188c2..000000000
--- a/src/Discord.Net/DiscordDataSocket.Events.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Newtonsoft.Json.Linq;
-using System;
-
-namespace Discord
-{
- internal partial class DiscordDataSocket
- {
- public event EventHandler 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));
- }
- }
-}
diff --git a/src/Discord.Net/DiscordDataSocket.cs b/src/Discord.Net/DiscordDataSocket.cs
deleted file mode 100644
index ec9331d15..000000000
--- a/src/Discord.Net/DiscordDataSocket.cs
+++ /dev/null
@@ -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(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();
- _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();
- 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;
- }
- }
-}
diff --git a/src/Discord.Net/DiscordWebSocket.Events.cs b/src/Discord.Net/DiscordWebSocket.Events.cs
deleted file mode 100644
index 607820e7d..000000000
--- a/src/Discord.Net/DiscordWebSocket.Events.cs
+++ /dev/null
@@ -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 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 Disconnected;
- private void RaiseDisconnected(bool wasUnexpected)
- {
- if (Disconnected != null)
- Disconnected(this, new DisconnectedEventArgs(wasUnexpected));
- }
- }
-}
diff --git a/src/Discord.Net/DiscordWebSocket.cs b/src/Discord.Net/DiscordWebSocket.cs
deleted file mode 100644
index 0c577ba13..000000000
--- a/src/Discord.Net/DiscordWebSocket.cs
+++ /dev/null
@@ -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 _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();
- }
-
- 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(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(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
- }
-}
diff --git a/src/Discord.Net/Enums/Regions.cs b/src/Discord.Net/Enums/Regions.cs
index 087b2c552..ea38a6bd7 100644
--- a/src/Discord.Net/Enums/Regions.cs
+++ b/src/Discord.Net/Enums/Regions.cs
@@ -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";
- }
+ }
}
diff --git a/src/Discord.Net/Enums/UserStatus.cs b/src/Discord.Net/Enums/UserStatus.cs
index 843345b38..76bf7f163 100644
--- a/src/Discord.Net/Enums/UserStatus.cs
+++ b/src/Discord.Net/Enums/UserStatus.cs
@@ -1,6 +1,6 @@
namespace Discord
{
- public static class UserStatus
+ public static class UserStatus
{
/// User is currently online and active.
public const string Online = "online";
diff --git a/src/Discord.Net/Format.cs b/src/Discord.Net/Format.cs
index 9b8730696..cdee8e473 100644
--- a/src/Discord.Net/Format.cs
+++ b/src/Discord.Net/Format.cs
@@ -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!
/// Removes all special formatting characters from the provided text.
private static string Escape(string text)
@@ -21,27 +21,13 @@ namespace Discord
{
builder.Insert(i, '\\');
length++;
- }
- }
+ }
+ }
}
return builder.ToString();
}
return text;
- }
-
- /// Returns the string used to create a user mention.
- public static string User(User user)
- => $"<@{user.Id}>";
- /// Returns the string used to create a user mention.
- public static string User(string userId)
- => $"<@{userId}>";
-
- /// Returns the string used to create a channel mention.
- public static string Channel(Channel channel)
- => $"<#{channel.Id}>";
- /// Returns the string used to create a channel mention.
- public static string Channel(string channelId)
- => $"<#{channelId}>";
+ }
/// Returns a markdown-formatted string with no formatting, optionally escaping the contents.
public static string Normal(string text, bool escape = true)
diff --git a/src/Discord.Net/Helpers/AsyncCache.cs b/src/Discord.Net/Helpers/AsyncCache.cs
deleted file mode 100644
index 46a4fa3b1..000000000
--- a/src/Discord.Net/Helpers/AsyncCache.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-
-namespace Discord.Helpers
-{
- public class AsyncCache : IEnumerable
- where TValue : class
- where TModel : class
- {
- protected readonly ConcurrentDictionary _dictionary;
- private readonly Func _onCreate;
- private readonly Action _onUpdate;
- private readonly Action _onRemove;
-
- public AsyncCache(Func onCreate, Action onUpdate, Action onRemove = null)
- {
- _dictionary = new ConcurrentDictionary();
- _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 GetEnumerator()
- {
- return _dictionary.Values.GetEnumerator();
- }
- IEnumerator IEnumerable.GetEnumerator()
- {
- return _dictionary.Values.GetEnumerator();
- }
- }
-}
diff --git a/src/Discord.Net/Helpers/Extensions.cs b/src/Discord.Net/Helpers/Extensions.cs
index cdbe47284..74af2c3f2 100644
--- a/src/Discord.Net/Helpers/Extensions.cs
+++ b/src/Discord.Net/Helpers/Extensions.cs
@@ -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
}
}
}
diff --git a/src/Discord.Net/Helpers/JsonHttpClient.Events.cs b/src/Discord.Net/Helpers/JsonHttpClient.Events.cs
deleted file mode 100644
index 02c9afd99..000000000
--- a/src/Discord.Net/Helpers/JsonHttpClient.Events.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-
-namespace Discord.Helpers
-{
- internal partial class JsonHttpClient
- {
- public event EventHandler OnDebugMessage;
- protected void RaiseOnDebugMessage(DebugMessageType type, string message)
- {
- if (OnDebugMessage != null)
- OnDebugMessage(this, new LogMessageEventArgs(type, message));
- }
- }
-}
diff --git a/src/Discord.Net/Helpers/JsonHttpClient.cs b/src/Discord.Net/Helpers/JsonHttpClient.cs
deleted file mode 100644
index 95772d82a..000000000
--- a/src/Discord.Net/Helpers/JsonHttpClient.cs
+++ /dev/null
@@ -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 Get(string path)
- where ResponseT : class
- => Send(HttpMethod.Get, path, null);
- internal Task Get(string path)
- => Send(HttpMethod.Get, path, null);
-
- internal Task Post(string path, object data)
- where ResponseT : class
- => Send(HttpMethod.Post, path, AsJson(data));
- internal Task Post(string path, object data)
- => Send(HttpMethod.Post, path, AsJson(data));
- internal Task Post(string path)
- where ResponseT : class
- => Send(HttpMethod.Post, path, null);
- internal Task Post(string path)
- => Send(HttpMethod.Post, path, null);
-
- internal Task Put(string path, object data)
- where ResponseT : class
- => Send(HttpMethod.Put, path, AsJson(data));
- internal Task Put(string path, object data)
- => Send(HttpMethod.Put, path, AsJson(data));
- internal Task Put(string path)
- where ResponseT : class
- => Send(HttpMethod.Put, path, null);
- internal Task Put(string path)
- => Send(HttpMethod.Put, path, null);
-
- internal Task Patch(string path, object data)
- where ResponseT : class
- => Send(_patch, path, AsJson(data));
- internal Task Patch(string path, object data)
- => Send(_patch, path, AsJson(data));
- internal Task Patch(string path)
- where ResponseT : class
- => Send(_patch, path, null);
- internal Task Patch(string path)
- => Send(_patch, path, null);
-
- internal Task Delete(string path, object data)
- where ResponseT : class
- => Send(HttpMethod.Delete, path, AsJson(data));
- internal Task Delete(string path, object data)
- => Send(HttpMethod.Delete, path, AsJson(data));
- internal Task Delete(string path)
- where ResponseT : class
- => Send(HttpMethod.Delete, path, null);
- internal Task Delete(string path)
- => Send(HttpMethod.Delete, path, null);
-
- internal Task File(string path, Stream stream, string filename = null)
- where ResponseT : class
- => Send(HttpMethod.Post, path, AsFormData(stream, filename));
- internal Task File(string path, Stream stream, string filename = null)
- => Send(HttpMethod.Post, path, AsFormData(stream, filename));
-
- private async Task Send(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(responseJson, _settings);
-#endif
- return JsonConvert.DeserializeObject(responseJson);
- }
-#if TEST_RESPONSES
- private async Task 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 Send(HttpMethod method, string path, HttpContent content)
- => SendRequest(method, path, content, false);
-#endif
-
- private async Task 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;
- }
- }
-}
diff --git a/src/Discord.Net/Helpers/MessageCleaner.cs b/src/Discord.Net/Helpers/MessageCleaner.cs
new file mode 100644
index 000000000..624760bfc
--- /dev/null
+++ b/src/Discord.Net/Helpers/MessageCleaner.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/Discord.Net/Helpers/TaskHelper.cs b/src/Discord.Net/Helpers/TaskHelper.cs
index 31d902465..9c2fd2c77 100644
--- a/src/Discord.Net/Helpers/TaskHelper.cs
+++ b/src/Discord.Net/Helpers/TaskHelper.cs
@@ -2,8 +2,8 @@
namespace Discord.Helpers
{
- internal static class TaskHelper
- {
+ internal static class TaskHelper
+ {
public static Task CompletedTask { get; }
static TaskHelper()
{
diff --git a/src/Discord.Net/Mention.cs b/src/Discord.Net/Mention.cs
new file mode 100644
index 000000000..f80284b6c
--- /dev/null
+++ b/src/Discord.Net/Mention.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ public static class Mention
+ {
+ /// Returns the string used to create a user mention.
+ public static string User(User user)
+ => $"<@{user.Id}>";
+ /// Returns the string used to create a user mention.
+ public static string User(string userId)
+ => $"<@{userId}>";
+
+ /// Returns the string used to create a channel mention.
+ public static string Channel(Channel channel)
+ => $"<#{channel.Id}>";
+ /// Returns the string used to create a channel mention.
+ public static string Channel(string channelId)
+ => $"<#{channelId}>";
+ }
+}
diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs
index 880a35ca3..f732ef584 100644
--- a/src/Discord.Net/Models/Channel.cs
+++ b/src/Discord.Net/Models/Channel.cs
@@ -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 _messages;
/// Returns the unique identifier for this channel.
public string Id { get; }
private string _name;
/// Returns the name of this channel.
- 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; } }
/// Returns the position of this channel in the channel list for this server.
public int Position { get; internal set; }
/// Returns false is this is a public chat and true if this is a private chat with another user (see Recipient).
- public bool IsPrivate { get; }
+ public bool IsPrivate => ServerId == null;
/// Returns the type of this channel (see ChannelTypes).
public string Type { get; internal set; }
@@ -34,33 +37,70 @@ namespace Discord
public string ServerId { get; }
/// Returns the server containing this channel.
[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];
- /// Returns a collection of all messages the client has in cache.
+ /// Returns a collection of the ids of all messages the client has seen posted in this channel.
+ /// This collection does not guarantee any ordering.
+ [JsonIgnore]
+ public IEnumerable MessageIds => _messages.Select(x => x.Key);
+ /// Returns a collection of all messages the client has seen posted in this channel.
/// This collection does not guarantee any ordering.
[JsonIgnore]
- public IEnumerable Messages => _client.Messages.Where(x => x.ChannelId == Id);
+ public IEnumerable Messages => _messages.Select(x => _client.Messages[x.Key]);
/// Returns a collection of all custom permissions used for this channel.
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();
}
- 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);
}
}
}
diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs
index ba5170aea..f01838f9a 100644
--- a/src/Discord.Net/Models/Invite.cs
+++ b/src/Discord.Net/Models/Invite.cs
@@ -1,4 +1,5 @@
-using Newtonsoft.Json;
+using Discord.Net.API;
+using Newtonsoft.Json;
namespace Discord
{
@@ -8,7 +9,7 @@ namespace Discord
/// Returns the unique identifier for this invite.
public string Id { get; }
-
+
/// Time (in seconds) until the invite expires. Set to 0 to never expire.
public int MaxAge { get; internal set; }
/// The amount of times this invite has been used.
@@ -23,31 +24,51 @@ namespace Discord
public string XkcdPass { get; }
/// Returns a URL for this invite using XkcdPass if available or Id if not.
- public string Url => API.Endpoints.InviteUrl(XkcdPass ?? Id);
+ public string Url => Endpoints.InviteUrl(XkcdPass ?? Id);
/// Returns the id of the user that created this invite.
public string InviterId { get; internal set; }
/// Returns the user that created this invite.
[JsonIgnore]
- public User Inviter => _client.GetUser(InviterId);
+ public User Inviter => _client.Users[InviterId];
/// Returns the id of the server this invite is to.
public string ServerId { get; internal set; }
/// Returns the server this invite is to.
[JsonIgnore]
- public Server Server => _client.GetServer(ServerId);
+ public Server Server => _client.Servers[ServerId];
/// Returns the id of the channel this invite is to.
public string ChannelId { get; internal set; }
/// Returns the channel this invite is to.
[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;
+ }
}
}
diff --git a/src/Discord.Net/Models/Member.cs b/src/Discord.Net/Models/Member.cs
index 686d1b88f..ce1777915 100644
--- a/src/Discord.Net/Models/Member.cs
+++ b/src/Discord.Net/Models/Member.cs
@@ -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
/// Returns the current status for this user.
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 Roles => RoleIds.Select(x => _client.GetRole(x));
+ [JsonIgnore]
+ public IEnumerable Roles => RoleIds.Select(x => _client.Roles[x]);
+
+ /// Returns a collection of all messages this user has sent on this server that are still in cache.
+ public IEnumerable 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;
}
- }
+ }
}
diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs
index b66078b8b..3760b85f5 100644
--- a/src/Discord.Net/Models/Message.cs
+++ b/src/Discord.Net/Models/Message.cs
@@ -54,7 +54,7 @@ namespace Discord
private readonly DiscordClient _client;
private string _cleanText;
-
+
/// Returns the global unique identifier for this message.
public string Id { get; internal set; }
/// Returns the local unique identifier for this message.
@@ -75,7 +75,7 @@ namespace Discord
public string RawText { get; internal set; }
/// Returns the content of this message with any special references such as mentions converted.
/// This value is lazy loaded and only processed on first request. Each subsequent request will pull from cache.
- public string Text => _cleanText != null ? _cleanText : (_cleanText = _client.CleanMessageText(RawText));
+ public string Text => _cleanText != null ? _cleanText : (_cleanText = _client.Messages.CleanText(RawText));
/// Returns the timestamp for when this message was sent.
public DateTime Timestamp { get; internal set; }
/// Returns the timestamp for when this message was last edited.
@@ -89,40 +89,108 @@ namespace Discord
public string[] MentionIds { get; internal set; }
/// Returns a collection of all users mentioned in this message.
[JsonIgnore]
- public IEnumerable Mentions => MentionIds.Select(x => _client.GetUser(x)).Where(x => x != null);
+ public IEnumerable Mentions => MentionIds.Select(x => _client.Users[x]).Where(x => x != null);
/// Returns the id of the server containing the channel this message was sent to.
public string ServerId => Channel.ServerId;
/// Returns the server containing the channel this message was sent to.
[JsonIgnore]
- public Server Server => _client.GetServer(Channel.ServerId);
+ public Server Server => _client.Servers[Channel.ServerId];
/// Returns the id of the channel this message was sent to.
public string ChannelId { get; }
/// Returns the channel this message was sent to.
[JsonIgnore]
- public Channel Channel => _client.GetChannel(ChannelId);
+ public Channel Channel => _client.Channels[ChannelId];
+ /// Returns true if the current user created this message.
+ public bool IsAuthor => _client.CurrentUserId == UserId;
/// Returns the id of the author of this message.
public string UserId { get; internal set; }
/// Returns the author of this message.
[JsonIgnore]
- public User User => _client.GetUser(UserId);
+ public User User => _client.Users[UserId];
+ /// Returns the author of this message.
[JsonIgnore]
- public Member Member => _client.GetMember(ServerId, UserId);
- /// Returns true if the current user created this message.
- 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;
+ }
}
diff --git a/src/Discord.Net/Models/PackedPermissions.cs b/src/Discord.Net/Models/PackedPermissions.cs
index 1d6b7ba02..f16a49e4a 100644
--- a/src/Discord.Net/Models/PackedPermissions.cs
+++ b/src/Discord.Net/Models/PackedPermissions.cs
@@ -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; }
/// If True, a user may create invites.
- public bool General_CreateInstantInvite => ((_rawValue >> 0) & 0x1) == 1;
+ public bool General_CreateInstantInvite { get { return GetBit(1); } set { SetBit(1, value); } }
/// If True, a user may ban users from the server.
- public bool General_BanMembers => ((_rawValue >> 1) & 0x1) == 1;
+ public bool General_BanMembers { get { return GetBit(2); } set { SetBit(2, value); } }
/// If True, a user may kick users from the server.
- public bool General_KickMembers => ((_rawValue >> 2) & 0x1) == 1;
- /// If True, a user may adjust roles. This also bypasses all other permissions, granting all the others.
- public bool General_ManageRoles => ((_rawValue >> 3) & 0x1) == 1;
+ public bool General_KickMembers { get { return GetBit(3); } set { SetBit(3, value); } }
+ /// If True, a user may adjust roles. This also implictly grants all other permissions.
+ public bool General_ManageRoles { get { return GetBit(4); } set { SetBit(4, value); } }
/// If True, a user may create, delete and modify channels.
- public bool General_ManageChannels => ((_rawValue >> 4) & 0x1) == 1;
+ public bool General_ManageChannels { get { return GetBit(5); } set { SetBit(5, value); } }
/// If True, a user may adjust server properties.
- public bool General_ManageServer => ((_rawValue >> 5) & 0x1) == 1;
+ public bool General_ManageServer { get { return GetBit(6); } set { SetBit(6, value); } }
//4 Unused
/// If True, a user may join channels.
- /// Note that without this permission, a channel is not sent by the server.
- public bool Text_ReadMessages => ((_rawValue >> 10) & 0x1) == 1;
+ public bool Text_ReadMessages { get { return GetBit(11); } set { SetBit(11, value); } }
/// If True, a user may send messages.
- public bool Text_SendMessages => ((_rawValue >> 11) & 0x1) == 1;
+ public bool Text_SendMessages { get { return GetBit(12); } set { SetBit(12, value); } }
/// If True, a user may send text-to-speech messages.
- public bool Text_SendTTSMessages => ((_rawValue >> 12) & 0x1) == 1;
+ public bool Text_SendTTSMessages { get { return GetBit(13); } set { SetBit(13, value); } }
/// If True, a user may delete messages.
- public bool Text_ManageMessages => ((_rawValue >> 13) & 0x1) == 1;
+ public bool Text_ManageMessages { get { return GetBit(14); } set { SetBit(14, value); } }
/// If True, Discord will auto-embed links sent by this user.
- public bool Text_EmbedLinks => ((_rawValue >> 14) & 0x1) == 1;
+ public bool Text_EmbedLinks { get { return GetBit(15); } set { SetBit(15, value); } }
/// If True, a user may send files.
- public bool Text_AttachFiles => ((_rawValue >> 15) & 0x1) == 1;
+ public bool Text_AttachFiles { get { return GetBit(16); } set { SetBit(16, value); } }
/// If True, a user may read previous messages.
- public bool Text_ReadMessageHistory => ((_rawValue >> 16) & 0x1) == 1;
+ public bool Text_ReadMessageHistory { get { return GetBit(17); } set { SetBit(17, value); } }
/// If True, a user may mention @everyone.
- public bool Text_MentionEveryone => ((_rawValue >> 17) & 0x1) == 1;
+ public bool Text_MentionEveryone { get { return GetBit(18); } set { SetBit(18, value); } }
//2 Unused
/// If True, a user may connect to a voice channel.
- public bool Voice_Connect => ((_rawValue >> 20) & 0x1) == 1;
+ public bool Voice_Connect { get { return GetBit(21); } set { SetBit(21, value); } }
/// If True, a user may speak in a voice channel.
- public bool Voice_Speak => ((_rawValue >> 21) & 0x1) == 1;
+ public bool Voice_Speak { get { return GetBit(22); } set { SetBit(22, value); } }
/// If True, a user may mute users.
- public bool Voice_MuteMembers => ((_rawValue >> 22) & 0x1) == 1;
+ public bool Voice_MuteMembers { get { return GetBit(23); } set { SetBit(23, value); } }
/// If True, a user may deafen users.
- public bool Voice_DeafenMembers => ((_rawValue >> 23) & 0x1) == 1;
+ public bool Voice_DeafenMembers { get { return GetBit(24); } set { SetBit(24, value); } }
/// If True, a user may move other users between voice channels.
- public bool Voice_MoveMembers => ((_rawValue >> 24) & 0x1) == 1;
+ public bool Voice_MoveMembers { get { return GetBit(25); } set { SetBit(25, value); } }
/// If True, a user may use voice activation rather than push-to-talk.
- 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);
}
}
diff --git a/src/Discord.Net/Models/Role.cs b/src/Discord.Net/Models/Role.cs
index 515274710..4eb1a1f25 100644
--- a/src/Discord.Net/Models/Role.cs
+++ b/src/Discord.Net/Models/Role.cs
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
+using System.Threading;
namespace Discord
{
@@ -18,19 +19,22 @@ namespace Discord
public string ServerId { get; }
/// Returns the server this role is a member of.
[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;
}
}
diff --git a/src/Discord.Net/Models/Server.cs b/src/Discord.Net/Models/Server.cs
index d8e60fdbd..16c1b3671 100644
--- a/src/Discord.Net/Models/Server.cs
+++ b/src/Discord.Net/Models/Server.cs
@@ -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 _bans, _channels, _invites, _members, _roles;
/// Returns the unique identifier for this server.
public string Id { get; }
@@ -24,116 +26,128 @@ namespace Discord
/// Returns the endpoint for this server's voice server.
internal string VoiceServer { get; set; }
+ /// Returns true if the current user created this server.
+ public bool IsOwner => _client.CurrentUserId == OwnerId;
/// Returns the id of the user that first created this server.
public string OwnerId { get; internal set; }
/// Returns the user that first created this server.
- public User Owner => _client.GetUser(OwnerId);
- /// Returns true if the current user created this server.
- public bool IsOwner => _client.User?.Id == OwnerId;
+ [JsonIgnore]
+ public User Owner => _client.Users[OwnerId];
/// Returns the id of the AFK voice channel for this server (see AFKTimeout).
public string AFKChannelId { get; internal set; }
/// Returns the AFK voice channel for this server (see AFKTimeout).
- public Channel AFKChannel => _client.GetChannel(AFKChannelId);
+ [JsonIgnore]
+ public Channel AFKChannel => _client.Channels[AFKChannelId];
/// Returns the id of the default channel for this server.
public string DefaultChannelId => Id;
/// Returns the default channel for this server.
- public Channel DefaultChannel =>_client.GetChannel(DefaultChannelId);
-
- internal AsyncCache _members;
- /// Returns a collection of all channels within this server.
- public IEnumerable Members => _members;
-
- internal ConcurrentDictionary _bans;
- /// Returns a collection of all users banned on this server.
- /// Only users seen in other servers will be returned. To get a list of all users, use BanIds.
- public IEnumerable Bans => _bans.Keys.Select(x => _client.GetUser(x));
+ [JsonIgnore]
+ public Channel DefaultChannel => _client.Channels[DefaultChannelId];
+
/// Returns a collection of the ids of all users banned on this server.
- public IEnumerable BanIds => _bans.Keys;
+ [JsonIgnore]
+ public IEnumerable BanIds => _bans.Select(x => x.Key);
+ /// Returns a collection of the ids of all channels within this server.
+ [JsonIgnore]
+ public IEnumerable ChannelIds => _channels.Select(x => x.Key);
/// Returns a collection of all channels within this server.
- public IEnumerable Channels => _client.Channels.Where(x => x.ServerId == Id);
+ [JsonIgnore]
+ public IEnumerable Channels => _channels.Select(x => _client.Channels[x.Key]);
/// Returns a collection of all channels within this server.
- public IEnumerable TextChannels => _client.Channels.Where(x => x.ServerId == Id && x.Type == ChannelTypes.Text);
+ [JsonIgnore]
+ public IEnumerable TextChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelTypes.Text);
/// Returns a collection of all channels within this server.
- public IEnumerable VoiceChannels => _client.Channels.Where(x => x.ServerId == Id && x.Type == ChannelTypes.Voice);
+ [JsonIgnore]
+ public IEnumerable VoiceChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelTypes.Voice);
+
+ ///