From 06fae0c95a58ac485c93ca4ce5524458acccbe86 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 23 Dec 2015 02:49:07 -0400 Subject: [PATCH] Re-added model functions, and added legacy extensions --- src/Discord.Net.Commands/CommandExtensions.cs | 2 +- src/Discord.Net/DiscordClient.Obsolete.cs | 992 ++++-------------- src/Discord.Net/DiscordClient.cs | 700 ++++++------ src/Discord.Net/Extensions.cs | 104 +- src/Discord.Net/IService.cs | 7 + src/Discord.Net/Mention.cs | 95 -- src/Discord.Net/Models/Channel.cs | 313 +++++- src/Discord.Net/Models/Invite.cs | 27 +- src/Discord.Net/Models/Message.cs | 140 ++- src/Discord.Net/Models/Permissions.cs | 4 +- src/Discord.Net/Models/Profile.cs | 42 +- src/Discord.Net/Models/Role.cs | 59 +- src/Discord.Net/Models/Server.cs | 290 ++++- src/Discord.Net/Models/User.cs | 140 ++- .../Net/WebSockets/GatewaySocket.cs | 9 +- src/Discord.Net/Optional.cs | 28 - src/Discord.Net/RelativeDirection.cs | 12 + src/Discord.Net/ServiceManager.cs | 39 + 18 files changed, 1631 insertions(+), 1372 deletions(-) create mode 100644 src/Discord.Net/IService.cs delete mode 100644 src/Discord.Net/Mention.cs delete mode 100644 src/Discord.Net/Optional.cs create mode 100644 src/Discord.Net/RelativeDirection.cs create mode 100644 src/Discord.Net/ServiceManager.cs diff --git a/src/Discord.Net.Commands/CommandExtensions.cs b/src/Discord.Net.Commands/CommandExtensions.cs index e6e732472..497ce6721 100644 --- a/src/Discord.Net.Commands/CommandExtensions.cs +++ b/src/Discord.Net.Commands/CommandExtensions.cs @@ -3,6 +3,6 @@ public static class CommandExtensions { public static CommandService Commands(this DiscordClient client, bool required = true) - => client.GetService(required); + => client.Services.Get(required); } } diff --git a/src/Discord.Net/DiscordClient.Obsolete.cs b/src/Discord.Net/DiscordClient.Obsolete.cs index c855afdca..4d87ee6f6 100644 --- a/src/Discord.Net/DiscordClient.Obsolete.cs +++ b/src/Discord.Net/DiscordClient.Obsolete.cs @@ -1,484 +1,182 @@ -namespace Discord +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.Legacy { - /*public enum RelativeDirection { Before, After } - public partial class DiscordClient + public static class Mention { - /// Returns the channel with the specified id, or null if none was found. - public Channel GetChannel(ulong id) - { - CheckReady(); - - return _channels[id]; - } - - /// Returns all channels with the specified server and name. - /// Name formats supported: Name, #Name and <#Id>. Search is case-insensitive if exactMatch is false. - public IEnumerable FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false) + /// Returns the string used to create a user mention. + [Obsolete("Use User.Mention instead")] + public static string User(User user) + => user.Mention; + + /// Returns the string used to create a channel mention. + [Obsolete("Use Channel.Mention instead")] + public static string Channel(Channel channel) + => channel.Mention; + + /// Returns the string used to create a mention to everyone in a channel. + [Obsolete("Use Server.EveryoneRole.Mention instead")] + public static string Everyone() + => $"@everyone"; + } + + public static class LegacyExtensions + { + [Obsolete("Use Server.FindChannels")] + public static IEnumerable FindChannels(this DiscordClient client, Server server, string name, ChannelType type = null, bool exactMatch = false) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - var query = server.Channels.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); - - if (!exactMatch && name.Length >= 2) - { - if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention - { - var id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); - var channel = _channels[id]; - if (channel != null) - query = query.Concat(new Channel[] { channel }); - } - else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention - { - string name2 = name.Substring(1); - query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); - } - } - - if (type != null) - query = query.Where(x => x.Type == type); - return query; + return server.FindChannels(name, type, exactMatch); } - - /// Creates a new channel with the provided name and type. - public async Task CreateChannel(Server server, string name, ChannelType type) + + [Obsolete("Use Server.CreateChannel")] + public static Task CreateChannel(this DiscordClient client, Server server, string name, ChannelType type) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (type == null) throw new ArgumentNullException(nameof(type)); - CheckReady(); - - var request = new CreateChannelRequest(server.Id) { Name = name, Type = type.Value }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - - var channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id); - channel.Update(response); - return channel; - } - - /// Returns the private channel with the provided user, creating one if it does not currently exist. - public async Task CreatePMChannel(User user) + return server.CreateChannel(name, type); + } + [Obsolete("Use User.CreateChannel")] + public static Task CreatePMChannel(this DiscordClient client, User user) { if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - Channel channel = null; - if (user != null) - channel = user.Global.PrivateChannel; - if (channel == null) - { - var request = new CreatePrivateChannelRequest() { RecipientId = user.Id }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - - var recipient = _users.GetOrAdd(response.Recipient.Id, null); - recipient.Update(response.Recipient); - channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient.Id); - channel.Update(response); - } - return channel; - } - - /// Edits the provided channel, changing only non-null attributes. - public async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null) + return user.CreateChannel(); + } + [Obsolete("Use Channel.Edit")] + public static Task EditChannel(this DiscordClient client, Channel channel, string name = null, string topic = null, int? position = null) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - CheckReady(); - - if (name != null || topic != null) - { - var request = new UpdateChannelRequest(channel.Id) - { - Name = name ?? channel.Name, - Topic = topic ?? channel.Topic, - Position = channel.Position - }; - await _clientRest.Send(request).ConfigureAwait(false); - } - - if (position != null) - { - Channel[] channels = channel.Server.Channels.Where(x => x.Type == channel.Type).OrderBy(x => x.Position).ToArray(); - int oldPos = Array.IndexOf(channels, channel); - var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault(); - int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1; - if (newPos < 0) - newPos = 0; - int minPos; - - if (oldPos < newPos) //Moving Down - { - minPos = oldPos; - for (int i = oldPos; i < newPos; i++) - channels[i] = channels[i + 1]; - channels[newPos] = channel; - } - else //(oldPos > newPos) Moving Up - { - minPos = newPos; - for (int i = oldPos; i > newPos; i--) - channels[i] = channels[i - 1]; - channels[newPos] = channel; - } - Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null; - await ReorderChannels(channel.Server, channels.Skip(minPos), after).ConfigureAwait(false); - } + return channel.Edit(name, topic, position); } - - /// Reorders the provided channels in the server's channel list and places them after a certain channel. - public Task ReorderChannels(Server server, IEnumerable channels, Channel after = null) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (channels == null) throw new ArgumentNullException(nameof(channels)); - CheckReady(); - - var request = new ReorderChannelsRequest(server.Id) - { - ChannelIds = channels.Select(x => x.Id).ToArray(), - StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position) - }; - return _clientRest.Send(request); - } - - /// Destroys the provided channel. - public async Task DeleteChannel(Channel channel) + [Obsolete("Use Channel.Delete")] + public static Task DeleteChannel(this DiscordClient client, Channel channel) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - CheckReady(); - - try { await _clientRest.Send(new DeleteChannelRequest(channel.Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + return channel.Delete(); } - /// Gets more info about the provided invite code. - /// Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode - public async Task GetInvite(string inviteIdOrXkcd) + [Obsolete("Use Server.ReorderChannels")] + public static Task ReorderChannels(this DiscordClient client, Server server, IEnumerable channels, Channel after = null) { - if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); - CheckReady(); - - //Remove trailing slash - if (inviteIdOrXkcd.Length > 0 && inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/') - inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1); - //Remove leading URL - int index = inviteIdOrXkcd.LastIndexOf('/'); - if (index >= 0) - inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1); - - var response = await _clientRest.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); - var invite = new Invite(response.Code, response.XkcdPass); - invite.Update(response); - return invite; - } - - /// Gets all active (non-expired) invites to a provided server. - public async Task GetInvites(Server server) + if (server == null) throw new ArgumentNullException(nameof(server)); + return server.ReorderChannels(channels, after); + } + + [Obsolete("Use Server.GetInvites")] + public static Task> GetInvites(this DiscordClient client, Server server) { if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - var response = await _clientRest.Send(new GetInvitesRequest(server.Id)).ConfigureAwait(false); - return response.Select(x => - { - var invite = new Invite(x.Code, x.XkcdPass); - invite.Update(x); - return invite; - }).ToArray(); + return server.GetInvites(); } - - /// 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. Set to 0 to have unlimited uses. - public Task CreateInvite(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false) + + [Obsolete("Use Server.CreateInvite")] + public static Task CreateInvite(this DiscordClient client, Server server, int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) { if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - return CreateInvite(server.DefaultChannel, maxAge, maxUses, tempMembership, hasXkcd); + return server.CreateInvite(maxAge, maxUses, tempMembership, withXkcd); } - /// 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. Set to 0 to have unlimited uses. - public async Task CreateInvite(Channel channel, int maxAge = 1800, int maxUses = 0, bool isTemporary = false, bool withXkcd = false) + [Obsolete("Use Channel.CreateInvite")] + public static Task CreateInvite(this DiscordClient client, Channel channel, int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); - if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); - CheckReady(); - - var request = new CreateInviteRequest(channel.Id) - { - MaxAge = maxAge, - MaxUses = maxUses, - IsTemporary = isTemporary, - WithXkcdPass = withXkcd - }; - - var response = await _clientRest.Send(request).ConfigureAwait(false); - var invite = new Invite(response.Code, response.XkcdPass); - return invite; + return channel.CreateInvite(maxAge, maxUses, tempMembership, withXkcd); } - - /// Deletes the provided invite. - public async Task DeleteInvite(Invite invite) + + [Obsolete("Use Invite.Delete")] + public static Task DeleteInvite(this DiscordClient client, Invite invite) { if (invite == null) throw new ArgumentNullException(nameof(invite)); - CheckReady(); - - try { await _clientRest.Send(new DeleteInviteRequest(invite.Code)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - /// Accepts the provided invite. - public Task AcceptInvite(Invite invite) + return invite.Delete(); + } + [Obsolete("Use Invite.Accept")] + public static Task AcceptInvite(this DiscordClient client, Invite invite) { if (invite == null) throw new ArgumentNullException(nameof(invite)); - CheckReady(); - - return _clientRest.Send(new AcceptInviteRequest(invite.Code)); + return invite.Accept(); } - - - /// Returns the message with the specified id, or null if none was found. - public Message GetMessage(ulong id) - { - if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); - CheckReady(); - - return _messages[id]; - } - - /// Sends a message to the provided channel. To include a mention, see the Mention static helper class. - public Task SendMessage(Channel channel, string text) + + [Obsolete("Use Channel.SendMessage")] + public static Task SendMessage(this DiscordClient client, Channel channel, string text) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (text == null) throw new ArgumentNullException(nameof(text)); - CheckReady(); - - return SendMessageInternal(channel, text, false); + return channel.SendMessage(text); } - /// Sends a private message to the provided user. - public async Task SendMessage(User user, string text) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (text == null) throw new ArgumentNullException(nameof(text)); - CheckReady(); - - var channel = await CreatePMChannel(user).ConfigureAwait(false); - return await SendMessageInternal(channel, text, false).ConfigureAwait(false); - } - /// Sends a text-to-speech message to the provided channel. To include a mention, see the Mention static helper class. - public Task SendTTSMessage(Channel channel, string text) + [Obsolete("Use Channel.SendTTSMessage")] + public static Task SendTTSMessage(this DiscordClient client, Channel channel, string text) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (text == null) throw new ArgumentNullException(nameof(text)); - CheckReady(); - - return SendMessageInternal(channel, text, true); + return channel.SendTTSMessage(text); } - /// Sends a file to the provided channel. - public Task SendFile(Channel channel, string filePath) + [Obsolete("Use Channel.SendFile")] + public static Task SendFile(this DiscordClient client, Channel channel, string filePath) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (filePath == null) throw new ArgumentNullException(nameof(filePath)); - CheckReady(); - - return SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)); + return channel.SendFile(filePath); } - /// Sends a file to the provided channel. - public async Task SendFile(Channel channel, string filename, Stream stream) + [Obsolete("Use Channel.SendFile")] + public static Task SendFile(this DiscordClient client, Channel channel, string filename, Stream stream) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (filename == null) throw new ArgumentNullException(nameof(filename)); - if (stream == null) throw new ArgumentNullException(nameof(stream)); - CheckReady(); - - var request = new SendFileRequest(channel.Id) - { - Filename = filename, - Stream = stream - }; - var model = await _clientRest.Send(request).ConfigureAwait(false); - - var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); - msg.Update(model); - RaiseMessageSent(msg); - return msg; + return channel.SendFile(filename, stream); } - /// Sends a file to the provided channel. - public async Task SendFile(User user, string filePath) + [Obsolete("Use User.SendMessage")] + public static Task SendMessage(this DiscordClient client, User user, string text) { if (user == null) throw new ArgumentNullException(nameof(user)); - if (filePath == null) throw new ArgumentNullException(nameof(filePath)); - CheckReady(); - - var channel = await CreatePMChannel(user).ConfigureAwait(false); - return await SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)).ConfigureAwait(false); + return user.SendMessage(text); } - /// Sends a file to the provided channel. - public async Task SendFile(User user, string filename, Stream stream) + [Obsolete("Use User.SendFile")] + public static Task SendFile(this DiscordClient client, User user, string filePath) { if (user == null) throw new ArgumentNullException(nameof(user)); - if (filename == null) throw new ArgumentNullException(nameof(filename)); - if (stream == null) throw new ArgumentNullException(nameof(stream)); - CheckReady(); - - var channel = await CreatePMChannel(user).ConfigureAwait(false); - return await SendFile(channel, filename, stream).ConfigureAwait(false); + return user.SendFile(filePath); } - private async Task SendMessageInternal(Channel channel, string text, bool isTextToSpeech) + [Obsolete("Use User.SendFile")] + public static Task SendFile(this DiscordClient client, User user, string filename, Stream stream) { - Message msg; - var server = channel.Server; - - var mentionedUsers = new List(); - text = Mention.CleanUserMentions(this, server, text, mentionedUsers); - if (text.Length > MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); - - if (Config.UseMessageQueue) - { - var nonce = GenerateNonce(); - msg = new Message(this, 0, channel.Id, _currentUser.Id); //_messages.GetOrAdd(nonce, channel.Id, _privateUser.Id); - var currentUser = msg.User; - msg.Update(new APIMessage - { - Content = text, - Timestamp = DateTime.UtcNow, - Author = new APIUser { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _currentUser.Id, Username = currentUser.Name }, - ChannelId = channel.Id, - Nonce = IdConvert.ToString(nonce), - IsTextToSpeech = isTextToSpeech - }); - msg.State = MessageState.Queued; - - _pendingMessages.Enqueue(new MessageQueueItem(msg, text, mentionedUsers.Select(x => x.Id).ToArray())); - } - else - { - var request = new SendMessageRequest(channel.Id) - { - Content = text, - MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(), - Nonce = null, - IsTTS = isTextToSpeech - }; - var model = await _clientRest.Send(request).ConfigureAwait(false); - msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); - msg.Update(model); - RaiseMessageSent(msg); - } - return msg; + if (user == null) throw new ArgumentNullException(nameof(user)); + return user.SendFile(filename, stream); } - - /// Edits the provided message, changing only non-null attributes. - /// While not required, it is recommended to include a mention reference in the text (see Mention.User). - public async Task EditMessage(Message message, string text) + + [Obsolete("Use Message.Edit")] + public static Task EditMessage(this DiscordClient client, Message message, string text) { if (message == null) throw new ArgumentNullException(nameof(message)); - if (text == null) throw new ArgumentNullException(nameof(text)); - CheckReady(); - - var channel = message.Channel; - var mentionedUsers = new List(); - if (!channel.IsPrivate) - text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers); - - if (text.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); - - if (Config.UseMessageQueue) - _pendingMessages.Enqueue(new MessageQueueItem(message, text, mentionedUsers.Select(x => x.Id).ToArray())); - else - { - var request = new UpdateMessageRequest(message.Channel.Id, message.Id) - { - Content = text, - MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray() - }; - await _clientRest.Send(request).ConfigureAwait(false); - } + return message.Edit(text); } - - /// Deletes the provided message. - public async Task DeleteMessage(Message message) + + [Obsolete("Use Message.Delete")] + public static Task DeleteMessage(this DiscordClient client, Message message) { if (message == null) throw new ArgumentNullException(nameof(message)); - CheckReady(); - - var request = new DeleteMessageRequest(message.Id, message.Channel.Id); - try { await _clientRest.Send(request).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + return message.Delete(); } - public async Task DeleteMessages(IEnumerable messages) + [Obsolete("Use Message.Delete")] + public static async Task DeleteMessages(this DiscordClient client, IEnumerable messages) { if (messages == null) throw new ArgumentNullException(nameof(messages)); - CheckReady(); foreach (var message in messages) - { - var request = new DeleteMessageRequest(message.Id, message.Channel.Id); - try { await _clientRest.Send(request).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } + await message.Delete(); } - - /// Downloads messages from the server, returning all messages before or after relativeMessageId, if it's provided. - public async Task DownloadMessages(Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) + + [Obsolete("Use Channel.DownloadMessages")] + public static Task DownloadMessages(this DiscordClient client, Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (limit < 0) throw new ArgumentNullException(nameof(limit)); - CheckReady(); - - if (limit == 0) return new Message[0]; - if (channel != null && channel.Type == ChannelType.Text) - { - try - { - var request = new GetMessagesRequest(channel.Id) - { - Limit = limit, - RelativeDir = relativeDir == RelativeDirection.Before ? "before" : "after", - RelativeId = relativeMessageId - }; - var msgs = await _clientRest.Send(request).ConfigureAwait(false); - return msgs.Select(x => - { - Message msg = null; - if (useCache) - { - msg = _messages.GetOrAdd(x.Id, x.ChannelId, x.Author.Id); - var user = msg.User; - if (user != null) - user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); - } - else - msg = new Message(this, x.Id, x.ChannelId, x.Author.Id); - msg.Update(x); - return msg; - }) - .ToArray(); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) { } //Bad Permissions - } - return new Message[0]; + return channel.DownloadMessages(limit, relativeMessageId, relativeDir, useCache); } - - /// Marks a given message as read. - public void AckMessage(Message message) + + [Obsolete("Use Message.Acknowledge")] + public static Task AckMessage(this DiscordClient client, Message message) { if (message == null) throw new ArgumentNullException(nameof(message)); - - if (!message.IsAuthor) - _clientRest.Send(new AckMessageRequest(message.Id, message.Channel.Id)); + return message.Acknowledge(); } - /// Deserializes messages from JSON format and imports them into the message cache. + /*[Obsolete("Use Channel.ImportMessages")] public IEnumerable ImportMessages(Channel channel, string json) { if (json == null) throw new ArgumentNullException(nameof(json)); @@ -506,467 +204,193 @@ } return dic.Values; } - - /// Serializes the message cache for a given channel to JSON. + + [Obsolete("Use Channel.ExportMessages")] public string ExportMessages(Channel channel) { if (channel == null) throw new ArgumentNullException(nameof(channel)); return JsonConvert.SerializeObject(channel.Messages); - } - - /// Returns the user with the specified id, along with their server-specific data, or null if none was found. - public User GetUser(Server server, ulong userId) + }*/ + + [Obsolete("Use Server.GetUser")] + public static User GetUser(this DiscordClient client, Server server, ulong userId) { if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - return _users[userId, server.Id]; + return server.GetUser(userId); } - /// Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. - public User GetUser(Server server, string username, ushort discriminator) + [Obsolete("Use Server.GetUser")] + public static User GetUser(this DiscordClient client, Server server, string username, ushort discriminator) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (username == null) throw new ArgumentNullException(nameof(username)); - CheckReady(); - - return FindUsers(server.Members, server.Id, username, discriminator, true).FirstOrDefault(); + return server.GetUser(username, discriminator); } - - /// Returns all users with the specified server and name, along with their server-specific data. - /// Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false. - public IEnumerable FindUsers(Server server, string name, bool exactMatch = false) + + [Obsolete("Use Server.FindUsers")] + public static IEnumerable FindUsers(this DiscordClient client, Server server, string name, bool exactMatch = false) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - return FindUsers(server.Members, server.Id, name, exactMatch: exactMatch); + return server.FindUsers(name, exactMatch); } - /// Returns all users with the specified channel and name, along with their server-specific data. - /// Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false. - public IEnumerable FindUsers(Channel channel, string name, bool exactMatch = false) + [Obsolete("Use Channel.FindUsers")] + public static IEnumerable FindUsers(this DiscordClient client, Channel channel, string name, bool exactMatch = false) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - return FindUsers(channel.Members, channel.IsPrivate ? (ulong?)null : channel.Server.Id, name, exactMatch: exactMatch); + return channel.FindUsers(name, exactMatch); } - private IEnumerable FindUsers(IEnumerable users, ulong? serverId, string name, ushort? discriminator = null, bool exactMatch = false) - { - var query = users.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); - - if (!exactMatch && name.Length >= 2) - { - if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention - { - ulong id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); - var user = _users[id, serverId]; - if (user != null) - query = query.Concat(new User[] { user }); - } - else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention - { - string name2 = name.Substring(1); - query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); - } - } - - if (discriminator != null) - query = query.Where(x => x.Discriminator == discriminator.Value); - return query; - } - - public Task EditUser(User user, bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable roles = null) + [Obsolete("Use User.Edit")] + public static Task EditUser(this DiscordClient client, User user, bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable roles = null) { if (user == null) throw new ArgumentNullException(nameof(user)); - if (user.IsPrivate) throw new InvalidOperationException("Unable to edit users in a private channel"); - CheckReady(); - - //Modify the roles collection and filter out the everyone role - var roleIds = roles == null ? null : user.Roles.Where(x => !x.IsEveryone).Select(x => x.Id); - - var request = new UpdateMemberRequest(user.Server.Id, user.Id) - { - IsMuted = isMuted ?? user.IsServerMuted, - IsDeafened = isDeafened ?? user.IsServerDeafened, - VoiceChannelId = voiceChannel?.Id, - RoleIds = roleIds.ToArray() - }; - return _clientRest.Send(request); + return user.Edit(isMuted, isDeafened, voiceChannel, roles); } - public Task KickUser(User user) + [Obsolete("Use User.Kick")] + public static Task KickUser(this DiscordClient client, User user) { if (user == null) throw new ArgumentNullException(nameof(user)); - if (user.IsPrivate) throw new InvalidOperationException("Unable to kick users from a private channel"); - CheckReady(); - - var request = new KickMemberRequest(user.Server.Id, user.Id); - return _clientRest.Send(request); + return user.Kick(); } - public Task BanUser(User user, int pruneDays = 0) + [Obsolete("Use Server.Ban")] + public static Task BanUser(this DiscordClient client, Server server, User user, int pruneDays = 0) { + if (server == null) throw new ArgumentNullException(nameof(server)); if (user == null) throw new ArgumentNullException(nameof(user)); - if (user.IsPrivate) throw new InvalidOperationException("Unable to ban users from a private channel"); - CheckReady(); - - var request = new AddGuildBanRequest(user.Server.Id, user.Id); - request.PruneDays = pruneDays; - return _clientRest.Send(request); + return server.Ban(user, pruneDays); } - public async Task UnbanUser(Server server, ulong userId) + [Obsolete("Use Server.Unban")] + public static Task UnbanUser(this DiscordClient client, Server server, User user) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId)); - CheckReady(); - - try { await _clientRest.Send(new RemoveGuildBanRequest(server.Id, userId)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + if (user == null) throw new ArgumentNullException(nameof(user)); + return server.Unban(user); } - - public async Task PruneUsers(Server server, int days, bool simulate = false) + [Obsolete("Use Server.Unban")] + public static Task UnbanUser(this DiscordClient client, Server server, ulong userId) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); - CheckReady(); - - var request = new PruneMembersRequest(server.Id) - { - Days = days, - IsSimulation = simulate - }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - return response.Pruned; + return server.Unban(userId); } - /// When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for a particular server. - public void RequestOfflineUsers(Server server) + [Obsolete("Use Server.PruneUsers")] + public static Task PruneUsers(this DiscordClient client, Server server, int days = 30, bool simulate = false) { if (server == null) throw new ArgumentNullException(nameof(server)); - - _webSocket.SendRequestMembers(server.Id, "", 0); + return server.PruneUsers(days, simulate); } - public async Task EditProfile(string currentPassword = "", + [Obsolete("Use DiscordClient.CurrentUser.Edit")] + public static Task EditProfile(this DiscordClient client, string currentPassword = "", string username = null, string email = null, string password = null, Stream avatar = null, ImageType avatarType = ImageType.Png) - { - if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); - CheckReady(); - - var request = new UpdateProfileRequest() - { - CurrentPassword = currentPassword, - Email = email ?? _currentUser?.Email, - Password = password, - Username = username ?? _privateUser?.Name, - AvatarBase64 = Base64Image(avatarType, avatar, _privateUser?.AvatarId) - }; - - await _clientRest.Send(request).ConfigureAwait(false); - - if (password != null) - { - var loginRequest = new LoginRequest() - { - Email = _currentUser.Email, - Password = password - }; - var loginResponse = await _clientRest.Send(loginRequest).ConfigureAwait(false); - _clientRest.SetToken(loginResponse.Token); - } - } + => client.CurrentUser.Edit(currentPassword, username, email, password, avatar, avatarType); - /// Returns the role with the specified id, or null if none was found. - public Role GetRole(ulong id) + [Obsolete("Use Server.GetRole")] + public static Role GetRole(this DiscordClient client, Server server, ulong id) { - CheckReady(); - - return _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) + if (server == null) throw new ArgumentNullException(nameof(server)); + return server.GetRole(id); + } + [Obsolete("Use Server.FindRoles")] + public static IEnumerable FindRoles(this DiscordClient client, Server server, string name) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - // if (name.StartsWith("@")) - // { - // string name2 = name.Substring(1); - // return _roles.Where(x => x.Server.Id == server.Id && - // string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || - // string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); - // } - // else - // { - return _roles.Where(x => x.Server.Id == server.Id && - string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - // } + return server.FindRoles(name); } - - /// Note: due to current API limitations, the created role cannot be returned. - public async Task CreateRole(Server server, string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false) + + [Obsolete("Use Server.CreateRole")] + public static Task CreateRole(this DiscordClient client, Server server, string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - var request1 = new CreateRoleRequest(server.Id); - var response1 = await _clientRest.Send(request1).ConfigureAwait(false); - var role = _roles.GetOrAdd(response1.Id, server.Id); - role.Update(response1); - - var request2 = new UpdateRoleRequest(role.Server.Id, role.Id) - { - Name = name, - Permissions = (permissions ?? role.Permissions).RawValue, - Color = (color ?? Color.Default).RawValue, - IsHoisted = isHoisted - }; - var response2 = await _clientRest.Send(request2).ConfigureAwait(false); - role.Update(response2); - - return role; + return server.CreateRole(name, permissions, color); } - - public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null) + [Obsolete("Use Role.Edit")] + public static Task EditRole(this DiscordClient client, Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null) { if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - var request1 = new UpdateRoleRequest(role.Server.Id, role.Id) - { - Name = name ?? role.Name, - Permissions = (permissions ?? role.Permissions).RawValue, - Color = (color ?? role.Color).RawValue, - IsHoisted = isHoisted ?? role.IsHoisted - }; - - var response = await _clientRest.Send(request1).ConfigureAwait(false); - - if (position != null) - { - int oldPos = role.Position; - int newPos = position.Value; - int minPos; - Role[] roles = role.Server.Roles.OrderBy(x => x.Position).ToArray(); - - if (oldPos < newPos) //Moving Down - { - minPos = oldPos; - for (int i = oldPos; i < newPos; i++) - roles[i] = roles[i + 1]; - roles[newPos] = role; - } - else //(oldPos > newPos) Moving Up - { - minPos = newPos; - for (int i = oldPos; i > newPos; i--) - roles[i] = roles[i - 1]; - roles[newPos] = role; - } - - var request2 = new ReorderRolesRequest(role.Server.Id) - { - RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), - StartPos = minPos - }; - await _clientRest.Send(request2).ConfigureAwait(false); - } + return role.Edit(name, permissions, color, isHoisted, position); } - public async Task DeleteRole(Role role) + [Obsolete("Use Role.Delete")] + public static Task DeleteRole(this DiscordClient client, Role role) { if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - try { await _clientRest.Send(new DeleteRoleRequest(role.Server.Id, role.Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + return role.Delete(); } - - public Task ReorderRoles(Server server, IEnumerable roles, int startPos = 0) + + [Obsolete("Use Server.ReorderRoles")] + public static Task ReorderRoles(this DiscordClient client, Server server, IEnumerable roles, Role after = null) { if (server == null) throw new ArgumentNullException(nameof(server)); - if (roles == null) throw new ArgumentNullException(nameof(roles)); - if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer."); - CheckReady(); - - return _clientRest.Send(new ReorderRolesRequest(server.Id) - { - RoleIds = roles.Select(x => x.Id).ToArray(), - StartPos = startPos - }); + return server.ReorderRoles(roles, after); } - - /// Returns the server with the specified id, or null if none was found. - public Server GetServer(ulong id) - { - CheckReady(); - - return _servers[id]; - } - - /// Returns all servers with the specified name. - /// Search is case-insensitive. - public IEnumerable FindServers(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - return _servers.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - } - - /// Creates a new server with the provided name and region (see Regions). - public async Task CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (region == null) throw new ArgumentNullException(nameof(region)); - CheckReady(); - - var request = new CreateGuildRequest() - { - Name = name, - Region = region.Id, - IconBase64 = Base64Image(iconType, icon, null) - }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - - var server = _servers.GetOrAdd(response.Id); - server.Update(response); - return server; - } - - /// Edits the provided server, changing only non-null attributes. - public async Task EditServer(Server server, string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) + + [Obsolete("Use Server.Edit")] + public static Task EditServer(this DiscordClient client, Server server, string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) { if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - var request = new UpdateGuildRequest(server.Id) - { - Name = name ?? server.Name, - Region = region ?? server.Region, - IconBase64 = Base64Image(iconType, icon, server.IconId), - AFKChannelId = server.AFKChannel?.Id, - AFKTimeout = server.AFKTimeout - }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - server.Update(response); + return server.Edit(name, region, icon, iconType); } - - /// Leaves the provided server, destroying it if you are the owner. - public async Task LeaveServer(Server server) + + [Obsolete("Use Server.Leave")] + public static Task LeaveServer(this DiscordClient client, Server server) { if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - try { await _clientRest.Send(new LeaveGuildRequest(server.Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + return server.Leave(); } - public async Task> GetVoiceRegions() - { - CheckReady(); + [Obsolete("Use DiscordClient.Regions")] + public static IEnumerable GetVoiceRegions(this DiscordClient client) + => client.Regions; - var regions = await _clientRest.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false); - return regions.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port)); - } - public DualChannelPermissions GetChannelPermissions(Channel channel, User user) + [Obsolete("Use Channel.GetPermissions")] + public static DualChannelPermissions GetChannelPermissions(this DiscordClient client, Channel channel, User user) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - return channel.PermissionOverwrites - .Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id) - .Select(x => x.Permissions) - .FirstOrDefault(); + return channel.GetPermissionsRule(user); } - public DualChannelPermissions GetChannelPermissions(Channel channel, Role role) + [Obsolete("Use Channel.GetPermissions")] + public static DualChannelPermissions GetChannelPermissions(this DiscordClient client, Channel channel, Role role) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - return channel.PermissionOverwrites - .Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id) - .Select(x => x.Permissions) - .FirstOrDefault(); + return channel.GetPermissionsRule(role); } - - public Task SetChannelPermissions(Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null) + [Obsolete("Use Channel.SetPermissions")] + public static Task SetChannelPermissions(this DiscordClient client, Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - return SetChannelPermissions(channel, user.Id, PermissionTarget.User, allow, deny); + return channel.AddPermissionsRule(user, allow, deny); } - public Task SetChannelPermissions(Channel channel, User user, DualChannelPermissions permissions = null) + [Obsolete("Use Channel.SetPermissions")] + public static Task SetChannelPermissions(this DiscordClient client, Channel channel, User user, DualChannelPermissions permissions = null) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - return SetChannelPermissions(channel, user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny); + return channel.AddPermissionsRule(user, permissions); } - public Task SetChannelPermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) + [Obsolete("Use Channel.SetPermissions")] + public static Task SetChannelPermissions(this DiscordClient client, Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, allow, deny); + return channel.AddPermissionsRule(role, allow, deny); } - public Task SetChannelPermissions(Channel channel, Role role, DualChannelPermissions permissions = null) + [Obsolete("Use Channel.SetPermissions")] + public static Task SetChannelPermissions(this DiscordClient client, Channel channel, Role role, DualChannelPermissions permissions = null) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny); + return channel.AddPermissionsRule(role, permissions); } - private Task SetChannelPermissions(Channel channel, ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null) - { - var request = new AddChannelPermissionsRequest(channel.Id) - { - TargetId = targetId, - TargetType = targetType.Value, - Allow = allow?.RawValue ?? 0, - Deny = deny?.RawValue ?? 0 - }; - return _clientRest.Send(request); - } - - public Task RemoveChannelPermissions(Channel channel, User user) + [Obsolete("Use Channel.RemovePermissions")] + public static Task RemoveChannelPermissions(this DiscordClient client, Channel channel, User user) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - return RemoveChannelPermissions(channel, user.Id, PermissionTarget.User); + return channel.RemovePermissionsRule(user); } - public Task RemoveChannelPermissions(Channel channel, Role role) + [Obsolete("Use Channel.RemovePermissions")] + public static Task RemoveChannelPermissions(this DiscordClient client, Channel channel, Role role) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - return RemoveChannelPermissions(channel, role.Id, PermissionTarget.Role); - } - private async Task RemoveChannelPermissions(Channel channel, ulong userOrRoleId, PermissionTarget targetType) - { - try - { - var perms = channel.PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault(); - await _clientRest.Send(new RemoveChannelPermissionsRequest(channel.Id, userOrRoleId)).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + return channel.RemovePermissionsRule(role); } - }*/ + } } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index e57031f0e..8151773cd 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -28,7 +28,6 @@ namespace Discord private readonly ConcurrentDictionary _channels; private readonly ConcurrentDictionary _privateChannels; //Key = RecipientId private readonly JsonSerializer _serializer; - private readonly Region _unknownRegion; private Dictionary _regions; private CancellationTokenSource _cancelTokenSource; @@ -42,6 +41,8 @@ namespace Discord public RestClient StatusAPI { get; } /// Gets the internal WebSocket for the Gateway event stream. public GatewaySocket GatewaySocket { get; } + /// Gets the service manager used for adding extensions to this client. + public ServiceManager Services { get; } /// Gets the queue used for outgoing messages, if enabled. internal MessageQueue MessageQueue { get; } /// Gets the logger used for this client. @@ -66,6 +67,8 @@ namespace Discord // public IEnumerable Channels => _servers.Select(x => x.Value); /// Gets a collection of all private channels this client is a member of. public IEnumerable PrivateChannels => _channels.Select(x => x.Value); + /// Gets a collection of all voice regions currently offered by Discord. + public IEnumerable Regions => _regions.Select(x => x.Value); /// Initializes a new instance of the DiscordClient class. public DiscordClient(DiscordConfig config = null) @@ -75,8 +78,8 @@ namespace Discord State = (int)ConnectionState.Disconnected; Status = UserStatus.Online; - - //Services + + //Logging Log = new LogManager(this); Logger = Log.CreateLogger("Discord"); @@ -91,7 +94,6 @@ namespace Discord _servers = new ConcurrentDictionary(); _channels = new ConcurrentDictionary(); _privateChannels = new ConcurrentDictionary(); - _unknownRegion = new Region("", "Unknown", "", 0); //Serialization _serializer = new JsonSerializer(); @@ -126,10 +128,13 @@ namespace Discord ClientAPI.CancelToken = CancelToken; await SendStatus(); }; - - //Import/Export - //_messageImporter = new JsonSerializer(); - //_messageImporter.ContractResolver = new Message.ImportResolver(); + + //Extensibility + Services = new ServiceManager(this); + + //Import/Export + //_messageImporter = new JsonSerializer(); + //_messageImporter.ContractResolver = new Message.ImportResolver(); } /// Connects to the Discord server with the provided email and password. @@ -167,7 +172,8 @@ namespace Discord await Login(email, password, token).ConfigureAwait(false); ClientAPI.Token = token; - await GatewaySocket.Connect(token).ConfigureAwait(false); + GatewaySocket.Token = token; + await GatewaySocket.Connect().ConfigureAwait(false); List tasks = new List(); tasks.Add(CancelToken.Wait()); @@ -265,7 +271,10 @@ namespace Discord await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); - _servers.Clear(); + ClientAPI.Token = null; + GatewaySocket.Token = null; + + _servers.Clear(); _channels.Clear(); _privateChannels.Clear(); @@ -276,45 +285,197 @@ namespace Discord _connectedEvent.Reset(); _disconnectedEvent.Set(); } - - private void OnReceivedEvent(WebSocketEventEventArgs e) - { - try - { - switch (e.Type) - { - //Global - case "READY": //Resync - { - var data = e.Payload.ToObject(_serializer); + + public Task SetStatus(UserStatus status) + { + if (status == null) throw new ArgumentNullException(nameof(status)); + if (status != UserStatus.Online && status != UserStatus.Idle) + throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); + + Status = status; + return SendStatus(); + } + public Task SetGame(int? gameId) + { + CurrentGameId = gameId; + return SendStatus(); + } + private Task SendStatus() + { + GatewaySocket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGameId); + return TaskHelper.CompletedTask; + } + + #region Channels + private Channel AddChannel(ulong id, ulong? guildId, ulong? recipientId) + { + Channel channel; + if (recipientId != null) + { + channel = _privateChannels.GetOrAdd(recipientId.Value, + x => new Channel(this, x, new User(this, recipientId.Value, null))); + } + else + { + var server = GetServer(guildId.Value); + channel = server.AddChannel(id); + } + _channels[channel.Id] = channel; + return channel; + } + private Channel RemoveChannel(ulong id) + { + Channel channel; + if (_channels.TryRemove(id, out channel)) + { + if (channel.IsPrivate) + _privateChannels.TryRemove(channel.Recipient.Id, out channel); + else + channel.Server.RemoveChannel(id); + } + return channel; + } + + internal Channel GetChannel(ulong id) + { + Channel channel; + _channels.TryGetValue(id, out channel); + return channel; + } + internal Channel GetPrivateChannel(ulong recipientId) + { + Channel channel; + _privateChannels.TryGetValue(recipientId, out channel); + return channel; + } + + internal async Task CreatePrivateChannel(User user) + { + var channel = GetPrivateChannel(user.Id); + if (channel != null) return channel; + + var request = new CreatePrivateChannelRequest() { RecipientId = user.Id }; + var response = await ClientAPI.Send(request).ConfigureAwait(false); + + channel = AddChannel(response.Id, null, response.Recipient.Id); + channel.Update(response); + return channel; + } + #endregion + + #region Invites + /// Gets more info about the provided invite code. + /// Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode + public async Task GetInvite(string inviteIdOrXkcd) + { + if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); + + //Remove trailing slash + if (inviteIdOrXkcd.Length > 0 && inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/') + inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1); + //Remove leading URL + int index = inviteIdOrXkcd.LastIndexOf('/'); + if (index >= 0) + inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1); + + var response = await ClientAPI.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); + var invite = new Invite(this, response.Code, response.XkcdPass); + invite.Update(response); + return invite; + } + #endregion + + #region Regions + public Region GetRegion(string id) + { + Region region; + if (_regions.TryGetValue(id, out region)) + return region; + else + return new Region(id, id, "", 0); + } + #endregion + + #region Servers + private Server AddServer(ulong id) + => _servers.GetOrAdd(id, x => new Server(this, x)); + private Server RemoveServer(ulong id) + { + Server server; + _servers.TryRemove(id, out server); + return server; + } + + public Server GetServer(ulong id) + { + Server server; + _servers.TryGetValue(id, out server); + return server; + } + public IEnumerable FindServers(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + return _servers.Select(x => x.Value).Find(name); + } + + /// Creates a new server with the provided name and region. + public async Task CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (region == null) throw new ArgumentNullException(nameof(region)); + + var request = new CreateGuildRequest() + { + Name = name, + Region = region.Id, + IconBase64 = icon.Base64(iconType, null) + }; + var response = await ClientAPI.Send(request).ConfigureAwait(false); + + var server = AddServer(response.Id); + server.Update(response); + return server; + } + #endregion + + private void OnReceivedEvent(WebSocketEventEventArgs e) + { + try + { + switch (e.Type) + { + //Global + case "READY": //Resync + { + var data = e.Payload.ToObject(_serializer); //SessionId = data.SessionId; - PrivateUser = new User(data.User.Id, null); + PrivateUser = new User(this, data.User.Id, null); PrivateUser.Update(data.User); CurrentUser.Update(data.User); foreach (var model in data.Guilds) - { - if (model.Unavailable != true) - { - var server = AddServer(model.Id); - server.Update(model); - } - } - foreach (var model in data.PrivateChannels) + { + if (model.Unavailable != true) + { + var server = AddServer(model.Id); + server.Update(model); + } + } + foreach (var model in data.PrivateChannels) { var channel = AddChannel(model.Id, null, model.Recipient.Id); - channel.Update(model); - } - } - break; - - //Servers - case "GUILD_CREATE": - { - var data = e.Payload.ToObject(_serializer); - if (data.Unavailable != true) - { - var server = AddServer(data.Id); - server.Update(data); + channel.Update(model); + } + } + break; + + //Servers + case "GUILD_CREATE": + { + var data = e.Payload.ToObject(_serializer); + if (data.Unavailable != true) + { + var server = AddServer(data.Id); + server.Update(data); if (data.Unavailable != false) { Logger.Info($"Server Created: {server.Name}"); @@ -324,26 +485,26 @@ namespace Discord Logger.Info($"Server Available: {server.Name}"); OnServerAvailable(server); } - } - break; - case "GUILD_UPDATE": - { - var data = e.Payload.ToObject(_serializer); - var server = GetServer(data.Id); + } + break; + case "GUILD_UPDATE": + { + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.Id); if (server != null) - { - server.Update(data); + { + server.Update(data); Logger.Info($"Server Updated: {server.Name}"); OnServerUpdated(server); - } - } - break; - case "GUILD_DELETE": - { - var data = e.Payload.ToObject(_serializer); + } + } + break; + case "GUILD_DELETE": + { + var data = e.Payload.ToObject(_serializer); Server server = RemoveServer(data.Id); - if (server != null) - { + if (server != null) + { if (data.Unavailable != true) Logger.Info($"Server Destroyed: {server.Name}"); else @@ -353,61 +514,61 @@ namespace Discord if (data.Unavailable != true) OnLeftServer(server); } - } - break; + } + break; - //Channels - case "CHANNEL_CREATE": - { - var data = e.Payload.ToObject(_serializer); + //Channels + case "CHANNEL_CREATE": + { + var data = e.Payload.ToObject(_serializer); Channel channel = AddChannel(data.Id, data.GuildId, data.Recipient.Id); - channel.Update(data); + channel.Update(data); Logger.Info($"Channel Created: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); OnChannelCreated(channel); - } - break; - case "CHANNEL_UPDATE": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "CHANNEL_UPDATE": + { + var data = e.Payload.ToObject(_serializer); var channel = GetChannel(data.Id); - if (channel != null) - { - channel.Update(data); + if (channel != null) + { + channel.Update(data); Logger.Info($"Channel Updated: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); OnChannelUpdated(channel); - } - } - break; - case "CHANNEL_DELETE": - { - var data = e.Payload.ToObject(_serializer); + } + } + break; + case "CHANNEL_DELETE": + { + var data = e.Payload.ToObject(_serializer); var channel = RemoveChannel(data.Id); if (channel != null) { Logger.Info($"Channel Destroyed: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); OnChannelDestroyed(channel); } - } - break; + } + break; - //Members - case "GUILD_MEMBER_ADD": - { - var data = e.Payload.ToObject(_serializer); + //Members + case "GUILD_MEMBER_ADD": + { + var data = e.Payload.ToObject(_serializer); var server = GetServer(data.GuildId.Value); if (server != null) { - var user = server.AddMember(data.User.Id); + var user = server.AddUser(data.User.Id); user.Update(data); user.UpdateActivity(); Logger.Info($"User Joined: {server.Name}/{user.Name}"); OnUserJoined(user); } - } - break; - case "GUILD_MEMBER_UPDATE": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "GUILD_MEMBER_UPDATE": + { + var data = e.Payload.ToObject(_serializer); var server = GetServer(data.GuildId.Value); if (server != null) { @@ -419,43 +580,43 @@ namespace Discord OnUserUpdated(user); } } - } - break; - case "GUILD_MEMBER_REMOVE": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "GUILD_MEMBER_REMOVE": + { + var data = e.Payload.ToObject(_serializer); var server = GetServer(data.GuildId.Value); if (server != null) { - var user = server.RemoveMember(data.User.Id); + var user = server.RemoveUser(data.User.Id); if (user != null) { Logger.Info($"User Left: {server.Name}/{user.Name}"); OnUserLeft(user); } } - } - break; - case "GUILD_MEMBERS_CHUNK": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "GUILD_MEMBERS_CHUNK": + { + var data = e.Payload.ToObject(_serializer); foreach (var memberData in data.Members) { var server = GetServer(memberData.GuildId.Value); if (server != null) { - var user = server.AddMember(memberData.User.Id); + var user = server.AddUser(memberData.User.Id); user.Update(memberData); //OnUserAdded(user); } } } - break; + break; - //Roles - case "GUILD_ROLE_CREATE": - { - var data = e.Payload.ToObject(_serializer); + //Roles + case "GUILD_ROLE_CREATE": + { + var data = e.Payload.ToObject(_serializer); var server = GetServer(data.GuildId); if (server != null) { @@ -464,11 +625,11 @@ namespace Discord Logger.Info($"Role Created: {server.Name}/{role.Name}"); OnRoleUpdated(role); } - } - break; - case "GUILD_ROLE_UPDATE": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "GUILD_ROLE_UPDATE": + { + var data = e.Payload.ToObject(_serializer); var server = GetServer(data.GuildId); if (server != null) { @@ -480,11 +641,11 @@ namespace Discord OnRoleUpdated(role); } } - } - break; - case "GUILD_ROLE_DELETE": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "GUILD_ROLE_DELETE": + { + var data = e.Payload.ToObject(_serializer); var server = GetServer(data.GuildId); if (server != null) { @@ -495,41 +656,41 @@ namespace Discord OnRoleDeleted(role); } } - } - break; - - //Bans - case "GUILD_BAN_ADD": - { - var data = e.Payload.ToObject(_serializer); - var server = GetServer(data.GuildId); + } + break; + + //Bans + case "GUILD_BAN_ADD": + { + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId); if (server != null) - { + { server.AddBan(data.UserId); Logger.Info($"User Banned: {server.Name}/{data.UserId}"); OnUserBanned(server, data.UserId); - } - } - break; - case "GUILD_BAN_REMOVE": - { - var data = e.Payload.ToObject(_serializer); - var server = GetServer(data.GuildId); + } + } + break; + case "GUILD_BAN_REMOVE": + { + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId); if (server != null) - { + { if (server.RemoveBan(data.UserId)) { Logger.Info($"User Unbanned: {server.Name}/{data.UserId}"); OnUserUnbanned(server, data.UserId); } - } - } - break; + } + } + break; - //Messages - case "MESSAGE_CREATE": - { - var data = e.Payload.ToObject(_serializer); + //Messages + case "MESSAGE_CREATE": + { + var data = e.Payload.ToObject(_serializer); Channel channel = GetChannel(data.ChannelId); if (channel != null) @@ -548,7 +709,7 @@ namespace Discord msg = channel.AddMessage(data.Id, data.Author.Id, data.Timestamp.Value); //nonce = 0; } - + msg.Update(data); var user = msg.User; if (user != null) @@ -566,11 +727,11 @@ namespace Discord Logger.Info($"Message Received: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{msg.Id}"); OnMessageReceived(msg); } - } - break; - case "MESSAGE_UPDATE": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "MESSAGE_UPDATE": + { + var data = e.Payload.ToObject(_serializer); var channel = GetChannel(data.ChannelId); if (channel != null) { @@ -583,11 +744,11 @@ namespace Discord OnMessageUpdated(msg); } } - } - break; - case "MESSAGE_DELETE": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "MESSAGE_DELETE": + { + var data = e.Payload.ToObject(_serializer); var channel = GetChannel(data.ChannelId); if (channel != null) { @@ -598,11 +759,11 @@ namespace Discord OnMessageDeleted(msg); } } - } - break; - case "MESSAGE_ACK": - { - var data = e.Payload.ToObject(_serializer); + } + break; + case "MESSAGE_ACK": + { + var data = e.Payload.ToObject(_serializer); var channel = GetChannel(data.ChannelId); if (channel != null) { @@ -613,13 +774,13 @@ namespace Discord OnMessageAcknowledged(msg); } } - } - break; + } + break; - //Statuses - case "PRESENCE_UPDATE": - { - var data = e.Payload.ToObject(_serializer); + //Statuses + case "PRESENCE_UPDATE": + { + var data = e.Payload.ToObject(_serializer); User user; Server server; if (data.GuildId == null) @@ -633,20 +794,20 @@ namespace Discord user = server?.GetUser(data.User.Id); } - if (user != null) - { - user.Update(data); + if (user != null) + { + user.Update(data); Logger.Verbose($"Presence Updated: {server.Name}/{user.Name}"); OnUserPresenceUpdated(user); - } - } - break; - case "TYPING_START": - { - var data = e.Payload.ToObject(_serializer); - var channel = GetChannel(data.ChannelId); - if (channel != null) - { + } + } + break; + case "TYPING_START": + { + var data = e.Payload.ToObject(_serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { User user; if (channel.IsPrivate) { @@ -657,20 +818,20 @@ namespace Discord } else user = channel.Server.GetUser(data.UserId); - if (user != null) + if (user != null) { Logger.Verbose($"Is Typing: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{user.Name}"); - OnUserIsTypingUpdated(channel, user); - user.UpdateActivity(); + OnUserIsTypingUpdated(channel, user); + user.UpdateActivity(); } } - } - break; + } + break; - //Voice - case "VOICE_STATE_UPDATE": - { - var data = e.Payload.ToObject(_serializer); + //Voice + case "VOICE_STATE_UPDATE": + { + var data = e.Payload.ToObject(_serializer); var server = GetServer(data.GuildId); if (server != null) { @@ -682,126 +843,46 @@ namespace Discord OnUserVoiceStateUpdated(user); } } - } - break; + } + break; - //Settings - case "USER_UPDATE": - { - var data = e.Payload.ToObject(_serializer); + //Settings + case "USER_UPDATE": + { + var data = e.Payload.ToObject(_serializer); if (data.Id == CurrentUser.Id) { CurrentUser.Update(data); PrivateUser.Update(data); - foreach (var server in _servers) - server.Value.CurrentUser.Update(data); + foreach (var server in _servers) + server.Value.CurrentUser.Update(data); Logger.Info("Profile Updated"); OnProfileUpdated(CurrentUser); - } - } - break; - - //Ignored - case "USER_SETTINGS_UPDATE": - case "GUILD_INTEGRATIONS_UPDATE": - case "VOICE_SERVER_UPDATE": - break; - - case "RESUMED": //Handled in DataWebSocket - break; - - //Others - default: - Logger.Warning($"Unknown message type: {e.Type}"); - break; - } - } - catch (Exception ex) - { - Logger.Error($"Error handling {e.Type} event", ex); - } - } + } + } + break; - public Task SetStatus(UserStatus status) - { - if (status == null) throw new ArgumentNullException(nameof(status)); - if (status != UserStatus.Online && status != UserStatus.Idle) - throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); - CheckReady(); + //Ignored + case "USER_SETTINGS_UPDATE": + case "GUILD_INTEGRATIONS_UPDATE": + case "VOICE_SERVER_UPDATE": + break; - Status = status; - return SendStatus(); - } - public Task SetGame(int? gameId) - { - CheckReady(); + case "RESUMED": //Handled in DataWebSocket + break; - CurrentGameId = gameId; - return SendStatus(); - } - private Task SendStatus() - { - GatewaySocket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGameId); - return TaskHelper.CompletedTask; - } - - private Server AddServer(ulong id) - => _servers.GetOrAdd(id, x => new Server(this, x)); - private Server RemoveServer(ulong id) - { - Server server; - _servers.TryRemove(id, out server); - return server; - } - public Server GetServer(ulong id) - { - Server server; - _servers.TryGetValue(id, out server); - return server; - } - - private Channel AddChannel(ulong id, ulong? guildId, ulong? recipientId) - { - Channel channel; - if (recipientId != null) - { - channel = _privateChannels.GetOrAdd(recipientId.Value, - x => new Channel(this, x, new User(recipientId.Value, null))); - } - else - { - var server = GetServer(guildId.Value); - channel = server.AddChannel(id); + //Others + default: + Logger.Warning($"Unknown message type: {e.Type}"); + break; + } } - _channels[channel.Id] = channel; - return channel; - } - private Channel RemoveChannel(ulong id) - { - Channel channel; - if (_channels.TryRemove(id, out channel)) + catch (Exception ex) { - if (channel.IsPrivate) - _privateChannels.TryRemove(channel.Recipient.Id, out channel); - else - channel.Server.RemoveChannel(id); + Logger.Error($"Error handling {e.Type} event", ex); } - return channel; - } - internal Channel GetChannel(ulong id) - { - Channel channel; - _channels.TryGetValue(id, out channel); - return channel; - } - internal Channel GetPrivateChannel(ulong recipientId) - { - Channel channel; - _privateChannels.TryGetValue(recipientId, out channel); - return channel; } - #region Async Wrapper /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. public void Run(Func asyncAction) @@ -843,19 +924,6 @@ namespace Discord #endregion //Helpers - private void CheckReady() - { - switch (State) - { - case ConnectionState.Disconnecting: - throw new InvalidOperationException("The client is disconnecting."); - case ConnectionState.Disconnected: - throw new InvalidOperationException("The client is not connected to Discord"); - case ConnectionState.Connecting: - throw new InvalidOperationException("The client is connecting."); - } - } - private string GetTokenCachePath(string email) { using (var md5 = MD5.Create()) @@ -922,29 +990,7 @@ namespace Discord } } - private static string Base64Image(ImageType type, Stream stream, string existingId) - { - if (type == ImageType.None) - return null; - else if (stream != null) - { - byte[] bytes = new byte[stream.Length - stream.Position]; - stream.Read(bytes, 0, bytes.Length); - - string base64 = Convert.ToBase64String(bytes); - string imageType = type == ImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64"; - return $"data:{imageType},{base64}"; - } - return existingId; - } + - public Region GetRegion(string regionName) - { - Region region; - if (_regions.TryGetValue(regionName, out region)) - return region; - else - return _unknownRegion; - } } } \ No newline at end of file diff --git a/src/Discord.Net/Extensions.cs b/src/Discord.Net/Extensions.cs index 675efb253..30acf0e0b 100644 --- a/src/Discord.Net/Extensions.cs +++ b/src/Discord.Net/Extensions.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.Linq; using System.Runtime.CompilerServices; namespace Discord @@ -13,8 +16,10 @@ namespace Discord => ulong.Parse(value, NumberStyles.None, _format); public static ulong? ToNullableId(this string value) => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); - - public static string ToIdString(this ulong value) + public static bool TryToId(this string value, out ulong result) + => ulong.TryParse(value, NumberStyles.None, _format, out result); + + public static string ToIdString(this ulong value) => value.ToString(_format); public static string ToIdString(this ulong? value) => value?.ToString(_format); @@ -33,5 +38,100 @@ namespace Discord return true; } } + + public static IEnumerable Find(this IEnumerable channels, string name, ChannelType type = null, bool exactMatch = false) + { + //Search by name + var query = channels + .Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + + if (!exactMatch) + { + if (name.Length >= 2 && name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Search by raw mention + { + ulong id; + if (name.Substring(2, name.Length - 3).TryToId(out id)) + { + var channel = channels.Where(x => x.Id == id).FirstOrDefault(); + if (channel != null) + query = query.Concat(new Channel[] { channel }); + } + } + if (name.Length >= 1 && name[0] == '#' && (type == null || type == ChannelType.Text)) //Search by clean mention + { + string name2 = name.Substring(1); + query = query.Concat(channels + .Where(x => x.Type == ChannelType.Text && string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); + } + } + + if (type != null) + query = query.Where(x => x.Type == type); + return query; + } + + public static IEnumerable Find(this IEnumerable users, + string name, ushort? discriminator = null, bool exactMatch = false) + { + //Search by name + var query = users + .Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + + if (!exactMatch) + { + if (name.Length >= 2 && name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Search by raw mention + { + ulong id; + if (name.Substring(2, name.Length - 3).TryToId(out id)) + { + var user = users.Where(x => x.Id == id).FirstOrDefault(); + if (user != null) + query = query.Concat(new User[] { user }); + } + } + if (name.Length >= 1 && name[0] == '@') //Search by clean mention + { + string name2 = name.Substring(1); + query = query.Concat(users + .Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); + } + } + + if (discriminator != null) + query = query.Where(x => x.Discriminator == discriminator.Value); + return query; + } + + public static IEnumerable Find(this IEnumerable roles, string name, bool exactMatch = false) + { + // if (name.StartsWith("@")) + // { + // string name2 = name.Substring(1); + // return _roles.Where(x => x.Server.Id == server.Id && + // string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || + // string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); + // } + // else + return roles.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + } + + public static IEnumerable Find(this IEnumerable servers, string name, bool exactMatch = false) + => servers.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + + public static string Base64(this Stream stream, ImageType type, string existingId) + { + if (type == ImageType.None) + return null; + else if (stream != null) + { + byte[] bytes = new byte[stream.Length - stream.Position]; + stream.Read(bytes, 0, bytes.Length); + + string base64 = Convert.ToBase64String(bytes); + string imageType = type == ImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64"; + return $"data:{imageType},{base64}"; + } + return existingId; + } } } diff --git a/src/Discord.Net/IService.cs b/src/Discord.Net/IService.cs new file mode 100644 index 000000000..15f79b0c4 --- /dev/null +++ b/src/Discord.Net/IService.cs @@ -0,0 +1,7 @@ +namespace Discord +{ + public interface IService + { + void Install(DiscordClient client); + } +} diff --git a/src/Discord.Net/Mention.cs b/src/Discord.Net/Mention.cs deleted file mode 100644 index e9f1e3669..000000000 --- a/src/Discord.Net/Mention.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace Discord -{ - public static class Mention - { - private static readonly Regex _userRegex = new Regex(@"<@([0-9]+)>", RegexOptions.Compiled); - private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); - private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled); - - /// Returns the string used to create a user mention. - [Obsolete("Use User.Mention instead")] - public static string User(User user) - => $"<@{user.Id}>"; - /// Returns the string used to create a channel mention. - [Obsolete("Use Channel.Mention instead")] - public static string Channel(Channel channel) - => $"<#{channel.Id}>"; - /// Returns the string used to create a mention to everyone in a channel. - [Obsolete("Use Server.EveryoneRole.Mention instead")] - public static string Everyone() - => $"@everyone"; - - internal static string CleanUserMentions(DiscordClient client, Channel channel, string text, List users = null) - { - return _userRegex.Replace(text, new MatchEvaluator(e => - { - var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); - var user = channel.GetUser(id); - if (user != null) - { - if (users != null) - users.Add(user); - return '@' + user.Name; - } - else //User not found - return '@' + e.Value; - })); - } - internal static string CleanChannelMentions(DiscordClient client, Channel channel, string text, List channels = null) - { - var server = channel.Server; - if (server == null) return text; - - return _channelRegex.Replace(text, new MatchEvaluator(e => - { - var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); - var mentionedChannel = server.GetChannel(id); - if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id) - { - if (channels != null) - channels.Add(mentionedChannel); - return '#' + mentionedChannel.Name; - } - else //Channel not found - return '#' + e.Value; - })); - } - /*internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List roles = null) - { - var server = channel.Server; - if (server == null) return text; - - return _roleRegex.Replace(text, new MatchEvaluator(e => - { - if (roles != null && user.GetPermissions(channel).MentionEveryone) - roles.Add(server.EveryoneRole); - return e.Value; - })); - }*/ - - /// Resolves all mentions in a provided string to those users, channels or roles' names. - public static string Resolve(Message source, string text) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (text == null) throw new ArgumentNullException(nameof(text)); - - return Resolve(source.Channel, text); - } - - /// Resolves all mentions in a provided string to those users, channels or roles' names. - public static string Resolve(Channel channel, string text) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - - var client = channel.Client; - text = CleanUserMentions(client, channel, text); - text = CleanChannelMentions(client, channel, text); - //text = CleanRoleMentions(_client, channel, text); - return text; - } - } -} diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index 525c5bdd8..552c146c9 100644 --- a/src/Discord.Net/Models/Channel.cs +++ b/src/Discord.Net/Models/Channel.cs @@ -1,8 +1,13 @@ using Discord.API.Client; +using Discord.API.Client.Rest; +using Discord.Net; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net; +using System.Threading.Tasks; using APIChannel = Discord.API.Client.Channel; namespace Discord @@ -38,9 +43,9 @@ namespace Discord private readonly ConcurrentDictionary _users; private readonly ConcurrentDictionary _messages; private Dictionary _permissionOverwrites; - - /// Gets the client that generated this channel object. + internal DiscordClient Client { get; } + /// Gets the unique identifier for this channel. public ulong Id { get; } /// Gets the server owning this channel, if this is a public chat. @@ -149,32 +154,85 @@ namespace Discord } } - //Members - internal void AddUser(User user) + /// Edits this channel, changing only non-null attributes. + public async Task Edit(string name = null, string topic = null, int? position = null) { - if (!Client.Config.UsePermissionsCache) - return; + if (name != null || topic != null) + { + var request = new UpdateChannelRequest(Id) + { + Name = name ?? Name, + Topic = topic ?? Topic, + Position = Position + }; + await Client.ClientAPI.Send(request).ConfigureAwait(false); + } - var member = new Member(user); - if (_users.TryAdd(user.Id, member)) - UpdatePermissions(user, member.Permissions); + if (position != null) + { + Channel[] channels = Server.Channels.Where(x => x.Type == Type).OrderBy(x => x.Position).ToArray(); + int oldPos = Array.IndexOf(channels, this); + var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault(); + int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1; + if (newPos < 0) + newPos = 0; + int minPos; + + if (oldPos < newPos) //Moving Down + { + minPos = oldPos; + for (int i = oldPos; i < newPos; i++) + channels[i] = channels[i + 1]; + channels[newPos] = this; + } + else //(oldPos > newPos) Moving Up + { + minPos = newPos; + for (int i = oldPos; i > newPos; i--) + channels[i] = channels[i - 1]; + channels[newPos] = this; + } + Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null; + await Server.ReorderChannels(channels.Skip(minPos), after).ConfigureAwait(false); + } } - internal void RemoveUser(ulong id) + + public async Task Delete() { - if (!Client.Config.UsePermissionsCache) - return; - - Member ignored; - _users.TryRemove(id, out ignored); + try { await Client.ClientAPI.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } } - public User GetUser(ulong id) + + #region Invites + /// Gets all active (non-expired) invites to this server. + public async Task> GetInvites() + => (await Server.GetInvites()).Where(x => x.Channel.Id == Id); + + /// Creates a new invite to this channel. + /// Time (in seconds) until the invite expires. Set to null to never expire. + /// The max amount of times this invite may be used. Set to null to have unlimited uses. + /// 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 null. + public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) { - Member result; - _users.TryGetValue(id, out result); - return result.User; + if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); + if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); + + var request = new CreateInviteRequest(Id) + { + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0, + IsTemporary = tempMembership, + WithXkcdPass = withXkcd + }; + + var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); + var invite = new Invite(Client, response.Code, response.XkcdPass); + return invite; } + #endregion - //Messages + #region Messages internal Message AddMessage(ulong id, ulong userId, DateTime timestamp) { Message message = new Message(id, this, userId); @@ -202,14 +260,117 @@ namespace Discord } return null; } + public Message GetMessage(ulong id) { Message result; _messages.TryGetValue(id, out result); return result; } + public async Task DownloadMessages(int limit = 100, ulong? relativeMessageId = null, + RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) + { + if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); + if (limit == 0 || Type != ChannelType.Text) return new Message[0]; + + try + { + var request = new GetMessagesRequest(Id) + { + Limit = limit, + RelativeDir = relativeMessageId.HasValue ? relativeDir == RelativeDirection.Before ? "before" : "after" : null, + RelativeId = relativeMessageId ?? 0 + }; + var msgs = await Client.ClientAPI.Send(request).ConfigureAwait(false); + return msgs.Select(x => + { + Message msg = null; + if (useCache) + { + msg = AddMessage(x.Id, x.Author.Id, x.Timestamp.Value); + var user = msg.User; + if (user != null) + user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); + } + else + msg = new Message(x.Id, this, x.Author.Id); + msg.Update(x); + return msg; + }) + .ToArray(); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + return new Message[0]; + } + } + + /// Returns all members of this channel with the specified name. + /// Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false. + public IEnumerable FindUsers(string name, bool exactMatch = false) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + return _users.Select(x => x.Value.User).Find(name, exactMatch: exactMatch); + } + + public Task SendMessage(string text) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + return SendMessageInternal(text, false); + } + public Task SendTTSMessage(string text) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + return SendMessageInternal(text, true); + } + private async Task SendMessageInternal(string text, bool isTTS) + { + Message msg = null; + var mentionedUsers = new List(); + text = Message.CleanUserMentions(this, text, mentionedUsers); + if (text.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); + + if (Client.Config.UseMessageQueue) + Client.MessageQueue.QueueSend(Id, text, mentionedUsers.Select(x => x.Id).ToArray(), isTTS); + else + { + var request = new SendMessageRequest(Id) + { + Content = text, + MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(), + Nonce = null, + IsTTS = isTTS + }; + var model = await Client.ClientAPI.Send(request).ConfigureAwait(false); + msg = AddMessage(model.Id, model.Author.Id, model.Timestamp.Value); + msg.Update(model); + } + return msg; + } - //Permissions + public Task SendFile(string filePath) + => SendFile(Path.GetFileName(filePath), File.OpenRead(filePath)); + public async Task SendFile(string filename, Stream stream) + { + if (filename == null) throw new ArgumentNullException(nameof(filename)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + var request = new SendFileRequest(Id) + { + Filename = filename, + Stream = stream + }; + var model = await Client.ClientAPI.Send(request).ConfigureAwait(false); + + var msg = AddMessage(model.Id, model.Author.Id, model.Timestamp.Value); + msg.Update(model); + return msg; + } + #endregion + + #region Permissions internal void UpdatePermissions() { if (!Client.Config.UsePermissionsCache) @@ -291,6 +452,116 @@ namespace Discord } } + [Obsolete("Use Channel.GetPermissions")] + public DualChannelPermissions GetPermissionsRule(User user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + return PermissionOverwrites + .Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id) + .Select(x => x.Permissions) + .FirstOrDefault(); + } + [Obsolete("Use Channel.GetPermissions")] + public DualChannelPermissions GetPermissionsRule(Role role) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + + return PermissionOverwrites + .Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id) + .Select(x => x.Permissions) + .FirstOrDefault(); + } + + [Obsolete("Use Channel.SetPermissions")] + public Task AddPermissionsRule(User user, ChannelPermissions allow = null, ChannelPermissions deny = null) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + return AddPermissionsRule(user.Id, PermissionTarget.User, allow, deny); + } + [Obsolete("Use Channel.SetPermissions")] + public Task AddPermissionsRule(User user, DualChannelPermissions permissions = null) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + return AddPermissionsRule(user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny); + } + [Obsolete("Use Channel.SetPermissions")] + public Task AddPermissionsRule(Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + + return AddPermissionsRule(role.Id, PermissionTarget.Role, allow, deny); + } + [Obsolete("Use Channel.SetPermissions")] + public Task AddPermissionsRule(Role role, DualChannelPermissions permissions = null) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + + return AddPermissionsRule(role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny); + } + private Task AddPermissionsRule(ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null) + { + var request = new AddChannelPermissionsRequest(Id) + { + TargetId = targetId, + TargetType = targetType.Value, + Allow = allow?.RawValue ?? 0, + Deny = deny?.RawValue ?? 0 + }; + return Client.ClientAPI.Send(request); + } + + [Obsolete("Use Channel.RemovePermissions")] + public Task RemovePermissionsRule(User user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + return RemovePermissionsRule(user.Id, PermissionTarget.User); + } + [Obsolete("Use Channel.RemovePermissions")] + public Task RemovePermissionsRule(Role role) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + return RemovePermissionsRule(role.Id, PermissionTarget.Role); + } + private async Task RemovePermissionsRule(ulong userOrRoleId, PermissionTarget targetType) + { + try + { + var perms = PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault(); + await Client.ClientAPI.Send(new RemoveChannelPermissionsRequest(Id, userOrRoleId)).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + #endregion + + #region Users + internal void AddUser(User user) + { + if (!Client.Config.UsePermissionsCache) + return; + + var member = new Member(user); + if (_users.TryAdd(user.Id, member)) + UpdatePermissions(user, member.Permissions); + } + internal void RemoveUser(ulong id) + { + if (!Client.Config.UsePermissionsCache) + return; + + Member ignored; + _users.TryRemove(id, out ignored); + } + public User GetUser(ulong id) + { + Member result; + _users.TryGetValue(id, out result); + return result.User; + } + #endregion + public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658); public override string ToString() => Name ?? Id.ToIdString(); diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs index b73a68782..9639d1f6d 100644 --- a/src/Discord.Net/Models/Invite.cs +++ b/src/Discord.Net/Models/Invite.cs @@ -1,5 +1,9 @@ using Discord.API.Client; +using Discord.API.Client.Rest; +using Discord.Net; using System; +using System.Net; +using System.Threading.Tasks; using APIInvite = Discord.API.Client.Invite; namespace Discord @@ -55,6 +59,10 @@ namespace Discord } } + private ulong _serverId, _channelId; + + internal DiscordClient Client { get; } + /// Gets the unique code for this invite. public string Code { get; } /// Gets, if enabled, an alternative human-readable invite code. @@ -63,9 +71,9 @@ namespace Discord /// Gets information about the server this invite is attached to. public ServerInfo Server { get; private set; } /// Gets information about the channel this invite is attached to. - public ChannelInfo Channel { get; private set; } - /// Gets the time (in seconds) until the invite expires. - public int? MaxAge { get; private set; } + public ChannelInfo Channel { get; private set; } + /// Gets the time (in seconds) until the invite expires. + public int? MaxAge { get; private set; } /// Gets the amount of times this invite has been used. public int Uses { get; private set; } /// Gets the max amount of times this invite may be used. @@ -80,8 +88,9 @@ namespace Discord /// Returns a URL for this invite using XkcdCode if available or Id if not. public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; - internal Invite(string code, string xkcdPass) + internal Invite(DiscordClient client, string code, string xkcdPass) { + Client = client; Code = code; XkcdCode = xkcdPass; } @@ -110,8 +119,16 @@ namespace Discord if (model.CreatedAt != null) CreatedAt = model.CreatedAt.Value; } + + public async Task Delete() + { + try { await Client.ClientAPI.Send(new DeleteInviteRequest(Code)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + public Task Accept() + => Client.ClientAPI.Send(new AcceptInviteRequest(Code)); - public override bool Equals(object obj) => obj is Invite && (obj as Invite).Code == Code; + public override bool Equals(object obj) => obj is Invite && (obj as Invite).Code == Code; public override int GetHashCode() => unchecked(Code.GetHashCode() + 9980); public override string ToString() => XkcdCode ?? Code; } diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 21872a3b3..78adb0170 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -1,9 +1,14 @@ -using Newtonsoft.Json; +using Discord.API.Client.Rest; +using Discord.Net; +using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; using APIMessage = Discord.API.Client.Message; namespace Discord @@ -16,8 +21,73 @@ namespace Discord } public sealed class Message - { - /*internal class ImportResolver : DefaultContractResolver + { + private static readonly Regex _userRegex = new Regex(@"<@([0-9]+)>", RegexOptions.Compiled); + private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); + private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled); + private static readonly Attachment[] _initialAttachments = new Attachment[0]; + private static readonly Embed[] _initialEmbeds = new Embed[0]; + + internal static string CleanUserMentions(Channel channel, string text, List users = null) + { + return _userRegex.Replace(text, new MatchEvaluator(e => + { + var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); + var user = channel.GetUser(id); + if (user != null) + { + if (users != null) + users.Add(user); + return '@' + user.Name; + } + else //User not found + return '@' + e.Value; + })); + } + internal static string CleanChannelMentions(Channel channel, string text, List channels = null) + { + var server = channel.Server; + if (server == null) return text; + + return _channelRegex.Replace(text, new MatchEvaluator(e => + { + var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); + var mentionedChannel = server.GetChannel(id); + if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id) + { + if (channels != null) + channels.Add(mentionedChannel); + return '#' + mentionedChannel.Name; + } + else //Channel not found + return '#' + e.Value; + })); + } + /*internal static string CleanRoleMentions(User user, Channel channel, string text, List roles = null) + { + var server = channel.Server; + if (server == null) return text; + + return _roleRegex.Replace(text, new MatchEvaluator(e => + { + if (roles != null && user.GetPermissions(channel).MentionEveryone) + roles.Add(server.EveryoneRole); + return e.Value; + })); + }*/ + + private static string Resolve(Channel channel, string text) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + + var client = channel.Client; + text = CleanUserMentions(channel, text); + text = CleanChannelMentions(channel, text); + //text = CleanRoleMentions(Channel, text); + return text; + } + + /*internal class ImportResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { @@ -33,7 +103,7 @@ namespace Discord } }*/ - public sealed class Attachment : File + public sealed class Attachment : File { /// Unique identifier for this file. public string Id { get; internal set; } @@ -89,11 +159,10 @@ namespace Discord internal File() { } } - private static readonly Attachment[] _initialAttachments = new Attachment[0]; - private static readonly Embed[] _initialEmbeds = new Embed[0]; - private readonly ulong _userId; + internal DiscordClient Client => Channel.Client; + /// Returns the unique identifier for this message. public ulong Id { get; } /// Returns the channel this message was sent to. @@ -117,7 +186,7 @@ namespace Discord public Attachment[] Attachments { get; private set; } /// Returns a collection of all embeded content in this message. public Embed[] Embeds { get; private set; } - + /// Returns a collection of all users mentioned in this message. public IEnumerable MentionedUsers { get; internal set; } /// Returns a collection of all channels mentioned in this message. @@ -210,11 +279,11 @@ namespace Discord //var mentionedUsers = new List(); var mentionedChannels = new List(); //var mentionedRoles = new List(); - text = Mention.CleanUserMentions(Channel.Client, channel, text/*, mentionedUsers*/); + text = CleanUserMentions(Channel, text/*, mentionedUsers*/); if (server != null) { - text = Mention.CleanChannelMentions(Channel.Client, channel, text, mentionedChannels); - //text = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles); + text = CleanChannelMentions(Channel, text, mentionedChannels); + //text = CleanRoleMentions(_client, User, channel, text, mentionedRoles); } Text = text; @@ -236,7 +305,54 @@ namespace Discord } } - public override bool Equals(object obj) => obj is Message && (obj as Message).Id == Id; + public async Task Edit(string text) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + + var channel = Channel; + var mentionedUsers = new List(); + if (!channel.IsPrivate) + text = CleanUserMentions(channel, text, mentionedUsers); + + if (text.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); + + if (Client.Config.UseMessageQueue) + Client.MessageQueue.QueueEdit(channel.Id, Id, text, mentionedUsers.Select(x => x.Id).ToArray()); + else + { + var request = new UpdateMessageRequest(Channel.Id, Id) + { + Content = text, + MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray() + }; + await Client.ClientAPI.Send(request).ConfigureAwait(false); + } + } + + public async Task Delete() + { + var request = new DeleteMessageRequest(Channel.Id, Id); + try { await Client.ClientAPI.Send(request).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + public Task Acknowledge() + { + if (_userId != Client.CurrentUser.Id) + return Client.ClientAPI.Send(new AckMessageRequest(Channel.Id, Id)); + else + return TaskHelper.CompletedTask; + } + + /// Resolves all mentions in a provided string to those users, channels or roles' names. + public string Resolve(string text) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + return Resolve(Channel, text); + } + + public override bool Equals(object obj) => obj is Message && (obj as Message).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 9979); public override string ToString() => $"{User}: {RawText}"; } diff --git a/src/Discord.Net/Models/Permissions.cs b/src/Discord.Net/Models/Permissions.cs index 3d98525b7..206faa6ea 100644 --- a/src/Discord.Net/Models/Permissions.cs +++ b/src/Discord.Net/Models/Permissions.cs @@ -6,8 +6,8 @@ namespace Discord { //General CreateInstantInvite = 0, - BanMembers = 1, - KickMembers = 2, + KickMembers = 1, + BanMembers = 2, ManageRolesOrPermissions = 3, ManageChannel = 4, ManageServer = 5, diff --git a/src/Discord.Net/Models/Profile.cs b/src/Discord.Net/Models/Profile.cs index 62662fc76..65f73cfe1 100644 --- a/src/Discord.Net/Models/Profile.cs +++ b/src/Discord.Net/Models/Profile.cs @@ -1,10 +1,15 @@ -using Newtonsoft.Json; +using Discord.API.Client.Rest; +using System; +using System.IO; +using System.Threading.Tasks; using APIUser = Discord.API.Client.User; namespace Discord { public sealed class Profile { + internal DiscordClient Client { get; } + /// Gets the unique identifier for this user. public ulong Id { get; private set; } /// Gets the email for this user. @@ -12,7 +17,10 @@ namespace Discord /// Gets if the email for this user has been verified. public bool? IsVerified { get; private set; } - internal Profile() { } + internal Profile(DiscordClient client) + { + Client = client; + } internal void Update(APIUser model) { @@ -21,6 +29,36 @@ namespace Discord IsVerified = model.IsVerified; } + public async Task Edit(string currentPassword = "", + string username = null, string email = null, string password = null, + Stream avatar = null, ImageType avatarType = ImageType.Png) + { + if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); + + var request = new UpdateProfileRequest() + { + CurrentPassword = currentPassword, + Email = email ?? Email, + Password = password, + Username = username ?? Client.PrivateUser.Name, + AvatarBase64 = avatar.Base64(avatarType, Client.PrivateUser.AvatarId) + }; + + await Client.ClientAPI.Send(request).ConfigureAwait(false); + + if (password != null) + { + var loginRequest = new LoginRequest() + { + Email = Email, + Password = password + }; + var loginResponse = await Client.ClientAPI.Send(loginRequest).ConfigureAwait(false); + Client.ClientAPI.Token = loginResponse.Token; + Client.GatewaySocket.Token = loginResponse.Token; + } + } + public override bool Equals(object obj) => (obj is Profile && (obj as Profile).Id == Id) || (obj is User && (obj as User).Id == Id); public override int GetHashCode() => unchecked(Id.GetHashCode() + 2061); diff --git a/src/Discord.Net/Models/Role.cs b/src/Discord.Net/Models/Role.cs index 7d7c7c82e..44aae34cc 100644 --- a/src/Discord.Net/Models/Role.cs +++ b/src/Discord.Net/Models/Role.cs @@ -1,13 +1,17 @@ -using System; +using Discord.API.Client.Rest; +using Discord.Net; +using System; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Threading.Tasks; using APIRole = Discord.API.Client.Role; namespace Discord { public sealed class Role { - private readonly DiscordClient _client; + internal DiscordClient Client => Server.Client; /// Gets the unique identifier for this role. public ulong Id { get; } @@ -72,8 +76,57 @@ namespace Discord foreach (var member in Members) Server.UpdatePermissions(member); } + + public async Task Edit(string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null) + { + var updateRequest = new UpdateRoleRequest(Server.Id, Id) + { + Name = name ?? Name, + Permissions = (permissions ?? Permissions).RawValue, + Color = (color ?? Color).RawValue, + IsHoisted = isHoisted ?? IsHoisted + }; + + var updateResponse = await Client.ClientAPI.Send(updateRequest).ConfigureAwait(false); + + if (position != null) + { + int oldPos = Position; + int newPos = position.Value; + int minPos; + Role[] roles = Server.Roles.OrderBy(x => x.Position).ToArray(); + + if (oldPos < newPos) //Moving Down + { + minPos = oldPos; + for (int i = oldPos; i < newPos; i++) + roles[i] = roles[i + 1]; + roles[newPos] = this; + } + else //(oldPos > newPos) Moving Up + { + minPos = newPos; + for (int i = oldPos; i > newPos; i--) + roles[i] = roles[i - 1]; + roles[newPos] = this; + } + + var reorderRequest = new ReorderRolesRequest(Server.Id) + { + RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), + StartPos = minPos + }; + await Client.ClientAPI.Send(reorderRequest).ConfigureAwait(false); + } + } + + public async Task Delete() + { + try { await Client.ClientAPI.Send(new DeleteRoleRequest(Server.Id, Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } - public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id; + public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 6653); public override string ToString() => Name ?? Id.ToIdString(); } diff --git a/src/Discord.Net/Models/Server.cs b/src/Discord.Net/Models/Server.cs index 785f9df2b..71f6ecab6 100644 --- a/src/Discord.Net/Models/Server.cs +++ b/src/Discord.Net/Models/Server.cs @@ -1,14 +1,22 @@ using Discord.API.Client; +using Discord.API.Client.Rest; +using Discord.Net; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net; +using System.Threading.Tasks; namespace Discord { /// Represents a Discord server (also known as a guild). public sealed class Server { + internal static string GetIconUrl(ulong serverId, string iconId) + => iconId != null ? $"{DiscordConfig.CDNUrl}/icons/{serverId}/{iconId}.jpg" : null; + private struct Member { public readonly User User; @@ -19,7 +27,7 @@ namespace Discord Permissions = new ServerPermissions(); Permissions.Lock(); } - } + } private readonly ConcurrentDictionary _roles; private readonly ConcurrentDictionary _users; @@ -27,9 +35,9 @@ namespace Discord private readonly ConcurrentDictionary _bans; private ulong _ownerId; private ulong? _afkChannelId; - - /// Gets the client that generated this server object. + internal DiscordClient Client { get; } + /// Gets the unique identifier for this server. public ulong Id { get; } /// Gets the default channel for this server. @@ -39,19 +47,14 @@ namespace Discord /// Gets the name of this server. public string Name { get; private set; } - - /// Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel, if one is set. - public int AFKTimeout { get; private set; } - /// Gets the date and time you joined this server. - public DateTime JoinedAt { get; private set; } /// Gets the voice region for this server. public Region Region { get; private set; } /// Gets the unique identifier for this user's current avatar. public string IconId { get; private set; } - /// Gets the URL to this user's current avatar. - public string IconUrl => GetIconUrl(Id, IconId); - internal static string GetIconUrl(ulong serverId, string iconId) - => iconId != null ? $"{DiscordConfig.CDNUrl}/icons/{serverId}/{iconId}.jpg" : null; + /// Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK voice channel, if one is set. + public int AFKTimeout { get; private set; } + /// Gets the date and time you joined this server. + public DateTime JoinedAt { get; private set; } /// Gets the user that created this server. public User Owner => GetUser(_ownerId); @@ -59,16 +62,18 @@ namespace Discord public Channel AFKChannel => _afkChannelId != null ? GetChannel(_afkChannelId.Value) : null; /// Gets the current user in this server. public User CurrentUser => GetUser(Client.CurrentUser.Id); + /// Gets the URL to this user's current avatar. + public string IconUrl => GetIconUrl(Id, IconId); /// Gets a collection of the ids of all users banned on this server. public IEnumerable BannedUserIds => _bans.Select(x => x.Key); - /// Gets a collection of all channels within this server. + /// Gets a collection of all channels in this server. public IEnumerable Channels => _channels.Select(x => x.Value); - /// Gets a collection of all users within this server with their server-specific data. + /// Gets a collection of all members in this server. public IEnumerable Users => _users.Select(x => x.Value.User); - /// Gets a collection of all roles within this server. + /// Gets a collection of all roles in this server. public IEnumerable Roles => _roles.Select(x => x.Value); - + internal Server(DiscordClient client, ulong id) { Client = client; @@ -80,7 +85,7 @@ namespace Discord DefaultChannel = AddChannel(id); EveryoneRole = AddRole(id); } - + internal void Update(GuildReference model) { if (model.Name != null) @@ -120,7 +125,7 @@ namespace Discord if (model.Members != null) { foreach (var subModel in model.Members) - AddMember(subModel.User.Id).Update(subModel); + AddUser(subModel.User.Id).Update(subModel); } if (model.VoiceStates != null) { @@ -133,8 +138,39 @@ namespace Discord GetUser(subModel.User.Id)?.Update(subModel); } } + + /// Edits this server, changing only non-null attributes. + public Task Edit(string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) + { + var request = new UpdateGuildRequest(Id) + { + Name = name ?? Name, + Region = region ?? Region.Id, + IconBase64 = icon.Base64(iconType, IconId), + AFKChannelId = AFKChannel?.Id, + AFKTimeout = AFKTimeout + }; + return Client.ClientAPI.Send(request); + } - //Bans + /// Leaves this server. This function will fail if you're the owner - use Delete instead. + public async Task Leave() + { + if (_ownerId == CurrentUser.Id) + throw new InvalidOperationException("Unable to leave a server you own, use Server.Delete instead"); + try { await Client.ClientAPI.Send(new LeaveGuildRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + /// Deletes this server. This function will fail if you're not the owner - use Leave instead. + public async Task Delete() + { + if (_ownerId != CurrentUser.Id) + throw new InvalidOperationException("Unable to delete a server you don't own, use Server.Leave instead"); + try { await Client.ClientAPI.Send(new LeaveGuildRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + #region Bans internal void AddBan(ulong banId) => _bans.TryAdd(banId, true); internal bool RemoveBan(ulong banId) @@ -143,7 +179,22 @@ namespace Discord return _bans.TryRemove(banId, out ignored); } - //Channels + public Task Ban(User user, int pruneDays = 0) + { + var request = new AddGuildBanRequest(user.Server.Id, user.Id); + request.PruneDays = pruneDays; + return Client.ClientAPI.Send(request); + } + public Task Unban(User user, int pruneDays = 0) + => Unban(user.Id); + public async Task Unban(ulong userId) + { + try { await Client.ClientAPI.Send(new RemoveGuildBanRequest(Id, userId)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + #endregion + + #region Channels internal Channel AddChannel(ulong id) => _channels.GetOrAdd(id, x => new Channel(Client, x, this)); internal Channel RemoveChannel(ulong id) @@ -152,6 +203,8 @@ namespace Discord _channels.TryRemove(id, out channel); return channel; } + + /// Gets the channel with the provided id and owned by this server, or null if not found. public Channel GetChannel(ulong id) { Channel result; @@ -159,36 +212,67 @@ namespace Discord return result; } - //Members - internal User AddMember(ulong id) + /// Returns all channels with the specified server and name. + /// Name formats supported: Name, #Name and <#Id>. Search is case-insensitive if exactMatch is false. + public IEnumerable FindChannels(string name, ChannelType type = null, bool exactMatch = false) { - User newUser = null; - var user = _users.GetOrAdd(id, x => new Member(new User(id, this))); - if (user.User == newUser) - { - foreach (var channel in Channels) - channel.AddUser(newUser); - } - return user.User; + if (name == null) throw new ArgumentNullException(nameof(name)); + if (type == null) throw new ArgumentNullException(nameof(type)); + + return _channels.Select(x => x.Value).Find(name, type, exactMatch); } - internal User RemoveMember(ulong id) - { - Member member; - if (_users.TryRemove(id, out member)) - { - foreach (var channel in Channels) - channel.RemoveUser(id); - } - return member.User; + + /// Creates a new channel. + public async Task CreateChannel(string name, ChannelType type) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (type == null) throw new ArgumentNullException(nameof(type)); + + var request = new CreateChannelRequest(Id) { Name = name, Type = type.Value }; + var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); + + var channel = AddChannel(response.Id); + channel.Update(response); + return channel; } - public User GetUser(ulong id) + + /// Reorders the provided channels and optionally places them after a certain channel. + public Task ReorderChannels(IEnumerable channels, Channel after = null) { - Member result; - _users.TryGetValue(id, out result); - return result.User; + if (channels == null) throw new ArgumentNullException(nameof(channels)); + + var request = new ReorderChannelsRequest(Id) + { + ChannelIds = channels.Select(x => x.Id).ToArray(), + StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position) + }; + return Client.ClientAPI.Send(request); } + #endregion - //Roles + #region Invites + /// Gets all active (non-expired) invites to this server. + public async Task> GetInvites() + { + var response = await Client.ClientAPI.Send(new GetInvitesRequest(Id)).ConfigureAwait(false); + return response.Select(x => + { + var invite = new Invite(Client, x.Code, x.XkcdPass); + invite.Update(x); + return invite; + }); + } + + /// Creates a new invite to the default channel of this server. + /// Time (in seconds) until the invite expires. Set to null to never expire. + /// The max amount of times this invite may be used. Set to null to have unlimited uses. + /// 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 null. + public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) + => DefaultChannel.CreateInvite(maxAge, maxUses, tempMembership, withXkcd); + #endregion + + #region Roles internal Role AddRole(ulong id) => _roles.GetOrAdd(id, x => new Role(x, this)); internal Role RemoveRole(ulong id) @@ -197,14 +281,59 @@ namespace Discord _roles.TryRemove(id, out role); return role; } + + /// Gets the role with the provided id and owned by this server, or null if not found. public Role GetRole(ulong id) { Role result; _roles.TryGetValue(id, out result); return result; } + /// Returns all roles with the specified server and name. + /// Search is case-insensitive if exactMatch is false. + public IEnumerable FindRoles(string name, bool exactMatch = false) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + return _roles.Select(x => x.Value).Find(name, exactMatch); + } + + /// Creates a new role. + public async Task CreateRole(string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false) + { + if (name == null) throw new ArgumentNullException(nameof(name)); - //Permissions + var createRequest = new CreateRoleRequest(Id); + var createResponse = await Client.ClientAPI.Send(createRequest).ConfigureAwait(false); + var role = AddRole(createResponse.Id); + role.Update(createResponse); + + var editRequest = new UpdateRoleRequest(role.Server.Id, role.Id) + { + Name = name, + Permissions = (permissions ?? role.Permissions).RawValue, + Color = (color ?? Color.Default).RawValue, + IsHoisted = isHoisted + }; + var editResponse = await Client.ClientAPI.Send(editRequest).ConfigureAwait(false); + role.Update(editResponse); + + return role; + } + + /// Reorders the provided roles and optionally places them after a certain role. + public Task ReorderRoles(IEnumerable roles, Role after = null) + { + if (roles == null) throw new ArgumentNullException(nameof(roles)); + + return Client.ClientAPI.Send(new ReorderRolesRequest(Id) + { + RoleIds = roles.Select(x => x.Id).ToArray(), + StartPos = after != null ? after.Position + 1 : roles.Min(x => x.Position) + }); + } + #endregion + + #region Permissions internal ServerPermissions GetPermissions(User user) { Member member; @@ -213,12 +342,14 @@ namespace Discord else return null; } + internal void UpdatePermissions(User user) { Member member; if (_users.TryGetValue(user.Id, out member)) UpdatePermissions(member.User, member.Permissions); } + private void UpdatePermissions(User user, ServerPermissions permissions) { uint newPermissions = 0; @@ -241,8 +372,75 @@ namespace Discord channel.Value.UpdatePermissions(user); } } + #endregion + + #region Users + internal User AddUser(ulong id) + { + User newUser = null; + var user = _users.GetOrAdd(id, x => new Member(new User(Client, id, this))); + if (user.User == newUser) + { + foreach (var channel in Channels) + channel.AddUser(newUser); + } + return user.User; + } + internal User RemoveUser(ulong id) + { + Member member; + if (_users.TryRemove(id, out member)) + { + foreach (var channel in Channels) + channel.RemoveUser(id); + } + return member.User; + } + + /// Gets the user with the provided id and is a member of this server, or null if not found. + public User GetUser(ulong id) + { + Member result; + _users.TryGetValue(id, out result); + return result.User; + } + /// Gets the user with the provided username and discriminator, that is a member of this server, or null if not found. + public User GetUser(string name, ushort discriminator) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + return _users.Select(x => x.Value.User).Find(name, discriminator: discriminator, exactMatch: false).FirstOrDefault(); + } + /// Returns all members of this server with the specified name. + /// Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false. + public IEnumerable FindUsers(string name, bool exactMatch = false) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + return _users.Select(x => x.Value.User).Find(name, exactMatch: exactMatch); + } + + /// Kicks all users with an inactivity greater or equal to the provided number of days. + /// If true, no pruning will actually be done but instead return the number of users that would be pruned. + public async Task PruneUsers(int days = 30, bool simulate = false) + { + if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); + + var request = new PruneMembersRequest(Id) + { + Days = days, + IsSimulation = simulate + }; + var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); + return response.Pruned; + } + + /// When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for this server. + public void RequestOfflineUsers() + => Client.GatewaySocket.SendRequestMembers(Id, "", 0); + #endregion - public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id; + public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 5175); public override string ToString() => Name ?? Id.ToIdString(); } diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs index 8a253a9cb..6a2948dab 100644 --- a/src/Discord.Net/Models/User.cs +++ b/src/Discord.Net/Models/User.cs @@ -1,13 +1,19 @@ using Discord.API.Client; +using Discord.API.Client.Rest; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading.Tasks; using APIMember = Discord.API.Client.Member; namespace Discord { public class User - { + { + internal static string GetAvatarUrl(ulong userId, string avatarId) + => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null; + [Flags] private enum VoiceState : byte { @@ -34,15 +40,13 @@ namespace Discord => unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23); } - internal static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null; - private VoiceState _voiceState; private DateTime? _lastOnline; private ulong? _voiceChannelId; private Dictionary _roles; - - /// Gets the client that generated this user object. + internal DiscordClient Client { get; } + /// Gets the unique identifier for this user. public ulong Id { get; } /// Gets the server this user is a member of. @@ -129,9 +133,9 @@ namespace Discord } } - - internal User(ulong id, Server server) + internal User(DiscordClient client, ulong id, Server server) { + Client = client; Server = server; _roles = new Dictionary(); @@ -220,51 +224,109 @@ namespace Discord _voiceChannelId = model.ChannelId; //Allows null } - private void UpdateRoles(IEnumerable roles) - { - var newRoles = new Dictionary(); - if (roles != null) - { - foreach (var r in roles) - { - if (r != null) - newRoles[r.Id] = r; - } - } - - if (Server != null) - { - var everyone = Server.EveryoneRole; - newRoles[everyone.Id] = everyone; - } - _roles = newRoles; - - if (Server != null) - Server.UpdatePermissions(this); - } internal void UpdateActivity(DateTime? activity = null) { if (LastActivityAt == null || activity > LastActivityAt.Value) LastActivityAt = activity ?? DateTime.UtcNow; } - - public ServerPermissions ServerPermissions => Server.GetPermissions(this); + + public Task Edit(bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable roles = null) + { + if (Server == null) throw new InvalidOperationException("Unable to edit users in a private channel"); + + //Modify the roles collection and filter out the everyone role + var roleIds = roles == null ? null : Roles.Where(x => !x.IsEveryone).Select(x => x.Id); + + var request = new UpdateMemberRequest(Server.Id, Id) + { + IsMuted = isMuted ?? IsServerMuted, + IsDeafened = isDeafened ?? IsServerDeafened, + VoiceChannelId = voiceChannel?.Id, + RoleIds = roleIds.ToArray() + }; + return Client.ClientAPI.Send(request); + } + + public Task Kick() + { + if (Server == null) throw new InvalidOperationException("Unable to kick users from a private channel"); + + var request = new KickMemberRequest(Server.Id, Id); + return Client.ClientAPI.Send(request); + } + + #region Permissions + public ServerPermissions ServerPermissions => Server.GetPermissions(this); public ChannelPermissions GetPermissions(Channel channel) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - return channel.GetPermissions(this); } + #endregion - public bool HasRole(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - - return _roles.ContainsKey(role.Id); - } + #region Channels + public Task CreateChannel() + => Client.CreatePrivateChannel(this); + #endregion + + #region Messages + public async Task SendMessage(string text) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + + var channel = await CreateChannel().ConfigureAwait(false); + return await channel.SendMessage(text).ConfigureAwait(false); + } + public async Task SendFile(string filePath) + { + if (filePath == null) throw new ArgumentNullException(nameof(filePath)); + + var channel = await CreateChannel().ConfigureAwait(false); + return await channel.SendFile(filePath).ConfigureAwait(false); + } + public async Task SendFile(string filename, Stream stream) + { + if (filename == null) throw new ArgumentNullException(nameof(filename)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + var channel = await CreateChannel().ConfigureAwait(false); + return await channel.SendFile(filename, stream).ConfigureAwait(false); + } + #endregion + + #region Roles + private void UpdateRoles(IEnumerable roles) + { + var newRoles = new Dictionary(); + if (roles != null) + { + foreach (var r in roles) + { + if (r != null) + newRoles[r.Id] = r; + } + } + + if (Server != null) + { + var everyone = Server.EveryoneRole; + newRoles[everyone.Id] = everyone; + } + _roles = newRoles; + + if (Server != null) + Server.UpdatePermissions(this); + } + public bool HasRole(Role role) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + + return _roles.ContainsKey(role.Id); + } + #endregion - public override bool Equals(object obj) => obj is User && (obj as User).Id == Id; + public override bool Equals(object obj) => obj is User && (obj as User).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 7230); public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString(); } diff --git a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs index 51f925cc2..71d7d356e 100644 --- a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs +++ b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs @@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets private int _lastSequence; private string _sessionId; - public string Token { get; private set; } + public string Token { get; internal set; } public GatewaySocket(DiscordClient client, JsonSerializer serializer, Logger logger) : base(client, serializer, logger) @@ -26,11 +26,10 @@ namespace Discord.Net.WebSockets }; } - public async Task Connect(string token) + public async Task Connect() { - Token = token; await BeginConnect().ConfigureAwait(false); - SendIdentify(token); + SendIdentify(Token); } private async Task Redirect() { @@ -47,7 +46,7 @@ namespace Discord.Net.WebSockets { try { - await Connect(Token).ConfigureAwait(false); + await Connect().ConfigureAwait(false); break; } catch (OperationCanceledException) { throw; } diff --git a/src/Discord.Net/Optional.cs b/src/Discord.Net/Optional.cs deleted file mode 100644 index 8088691a5..000000000 --- a/src/Discord.Net/Optional.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -namespace Discord -{ - /*public struct Optional - { - public bool HasValue { get; } - public T Value { get; } - - public Optional(T value) - { - HasValue = true; - Value = value; - } - - public static implicit operator Optional(T value) => new Optional(value); - public static bool operator ==(Optional a, Optional b) => - a.HasValue == b.HasValue && EqualityComparer.Default.Equals(a.Value, b.Value); - public static bool operator !=(Optional a, Optional b) => - a.HasValue != b.HasValue || EqualityComparer.Default.Equals(a.Value, b.Value); - public override bool Equals(object obj) => - this == ((Optional)obj); - public override int GetHashCode() => - unchecked(HasValue.GetHashCode() + Value?.GetHashCode() ?? 0); - - public override string ToString() => Value?.ToString() ?? "null"; - }*/ -} diff --git a/src/Discord.Net/RelativeDirection.cs b/src/Discord.Net/RelativeDirection.cs new file mode 100644 index 000000000..787ad5844 --- /dev/null +++ b/src/Discord.Net/RelativeDirection.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + public enum RelativeDirection + { + Before, After + } +} diff --git a/src/Discord.Net/ServiceManager.cs b/src/Discord.Net/ServiceManager.cs new file mode 100644 index 000000000..b102d20e1 --- /dev/null +++ b/src/Discord.Net/ServiceManager.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + public class ServiceManager + { + private readonly Dictionary _services; + + internal DiscordClient Client { get; } + + internal ServiceManager(DiscordClient client) + { + Client = client; + _services = new Dictionary(); + } + + public void Add(T service) + where T : class, IService + { + _services.Add(typeof(T), service); + service.Install(Client); + } + + public T Get(bool isRequired = true) + where T : class, IService + { + IService service; + T singletonT = null; + + if (_services.TryGetValue(typeof(T), out service)) + singletonT = service as T; + + if (singletonT == null && isRequired) + throw new InvalidOperationException($"This operation requires {typeof(T).Name} to be added to {nameof(DiscordClient)}."); + return singletonT; + } + } +}