diff --git a/src/Discord.Net.Commands/DiscordBotClient.cs b/src/Discord.Net.Commands/DiscordBotClient.cs
index 6745c7aba..c8b366635 100644
--- a/src/Discord.Net.Commands/DiscordBotClient.cs
+++ b/src/Discord.Net.Commands/DiscordBotClient.cs
@@ -28,7 +28,7 @@ namespace Discord
MessageCreated += async (s, e) =>
{
//Ignore messages from ourselves
- if (e.Message.UserId == UserId)
+ if (e.Message.UserId == _myId)
return;
//Check for the command character
diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj
index ea99530de..126b6dcec 100644
--- a/src/Discord.Net.Net45/Discord.Net.csproj
+++ b/src/Discord.Net.Net45/Discord.Net.csproj
@@ -80,6 +80,12 @@
ChannelTypes.cs
+
+ DiscordClient.API.cs
+
+
+ DiscordClient.Cache.cs
+
DiscordClient.cs
@@ -107,6 +113,9 @@
Helpers\AsyncCache.cs
+
+ Helpers\Extensions.cs
+
Helpers\JsonHttpClient.cs
diff --git a/src/Discord.Net/API/DiscordAPI.cs b/src/Discord.Net/API/DiscordAPI.cs
index 9dcae95ee..c54dc76e5 100644
--- a/src/Discord.Net/API/DiscordAPI.cs
+++ b/src/Discord.Net/API/DiscordAPI.cs
@@ -141,10 +141,10 @@ namespace Discord.API
var request = new APIRequests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _http.Patch(Endpoints.UserMe, request);
}
- public Task ChangeAvatar(DiscordClient.AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
+ public Task ChangeAvatar(AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
{
string base64 = Convert.ToBase64String(bytes);
- string type = imageType == DiscordClient.AvatarImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64";
+ 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/DiscordClient.API.cs b/src/Discord.Net/DiscordClient.API.cs
new file mode 100644
index 000000000..0d142788f
--- /dev/null
+++ b/src/Discord.Net/DiscordClient.API.cs
@@ -0,0 +1,559 @@
+using Discord.API;
+using Discord.API.Models;
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ public enum AvatarImageType
+ {
+ Jpeg,
+ Png
+ }
+ public partial class DiscordClient
+ {
+ //Servers
+ /// Creates a new server with the provided name and region (see Regions).
+ public async Task CreateServer(string name, string region)
+ {
+ CheckReady();
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (region == null) throw new ArgumentNullException(nameof(region));
+
+ var response = await _api.CreateServer(name, region);
+ return _servers.Update(response.Id, response);
+ }
+
+ /// Leaves the provided server, destroying it if you are the owner.
+ public Task LeaveServer(Server server)
+ => LeaveServer(server?.Id);
+ /// Leaves the provided server, destroying it if you are the owner.
+ public async Task LeaveServer(string serverId)
+ {
+ CheckReady();
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+
+ try { await _api.LeaveServer(serverId); }
+ catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
+ return _servers.Remove(serverId);
+ }
+
+ //Channels
+ /// Creates a new channel with the provided name and type (see ChannelTypes).
+ public Task CreateChannel(Server server, string name, string type)
+ => CreateChannel(server?.Id, name, type);
+ /// Creates a new channel with the provided name and type (see ChannelTypes).
+ public async Task CreateChannel(string serverId, string name, string type)
+ {
+ CheckReady();
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (type == null) throw new ArgumentNullException(nameof(type));
+
+ var response = await _api.CreateChannel(serverId, name, type);
+ return _channels.Update(response.Id, serverId, response);
+ }
+
+ /// Creates a new private channel with the provided user.
+ public Task CreatePMChannel(User user)
+ => CreatePMChannel(user?.Id);
+ /// Creates a new private channel with the provided user.
+ public async Task CreatePMChannel(string userId)
+ {
+ CheckReady();
+ if (userId == null) throw new ArgumentNullException(nameof(userId));
+
+ var response = await _api.CreatePMChannel(_myId, userId);
+ return _channels.Update(response.Id, response);
+ }
+
+ /// Destroys the provided channel.
+ public Task DestroyChannel(Channel channel)
+ => DestroyChannel(channel?.Id);
+ /// Destroys the provided channel.
+ public async Task DestroyChannel(string channelId)
+ {
+ CheckReady();
+ if (channelId == null) throw new ArgumentNullException(nameof(channelId));
+
+ try { await _api.DestroyChannel(channelId); }
+ catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
+ return _channels.Remove(channelId);
+ }
+
+ //Bans
+ /// Bans a user from the provided server.
+ public Task Ban(Server server, User user)
+ => Ban(server?.Id, user?.Id);
+ /// Bans a user from the provided server.
+ public Task Ban(Server server, string userId)
+ => Ban(server?.Id, userId);
+ /// Bans a user from the provided server.
+ public Task Ban(string server, User user)
+ => Ban(server, user?.Id);
+ /// Bans a user from the provided server.
+ public Task Ban(string serverId, string userId)
+ {
+ CheckReady();
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (userId == null) throw new ArgumentNullException(nameof(userId));
+
+ return _api.Ban(serverId, userId);
+ }
+
+ /// Unbans a user from the provided server.
+ public Task Unban(Server server, User user)
+ => Unban(server?.Id, user?.Id);
+ /// Unbans a user from the provided server.
+ public Task Unban(Server server, string userId)
+ => Unban(server?.Id, userId);
+ /// Unbans a user from the provided server.
+ public Task Unban(string server, User user)
+ => Unban(server, user?.Id);
+ /// Unbans a user from the provided server.
+ public async Task Unban(string serverId, string userId)
+ {
+ CheckReady();
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (userId == null) throw new ArgumentNullException(nameof(userId));
+
+ try { await _api.Unban(serverId, userId); }
+ catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
+ }
+
+ //Invites
+ /// Creates a new invite to the default channel of the provided server.
+ /// Time (in seconds) until the invite expires. Set to 0 to never expire.
+ /// If true, a user accepting this invite will be kicked from the server after closing their client.
+ /// If true, creates a human-readable link. Not supported if maxAge is set to 0.
+ /// The max amount of times this invite may be used.
+ public Task CreateInvite(Server server, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
+ => CreateInvite(server?.DefaultChannelId, maxAge, maxUses, isTemporary, hasXkcdPass);
+ /// Creates a new invite to the provided channel.
+ /// Time (in seconds) until the invite expires. Set to 0 to never expire.
+ /// If true, a user accepting this invite will be kicked from the server after closing their client.
+ /// If true, creates a human-readable link. Not supported if maxAge is set to 0.
+ /// The max amount of times this invite may be used.
+ public Task CreateInvite(Channel channel, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
+ => CreateInvite(channel?.Id, maxAge, maxUses, isTemporary, hasXkcdPass);
+ /// Creates a new invite to the provided channel.
+ /// Time (in seconds) until the invite expires. Set to 0 to never expire.
+ /// If true, a user accepting this invite will be kicked from the server after closing their client.
+ /// If true, creates a human-readable link. Not supported if maxAge is set to 0.
+ /// The max amount of times this invite may be used.
+ public async Task CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
+ {
+ CheckReady();
+ if (channelId == null) throw new ArgumentNullException(nameof(channelId));
+ if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge));
+ if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses));
+
+ var response = await _api.CreateInvite(channelId, maxAge, maxUses, isTemporary, hasXkcdPass);
+ _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
+ };
+ }
+
+ /// Gets more info about the provided invite.
+ /// 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);
+ return new Invite(response.Code, response.XkcdPass, this)
+ {
+ ChannelId = response.Channel.Id,
+ InviterId = response.Inviter.Id,
+ ServerId = response.Server.Id
+ };
+ }
+
+ /// Accepts the provided invite.
+ public Task AcceptInvite(Invite invite)
+ {
+ CheckReady();
+ if (invite == null) throw new ArgumentNullException(nameof(invite));
+
+ return _api.AcceptInvite(invite.Id);
+ }
+ /// Accepts the provided invite.
+ public async Task AcceptInvite(string id)
+ {
+ CheckReady();
+ if (id == null) throw new ArgumentNullException(nameof(id));
+
+ //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);
+
+ //Check if this is a human-readable link and get its ID
+ var response = await _api.GetInvite(id);
+ await _api.AcceptInvite(response.Code);
+ }
+
+ /// Deletes the provided invite.
+ public async Task DeleteInvite(string id)
+ {
+ CheckReady();
+ if (id == null) throw new ArgumentNullException(nameof(id));
+
+ try
+ {
+ //Check if this is a human-readable link and get its ID
+ var response = await _api.GetInvite(id);
+ await _api.DeleteInvite(response.Code);
+ }
+ catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
+ }
+
+ //Chat
+ /// Sends a message to the provided channel.
+ public Task SendMessage(Channel channel, string text)
+ => SendMessage(channel?.Id, text, new string[0]);
+ /// Sends a message to the provided channel.
+ public Task SendMessage(string channelId, string text)
+ => SendMessage(channelId, text, new string[0]);
+ /// Sends a message to the provided channel, mentioning certain users.
+ /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
+ public Task SendMessage(Channel channel, string text, string[] mentions)
+ => SendMessage(channel?.Id, text, mentions);
+ /// Sends a message to the provided channel, mentioning certain users.
+ /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
+ public async Task SendMessage(string channelId, string text, string[] mentions)
+ {
+ CheckReady();
+ if (channelId == null) throw new ArgumentNullException(nameof(channelId));
+ 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);
+ Message[] result = new Message[blockCount];
+ for (int i = 0; i < blockCount; i++)
+ {
+ int index = i * DiscordAPI.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
+ {
+ Content = blockText,
+ Timestamp = DateTime.UtcNow,
+ Author = new UserReference { Avatar = User.AvatarId, Discriminator = User.Discriminator, Id = User.Id, Username = User.Name },
+ ChannelId = channelId
+ });
+ msg.IsQueued = true;
+ msg.Nonce = nonce;
+ result[i] = msg;
+ _pendingMessages.Enqueue(msg);
+ }
+ else
+ {
+ var msg = await _api.SendMessage(channelId, blockText, mentions, nonce);
+ result[i] = _messages.Update(msg.Id, channelId, msg);
+ result[i].Nonce = nonce;
+ try { RaiseMessageSent(result[i]); } catch { }
+ }
+ await Task.Delay(1000);
+ }
+ return result;
+ }
+
+ /// Edits a message the provided message.
+ public Task EditMessage(Message message, string text)
+ => EditMessage(message?.ChannelId, message?.Id, text, new string[0]);
+ /// Edits a message the provided message.
+ public Task EditMessage(Channel channel, string messageId, string text)
+ => EditMessage(channel?.Id, messageId, text, new string[0]);
+ /// Edits a message the provided message.
+ public Task EditMessage(string channelId, string messageId, string text)
+ => EditMessage(channelId, messageId, text, new string[0]);
+ /// Edits a message the provided message, mentioning certain users.
+ /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
+ public Task EditMessage(Message message, string text, string[] mentions)
+ => EditMessage(message?.ChannelId, message?.Id, text, mentions);
+ /// Edits a message the provided message, mentioning certain users.
+ /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
+ public Task EditMessage(Channel channel, string messageId, string text, string[] mentions)
+ => EditMessage(channel?.Id, messageId, text, mentions);
+ /// Edits a message the provided message, mentioning certain users.
+ /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
+ public async Task EditMessage(string channelId, string messageId, string text, string[] mentions)
+ {
+ CheckReady();
+ if (channelId == null) throw new ArgumentNullException(nameof(channelId));
+ if (messageId == null) throw new ArgumentNullException(nameof(messageId));
+ 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);
+
+ var msg = await _api.EditMessage(channelId, messageId, text, mentions);
+ _messages.Update(msg.Id, channelId, msg);
+ }
+
+ /// Deletes the provided message.
+ public Task DeleteMessage(Message msg)
+ => DeleteMessage(msg?.ChannelId, msg?.Id);
+ /// Deletes the provided message.
+ public async Task DeleteMessage(string channelId, string msgId)
+ {
+ CheckReady();
+ if (channelId == null) throw new ArgumentNullException(nameof(channelId));
+ if (msgId == null) throw new ArgumentNullException(nameof(msgId));
+
+ try
+ {
+ await _api.DeleteMessage(channelId, msgId);
+ return _messages.Remove(msgId);
+ }
+ catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
+ catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.InternalServerError) { } //TODO: Remove me - temporary fix for deleting nonexisting messages
+ return null;
+ }
+
+ /// Sends a file to the provided channel.
+ public Task SendFile(Channel channel, string path)
+ => SendFile(channel?.Id, path);
+ /// 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)
+ {
+ 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));
+
+ return _api.SendFile(channelId, stream, filename);
+ }
+
+ /// Downloads last count messages from the server, starting at beforeMessageId if it's provided.
+ public Task DownloadMessages(Channel channel, int count, string beforeMessageId = null)
+ => DownloadMessages(channel.Id, count);
+ /// Downloads last count messages from the server, starting at beforeMessageId if it's provided.
+ public async Task DownloadMessages(string channelId, int count, string beforeMessageId = null)
+ {
+ CheckReady();
+ if (channelId == null) throw new NullReferenceException(nameof(channelId));
+ if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
+ if (count == 0) return new Message[0];
+
+ Channel channel = GetChannel(channelId);
+ if (channel != null && channel.Type == ChannelTypes.Text)
+ {
+ try
+ {
+ var msgs = await _api.GetMessages(channel.Id, count);
+ return msgs.OrderBy(x => x.Timestamp)
+ .Select(x =>
+ {
+ var msg = _messages.Update(x.Id, x.ChannelId, x);
+ var user = msg.User;
+ if (user != null)
+ user.UpdateActivity(x.Timestamp);
+ return msg;
+ })
+ .ToArray();
+ }
+ catch (HttpException) { } //Bad Permissions?
+ }
+ return null;
+ }
+
+ //Voice
+ /// Mutes a user on the provided server.
+ public Task Mute(Server server, User user)
+ => Mute(server?.Id, user?.Id);
+ /// Mutes a user on the provided server.
+ public Task Mute(Server server, string userId)
+ => Mute(server?.Id, userId);
+ /// Mutes a user on the provided server.
+ public Task Mute(string server, User user)
+ => Mute(server, user?.Id);
+ /// Mutes a user on the provided server.
+ public Task Mute(string serverId, string userId)
+ {
+ CheckReady();
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (userId == null) throw new ArgumentNullException(nameof(userId));
+
+ return _api.Mute(serverId, userId);
+ }
+
+ /// Unmutes a user on the provided server.
+ public Task Unmute(Server server, User user)
+ => Unmute(server?.Id, user?.Id);
+ /// Unmutes a user on the provided server.
+ public Task Unmute(Server server, string userId)
+ => Unmute(server?.Id, userId);
+ /// Unmutes a user on the provided server.
+ public Task Unmute(string server, User user)
+ => Unmute(server, user?.Id);
+ /// Unmutes a user on the provided server.
+ public Task Unmute(string serverId, string userId)
+ {
+ CheckReady();
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (userId == null) throw new ArgumentNullException(nameof(userId));
+
+ return _api.Unmute(serverId, userId);
+ }
+
+ /// Deafens a user on the provided server.
+ public Task Deafen(Server server, User user)
+ => Deafen(server?.Id, user?.Id);
+ /// Deafens a user on the provided server.
+ public Task Deafen(Server server, string userId)
+ => Deafen(server?.Id, userId);
+ /// Deafens a user on the provided server.
+ public Task Deafen(string server, User user)
+ => Deafen(server, user?.Id);
+ /// Deafens a user on the provided server.
+ public Task Deafen(string serverId, string userId)
+ {
+ CheckReady();
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (userId == null) throw new ArgumentNullException(nameof(userId));
+
+ return _api.Deafen(serverId, userId);
+ }
+
+ /// Undeafens a user on the provided server.
+ public Task Undeafen(Server server, User user)
+ => Undeafen(server?.Id, user?.Id);
+ /// Undeafens a user on the provided server.
+ public Task Undeafen(Server server, string userId)
+ => Undeafen(server?.Id, userId);
+ /// Undeafens a user on the provided server.
+ public Task Undeafen(string server, User user)
+ => Undeafen(server, user?.Id);
+ /// Undeafens a user on the provided server.
+ public Task Undeafen(string serverId, string userId)
+ {
+ CheckReady();
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (userId == null) throw new ArgumentNullException(nameof(userId));
+
+ return _api.Undeafen(serverId, userId);
+ }
+
+#if !DNXCORE50
+ public Task JoinVoiceServer(Server server, Channel channel)
+ => JoinVoiceServer(server?.Id, channel?.Id);
+ public Task JoinVoiceServer(Server server, string channelId)
+ => JoinVoiceServer(server?.Id, channelId);
+ public Task JoinVoiceServer(string serverId, Channel channel)
+ => JoinVoiceServer(serverId, channel?.Id);
+ public async Task JoinVoiceServer(string serverId, string channelId)
+ {
+ CheckReady();
+ if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client.");
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (channelId == null) throw new ArgumentNullException(nameof(channelId));
+
+ await LeaveVoiceServer();
+ _currentVoiceServerId = serverId;
+ _webSocket.JoinVoice(serverId, channelId);
+ }
+
+ public async Task LeaveVoiceServer()
+ {
+ if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client.");
+
+ await _voiceWebSocket.DisconnectAsync();
+ if (_currentVoiceServerId != null)
+ _webSocket.LeaveVoice();
+ _currentVoiceServerId = null;
+ _currentVoiceToken = null;
+ }
+
+ /// Sends a PCM frame to the voice server.
+ /// PCM frame to send.
+ /// Number of bytes in this frame.
+ public void SendVoicePCM(byte[] data, int count)
+ {
+ CheckReady();
+ if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client.");
+ if (count == 0) return;
+
+ if (_isDebugMode)
+ RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Queued {count} bytes for voice output.");
+ _voiceWebSocket.SendPCMFrame(data, count);
+ }
+
+ /// Clears the PCM buffer.
+ public void ClearVoicePCM()
+ {
+ CheckReady();
+ if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client.");
+
+ if (_isDebugMode)
+ RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Cleared the voice buffer.");
+ _voiceWebSocket.ClearPCMFrames();
+ }
+#endif
+
+ //Profile
+ /// Changes your username to newName.
+ public async Task ChangeUsername(string newName, string currentEmail, string currentPassword)
+ {
+ CheckReady();
+ var response = await _api.ChangeUsername(newName, currentEmail, currentPassword);
+ _users.Update(response.Id, response);
+ }
+ /// Changes your email to newEmail.
+ public async Task ChangeEmail(string newEmail, string currentPassword)
+ {
+ CheckReady();
+ var response = await _api.ChangeEmail(newEmail, currentPassword);
+ _users.Update(response.Id, 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);
+ _users.Update(response.Id, response);
+ }
+
+ /// Changes your avatar.
+ /// Only supports PNG and JPEG (see AvatarImageType)
+ public async Task ChangeAvatar(AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
+ {
+ CheckReady();
+ var response = await _api.ChangeAvatar(imageType, bytes, currentEmail, currentPassword);
+ _users.Update(response.Id, response);
+ }
+ }
+}
diff --git a/src/Discord.Net/DiscordClient.Cache.cs b/src/Discord.Net/DiscordClient.Cache.cs
new file mode 100644
index 000000000..1f087b208
--- /dev/null
+++ b/src/Discord.Net/DiscordClient.Cache.cs
@@ -0,0 +1,457 @@
+using Discord.API.Models;
+using Discord.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+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;
+ private 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;
+ private 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;
+ private AsyncCache _channels;
+
+ /// Returns a collection of all messages the client has in cache.
+ /// This collection does not guarantee any ordering.
+ public IEnumerable Messages => _messages;
+ private 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;
+ private 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 user with the specified id, along with their server-specific data, or null if none was found.
+ public Membership GetMember(string serverId, User user)
+ => GetMember(_servers[serverId], user?.Id);
+ /// Returns the user with the specified id, along with their server-specific data, or null if none was found.
+ public Membership GetMember(string serverId, string userId)
+ => GetMember(_servers[serverId], userId);
+ /// Returns the user with the specified id, along with their server-specific data, or null if none was found.
+ public Membership GetMember(Server server, User user)
+ => GetMember(server, user?.Id);
+ /// Returns the user with the specified id, along with their server-specific data, or null if none was found.
+ public Membership GetMember(Server server, string userId)
+ {
+ if (server == null || userId == null)
+ return null;
+ return server.GetMember(userId);
+ }
+
+ /// 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);
+ /// 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 Membership[0];
+
+ if (name.StartsWith("@"))
+ {
+ string name2 = name.Substring(1);
+ return server.Members.Where(x =>
+ {
+ var user = x.User;
+ return string.Equals(user.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(user.Name, name2, StringComparison.OrdinalIgnoreCase);
+ });
+ }
+ else
+ {
+ return server.Members.Where(x =>
+ 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 a private channel with the provided user.
+ public Task GetPMChannel(string userId, bool createIfNotExists = false)
+ => GetPMChannel(_users[userId], createIfNotExists);
+ /// Returns a private channel with the provided user.
+ public async Task GetPMChannel(User user, bool createIfNotExists = false)
+ {
+ if (user == null)
+ {
+ if (createIfNotExists)
+ throw new ArgumentNullException(nameof(user));
+ else
+ return null;
+ }
+
+ var channel = user.PrivateChannel;
+ if (channel == null && createIfNotExists)
+ await CreatePMChannel(user);
+ return channel;
+ }
+ /// 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 role with the specified id, or null if none was found.
+ 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);
+ /// 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];
+
+ 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 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.cs b/src/Discord.Net/DiscordClient.cs
index 30300df4b..5a9399569 100644
--- a/src/Discord.Net/DiscordClient.cs
+++ b/src/Discord.Net/DiscordClient.cs
@@ -4,9 +4,6 @@ using Discord.Helpers;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
@@ -23,74 +20,48 @@ namespace Discord
#if !DNXCORE50
private readonly DiscordVoiceSocket _voiceWebSocket;
#endif
- private readonly ManualResetEventSlim _blockEvent;
+ private readonly JsonSerializer _serializer;
private readonly Regex _userRegex, _channelRegex;
private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator;
- private readonly JsonSerializer _serializer;
+ private readonly ManualResetEventSlim _blockEvent;
private readonly Random _rand;
+ private readonly ConcurrentQueue _pendingMessages;
+ private readonly DiscordClientConfig _config;
- private volatile Task _tasks;
+ private volatile Task _mainTask;
+ protected volatile string _myId, _sessionId;
/// Returns the User object for the current logged in user.
- public User User { get; private set; }
- /// Returns the id of the current logged in user.
- public string UserId { get; private set; }
-#if !DNXCORE50
- /// Returns the voice session id of the current logged in user.
- public string SessionId { get; private set; }
-#endif
-
- public DiscordClientConfig Config => _config;
- private readonly DiscordClientConfig _config;
-
- /// Returns a collection of all users the client can see across all servers.
- /// This collection does not guarantee any ordering.
- public IEnumerable Users => _users;
- private readonly 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;
- private readonly 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;
- private readonly AsyncCache _channels;
+ public User User => _user;
+ private User _user;
- /// Returns a collection of all messages the client has in cache.
- /// This collection does not guarantee any ordering.
- public IEnumerable Messages => _messages;
- private readonly AsyncCache _messages;
- private readonly ConcurrentQueue _pendingMessages;
-
- /// Returns a collection of all roles the client can see across all servers.
- /// This collection does not guarantee any ordering.
- public IEnumerable Roles => _roles;
- private readonly AsyncCache _roles;
-
-#if !DNXCORE50
- private string _currentVoiceServerId, _currentVoiceEndpoint, _currentVoiceToken;
- public string CurrentVoiceServerId => _currentVoiceEndpoint != null ? _currentVoiceToken : null;
- public Server CurrentVoiceServer => _servers[CurrentVoiceServerId];
-#endif
/// Returns true if the user has successfully logged in and the websocket connection has been established.
public bool IsConnected => _isConnected;
private bool _isConnected;
- private volatile CancellationTokenSource _disconnectToken;
/// 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 => _currentVoiceToken != null ? _servers[_currentVoiceServerId] : null;
+ private string _currentVoiceServerId, _currentVoiceToken;
+#endif
+
+ //Constructor
/// Initializes a new instance of the DiscordClient class.
public DiscordClient(DiscordClientConfig config = null)
{
_blockEvent = new ManualResetEventSlim(true);
_config = config ?? new DiscordClientConfig();
- _rand = new Random();
-
+ _isDebugMode = config.EnableDebug;
+ _rand = new Random();
+
_serializer = new JsonSerializer();
#if TEST_RESPONSES
_serializer.CheckAdditionalContent = true;
@@ -118,258 +89,16 @@ namespace Discord
return e.Value;
});
- _servers = new AsyncCache(
- (key, parentKey) =>
- {
- if (_config.EnableDebug)
- 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 (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated server {server.Name} ({server.Id}).");
- },
- server =>
- {
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed server {server.Name} ({server.Id}).");
- }
- );
-
- _channels = new AsyncCache(
- (key, parentKey) =>
- {
- if (_config.EnableDebug)
- {
- 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 (_config.EnableDebug)
- {
- 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 (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed private channel {channel.Name} ({channel.Id}).");
- }
- else
- {
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed channel {channel.Name} ({channel.Id}) in server {channel.Server?.Name} ({channel.ServerId}).");
- }
- });
- _messages = new AsyncCache(
- (key, parentKey) =>
- {
- if (_config.EnableDebug)
- 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(UserId);
- message.RawText = extendedModel.Content;
- message.Timestamp = extendedModel.Timestamp;
- message.EditedTimestamp = extendedModel.EditedTimestamp;
- if (extendedModel.Author != null)
- message.UserId = extendedModel.Author.Id;
- }
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated message {message.Id} in channel {message.Channel?.Name} ({message.ChannelId}).");
- },
- message =>
- {
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed message {message.Id} in channel {message.Channel?.Name} ({message.ChannelId}).");
- }
- );
if (_config.UseMessageQueue)
_pendingMessages = new ConcurrentQueue();
- _roles = new AsyncCache(
- (key, parentKey) =>
- {
- if (_config.EnableDebug)
- 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 (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated role {role.Name} ({role.Id}) in server {role.Server?.Name} ({role.ServerId}).");
- },
- role =>
- {
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed role {role.Name} ({role.Id}) in server {role.Server?.Name} ({role.ServerId}).");
- }
- );
- _users = new AsyncCache(
- (key, parentKey) =>
- {
- if (_config.EnableDebug)
- 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 (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {user?.Name} ({user.Id}).");
- },
- user =>
- {
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {user?.Name} ({user.Id}).");
- }
- );
_http = new JsonHttpClient(config.EnableDebug);
_api = new DiscordAPI(_http);
- if (_config.EnableDebug)
+ if (_isDebugMode)
_http.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Type, e.Message);
+ CreateCaches();
+
_webSocket = new DiscordDataSocket(this, config.ConnectionTimeout, config.WebSocketInterval, config.EnableDebug);
_webSocket.Connected += (s, e) => RaiseConnected();
_webSocket.Disconnected += async (s, e) =>
@@ -377,25 +106,28 @@ namespace Discord
RaiseDisconnected();
//Reconnect if we didn't cause the disconnect
- while (!_disconnectToken.IsCancellationRequested)
+ if (e.WasUnexpected)
{
- try
- {
- await Task.Delay(_config.ReconnectDelay);
- await _webSocket.ReconnectAsync();
- if (_http.Token != null)
- await _webSocket.Login(_http.Token);
- break;
- }
- catch (Exception ex)
+ while (!_disconnectToken.IsCancellationRequested)
{
- 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);
+ try
+ {
+ await Task.Delay(_config.ReconnectDelay);
+ await _webSocket.ReconnectAsync();
+ if (_http.Token != null)
+ await _webSocket.Login(_http.Token);
+ 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);
+ }
}
}
};
- if (_config.EnableDebug)
+ if (_isDebugMode)
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Type, $"DataSocket: {e.Message}");
#if !DNXCORE50
@@ -408,24 +140,28 @@ namespace Discord
RaiseVoiceDisconnected();
//Reconnect if we didn't cause the disconnect
- while (!_disconnectToken.IsCancellationRequested)
+ if (e.WasUnexpected)
{
- try
+ while (!_disconnectToken.IsCancellationRequested)
{
- await Task.Delay(_config.ReconnectDelay);
- await _voiceWebSocket.ReconnectAsync();
- break;
- }
- catch (Exception ex)
- {
- if (_config.EnableDebug)
- 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);
+ try
+ {
+ await Task.Delay(_config.ReconnectDelay);
+ await _voiceWebSocket.ReconnectAsync();
+ await _voiceWebSocket.Login(_currentVoiceServerId, _myId, _sessionId, _currentVoiceToken);
+ 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);
+ }
}
}
};
- if (_config.EnableDebug)
+ if (_isDebugMode)
_voiceWebSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Type, $"VoiceSocket: {e.Message}");
}
#endif
@@ -447,11 +183,11 @@ namespace Discord
_channels.Clear();
_users.Clear();
- UserId = data.User.Id;
+ _myId = data.User.Id;
#if !DNXCORE50
- SessionId = data.SessionId;
+ _sessionId = data.SessionId;
#endif
- User = _users.Update(data.User.Id, data.User);
+ _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)
@@ -595,7 +331,7 @@ namespace Discord
{
var data = e.Event.ToObject(_serializer);
Message msg = null;
- bool wasLocal = _config.UseMessageQueue && data.Author.Id == UserId && data.Nonce != null;
+ bool wasLocal = _config.UseMessageQueue && data.Author.Id == _myId && data.Nonce != null;
if (wasLocal)
{
msg = _messages.Remap("nonce" + data.Nonce, data.Id);
@@ -691,10 +427,9 @@ namespace Discord
#if !DNXCORE50
if (_config.EnableVoice && data.ServerId == _currentVoiceServerId)
{
- _currentVoiceEndpoint = data.Endpoint.Split(':')[0];
_currentVoiceToken = data.Token;
- await _voiceWebSocket.ConnectAsync("wss://" + _currentVoiceEndpoint);
- await _voiceWebSocket.Login(_currentVoiceServerId, UserId, SessionId, _currentVoiceToken);
+ await _voiceWebSocket.ConnectAsync("wss://" + data.Endpoint.Split(':')[0]);
+ await _voiceWebSocket.Login(_currentVoiceServerId, _myId, _myId, data.Token);
}
#endif
}
@@ -722,7 +457,8 @@ namespace Discord
};
}
- private async Task SendAsync()
+ //Async
+ private async Task MessageQueueLoop()
{
var cancelToken = _disconnectToken.Token;
try
@@ -756,192 +492,13 @@ namespace Discord
catch { }
finally { _disconnectToken.Cancel(); }
}
- private async Task EmptyAsync()
- {
- var cancelToken = _disconnectToken.Token;
- try
- {
- await Task.Delay(-1, cancelToken);
- }
- catch (OperationCanceledException) { }
- }
-
- /// 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.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.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 user with the specified id, along with their server-specific data, or null if none was found.
- public Membership GetMember(string serverId, User user)
- => GetMember(_servers[serverId], user.Id);
- /// Returns the user with the specified id, along with their server-specific data, or null if none was found.
- public Membership GetMember(string serverId, string userId)
- => GetMember(_servers[serverId], userId);
- /// Returns the user with the specified id, along with their server-specific data, or null if none was found.
- public Membership GetMember(Server server, User user)
- => GetMember(server, user.Id);
- /// Returns the user with the specified id, along with their server-specific data, or null if none was found.
- public Membership GetMember(Server server, string userId)
- {
- if (server == null)
- return null;
- return server.GetMember(userId);
- }
-
- /// 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);
- /// 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)
- return new Membership[0];
-
- if (name.StartsWith("@"))
- {
- string name2 = name.Substring(1);
- return server.Members.Where(x =>
- {
- var user = x.User;
- return string.Equals(user.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(user.Name, name2, StringComparison.OrdinalIgnoreCase);
- });
- }
- else
- {
- return server.Members.Where(x =>
- 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)
- {
- 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 a private channel with the provided user.
- public Task GetPMChannel(string userId, bool createIfNotExists = false)
- => GetPMChannel(_users[userId], createIfNotExists);
- /// Returns a private channel with the provided user.
- public async Task GetPMChannel(User user, bool createIfNotExists = false)
- {
- var channel = user.PrivateChannel;
- if (channel == null && createIfNotExists)
- await CreatePMChannel(user);
- return channel;
- }
- /// 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 (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 role with the specified id, or null if none was found.
- 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);
- /// 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 (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 message with the specified id, or null if none was found.
- public Message GetMessage(string id) => _messages[id];
-
- /// Downloads last count messages from the server, starting at beforeMessageId if it's provided.
- public Task DownloadMessages(Channel channel, int count, string beforeMessageId = null)
- => DownloadMessages(channel.Id, count);
- /// Downloads last count messages from the server, starting at beforeMessageId if it's provided.
- public async Task DownloadMessages(string channelId, int count, string beforeMessageId = null)
+ private string GenerateNonce()
{
- Channel channel = GetChannel(channelId);
- if (channel != null && channel.Type == ChannelTypes.Text)
- {
- try
- {
- var msgs = await _api.GetMessages(channel.Id, count);
- return msgs.OrderBy(x => x.Timestamp)
- .Select(x =>
- {
- var msg = _messages.Update(x.Id, x.ChannelId, x);
- var user = msg.User;
- if (user != null)
- user.UpdateActivity(x.Timestamp);
- return msg;
- })
- .ToArray();
- }
- catch (HttpException) { } //Bad Permissions?
- }
- return null;
+ lock (_rand)
+ return _rand.Next().ToString();
}
- //Auth
+ //Connection
/// Connects to the Discord server with the provided token.
public Task Connect(string token)
=> ConnectInternal(null, null, token);
@@ -957,7 +514,7 @@ namespace Discord
/// Returns a token for future connections.
public Task ConnectAnonymous(string username)
=> ConnectInternal(username, null, null);
- public async Task ConnectInternal(string emailOrUsername, string password, string token)
+ public async Task ConnectInternal(string emailOrUsername, string password, string token)
{
bool success = false;
await Disconnect();
@@ -966,79 +523,67 @@ namespace Discord
string url = (await _api.GetWebSocket()).Url;
- //Connect by Token
if (token != null)
{
try
{
+ //Login using cached token
await _webSocket.ConnectAsync(url);
- if (_config.EnableDebug)
+ if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket connected.");
_http.Token = token;
await _webSocket.Login(_http.Token);
- if (_config.EnableDebug)
+ if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket got token.");
success = true;
}
catch (InvalidOperationException) //Bad Token
{
- if (_config.EnableDebug)
+ if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket had a bad token.");
if (password == null) //If we don't have an alternate login, throw this error
throw;
}
}
- if (!success && password != null) //Email/Password login
+ if (!success)
{
//Open websocket while we wait for login response
Task socketTask = _webSocket.ConnectAsync(url);
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket connected.");
- var response = await _api.Login(emailOrUsername, password);
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket got token.");
-
- await socketTask;
-
- //Wait for websocket to finish connecting, then send token
- token = response.Token;
+ if (password != null) //Normal Login
+ {
+ var response = await _api.Login(emailOrUsername, password);
+ if (_isDebugMode)
+ RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket got token.");
+ token = response.Token;
+ }
+ else //Anonymous login
+ {
+ var response = await _api.LoginAnonymous(emailOrUsername);
+ if (_isDebugMode)
+ RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket generated anonymous token.");
+ token = response.Token;
+ }
_http.Token = token;
- await _webSocket.Login(_http.Token);
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket logged in.");
- success = true;
- }
- if (!success && password == null) //Anonymous login
- {
- //Open websocket while we wait for login response
- Task socketTask = _webSocket.ConnectAsync(url);
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket connected.");
-
- var response = await _api.LoginAnonymous(emailOrUsername);
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket generated anonymous token.");
-
- await socketTask;
//Wait for websocket to finish connecting, then send token
- token = response.Token;
- _http.Token = token;
+ await socketTask;
+ if (_isDebugMode)
+ RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket connected.");
await _webSocket.Login(_http.Token);
- if (_config.EnableDebug)
+ if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Connection, $"DataSocket logged in.");
success = true;
}
+
if (success)
{
- var cancelToken = _disconnectToken.Token;
if (_config.UseMessageQueue)
- _tasks = SendAsync();
+ _mainTask = MessageQueueLoop();
else
- _tasks = EmptyAsync();
- _tasks = _tasks.ContinueWith(async x =>
+ _mainTask = _disconnectToken.Wait();
+ _mainTask = _mainTask.ContinueWith(async x =>
{
await _webSocket.DisconnectAsync();
#if !DNXCORE50
@@ -1057,487 +602,22 @@ namespace Discord
_users.Clear();
_blockEvent.Set();
- _tasks = null;
+ _mainTask = null;
}).Unwrap();
_isConnected = true;
}
else
token = null;
- return token;
- }
+ return token;
+ }
/// Disconnects from the Discord server, canceling any pending requests.
public async Task Disconnect()
{
- if (_tasks != null)
+ if (_mainTask != null)
{
try { _disconnectToken.Cancel(); } catch (NullReferenceException) { }
- try { await _tasks; } catch (NullReferenceException) { }
- }
- }
-
- //Servers
- /// Creates a new server with the provided name and region (see Regions).
- public async Task CreateServer(string name, string region)
- {
- CheckReady();
- var response = await _api.CreateServer(name, region);
- return _servers.Update(response.Id, response);
- }
- /// Leaves the provided server, destroying it if you are the owner.
- public Task LeaveServer(Server server)
- => LeaveServer(server.Id);
- /// Leaves the provided server, destroying it if you are the owner.
- public async Task LeaveServer(string serverId)
- {
- CheckReady();
- try
- {
- await _api.LeaveServer(serverId);
+ try { await _mainTask; } catch (NullReferenceException) { }
}
- catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) {}
- return _servers.Remove(serverId);
- }
-
- //Channels
- /// Creates a new channel with the provided name and type (see ChannelTypes).
- public Task CreateChannel(Server server, string name, string type)
- => CreateChannel(server.Id, name, type);
- /// Creates a new channel with the provided name and type (see ChannelTypes).
- public async Task CreateChannel(string serverId, string name, string type)
- {
- CheckReady();
- var response = await _api.CreateChannel(serverId, name, type);
- return _channels.Update(response.Id, serverId, response);
- }
- /// Creates a new private channel with the provided user.
- public Task CreatePMChannel(User user)
- => CreatePMChannel(user.Id);
- /// Creates a new private channel with the provided user.
- public async Task CreatePMChannel(string userId)
- {
- CheckReady();
- var response = await _api.CreatePMChannel(UserId, userId);
- return _channels.Update(response.Id, response);
- }
- /// Destroys the provided channel.
- public Task DestroyChannel(Channel channel)
- => DestroyChannel(channel.Id);
- /// Destroys the provided channel.
- public async Task DestroyChannel(string channelId)
- {
- CheckReady();
- try
- {
- var response = await _api.DestroyChannel(channelId);
- }
- catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
- return _channels.Remove(channelId);
- }
-
- //Bans
- /// Bans a user from the provided server.
- public Task Ban(Server server, User user)
- => Ban(server.Id, user.Id);
- /// Bans a user from the provided server.
- public Task Ban(Server server, string userId)
- => Ban(server.Id, userId);
- /// Bans a user from the provided server.
- public Task Ban(string server, User user)
- => Ban(server, user.Id);
- /// Bans a user from the provided server.
- public Task Ban(string serverId, string userId)
- {
- CheckReady();
- return _api.Ban(serverId, userId);
- }
- /// Unbans a user from the provided server.
- public Task Unban(Server server, User user)
- => Unban(server.Id, user.Id);
- /// Unbans a user from the provided server.
- public Task Unban(Server server, string userId)
- => Unban(server.Id, userId);
- /// Unbans a user from the provided server.
- public Task Unban(string server, User user)
- => Unban(server, user.Id);
- /// Unbans a user from the provided server.
- public async Task Unban(string serverId, string userId)
- {
- CheckReady();
- try
- {
- await _api.Unban(serverId, userId);
- }
- catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
- }
-
- //Invites
- /// Creates a new invite to the default channel of the provided server.
- /// Time (in seconds) until the invite expires. Set to 0 to never expire.
- /// If true, a user accepting this invite will be kicked from the server after closing their client.
- /// If true, creates a human-readable link. Not supported if maxAge is set to 0.
- /// The max amount of times this invite may be used.
- public Task CreateInvite(Server server, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
- {
- return CreateInvite(server.DefaultChannelId, maxAge, maxUses, isTemporary, hasXkcdPass);
- }
- /// Creates a new invite to the provided channel.
- /// Time (in seconds) until the invite expires. Set to 0 to never expire.
- /// If true, a user accepting this invite will be kicked from the server after closing their client.
- /// If true, creates a human-readable link. Not supported if maxAge is set to 0.
- /// The max amount of times this invite may be used.
- public Task CreateInvite(Channel channel, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
- {
- return CreateInvite(channel, maxAge, maxUses, isTemporary, hasXkcdPass);
- }
- /// Creates a new invite to the provided channel.
- /// Time (in seconds) until the invite expires. Set to 0 to never expire.
- /// If true, a user accepting this invite will be kicked from the server after closing their client.
- /// If true, creates a human-readable link. Not supported if maxAge is set to 0.
- /// The max amount of times this invite may be used.
- public async Task CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
- {
- CheckReady();
- var response = await _api.CreateInvite(channelId, maxAge, maxUses, isTemporary, hasXkcdPass);
- _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
- };
- }
- /// Gets more info about the provided invite.
- /// Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode
- public async Task GetInvite(string id)
- {
- CheckReady();
- var response = await _api.GetInvite(id);
- return new Invite(response.Code, response.XkcdPass, this)
- {
- ChannelId = response.Channel.Id,
- InviterId = response.Inviter.Id,
- ServerId = response.Server.Id
- };
- }
- /// Accepts the provided invite.
- public Task AcceptInvite(Invite invite)
- {
- CheckReady();
- return _api.AcceptInvite(invite.Id);
- }
- /// Accepts the provided invite.
- public async Task AcceptInvite(string id)
- {
- CheckReady();
-
- //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);
-
- //Check if this is a human-readable link and get its ID
- var response = await _api.GetInvite(id);
- await _api.AcceptInvite(response.Code);
- }
- /// Deletes the provided invite.
- public async Task DeleteInvite(string id)
- {
- CheckReady();
- try
- {
- //Check if this is a human-readable link and get its ID
- var response = await _api.GetInvite(id);
- await _api.DeleteInvite(response.Code);
- }
- catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
- }
-
- //Chat
- /// Sends a message to the provided channel.
- public Task SendMessage(Channel channel, string text)
- {
- if (channel == null)
- {
- throw new ArgumentNullException("channel");
- }
-
- return SendMessage(channel.Id, text, new string[0]);
- }
- /// Sends a message to the provided channel.
- public Task SendMessage(string channelId, string text)
- => SendMessage(channelId, text, new string[0]);
- /// Sends a message to the provided channel, mentioning certain users.
- /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
- public Task SendMessage(Channel channel, string text, string[] mentions)
- => SendMessage(channel.Id, text, mentions);
- /// Sends a message to the provided channel, mentioning certain users.
- /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
- public async Task SendMessage(string channelId, string text, string[] mentions)
- {
- CheckReady();
-
- int blockCount = (int)Math.Ceiling(text.Length / (double)DiscordAPI.MaxMessageSize);
- Message[] result = new Message[blockCount];
- for (int i = 0; i < blockCount; i++)
- {
- int index = i * DiscordAPI.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
- {
- Content = blockText,
- Timestamp = DateTime.UtcNow,
- Author = new UserReference { Avatar = User.AvatarId, Discriminator = User.Discriminator, Id = User.Id, Username = User.Name },
- ChannelId = channelId
- });
- msg.IsQueued = true;
- msg.Nonce = nonce;
- result[i] = msg;
- _pendingMessages.Enqueue(msg);
- }
- else
- {
- var msg = await _api.SendMessage(channelId, blockText, mentions, nonce);
- result[i] = _messages.Update(msg.Id, channelId, msg);
- result[i].Nonce = nonce;
- try { RaiseMessageSent(result[i]); } catch { }
- }
- await Task.Delay(1000);
- }
- return result;
- }
-
- /// Edits a message the provided message.
- public Task EditMessage(Message message, string text)
- => EditMessage(message.ChannelId, message.Id, text, new string[0]);
- /// Edits a message the provided message.
- public Task EditMessage(Channel channel, string messageId, string text)
- => EditMessage(channel.Id, messageId, text, new string[0]);
- /// Edits a message the provided message.
- public Task EditMessage(string channelId, string messageId, string text)
- => EditMessage(channelId, messageId, text, new string[0]);
- /// Edits a message the provided message, mentioning certain users.
- /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
- public Task EditMessage(Message message, string text, string[] mentions)
- => EditMessage(message.ChannelId, message.Id, text, mentions);
- /// Edits a message the provided message, mentioning certain users.
- /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
- public Task EditMessage(Channel channel, string messageId, string text, string[] mentions)
- => EditMessage(channel.Id, messageId, text, mentions);
- /// Edits a message the provided message, mentioning certain users.
- /// While not required, it is recommended to include a mention reference in the text (see User.Mention).
- public async Task EditMessage(string channelId, string messageId, string text, string[] mentions)
- {
- CheckReady();
- if (text.Length > DiscordAPI.MaxMessageSize)
- text = text.Substring(0, DiscordAPI.MaxMessageSize);
-
- var msg = await _api.EditMessage(channelId, messageId, text, mentions);
- _messages.Update(msg.Id, channelId, msg);
- }
-
- /// Deletes the provided message.
- public Task DeleteMessage(Message msg)
- => DeleteMessage(msg.ChannelId, msg.Id);
- /// Deletes the provided message.
- public async Task DeleteMessage(string channelId, string msgId)
- {
- try
- {
- await _api.DeleteMessage(channelId, msgId);
- return _messages.Remove(msgId);
- }
- catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
- catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.InternalServerError) { } //TODO: Remove me - temporary fix for deleting nonexisting messages
- return null;
- }
-
- /// Sends a file to the provided channel.
- public Task SendFile(Channel channel, string path)
- => SendFile(channel.Id, path);
- /// Sends a file to the provided channel.
- public Task SendFile(string channelId, string path)
- => 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)
- {
- return _api.SendFile(channelId, stream, filename);
- }
-
-
- //Voice
- /// Mutes a user on the provided server.
- public Task Mute(Server server, User user)
- => Mute(server.Id, user.Id);
- /// Mutes a user on the provided server.
- public Task Mute(Server server, string userId)
- => Mute(server.Id, userId);
- /// Mutes a user on the provided server.
- public Task Mute(string server, User user)
- => Mute(server, user.Id);
- /// Mutes a user on the provided server.
- public Task Mute(string serverId, string userId)
- {
- CheckReady();
- return _api.Mute(serverId, userId);
- }
-
- /// Unmutes a user on the provided server.
- public Task Unmute(Server server, User user)
- => Unmute(server.Id, user.Id);
- /// Unmutes a user on the provided server.
- public Task Unmute(Server server, string userId)
- => Unmute(server.Id, userId);
- /// Unmutes a user on the provided server.
- public Task Unmute(string server, User user)
- => Unmute(server, user.Id);
- /// Unmutes a user on the provided server.
- public Task Unmute(string serverId, string userId)
- {
- CheckReady();
- return _api.Unmute(serverId, userId);
- }
-
- /// Deafens a user on the provided server.
- public Task Deafen(Server server, User user)
- => Deafen(server.Id, user.Id);
- /// Deafens a user on the provided server.
- public Task Deafen(Server server, string userId)
- => Deafen(server.Id, userId);
- /// Deafens a user on the provided server.
- public Task Deafen(string server, User user)
- => Deafen(server, user.Id);
- /// Deafens a user on the provided server.
- public Task Deafen(string serverId, string userId)
- {
- CheckReady();
- return _api.Deafen(serverId, userId);
- }
-
- /// Undeafens a user on the provided server.
- public Task Undeafen(Server server, User user)
- => Undeafen(server.Id, user.Id);
- /// Undeafens a user on the provided server.
- public Task Undeafen(Server server, string userId)
- => Undeafen(server.Id, userId);
- /// Undeafens a user on the provided server.
- public Task Undeafen(string server, User user)
- => Undeafen(server, user.Id);
- /// Undeafens a user on the provided server.
- public Task Undeafen(string serverId, string userId)
- {
- CheckReady();
- return _api.Undeafen(serverId, userId);
- }
-
-#if !DNXCORE50
- public Task JoinVoiceServer(Server server, Channel channel)
- => JoinVoiceServer(server.Id, channel.Id);
- public Task JoinVoiceServer(Server server, string channelId)
- => JoinVoiceServer(server.Id, channelId);
- public Task JoinVoiceServer(string serverId, Channel channel)
- => JoinVoiceServer(serverId, channel.Id);
- public async Task JoinVoiceServer(string serverId, string channelId)
- {
- if (!_config.EnableVoice)
- throw new InvalidOperationException("Voice is not enabled for this client.");
-
- await LeaveVoiceServer();
- _currentVoiceServerId = serverId;
- _webSocket.JoinVoice(serverId, channelId);
- }
-
- public async Task LeaveVoiceServer()
- {
- if (!_config.EnableVoice)
- throw new InvalidOperationException("Voice is not enabled for this client.");
-
- await _voiceWebSocket.DisconnectAsync();
- if (_currentVoiceEndpoint != null)
- _webSocket.LeaveVoice();
- _currentVoiceEndpoint = null;
- _currentVoiceServerId = null;
- _currentVoiceToken = null;
- }
-
- /// Sends a PCM frame to the voice server.
- /// PCM frame to send.
- /// Number of bytes in this frame.
- public void SendVoicePCM(byte[] data, int count)
- {
- if (!_config.EnableVoice)
- throw new InvalidOperationException("Voice is not enabled for this client.");
- if (count == 0) return;
-
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Queued {count} bytes for voice output.");
- _voiceWebSocket.SendPCMFrame(data, count);
- }
-
- /// Clears the PCM buffer.
- public void ClearVoicePCM()
- {
- if (!_config.EnableVoice)
- throw new InvalidOperationException("Voice is not enabled for this client.");
-
- if (_config.EnableDebug)
- RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Cleared the voice buffer.");
- _voiceWebSocket.ClearPCMFrames();
- }
-#endif
-
- //Profile
- /// Changes your username to newName.
- public async Task ChangeUsername(string newName, string currentEmail, string currentPassword)
- {
- CheckReady();
- var response = await _api.ChangeUsername(newName, currentEmail, currentPassword);
- _users.Update(response.Id, response);
- }
- /// Changes your email to newEmail.
- public async Task ChangeEmail(string newEmail, string currentPassword)
- {
- CheckReady();
- var response = await _api.ChangeEmail(newEmail, currentPassword);
- _users.Update(response.Id, 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);
- _users.Update(response.Id, response);
- }
-
- public enum AvatarImageType
- {
- Jpeg,
- Png
- }
- /// Changes your avatar.
- /// Only supports PNG and JPEG (see AvatarImageType)
- public async Task ChangeAvatar(AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
- {
- CheckReady();
- var response = await _api.ChangeAvatar(imageType, bytes, currentEmail, currentPassword);
- _users.Update(response.Id, response);
}
//Helpers
@@ -1554,11 +634,6 @@ namespace Discord
text = _channelRegex.Replace(text, _channelRegexEvaluator);
return text;
}
- private string GenerateNonce()
- {
- lock (_rand)
- return _rand.Next().ToString();
- }
/// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications.
public void Block()
diff --git a/src/Discord.Net/DiscordWebSocket.Events.cs b/src/Discord.Net/DiscordWebSocket.Events.cs
index 005fe37d1..607820e7d 100644
--- a/src/Discord.Net/DiscordWebSocket.Events.cs
+++ b/src/Discord.Net/DiscordWebSocket.Events.cs
@@ -2,6 +2,12 @@
namespace Discord
{
+ public class DisconnectedEventArgs : EventArgs
+ {
+ public readonly bool WasUnexpected;
+ internal DisconnectedEventArgs(bool wasUnexpected) { WasUnexpected = wasUnexpected; }
+ }
+
internal abstract partial class DiscordWebSocket
{
//Debug
@@ -19,11 +25,11 @@ namespace Discord
if (Connected != null)
Connected(this, EventArgs.Empty);
}
- public event EventHandler Disconnected;
- private void RaiseDisconnected()
+ public event EventHandler Disconnected;
+ private void RaiseDisconnected(bool wasUnexpected)
{
if (Disconnected != null)
- Disconnected(this, EventArgs.Empty);
+ Disconnected(this, new DisconnectedEventArgs(wasUnexpected));
}
}
}
diff --git a/src/Discord.Net/DiscordWebSocket.cs b/src/Discord.Net/DiscordWebSocket.cs
index 3dfd75b50..1538ff508 100644
--- a/src/Discord.Net/DiscordWebSocket.cs
+++ b/src/Discord.Net/DiscordWebSocket.cs
@@ -18,13 +18,13 @@ namespace Discord
protected readonly bool _isDebug;
private readonly ConcurrentQueue _sendQueue;
- protected volatile CancellationTokenSource _disconnectToken;
- private volatile ClientWebSocket _webSocket;
- private volatile Task _tasks;
+ protected CancellationTokenSource _disconnectToken;
+ private ClientWebSocket _webSocket;
+ private DateTime _lastHeartbeat;
+ private Task _task;
protected string _host;
protected int _timeout, _heartbeatInterval;
- private DateTime _lastHeartbeat;
- private bool _isConnected;
+ private bool _isConnected, _wasDisconnectedUnexpected;
public DiscordWebSocket(DiscordClient client, int timeout, int interval, bool isDebug)
{
@@ -54,7 +54,7 @@ namespace Discord
OnConnect();
_lastHeartbeat = DateTime.UtcNow;
- _tasks = Task.Factory.ContinueWhenAll(CreateTasks(), x =>
+ _task = Task.Factory.ContinueWhenAll(CreateTasks(), x =>
{
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.Connection, $"Disconnected.");
@@ -64,6 +64,7 @@ namespace Discord
_disconnectToken.Dispose();
_disconnectToken = null;
+ _wasDisconnectedUnexpected = false;
//Clear send queue
_heartbeatInterval = 0;
@@ -76,20 +77,20 @@ namespace Discord
if (_isConnected)
{
_isConnected = false;
- RaiseDisconnected();
+ RaiseDisconnected(_wasDisconnectedUnexpected);
}
- _tasks = null;
+ _task = null;
});
}
public Task ReconnectAsync()
=> ConnectAsync(_host);
public async Task DisconnectAsync()
{
- if (_tasks != null)
+ if (_task != null)
{
try { _disconnectToken.Cancel(); } catch (NullReferenceException) { }
- try { await _tasks; } catch (NullReferenceException) { }
+ try { await _task; } catch (NullReferenceException) { }
}
}
protected virtual void OnConnect() { }
@@ -130,6 +131,7 @@ namespace Discord
if (result.MessageType == WebSocketMessageType.Close)
{
+ _wasDisconnectedUnexpected = true;
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
RaiseOnDebugMessage(DebugMessageType.Connection, $"Got Close Message ({result.CloseStatus?.ToString() ?? "Unexpected"}, {result.CloseStatusDescription ?? "No Reason"})");
return;
diff --git a/src/Discord.Net/Helpers/Extensions.cs b/src/Discord.Net/Helpers/Extensions.cs
new file mode 100644
index 000000000..fe1bf2a44
--- /dev/null
+++ b/src/Discord.Net/Helpers/Extensions.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Threading;
+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); }
+ catch (OperationCanceledException) { }
+ }
+ public static async Task Wait(this CancellationToken token)
+ {
+ try { await Task.Delay(-1, token); }
+ catch (OperationCanceledException) { }
+ }
+ }
+}
diff --git a/src/Discord.Net/Message.cs b/src/Discord.Net/Message.cs
index 071c3adf2..307c6a6e5 100644
--- a/src/Discord.Net/Message.cs
+++ b/src/Discord.Net/Message.cs
@@ -103,7 +103,7 @@ namespace Discord
[JsonIgnore]
public User User => _client.GetUser(UserId);
/// Returns true if the current user created this message.
- public bool IsAuthor => _client.UserId == UserId;
+ public bool IsAuthor => _client.User?.Id == UserId;
internal Message(string id, string channelId, DiscordClient client)
{
diff --git a/src/Discord.Net/Server.cs b/src/Discord.Net/Server.cs
index 8e235376e..f4d650f9b 100644
--- a/src/Discord.Net/Server.cs
+++ b/src/Discord.Net/Server.cs
@@ -29,7 +29,7 @@ namespace Discord
/// 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.UserId == OwnerId;
+ public bool IsOwner => _client.User?.Id == OwnerId;
/// Returns the id of the AFK voice channel for this server (see AFKTimeout).
public string AFKChannelId { get; internal set; }
@@ -69,7 +69,7 @@ namespace Discord
_members = new AsyncCache(
(key, parentKey) =>
{
- if (_client.Config.EnableDebug)
+ if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key} in server {parentKey}.");
return new Membership(parentKey, key, _client);
},
@@ -108,12 +108,12 @@ namespace Discord
member.IsDeafened = extendedModel.IsDeafened;
member.IsMuted = extendedModel.IsMuted;
}
- if (_client.Config.EnableDebug)
+ if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId}).");
},
(member) =>
{
- if (_client.Config.EnableDebug)
+ if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId}).");
}
);