| @@ -28,7 +28,7 @@ namespace Discord | |||||
| MessageCreated += async (s, e) => | MessageCreated += async (s, e) => | ||||
| { | { | ||||
| //Ignore messages from ourselves | //Ignore messages from ourselves | ||||
| if (e.Message.UserId == UserId) | |||||
| if (e.Message.UserId == _myId) | |||||
| return; | return; | ||||
| //Check for the command character | //Check for the command character | ||||
| @@ -80,6 +80,12 @@ | |||||
| <Compile Include="..\Discord.Net\ChannelTypes.cs"> | <Compile Include="..\Discord.Net\ChannelTypes.cs"> | ||||
| <Link>ChannelTypes.cs</Link> | <Link>ChannelTypes.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\DiscordClient.API.cs"> | |||||
| <Link>DiscordClient.API.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\DiscordClient.Cache.cs"> | |||||
| <Link>DiscordClient.Cache.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\DiscordClient.cs"> | <Compile Include="..\Discord.Net\DiscordClient.cs"> | ||||
| <Link>DiscordClient.cs</Link> | <Link>DiscordClient.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -107,6 +113,9 @@ | |||||
| <Compile Include="..\Discord.Net\Helpers\AsyncCache.cs"> | <Compile Include="..\Discord.Net\Helpers\AsyncCache.cs"> | ||||
| <Link>Helpers\AsyncCache.cs</Link> | <Link>Helpers\AsyncCache.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\Helpers\Extensions.cs"> | |||||
| <Link>Helpers\Extensions.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\Helpers\JsonHttpClient.cs"> | <Compile Include="..\Discord.Net\Helpers\JsonHttpClient.cs"> | ||||
| <Link>Helpers\JsonHttpClient.cs</Link> | <Link>Helpers\JsonHttpClient.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -141,10 +141,10 @@ namespace Discord.API | |||||
| var request = new APIRequests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword }; | var request = new APIRequests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword }; | ||||
| return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request); | return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request); | ||||
| } | } | ||||
| public Task<SelfUserInfo> ChangeAvatar(DiscordClient.AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword) | |||||
| public Task<SelfUserInfo> ChangeAvatar(AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword) | |||||
| { | { | ||||
| string base64 = Convert.ToBase64String(bytes); | 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 }; | var request = new APIRequests.ChangeAvatar { Avatar = $"data:{type},/9j/{base64}", CurrentEmail = currentEmail, CurrentPassword = currentPassword }; | ||||
| return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request); | return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request); | ||||
| } | } | ||||
| @@ -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 | |||||
| /// <summary> Creates a new server with the provided name and region (see Regions). </summary> | |||||
| public async Task<Server> 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); | |||||
| } | |||||
| /// <summary> Leaves the provided server, destroying it if you are the owner. </summary> | |||||
| public Task<Server> LeaveServer(Server server) | |||||
| => LeaveServer(server?.Id); | |||||
| /// <summary> Leaves the provided server, destroying it if you are the owner. </summary> | |||||
| public async Task<Server> 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 | |||||
| /// <summary> Creates a new channel with the provided name and type (see ChannelTypes). </summary> | |||||
| public Task<Channel> CreateChannel(Server server, string name, string type) | |||||
| => CreateChannel(server?.Id, name, type); | |||||
| /// <summary> Creates a new channel with the provided name and type (see ChannelTypes). </summary> | |||||
| public async Task<Channel> 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); | |||||
| } | |||||
| /// <summary> Creates a new private channel with the provided user. </summary> | |||||
| public Task<Channel> CreatePMChannel(User user) | |||||
| => CreatePMChannel(user?.Id); | |||||
| /// <summary> Creates a new private channel with the provided user. </summary> | |||||
| public async Task<Channel> 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); | |||||
| } | |||||
| /// <summary> Destroys the provided channel. </summary> | |||||
| public Task<Channel> DestroyChannel(Channel channel) | |||||
| => DestroyChannel(channel?.Id); | |||||
| /// <summary> Destroys the provided channel. </summary> | |||||
| public async Task<Channel> 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 | |||||
| /// <summary> Bans a user from the provided server. </summary> | |||||
| public Task Ban(Server server, User user) | |||||
| => Ban(server?.Id, user?.Id); | |||||
| /// <summary> Bans a user from the provided server. </summary> | |||||
| public Task Ban(Server server, string userId) | |||||
| => Ban(server?.Id, userId); | |||||
| /// <summary> Bans a user from the provided server. </summary> | |||||
| public Task Ban(string server, User user) | |||||
| => Ban(server, user?.Id); | |||||
| /// <summary> Bans a user from the provided server. </summary> | |||||
| 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); | |||||
| } | |||||
| /// <summary> Unbans a user from the provided server. </summary> | |||||
| public Task Unban(Server server, User user) | |||||
| => Unban(server?.Id, user?.Id); | |||||
| /// <summary> Unbans a user from the provided server. </summary> | |||||
| public Task Unban(Server server, string userId) | |||||
| => Unban(server?.Id, userId); | |||||
| /// <summary> Unbans a user from the provided server. </summary> | |||||
| public Task Unban(string server, User user) | |||||
| => Unban(server, user?.Id); | |||||
| /// <summary> Unbans a user from the provided server. </summary> | |||||
| 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 | |||||
| /// <summary> Creates a new invite to the default channel of the provided server. </summary> | |||||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param> | |||||
| /// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||||
| /// <param name="hasXkcdPass"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> | |||||
| /// <param name="maxUses"> The max amount of times this invite may be used. </param> | |||||
| public Task<Invite> CreateInvite(Server server, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass) | |||||
| => CreateInvite(server?.DefaultChannelId, maxAge, maxUses, isTemporary, hasXkcdPass); | |||||
| /// <summary> Creates a new invite to the provided channel. </summary> | |||||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param> | |||||
| /// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||||
| /// <param name="hasXkcdPass"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> | |||||
| /// <param name="maxUses"> The max amount of times this invite may be used. </param> | |||||
| public Task<Invite> CreateInvite(Channel channel, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass) | |||||
| => CreateInvite(channel?.Id, maxAge, maxUses, isTemporary, hasXkcdPass); | |||||
| /// <summary> Creates a new invite to the provided channel. </summary> | |||||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param> | |||||
| /// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||||
| /// <param name="hasXkcdPass"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> | |||||
| /// <param name="maxUses"> The max amount of times this invite may be used. </param> | |||||
| public async Task<Invite> 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 | |||||
| }; | |||||
| } | |||||
| /// <summary> Gets more info about the provided invite. </summary> | |||||
| /// <remarks> Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode </remarks> | |||||
| public async Task<Invite> GetInvite(string id) | |||||
| { | |||||
| CheckReady(); | |||||
| if (id == null) throw new ArgumentNullException(nameof(id)); | |||||
| var response = await _api.GetInvite(id); | |||||
| return new Invite(response.Code, response.XkcdPass, this) | |||||
| { | |||||
| ChannelId = response.Channel.Id, | |||||
| InviterId = response.Inviter.Id, | |||||
| ServerId = response.Server.Id | |||||
| }; | |||||
| } | |||||
| /// <summary> Accepts the provided invite. </summary> | |||||
| public Task AcceptInvite(Invite invite) | |||||
| { | |||||
| CheckReady(); | |||||
| if (invite == null) throw new ArgumentNullException(nameof(invite)); | |||||
| return _api.AcceptInvite(invite.Id); | |||||
| } | |||||
| /// <summary> Accepts the provided invite. </summary> | |||||
| 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); | |||||
| } | |||||
| /// <summary> Deletes the provided invite. </summary> | |||||
| 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 | |||||
| /// <summary> Sends a message to the provided channel. </summary> | |||||
| public Task<Message[]> SendMessage(Channel channel, string text) | |||||
| => SendMessage(channel?.Id, text, new string[0]); | |||||
| /// <summary> Sends a message to the provided channel. </summary> | |||||
| public Task<Message[]> SendMessage(string channelId, string text) | |||||
| => SendMessage(channelId, text, new string[0]); | |||||
| /// <summary> Sends a message to the provided channel, mentioning certain users. </summary> | |||||
| /// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks> | |||||
| public Task<Message[]> SendMessage(Channel channel, string text, string[] mentions) | |||||
| => SendMessage(channel?.Id, text, mentions); | |||||
| /// <summary> Sends a message to the provided channel, mentioning certain users. </summary> | |||||
| /// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks> | |||||
| public async Task<Message[]> 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; | |||||
| } | |||||
| /// <summary> Edits a message the provided message. </summary> | |||||
| public Task EditMessage(Message message, string text) | |||||
| => EditMessage(message?.ChannelId, message?.Id, text, new string[0]); | |||||
| /// <summary> Edits a message the provided message. </summary> | |||||
| public Task EditMessage(Channel channel, string messageId, string text) | |||||
| => EditMessage(channel?.Id, messageId, text, new string[0]); | |||||
| /// <summary> Edits a message the provided message. </summary> | |||||
| public Task EditMessage(string channelId, string messageId, string text) | |||||
| => EditMessage(channelId, messageId, text, new string[0]); | |||||
| /// <summary> Edits a message the provided message, mentioning certain users. </summary> | |||||
| /// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks> | |||||
| public Task EditMessage(Message message, string text, string[] mentions) | |||||
| => EditMessage(message?.ChannelId, message?.Id, text, mentions); | |||||
| /// <summary> Edits a message the provided message, mentioning certain users. </summary> | |||||
| /// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks> | |||||
| public Task EditMessage(Channel channel, string messageId, string text, string[] mentions) | |||||
| => EditMessage(channel?.Id, messageId, text, mentions); | |||||
| /// <summary> Edits a message the provided message, mentioning certain users. </summary> | |||||
| /// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks> | |||||
| 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); | |||||
| } | |||||
| /// <summary> Deletes the provided message. </summary> | |||||
| public Task DeleteMessage(Message msg) | |||||
| => DeleteMessage(msg?.ChannelId, msg?.Id); | |||||
| /// <summary> Deletes the provided message. </summary> | |||||
| public async Task<Message> 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; | |||||
| } | |||||
| /// <summary> Sends a file to the provided channel. </summary> | |||||
| public Task SendFile(Channel channel, string path) | |||||
| => SendFile(channel?.Id, path); | |||||
| /// <summary> Sends a file to the provided channel. </summary> | |||||
| public Task SendFile(string channelId, string path) | |||||
| { | |||||
| if (path == null) throw new ArgumentNullException(nameof(path)); | |||||
| return SendFile(channelId, File.OpenRead(path), Path.GetFileName(path)); | |||||
| } | |||||
| /// <summary> Reads a stream and sends it to the provided channel as a file. </summary> | |||||
| /// <remarks> It is highly recommended that this stream be cached in memory or on disk, or the request may time out. </remarks> | |||||
| public Task SendFile(Channel channel, Stream stream, string filename = null) | |||||
| => SendFile(channel?.Id, stream, filename); | |||||
| /// <summary> Reads a stream and sends it to the provided channel as a file. </summary> | |||||
| /// <remarks> It is highly recommended that this stream be cached in memory or on disk, or the request may time out. </remarks> | |||||
| public Task SendFile(string channelId, Stream stream, string filename = null) | |||||
| { | |||||
| 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); | |||||
| } | |||||
| /// <summary> Downloads last count messages from the server, starting at beforeMessageId if it's provided. </summary> | |||||
| public Task<Message[]> DownloadMessages(Channel channel, int count, string beforeMessageId = null) | |||||
| => DownloadMessages(channel.Id, count); | |||||
| /// <summary> Downloads last count messages from the server, starting at beforeMessageId if it's provided. </summary> | |||||
| public async Task<Message[]> 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 | |||||
| /// <summary> Mutes a user on the provided server. </summary> | |||||
| public Task Mute(Server server, User user) | |||||
| => Mute(server?.Id, user?.Id); | |||||
| /// <summary> Mutes a user on the provided server. </summary> | |||||
| public Task Mute(Server server, string userId) | |||||
| => Mute(server?.Id, userId); | |||||
| /// <summary> Mutes a user on the provided server. </summary> | |||||
| public Task Mute(string server, User user) | |||||
| => Mute(server, user?.Id); | |||||
| /// <summary> Mutes a user on the provided server. </summary> | |||||
| 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); | |||||
| } | |||||
| /// <summary> Unmutes a user on the provided server. </summary> | |||||
| public Task Unmute(Server server, User user) | |||||
| => Unmute(server?.Id, user?.Id); | |||||
| /// <summary> Unmutes a user on the provided server. </summary> | |||||
| public Task Unmute(Server server, string userId) | |||||
| => Unmute(server?.Id, userId); | |||||
| /// <summary> Unmutes a user on the provided server. </summary> | |||||
| public Task Unmute(string server, User user) | |||||
| => Unmute(server, user?.Id); | |||||
| /// <summary> Unmutes a user on the provided server. </summary> | |||||
| 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); | |||||
| } | |||||
| /// <summary> Deafens a user on the provided server. </summary> | |||||
| public Task Deafen(Server server, User user) | |||||
| => Deafen(server?.Id, user?.Id); | |||||
| /// <summary> Deafens a user on the provided server. </summary> | |||||
| public Task Deafen(Server server, string userId) | |||||
| => Deafen(server?.Id, userId); | |||||
| /// <summary> Deafens a user on the provided server. </summary> | |||||
| public Task Deafen(string server, User user) | |||||
| => Deafen(server, user?.Id); | |||||
| /// <summary> Deafens a user on the provided server. </summary> | |||||
| 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); | |||||
| } | |||||
| /// <summary> Undeafens a user on the provided server. </summary> | |||||
| public Task Undeafen(Server server, User user) | |||||
| => Undeafen(server?.Id, user?.Id); | |||||
| /// <summary> Undeafens a user on the provided server. </summary> | |||||
| public Task Undeafen(Server server, string userId) | |||||
| => Undeafen(server?.Id, userId); | |||||
| /// <summary> Undeafens a user on the provided server. </summary> | |||||
| public Task Undeafen(string server, User user) | |||||
| => Undeafen(server, user?.Id); | |||||
| /// <summary> Undeafens a user on the provided server. </summary> | |||||
| 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; | |||||
| } | |||||
| /// <summary> Sends a PCM frame to the voice server. </summary> | |||||
| /// <param name="data">PCM frame to send.</param> | |||||
| /// <param name="count">Number of bytes in this frame. </param> | |||||
| 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); | |||||
| } | |||||
| /// <summary> Clears the PCM buffer. </summary> | |||||
| 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 | |||||
| /// <summary> Changes your username to newName. </summary> | |||||
| public async Task ChangeUsername(string newName, string currentEmail, string currentPassword) | |||||
| { | |||||
| CheckReady(); | |||||
| var response = await _api.ChangeUsername(newName, currentEmail, currentPassword); | |||||
| _users.Update(response.Id, response); | |||||
| } | |||||
| /// <summary> Changes your email to newEmail. </summary> | |||||
| public async Task ChangeEmail(string newEmail, string currentPassword) | |||||
| { | |||||
| CheckReady(); | |||||
| var response = await _api.ChangeEmail(newEmail, currentPassword); | |||||
| _users.Update(response.Id, response); | |||||
| } | |||||
| /// <summary> Changes your password to newPassword. </summary> | |||||
| public async Task ChangePassword(string newPassword, string currentEmail, string currentPassword) | |||||
| { | |||||
| CheckReady(); | |||||
| var response = await _api.ChangePassword(newPassword, currentEmail, currentPassword); | |||||
| _users.Update(response.Id, response); | |||||
| } | |||||
| /// <summary> Changes your avatar. </summary> | |||||
| /// <remarks>Only supports PNG and JPEG (see AvatarImageType)</remarks> | |||||
| 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| { | |||||
| /// <summary> Returns a collection of all users the client can see across all servers. </summary> | |||||
| /// <remarks> This collection does not guarantee any ordering. </remarks> | |||||
| public IEnumerable<User> Users => _users; | |||||
| private AsyncCache<User, API.Models.UserReference> _users; | |||||
| /// <summary> Returns a collection of all servers the client is a member of. </summary> | |||||
| /// <remarks> This collection does not guarantee any ordering. </remarks> | |||||
| public IEnumerable<Server> Servers => _servers; | |||||
| private AsyncCache<Server, API.Models.ServerReference> _servers; | |||||
| /// <summary> Returns a collection of all channels the client can see across all servers. </summary> | |||||
| /// <remarks> This collection does not guarantee any ordering. </remarks> | |||||
| public IEnumerable<Channel> Channels => _channels; | |||||
| private AsyncCache<Channel, API.Models.ChannelReference> _channels; | |||||
| /// <summary> Returns a collection of all messages the client has in cache. </summary> | |||||
| /// <remarks> This collection does not guarantee any ordering. </remarks> | |||||
| public IEnumerable<Message> Messages => _messages; | |||||
| private AsyncCache<Message, API.Models.MessageReference> _messages; | |||||
| /// <summary> Returns a collection of all roles the client can see across all servers. </summary> | |||||
| /// <remarks> This collection does not guarantee any ordering. </remarks> | |||||
| public IEnumerable<Role> Roles => _roles; | |||||
| private AsyncCache<Role, API.Models.Role> _roles; | |||||
| private void CreateCaches() | |||||
| { | |||||
| _servers = new AsyncCache<Server, API.Models.ServerReference>( | |||||
| (key, parentKey) => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Created server {key}."); | |||||
| return new Server(key, this); | |||||
| }, | |||||
| (server, model) => | |||||
| { | |||||
| server.Name = model.Name; | |||||
| _channels.Update(server.DefaultChannelId, server.Id, null); | |||||
| if (model is ExtendedServerInfo) | |||||
| { | |||||
| var extendedModel = model as ExtendedServerInfo; | |||||
| server.AFKChannelId = extendedModel.AFKChannelId; | |||||
| server.AFKTimeout = extendedModel.AFKTimeout; | |||||
| server.JoinedAt = extendedModel.JoinedAt ?? DateTime.MinValue; | |||||
| server.OwnerId = extendedModel.OwnerId; | |||||
| server.Region = extendedModel.Region; | |||||
| foreach (var role in extendedModel.Roles) | |||||
| _roles.Update(role.Id, model.Id, role); | |||||
| foreach (var channel in extendedModel.Channels) | |||||
| _channels.Update(channel.Id, model.Id, channel); | |||||
| foreach (var membership in extendedModel.Members) | |||||
| { | |||||
| _users.Update(membership.User.Id, membership.User); | |||||
| server.UpdateMember(membership); | |||||
| } | |||||
| foreach (var membership in extendedModel.VoiceStates) | |||||
| server.UpdateMember(membership); | |||||
| foreach (var membership in extendedModel.Presences) | |||||
| server.UpdateMember(membership); | |||||
| } | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated server {server.Name} ({server.Id})."); | |||||
| }, | |||||
| server => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed server {server.Name} ({server.Id})."); | |||||
| } | |||||
| ); | |||||
| _channels = new AsyncCache<Channel, API.Models.ChannelReference>( | |||||
| (key, parentKey) => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| { | |||||
| if (parentKey != null) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Created channel {key} in server {parentKey}."); | |||||
| else | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Created private channel {key}."); | |||||
| } | |||||
| return new Channel(key, parentKey, this); | |||||
| }, | |||||
| (channel, model) => | |||||
| { | |||||
| channel.Name = model.Name; | |||||
| channel.Type = model.Type; | |||||
| if (model is ChannelInfo) | |||||
| { | |||||
| var extendedModel = model as ChannelInfo; | |||||
| channel.Position = extendedModel.Position; | |||||
| if (extendedModel.IsPrivate) | |||||
| { | |||||
| var user = _users.Update(extendedModel.Recipient.Id, extendedModel.Recipient); | |||||
| channel.RecipientId = user.Id; | |||||
| user.PrivateChannelId = channel.Id; | |||||
| } | |||||
| if (extendedModel.PermissionOverwrites != null) | |||||
| { | |||||
| channel.PermissionOverwrites = extendedModel.PermissionOverwrites.Select(x => new Channel.PermissionOverwrite | |||||
| { | |||||
| Type = x.Type, | |||||
| Id = x.Id, | |||||
| Deny = new PackedPermissions(x.Deny), | |||||
| Allow = new PackedPermissions(x.Allow) | |||||
| }).ToArray(); | |||||
| } | |||||
| else | |||||
| channel.PermissionOverwrites = null; | |||||
| } | |||||
| if (_isDebugMode) | |||||
| { | |||||
| if (channel.IsPrivate) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated private channel {channel.Name} ({channel.Id})."); | |||||
| else | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated channel {channel.Name} ({channel.Id}) in server {channel.Server?.Name} ({channel.ServerId})."); | |||||
| } | |||||
| }, | |||||
| channel => | |||||
| { | |||||
| if (channel.IsPrivate) | |||||
| { | |||||
| var user = channel.Recipient; | |||||
| if (user.PrivateChannelId == channel.Id) | |||||
| user.PrivateChannelId = null; | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed private channel {channel.Name} ({channel.Id})."); | |||||
| } | |||||
| else | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed channel {channel.Name} ({channel.Id}) in server {channel.Server?.Name} ({channel.ServerId})."); | |||||
| } | |||||
| }); | |||||
| _messages = new AsyncCache<Message, API.Models.MessageReference>( | |||||
| (key, parentKey) => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Created message {key} in channel {parentKey}."); | |||||
| return new Message(key, parentKey, this); | |||||
| }, | |||||
| (message, model) => | |||||
| { | |||||
| if (model is API.Models.Message) | |||||
| { | |||||
| var extendedModel = model as API.Models.Message; | |||||
| if (extendedModel.Attachments != null) | |||||
| { | |||||
| message.Attachments = extendedModel.Attachments.Select(x => new Message.Attachment | |||||
| { | |||||
| Id = x.Id, | |||||
| Url = x.Url, | |||||
| ProxyUrl = x.ProxyUrl, | |||||
| Size = x.Size, | |||||
| Filename = x.Filename, | |||||
| Width = x.Width, | |||||
| Height = x.Height | |||||
| }).ToArray(); | |||||
| } | |||||
| else | |||||
| message.Attachments = new Message.Attachment[0]; | |||||
| if (extendedModel.Embeds != null) | |||||
| { | |||||
| message.Embeds = extendedModel.Embeds.Select(x => | |||||
| { | |||||
| var embed = new Message.Embed | |||||
| { | |||||
| Url = x.Url, | |||||
| Type = x.Type, | |||||
| Description = x.Description, | |||||
| Title = x.Title | |||||
| }; | |||||
| if (x.Provider != null) | |||||
| { | |||||
| embed.Provider = new Message.EmbedReference | |||||
| { | |||||
| Url = x.Provider.Url, | |||||
| Name = x.Provider.Name | |||||
| }; | |||||
| } | |||||
| if (x.Author != null) | |||||
| { | |||||
| embed.Author = new Message.EmbedReference | |||||
| { | |||||
| Url = x.Author.Url, | |||||
| Name = x.Author.Name | |||||
| }; | |||||
| } | |||||
| if (x.Thumbnail != null) | |||||
| { | |||||
| embed.Thumbnail = new Message.File | |||||
| { | |||||
| Url = x.Thumbnail.Url, | |||||
| ProxyUrl = x.Thumbnail.ProxyUrl, | |||||
| Width = x.Thumbnail.Width, | |||||
| Height = x.Thumbnail.Height | |||||
| }; | |||||
| } | |||||
| return embed; | |||||
| }).ToArray(); | |||||
| } | |||||
| else | |||||
| message.Embeds = new Message.Embed[0]; | |||||
| message.IsMentioningEveryone = extendedModel.IsMentioningEveryone; | |||||
| message.IsTTS = extendedModel.IsTextToSpeech; | |||||
| message.MentionIds = extendedModel.Mentions?.Select(x => x.Id)?.ToArray() ?? new string[0]; | |||||
| message.IsMentioningMe = message.MentionIds.Contains(_myId); | |||||
| message.RawText = extendedModel.Content; | |||||
| message.Timestamp = extendedModel.Timestamp; | |||||
| message.EditedTimestamp = extendedModel.EditedTimestamp; | |||||
| if (extendedModel.Author != null) | |||||
| message.UserId = extendedModel.Author.Id; | |||||
| } | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated message {message.Id} in channel {message.Channel?.Name} ({message.ChannelId})."); | |||||
| }, | |||||
| message => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed message {message.Id} in channel {message.Channel?.Name} ({message.ChannelId})."); | |||||
| } | |||||
| ); | |||||
| _roles = new AsyncCache<Role, API.Models.Role>( | |||||
| (key, parentKey) => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Created role {key} in server {parentKey}."); | |||||
| return new Role(key, parentKey, this); | |||||
| }, | |||||
| (role, model) => | |||||
| { | |||||
| role.Name = model.Name; | |||||
| role.Permissions.RawValue = (uint)model.Permissions; | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated role {role.Name} ({role.Id}) in server {role.Server?.Name} ({role.ServerId})."); | |||||
| }, | |||||
| role => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed role {role.Name} ({role.Id}) in server {role.Server?.Name} ({role.ServerId})."); | |||||
| } | |||||
| ); | |||||
| _users = new AsyncCache<User, API.Models.UserReference>( | |||||
| (key, parentKey) => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key}."); | |||||
| return new User(key, this); | |||||
| }, | |||||
| (user, model) => | |||||
| { | |||||
| user.AvatarId = model.Avatar; | |||||
| user.Discriminator = model.Discriminator; | |||||
| user.Name = model.Username; | |||||
| if (model is SelfUserInfo) | |||||
| { | |||||
| var extendedModel = model as SelfUserInfo; | |||||
| user.Email = extendedModel.Email; | |||||
| user.IsVerified = extendedModel.IsVerified; | |||||
| } | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {user?.Name} ({user.Id})."); | |||||
| }, | |||||
| user => | |||||
| { | |||||
| if (_isDebugMode) | |||||
| RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {user?.Name} ({user.Id})."); | |||||
| } | |||||
| ); | |||||
| } | |||||
| /// <summary> Returns the user with the specified id, or null if none was found. </summary> | |||||
| public User GetUser(string id) => _users[id]; | |||||
| /// <summary> Returns the user with the specified name and discriminator, or null if none was found. </summary> | |||||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks> | |||||
| public User GetUser(string name, string discriminator) | |||||
| { | |||||
| if (name == null || discriminator == null) | |||||
| return null; | |||||
| if (name.StartsWith("@")) | |||||
| name = name.Substring(1); | |||||
| return _users | |||||
| .Where(x => | |||||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) && | |||||
| x.Discriminator == discriminator | |||||
| ) | |||||
| .FirstOrDefault(); | |||||
| } | |||||
| /// <summary> Returns all users with the specified name across all servers. </summary> | |||||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks> | |||||
| public IEnumerable<User> FindUsers(string name) | |||||
| { | |||||
| if (name == null) | |||||
| return new User[0]; | |||||
| if (name.StartsWith("@")) | |||||
| { | |||||
| string name2 = name.Substring(1); | |||||
| return _users.Where(x => | |||||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| else | |||||
| { | |||||
| return _users.Where(x => | |||||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| } | |||||
| /// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary> | |||||
| public Membership GetMember(string serverId, User user) | |||||
| => GetMember(_servers[serverId], user?.Id); | |||||
| /// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary> | |||||
| public Membership GetMember(string serverId, string userId) | |||||
| => GetMember(_servers[serverId], userId); | |||||
| /// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary> | |||||
| public Membership GetMember(Server server, User user) | |||||
| => GetMember(server, user?.Id); | |||||
| /// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary> | |||||
| public Membership GetMember(Server server, string userId) | |||||
| { | |||||
| if (server == null || userId == null) | |||||
| return null; | |||||
| return server.GetMember(userId); | |||||
| } | |||||
| /// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary> | |||||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks> | |||||
| public IEnumerable<Membership> FindMembers(string serverId, string name) | |||||
| => FindMembers(GetServer(serverId), name); | |||||
| /// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary> | |||||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks> | |||||
| public IEnumerable<Membership> 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)); | |||||
| } | |||||
| } | |||||
| /// <summary> Returns the server with the specified id, or null if none was found. </summary> | |||||
| public Server GetServer(string id) => _servers[id]; | |||||
| /// <summary> Returns all servers with the specified name. </summary> | |||||
| /// <remarks> Search is case-insensitive. </remarks> | |||||
| public IEnumerable<Server> FindServers(string name) | |||||
| { | |||||
| if (name == null) | |||||
| return new Server[0]; | |||||
| return _servers.Where(x => | |||||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| /// <summary> Returns the channel with the specified id, or null if none was found. </summary> | |||||
| public Channel GetChannel(string id) => _channels[id]; | |||||
| /// <summary> Returns a private channel with the provided user. </summary> | |||||
| public Task<Channel> GetPMChannel(string userId, bool createIfNotExists = false) | |||||
| => GetPMChannel(_users[userId], createIfNotExists); | |||||
| /// <summary> Returns a private channel with the provided user. </summary> | |||||
| public async Task<Channel> 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; | |||||
| } | |||||
| /// <summary> Returns all channels with the specified server and name. </summary> | |||||
| /// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks> | |||||
| public IEnumerable<Channel> FindChannels(Server server, string name) | |||||
| => FindChannels(server?.Id, name); | |||||
| /// <summary> Returns all channels with the specified server and name. </summary> | |||||
| /// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks> | |||||
| public IEnumerable<Channel> FindChannels(string serverId, string name) | |||||
| { | |||||
| if (serverId == null || name == null) | |||||
| return new Channel[0]; | |||||
| if (name.StartsWith("#")) | |||||
| { | |||||
| string name2 = name.Substring(1); | |||||
| return _channels.Where(x => x.ServerId == serverId && | |||||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| else | |||||
| { | |||||
| return _channels.Where(x => x.ServerId == serverId && | |||||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| } | |||||
| /// <summary> Returns the role with the specified id, or null if none was found. </summary> | |||||
| public Role GetRole(string id) => _roles[id]; | |||||
| /// <summary> Returns all roles with the specified server and name. </summary> | |||||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks> | |||||
| public IEnumerable<Role> FindRoles(Server server, string name) | |||||
| => FindRoles(server?.Id, name); | |||||
| /// <summary> Returns all roles with the specified server and name. </summary> | |||||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks> | |||||
| public IEnumerable<Role> FindRoles(string serverId, string name) | |||||
| { | |||||
| if (serverId == null || name == null) | |||||
| return new Role[0]; | |||||
| if (name.StartsWith("@")) | |||||
| { | |||||
| string name2 = name.Substring(1); | |||||
| return _roles.Where(x => x.ServerId == serverId && | |||||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| else | |||||
| { | |||||
| return _roles.Where(x => x.ServerId == serverId && | |||||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| } | |||||
| /// <summary> Returns the message with the specified id, or null if none was found. </summary> | |||||
| public Message GetMessage(string id) => _messages[id]; | |||||
| } | |||||
| } | |||||
| @@ -2,6 +2,12 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public class DisconnectedEventArgs : EventArgs | |||||
| { | |||||
| public readonly bool WasUnexpected; | |||||
| internal DisconnectedEventArgs(bool wasUnexpected) { WasUnexpected = wasUnexpected; } | |||||
| } | |||||
| internal abstract partial class DiscordWebSocket | internal abstract partial class DiscordWebSocket | ||||
| { | { | ||||
| //Debug | //Debug | ||||
| @@ -19,11 +25,11 @@ namespace Discord | |||||
| if (Connected != null) | if (Connected != null) | ||||
| Connected(this, EventArgs.Empty); | Connected(this, EventArgs.Empty); | ||||
| } | } | ||||
| public event EventHandler Disconnected; | |||||
| private void RaiseDisconnected() | |||||
| public event EventHandler<DisconnectedEventArgs> Disconnected; | |||||
| private void RaiseDisconnected(bool wasUnexpected) | |||||
| { | { | ||||
| if (Disconnected != null) | if (Disconnected != null) | ||||
| Disconnected(this, EventArgs.Empty); | |||||
| Disconnected(this, new DisconnectedEventArgs(wasUnexpected)); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -18,13 +18,13 @@ namespace Discord | |||||
| protected readonly bool _isDebug; | protected readonly bool _isDebug; | ||||
| private readonly ConcurrentQueue<byte[]> _sendQueue; | private readonly ConcurrentQueue<byte[]> _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 string _host; | ||||
| protected int _timeout, _heartbeatInterval; | protected int _timeout, _heartbeatInterval; | ||||
| private DateTime _lastHeartbeat; | |||||
| private bool _isConnected; | |||||
| private bool _isConnected, _wasDisconnectedUnexpected; | |||||
| public DiscordWebSocket(DiscordClient client, int timeout, int interval, bool isDebug) | public DiscordWebSocket(DiscordClient client, int timeout, int interval, bool isDebug) | ||||
| { | { | ||||
| @@ -54,7 +54,7 @@ namespace Discord | |||||
| OnConnect(); | OnConnect(); | ||||
| _lastHeartbeat = DateTime.UtcNow; | _lastHeartbeat = DateTime.UtcNow; | ||||
| _tasks = Task.Factory.ContinueWhenAll(CreateTasks(), x => | |||||
| _task = Task.Factory.ContinueWhenAll(CreateTasks(), x => | |||||
| { | { | ||||
| if (_isDebug) | if (_isDebug) | ||||
| RaiseOnDebugMessage(DebugMessageType.Connection, $"Disconnected."); | RaiseOnDebugMessage(DebugMessageType.Connection, $"Disconnected."); | ||||
| @@ -64,6 +64,7 @@ namespace Discord | |||||
| _disconnectToken.Dispose(); | _disconnectToken.Dispose(); | ||||
| _disconnectToken = null; | _disconnectToken = null; | ||||
| _wasDisconnectedUnexpected = false; | |||||
| //Clear send queue | //Clear send queue | ||||
| _heartbeatInterval = 0; | _heartbeatInterval = 0; | ||||
| @@ -76,20 +77,20 @@ namespace Discord | |||||
| if (_isConnected) | if (_isConnected) | ||||
| { | { | ||||
| _isConnected = false; | _isConnected = false; | ||||
| RaiseDisconnected(); | |||||
| RaiseDisconnected(_wasDisconnectedUnexpected); | |||||
| } | } | ||||
| _tasks = null; | |||||
| _task = null; | |||||
| }); | }); | ||||
| } | } | ||||
| public Task ReconnectAsync() | public Task ReconnectAsync() | ||||
| => ConnectAsync(_host); | => ConnectAsync(_host); | ||||
| public async Task DisconnectAsync() | public async Task DisconnectAsync() | ||||
| { | { | ||||
| if (_tasks != null) | |||||
| if (_task != null) | |||||
| { | { | ||||
| try { _disconnectToken.Cancel(); } catch (NullReferenceException) { } | try { _disconnectToken.Cancel(); } catch (NullReferenceException) { } | ||||
| try { await _tasks; } catch (NullReferenceException) { } | |||||
| try { await _task; } catch (NullReferenceException) { } | |||||
| } | } | ||||
| } | } | ||||
| protected virtual void OnConnect() { } | protected virtual void OnConnect() { } | ||||
| @@ -130,6 +131,7 @@ namespace Discord | |||||
| if (result.MessageType == WebSocketMessageType.Close) | if (result.MessageType == WebSocketMessageType.Close) | ||||
| { | { | ||||
| _wasDisconnectedUnexpected = true; | |||||
| await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); | await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); | ||||
| RaiseOnDebugMessage(DebugMessageType.Connection, $"Got Close Message ({result.CloseStatus?.ToString() ?? "Unexpected"}, {result.CloseStatusDescription ?? "No Reason"})"); | RaiseOnDebugMessage(DebugMessageType.Connection, $"Got Close Message ({result.CloseStatus?.ToString() ?? "Unexpected"}, {result.CloseStatusDescription ?? "No Reason"})"); | ||||
| return; | return; | ||||
| @@ -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) { } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -103,7 +103,7 @@ namespace Discord | |||||
| [JsonIgnore] | [JsonIgnore] | ||||
| public User User => _client.GetUser(UserId); | public User User => _client.GetUser(UserId); | ||||
| /// <summary> Returns true if the current user created this message. </summary> | /// <summary> Returns true if the current user created this message. </summary> | ||||
| public bool IsAuthor => _client.UserId == UserId; | |||||
| public bool IsAuthor => _client.User?.Id == UserId; | |||||
| internal Message(string id, string channelId, DiscordClient client) | internal Message(string id, string channelId, DiscordClient client) | ||||
| { | { | ||||
| @@ -29,7 +29,7 @@ namespace Discord | |||||
| /// <summary> Returns the user that first created this server. </summary> | /// <summary> Returns the user that first created this server. </summary> | ||||
| public User Owner => _client.GetUser(OwnerId); | public User Owner => _client.GetUser(OwnerId); | ||||
| /// <summary> Returns true if the current user created this server. </summary> | /// <summary> Returns true if the current user created this server. </summary> | ||||
| public bool IsOwner => _client.UserId == OwnerId; | |||||
| public bool IsOwner => _client.User?.Id == OwnerId; | |||||
| /// <summary> Returns the id of the AFK voice channel for this server (see AFKTimeout). </summary> | /// <summary> Returns the id of the AFK voice channel for this server (see AFKTimeout). </summary> | ||||
| public string AFKChannelId { get; internal set; } | public string AFKChannelId { get; internal set; } | ||||
| @@ -69,7 +69,7 @@ namespace Discord | |||||
| _members = new AsyncCache<Membership, API.Models.MemberInfo>( | _members = new AsyncCache<Membership, API.Models.MemberInfo>( | ||||
| (key, parentKey) => | (key, parentKey) => | ||||
| { | { | ||||
| if (_client.Config.EnableDebug) | |||||
| if (_client.IsDebugMode) | |||||
| _client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key} in server {parentKey}."); | _client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key} in server {parentKey}."); | ||||
| return new Membership(parentKey, key, _client); | return new Membership(parentKey, key, _client); | ||||
| }, | }, | ||||
| @@ -108,12 +108,12 @@ namespace Discord | |||||
| member.IsDeafened = extendedModel.IsDeafened; | member.IsDeafened = extendedModel.IsDeafened; | ||||
| member.IsMuted = extendedModel.IsMuted; | 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})."); | _client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId})."); | ||||
| }, | }, | ||||
| (member) => | (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})."); | _client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId})."); | ||||
| } | } | ||||
| ); | ); | ||||