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