From b9d9191a3000acfee6c3ff2aa1c52321f6281f64 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 17 May 2016 05:29:44 -0300 Subject: [PATCH] Major cleanup, upgraded to RC2, ported several websocket features from 0.9. --- README.md | 14 +- global.json | 6 + src/Discord.Net/API/DiscordAPIClient.cs | 4 +- .../Common/Entities/Channels/IDMChannel.cs | 2 +- .../Common/Entities/Guilds/VoiceRegion.cs | 2 +- src/Discord.Net/Common/Entities/Invites/Invite.cs | 4 +- .../Common/Entities/Messages/IMessage.cs | 2 +- .../Entities/Permissions/ChannelPermissions.cs | 44 ++- .../Entities/Permissions/GuildPermissions.cs | 4 + .../Common/Entities/Permissions/Permissions.cs | 13 +- .../Common/Entities/Users/Connection.cs | 2 +- src/Discord.Net/Common/Entities/Users/IDMUser.cs | 8 - .../Common/Entities/Users/IGuildUser.cs | 3 - src/Discord.Net/Common/Entities/Users/IUser.cs | 1 + src/Discord.Net/Common/MentionUtils.cs | 6 +- src/Discord.Net/Discord.Net.csproj | 243 ---------------- src/Discord.Net/Discord.Net.xproj | 19 ++ src/Discord.Net/IDiscordClient.cs | 3 +- .../Net/Converters/DiscordContractResolver.cs | 88 +++--- .../Net/Converters/OptionalConverter.cs | 2 +- src/Discord.Net/Net/Rest/DefaultRestClient.cs | 31 ++- src/Discord.Net/Net/Rest/RestClientProvider.cs | 4 +- .../Net/WebSockets/BinaryMessageEventArgs.cs | 11 + .../Net/WebSockets/DefaultWebsocketClient.cs | 180 ++++++++++++ src/Discord.Net/Net/WebSockets/IWebSocketClient.cs | 19 ++ .../Net/WebSockets/TextMessageEventArgs.cs | 11 + .../Net/WebSockets/WebSocketProvider.cs | 4 + src/Discord.Net/Properties/AssemblyInfo.cs | 23 +- src/Discord.Net/Rest/DiscordClient.cs | 74 +++-- .../Rest/Entities/Channels/DMChannel.cs | 30 +- .../Rest/Entities/Channels/GuildChannel.cs | 18 +- .../Rest/Entities/Channels/TextChannel.cs | 16 +- .../Rest/Entities/Channels/VoiceChannel.cs | 2 +- src/Discord.Net/Rest/Entities/Guilds/Guild.cs | 53 ++-- .../Rest/Entities/Guilds/GuildIntegration.cs | 6 +- src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs | 4 +- src/Discord.Net/Rest/Entities/Message.cs | 59 ++-- src/Discord.Net/Rest/Entities/Role.cs | 8 +- src/Discord.Net/Rest/Entities/Users/DMUser.cs | 20 -- src/Discord.Net/Rest/Entities/Users/GuildUser.cs | 18 +- src/Discord.Net/Rest/Entities/Users/SelfUser.cs | 4 +- src/Discord.Net/Rest/Entities/Users/User.cs | 6 +- src/Discord.Net/WebSocket/DiscordClient.cs | 310 ++++++++++++++++----- src/Discord.Net/WebSocket/DiscordSocketConfig.cs | 39 +++ .../WebSocket/Entities/Channels/Channel.cs | 37 +++ .../WebSocket/Entities/Channels/DMChannel.cs | 58 ++-- .../WebSocket/Entities/Channels/GuildChannel.cs | 40 +-- .../WebSocket/Entities/Channels/TextChannel.cs | 18 +- .../WebSocket/Entities/Channels/VoiceChannel.cs | 2 +- src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs | 199 ++++++------- .../WebSocket/Entities/Guilds/GuildIntegration.cs | 10 +- src/Discord.Net/WebSocket/Entities/Message.cs | 74 ++--- src/Discord.Net/WebSocket/Entities/Role.cs | 15 +- src/Discord.Net/WebSocket/Entities/Users/DMUser.cs | 20 -- .../WebSocket/Entities/Users/GuildUser.cs | 83 +++--- .../WebSocket/Entities/Users/PublicUser.cs | 15 - .../WebSocket/Entities/Users/SelfUser.cs | 7 +- src/Discord.Net/WebSocket/Entities/Users/User.cs | 22 +- .../WebSocket/Events/ChannelEventArgs.cs | 14 + .../WebSocket/Events/ChannelUpdatedEventArgs.cs | 14 + .../WebSocket/Events/CurrentUserEventArgs.cs | 14 + .../Events/CurrentUserUpdatedEventArgs.cs | 14 + .../WebSocket/Events/DisconnectedEventArgs.cs | 16 ++ src/Discord.Net/WebSocket/Events/GuildEventArgs.cs | 14 + .../WebSocket/Events/GuildUpdatedEventArgs.cs | 14 + .../WebSocket/Events/MessageEventArgs.cs | 14 + .../WebSocket/Events/MessageUpdatedEventArgs.cs | 14 + src/Discord.Net/WebSocket/Events/RoleEventArgs.cs | 14 + .../WebSocket/Events/RoleUpdatedEventArgs.cs | 14 + .../WebSocket/Events/TypingEventArgs.cs | 16 ++ src/Discord.Net/WebSocket/Events/UserEventArgs.cs | 14 + .../WebSocket/Events/UserUpdatedEventArgs.cs | 14 + .../WebSocket/Events/VoiceChannelEventArgs.cs | 14 + src/Discord.Net/WebSocket/MessageCache.cs | 5 +- src/Discord.Net/project.json | 40 ++- 75 files changed, 1364 insertions(+), 915 deletions(-) create mode 100644 global.json delete mode 100644 src/Discord.Net/Common/Entities/Users/IDMUser.cs delete mode 100644 src/Discord.Net/Discord.Net.csproj create mode 100644 src/Discord.Net/Discord.Net.xproj create mode 100644 src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs create mode 100644 src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs create mode 100644 src/Discord.Net/Net/WebSockets/IWebSocketClient.cs create mode 100644 src/Discord.Net/Net/WebSockets/TextMessageEventArgs.cs create mode 100644 src/Discord.Net/Net/WebSockets/WebSocketProvider.cs delete mode 100644 src/Discord.Net/Rest/Entities/Users/DMUser.cs create mode 100644 src/Discord.Net/WebSocket/DiscordSocketConfig.cs create mode 100644 src/Discord.Net/WebSocket/Entities/Channels/Channel.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Users/DMUser.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Users/PublicUser.cs create mode 100644 src/Discord.Net/WebSocket/Events/ChannelEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/ChannelUpdatedEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/CurrentUserEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/CurrentUserUpdatedEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/DisconnectedEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/GuildEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/GuildUpdatedEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/MessageEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/MessageUpdatedEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/RoleEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/RoleUpdatedEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/TypingEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/UserEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/UserUpdatedEventArgs.cs create mode 100644 src/Discord.Net/WebSocket/Events/VoiceChannelEventArgs.cs diff --git a/README.md b/README.md index 123764bd3..06f3ae426 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # Discord.Net v1.0.0-dev +[![Build status](https://ci.appveyor.com/api/projects/status/p0n69xhqgmoobycf/branch/master?svg=true)](https://ci.appveyor.com/project/foxbot/discord-net/branch/master) + An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). -Check out the [documentation](https://discordnet.readthedocs.org/en/latest/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). +Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.html) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). -##### Warning: documentation is currently outdated. +##### Warning: Some of the documentation is outdated. It's current being rewritten. Until that's done, feel free to use my [DiscordBot](https://github.com/RogueException/DiscordBot) repo for reference. ### Installation @@ -16,9 +18,7 @@ You can download Discord.Net and its extensions from NuGet: ### Compiling In order to compile Discord.Net, you require at least the following: - [Visual Studio 2015](https://www.visualstudio.com/downloads/download-visual-studio-vs) -- Visual Studio 2015 Update 1 or higher (available through Visual Studio) -- [ASP.Net 5 RC1](https://get.asp.net) -- NuGet 3.3 (available through Visual Studio) +- [Visual Studio 2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx) +- [Visual Studio .Net Core Plugin](https://www.microsoft.com/net/core#windows) +- NuGet 3.3+ (available through Visual Studio) -### Known Issues -- .Net Core support is incomplete on non-Windows systems diff --git a/global.json b/global.json new file mode 100644 index 000000000..7ee23dc6a --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ "src", "test" ], + "sdk": { + "version": "1.0.0-preview1-002702" + } +} diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index bf0c1be96..bde034256 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -17,7 +17,7 @@ using System.Threading.Tasks; namespace Discord.API { - public class DiscordAPIClient + public class DiscordApiClient { internal event EventHandler SentRequest; @@ -30,7 +30,7 @@ namespace Discord.API public IRestClient RestClient { get; private set; } public IRequestQueue RequestQueue { get; private set; } - public DiscordAPIClient(RestClientProvider restClientProvider) + public DiscordApiClient(RestClientProvider restClientProvider) { _restClient = restClientProvider(DiscordConfig.ClientAPIUrl); _restClient.SetHeader("accept", "*/*"); diff --git a/src/Discord.Net/Common/Entities/Channels/IDMChannel.cs b/src/Discord.Net/Common/Entities/Channels/IDMChannel.cs index f6b53a91f..5038bf36c 100644 --- a/src/Discord.Net/Common/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net/Common/Entities/Channels/IDMChannel.cs @@ -5,7 +5,7 @@ namespace Discord public interface IDMChannel : IMessageChannel, IUpdateable { /// Gets the recipient of all messages in this channel. - IDMUser Recipient { get; } + IUser Recipient { get; } /// Closes this private channel, removing it from your channel list. Task Close(); diff --git a/src/Discord.Net/Common/Entities/Guilds/VoiceRegion.cs b/src/Discord.Net/Common/Entities/Guilds/VoiceRegion.cs index f58da92aa..126807202 100644 --- a/src/Discord.Net/Common/Entities/Guilds/VoiceRegion.cs +++ b/src/Discord.Net/Common/Entities/Guilds/VoiceRegion.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using Model = Discord.API.VoiceRegion; -namespace Discord.Rest +namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] public class VoiceRegion : IVoiceRegion diff --git a/src/Discord.Net/Common/Entities/Invites/Invite.cs b/src/Discord.Net/Common/Entities/Invites/Invite.cs index 88bd3b161..97e1fc051 100644 --- a/src/Discord.Net/Common/Entities/Invites/Invite.cs +++ b/src/Discord.Net/Common/Entities/Invites/Invite.cs @@ -47,13 +47,13 @@ namespace Discord /// public async Task Accept() { - await Discord.APIClient.AcceptInvite(Code).ConfigureAwait(false); + await Discord.ApiClient.AcceptInvite(Code).ConfigureAwait(false); } /// public async Task Delete() { - await Discord.APIClient.DeleteInvite(Code).ConfigureAwait(false); + await Discord.ApiClient.DeleteInvite(Code).ConfigureAwait(false); } /// diff --git a/src/Discord.Net/Common/Entities/Messages/IMessage.cs b/src/Discord.Net/Common/Entities/Messages/IMessage.cs index 1e5eb3b3f..35107ccf2 100644 --- a/src/Discord.Net/Common/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Common/Entities/Messages/IMessage.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Discord.API.Rest; +using System.Collections.Generic; namespace Discord { diff --git a/src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs index bc78704a8..f5760f1a9 100644 --- a/src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs @@ -7,23 +7,37 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { +#if CSHARP7 private static ChannelPermissions _allDM { get; } = new ChannelPermissions(0b000100_000000_0011111111_0000011001); private static ChannelPermissions _allText { get; } = new ChannelPermissions(0b000000_000000_0001110011_0000000000); private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(0b000100_111111_0000000000_0000011001); +#else + private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt64("00010000000000111111110000011001", 2)); + private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000000011100110000000000", 2)); + private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt64("00010011111100000000000000011001", 2)); +#endif /// Gets a blank ChannelPermissions that grants no permissions. public static ChannelPermissions None { get; } = new ChannelPermissions(); /// Gets a ChannelPermissions that grants all permissions for a given channelType. public static ChannelPermissions All(IChannel channel) { +#if CSHARP7 switch (channel) { case ITextChannel _: return _allText; case IVoiceChannel _: return _allVoice; - case IGuildChannel _: return _allDM; + case IDMChannel _: return _allDM; default: throw new ArgumentException("Unknown channel type", nameof(channel)); } +#else + if (channel is ITextChannel) return _allText; + if (channel is IVoiceChannel) return _allVoice; + if (channel is IDMChannel) return _allDM; + + throw new ArgumentException("Unknown channel type", nameof(channel)); +#endif } /// Gets a packed value representing all the permissions in this ChannelPermissions. @@ -70,9 +84,9 @@ namespace Discord /// Creates a new ChannelPermissions with the provided packed value. public ChannelPermissions(ulong rawValue) { RawValue = rawValue; } - private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) { @@ -100,25 +114,25 @@ namespace Discord } /// Creates a new ChannelPermissions with the provided permissions. - public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, - bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, - bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, + public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, + bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, + bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, bool moveMembers = false, bool useVoiceActivation = false, bool managePermissions = false) - : this(0, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, + : this(0, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions) { } /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. - public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) - => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, + => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions); - + public List ToList() { var perms = new List(); diff --git a/src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs index e5abb712f..899cac80a 100644 --- a/src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs @@ -10,7 +10,11 @@ namespace Discord /// Gets a blank GuildPermissions that grants no permissions. public static readonly GuildPermissions None = new GuildPermissions(); /// Gets a GuildPermissions that grants all permissions. +#if CSHARP7 public static readonly GuildPermissions All = new GuildPermissions(0b000111_111111_0011111111_0000111111); +#else + public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("00011111111100111111110000111111", 2)); +#endif /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } diff --git a/src/Discord.Net/Common/Entities/Permissions/Permissions.cs b/src/Discord.Net/Common/Entities/Permissions/Permissions.cs index 235437fe2..3cd17e66e 100644 --- a/src/Discord.Net/Common/Entities/Permissions/Permissions.cs +++ b/src/Discord.Net/Common/Entities/Permissions/Permissions.cs @@ -130,6 +130,7 @@ namespace Discord if (overwrites.TryGetValue(user.Id, out entry)) resolvedPermissions = (resolvedPermissions & ~entry.Permissions.DenyValue) | entry.Permissions.AllowValue; +#if CSHARP7 switch (channel) { case ITextChannel _: @@ -140,10 +141,16 @@ namespace Discord if (!GetValue(resolvedPermissions, ChannelPermission.Connect)) resolvedPermissions = 0; //No read permission on a text channel removes all other permissions break; - default: - resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) - break; } +#else + var textChannel = channel as ITextChannel; + var voiceChannel = channel as IVoiceChannel; + if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) + resolvedPermissions = 0; //No read permission on a text channel removes all other permissions + else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect)) + resolvedPermissions = 0; //No connect permission on a voice channel removes all other permissions +#endif + resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) } return resolvedPermissions; diff --git a/src/Discord.Net/Common/Entities/Users/Connection.cs b/src/Discord.Net/Common/Entities/Users/Connection.cs index c6eb65f2f..10852820e 100644 --- a/src/Discord.Net/Common/Entities/Users/Connection.cs +++ b/src/Discord.Net/Common/Entities/Users/Connection.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using Model = Discord.API.Connection; -namespace Discord.Rest +namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Connection : IConnection diff --git a/src/Discord.Net/Common/Entities/Users/IDMUser.cs b/src/Discord.Net/Common/Entities/Users/IDMUser.cs deleted file mode 100644 index e8fdd1f19..000000000 --- a/src/Discord.Net/Common/Entities/Users/IDMUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public interface IDMUser : IUser - { - /// Gets the private channel with this user. - IDMChannel Channel { get; } - } -} \ No newline at end of file diff --git a/src/Discord.Net/Common/Entities/Users/IGuildUser.cs b/src/Discord.Net/Common/Entities/Users/IGuildUser.cs index 5e038909e..f5d17688c 100644 --- a/src/Discord.Net/Common/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Common/Entities/Users/IGuildUser.cs @@ -29,9 +29,6 @@ namespace Discord /// Gets the channel-level permissions granted to this user for a given channel. ChannelPermissions GetPermissions(IGuildChannel channel); - /// Return true if this user has the provided role. - bool HasRole(IRole role); - /// Kicks this user from this guild. Task Kick(); /// Modifies this user's properties in this guild. diff --git a/src/Discord.Net/Common/Entities/Users/IUser.cs b/src/Discord.Net/Common/Entities/Users/IUser.cs index 9808c4a9f..c4754f3e3 100644 --- a/src/Discord.Net/Common/Entities/Users/IUser.cs +++ b/src/Discord.Net/Common/Entities/Users/IUser.cs @@ -17,6 +17,7 @@ namespace Discord /// Gets the username for this user. string Username { get; } + //TODO: CreateDMChannel is a candidate to move to IGuildUser, and User made a common class, depending on next friends list update /// Returns a private message channel to this user, creating one if it does not already exist. Task CreateDMChannel(); } diff --git a/src/Discord.Net/Common/MentionUtils.cs b/src/Discord.Net/Common/MentionUtils.cs index 7e4aba6d2..7d37ffc58 100644 --- a/src/Discord.Net/Common/MentionUtils.cs +++ b/src/Discord.Net/Common/MentionUtils.cs @@ -64,11 +64,11 @@ namespace Discord } /// Gets the ids of all users mentioned in a provided text. - public static IReadOnlyList GetUserMentions(string text) => GetMentions(text, _userRegex).ToArray(); + public static IImmutableList GetUserMentions(string text) => GetMentions(text, _userRegex).ToImmutableArray(); /// Gets the ids of all channels mentioned in a provided text. - public static IReadOnlyList GetChannelMentions(string text) => GetMentions(text, _channelRegex).ToArray(); + public static IImmutableList GetChannelMentions(string text) => GetMentions(text, _channelRegex).ToImmutableArray(); /// Gets the ids of all roles mentioned in a provided text. - public static IReadOnlyList GetRoleMentions(string text) => GetMentions(text, _roleRegex).ToArray(); + public static IImmutableList GetRoleMentions(string text) => GetMentions(text, _roleRegex).ToImmutableArray(); private static ImmutableArray.Builder GetMentions(string text, Regex regex) { var matches = regex.Matches(text); diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj deleted file mode 100644 index 45360f3ab..000000000 --- a/src/Discord.Net/Discord.Net.csproj +++ /dev/null @@ -1,243 +0,0 @@ - - - - - Debug - AnyCPU - {18F6FE23-73F6-4CA6-BBD9-F0139DC3EE90} - Library - Properties - Discord - Discord.Net - v4.6.1 - 512 - - - - true - full - false - bin\Debug\ - TRACE;DEBUG;__DEMO__,__DEMO_EXPERIMENTAL__ - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE;__DEMO__,__DEMO_EXPERIMENTAL__ - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.xproj b/src/Discord.Net/Discord.Net.xproj new file mode 100644 index 000000000..6759e09b4 --- /dev/null +++ b/src/Discord.Net/Discord.Net.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 91e9e7bd-75c9-4e98-84aa-2c271922e5c2 + Discord + .\obj + .\bin\ + v4.5.2 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index e7a1cc31f..3bdf82a43 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -10,7 +10,7 @@ namespace Discord public interface IDiscordClient { TokenType AuthTokenType { get; } - DiscordAPIClient APIClient { get; } + DiscordApiClient ApiClient { get; } IRestClient RestClient { get; } IRequestQueue RequestQueue { get; } @@ -36,6 +36,5 @@ namespace Discord Task> GetVoiceRegions(); Task GetVoiceRegion(string id); - Task GetOptimalVoiceRegion(); } } diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index a77d3cf28..678dc83cd 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -3,61 +3,73 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; -using System.Linq.Expressions; +using System.Linq; using System.Reflection; namespace Discord.Net.Converters { public class DiscordContractResolver : DefaultContractResolver - { + { + private static readonly TypeInfo _ienumerable = typeof(IEnumerable).GetTypeInfo(); + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); - var type = property.PropertyType; - JsonConverter converter = null; + var propInfo = member as PropertyInfo; - if (member.MemberType == MemberTypes.Property) + if (propInfo != null) { + JsonConverter converter = null; + var type = property.PropertyType; + var typeInfo = type.GetTypeInfo(); + //Primitives - if (type == typeof(ulong) && member.GetCustomAttribute() == null) - converter = UInt64Converter.Instance; - else if (type == typeof(ulong?) && member.GetCustomAttribute() == null) - converter = NullableUInt64Converter.Instance; - else if (typeof(IEnumerable).IsAssignableFrom(type) && member.GetCustomAttribute() == null) - converter = NullableUInt64Converter.Instance; + if (propInfo.GetCustomAttribute() == null) + { + if (type == typeof(ulong)) + converter = UInt64Converter.Instance; + else if (type == typeof(ulong?)) + converter = NullableUInt64Converter.Instance; + else if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEnumerable))) + converter = UInt64ArrayConverter.Instance; + } + if (converter == null) + { + //Enums + if (type == typeof(ChannelType)) + converter = ChannelTypeConverter.Instance; + else if (type == typeof(PermissionTarget)) + converter = PermissionTargetConverter.Instance; + else if (type == typeof(UserStatus)) + converter = UserStatusConverter.Instance; - //Enums - else if (type == typeof(ChannelType)) - converter = ChannelTypeConverter.Instance; - else if (type == typeof(PermissionTarget)) - converter = PermissionTargetConverter.Instance; - else if (type == typeof(UserStatus)) - converter = UserStatusConverter.Instance; + //Entities + if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) + converter = UInt64EntityConverter.Instance; + else if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) + converter = StringEntityConverter.Instance; - //Entities - else if (typeof(IEntity).IsAssignableFrom(type)) - converter = UInt64EntityConverter.Instance; - else if (typeof(IEntity).IsAssignableFrom(type)) - converter = StringEntityConverter.Instance; + //Special + else if (type == typeof(string) && propInfo.GetCustomAttribute() != null) + converter = ImageConverter.Instance; + else if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) + { + var lambda = (Func)propInfo.GetMethod.CreateDelegate(typeof(Func)); + /*var parentArg = Expression.Parameter(typeof(object)); + var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo); + var isSpecified = Expression.Property(optional, OptionalConverter.IsSpecifiedProperty); + var lambda = Expression.Lambda>(isSpecified, parentArg).Compile();*/ + property.ShouldSerialize = x => lambda(x); + converter = OptionalConverter.Instance; + } + } - //Special - else if (type == typeof(string) && member.GetCustomAttribute() != null) - converter = ImageConverter.Instance; - else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) + if (converter != null) { - var parentArg = Expression.Parameter(typeof(object)); - var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo); - var isSpecified = Expression.Property(optional, OptionalConverter.IsSpecifiedProperty); - var lambda = Expression.Lambda>(isSpecified, parentArg).Compile(); - property.ShouldSerialize = x => lambda(x); - converter = OptionalConverter.Instance; + property.Converter = converter; + property.MemberConverter = converter; } } - if (converter != null) - { - property.Converter = converter; - property.MemberConverter = converter; - } return property; } diff --git a/src/Discord.Net/Net/Converters/OptionalConverter.cs b/src/Discord.Net/Net/Converters/OptionalConverter.cs index 44c670d76..aa1abe9e2 100644 --- a/src/Discord.Net/Net/Converters/OptionalConverter.cs +++ b/src/Discord.Net/Net/Converters/OptionalConverter.cs @@ -8,7 +8,7 @@ namespace Discord.Net.Converters public class OptionalConverter : JsonConverter { public static readonly OptionalConverter Instance = new OptionalConverter(); - internal static readonly PropertyInfo IsSpecifiedProperty = typeof(IOptional).GetProperty(nameof(IOptional.IsSpecified)); + internal static readonly PropertyInfo IsSpecifiedProperty = typeof(IOptional).GetTypeInfo().GetDeclaredProperty(nameof(IOptional.IsSpecified)); public override bool CanConvert(Type objectType) => true; public override bool CanRead => false; diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 86dee1150..07a29342b 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -75,6 +75,7 @@ namespace Discord.Net.Rest { foreach (var p in multipartParams) { +#if CSHARP7 switch (p.Value) { case string value: @@ -92,6 +93,22 @@ namespace Discord.Net.Rest default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); } +#else + var stringValue = p.Value as string; + if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } + var byteArrayValue = p.Value as byte[]; + if (byteArrayValue != null) { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } + var streamValue = p.Value as Stream; + if (streamValue != null) { content.Add(new StreamContent(streamValue), p.Key); continue; } + if (p.Value is MultipartFile) + { + var fileValue = (MultipartFile)p.Value; + content.Add(new StreamContent(fileValue.Stream), fileValue.Filename, p.Key); + continue; + } + + throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); +#endif } } restRequest.Content = content; @@ -101,21 +118,9 @@ namespace Discord.Net.Rest private async Task SendInternal(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) { - int retryCount = 0; while (true) { - HttpResponseMessage response; - try - { - response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - } - catch (WebException ex) - { - //The request was aborted: Could not create SSL/TLS secure channel. - if (ex.HResult == HR_SECURECHANNELFAILED && retryCount++ < 5) - continue; //Retrying seems to fix this somehow? - throw; - } + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); int statusCode = (int)response.StatusCode; if (statusCode < 200 || statusCode >= 300) //2xx = Success diff --git a/src/Discord.Net/Net/Rest/RestClientProvider.cs b/src/Discord.Net/Net/Rest/RestClientProvider.cs index 341d18f02..51a7eb619 100644 --- a/src/Discord.Net/Net/Rest/RestClientProvider.cs +++ b/src/Discord.Net/Net/Rest/RestClientProvider.cs @@ -1,6 +1,4 @@ -using System.Threading; - -namespace Discord.Net.Rest +namespace Discord.Net.Rest { public delegate IRestClient RestClientProvider(string baseUrl); } diff --git a/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs b/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs new file mode 100644 index 000000000..3fd4425fa --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Net.WebSockets +{ + public class BinaryMessageEventArgs : EventArgs + { + public byte[] Data { get; } + + public BinaryMessageEventArgs(byte[] data) { } + } +} diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs new file mode 100644 index 000000000..23b2fd015 --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Concurrent; +using System.ComponentModel; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.WebSockets +{ + public class DefaultWebSocketClient : IWebSocketClient + { + public const int ReceiveChunkSize = 12 * 1024; //12KB + public const int SendChunkSize = 4 * 1024; //4KB + protected const int HR_TIMEOUT = -2147012894; + + public event EventHandler BinaryMessage = delegate { }; + public event EventHandler TextMessage = delegate { }; + + protected readonly ConcurrentQueue _sendQueue; + protected readonly ClientWebSocket _client; + protected Task _receiveTask, _sendTask; + protected CancellationTokenSource _cancelToken; + protected bool _isDisposed; + + public DefaultWebSocketClient() + { + _sendQueue = new ConcurrentQueue(); + + _client = new ClientWebSocket(); + _client.Options.Proxy = null; + _client.Options.KeepAliveInterval = TimeSpan.Zero; + } + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public async Task Connect(string host, CancellationToken cancelToken) + { + await Disconnect().ConfigureAwait(false); + + _cancelToken = new CancellationTokenSource(); + var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken.Token, cancelToken).Token; + + await _client.ConnectAsync(new Uri(host), combinedToken).ConfigureAwait(false); + _receiveTask = ReceiveAsync(combinedToken); + _sendTask = SendAsync(combinedToken); + } + public async Task Disconnect() + { + _cancelToken.Cancel(); + + string ignored; + while (_sendQueue.TryDequeue(out ignored)) { } + + _client.Abort(); + + var receiveTask = _receiveTask ?? Task.CompletedTask; + var sendTask = _sendTask ?? Task.CompletedTask; + await Task.WhenAll(receiveTask, sendTask).ConfigureAwait(false); + } + + public void SetHeader(string key, string value) + { + _client.Options.SetRequestHeader(key, value); + } + + public void QueueMessage(string message) + { + _sendQueue.Enqueue(message); + } + + //TODO: Check this code + private Task ReceiveAsync(CancellationToken cancelToken) + { + return Task.Run(async () => + { + var buffer = new ArraySegment(new byte[ReceiveChunkSize]); + var stream = new MemoryStream(); + + try + { + while (!cancelToken.IsCancellationRequested) + { + WebSocketReceiveResult result = null; + do + { + if (cancelToken.IsCancellationRequested) return; + + try + { + result = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + throw new Exception($"Connection timed out."); + } + + if (result.MessageType == WebSocketMessageType.Close) + throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription); + else + stream.Write(buffer.Array, 0, result.Count); + + } + while (result == null || !result.EndOfMessage); + + var array = stream.ToArray(); + if (result.MessageType == WebSocketMessageType.Binary) + BinaryMessage(this, new BinaryMessageEventArgs(array)); + else if (result.MessageType == WebSocketMessageType.Text) + { + string text = Encoding.UTF8.GetString(array, 0, array.Length); + TextMessage(this, new TextMessageEventArgs(text)); + } + + stream.Position = 0; + stream.SetLength(0); + } + } + catch (OperationCanceledException) { } + }); + } + + //TODO: Check this code + private Task SendAsync(CancellationToken cancelToken) + { + return Task.Run(async () => + { + byte[] bytes = new byte[SendChunkSize]; + + try + { + while (!cancelToken.IsCancellationRequested) + { + string json; + while (_sendQueue.TryDequeue(out json)) + { + int byteCount = Encoding.UTF8.GetBytes(json, 0, json.Length, bytes, 0); + int frameCount = (int)Math.Ceiling((double)byteCount / SendChunkSize); + + int offset = 0; + for (int i = 0; i < frameCount; i++, offset += SendChunkSize) + { + bool isLast = i == (frameCount - 1); + + int count; + if (isLast) + count = byteCount - (i * SendChunkSize); + else + count = SendChunkSize; + + try + { + await _client.SendAsync(new ArraySegment(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false); + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + return; + } + } + } + await Task.Delay(DiscordConfig.WebSocketQueueInterval, cancelToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } + }); + } + } +} diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs new file mode 100644 index 000000000..e6bbeb402 --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.WebSockets +{ + //TODO: Add ETF + public interface IWebSocketClient + { + event EventHandler BinaryMessage; + event EventHandler TextMessage; + + void SetHeader(string key, string value); + + Task Connect(string host, CancellationToken cancelToken); + Task Disconnect(); + void QueueMessage(string message); + } +} diff --git a/src/Discord.Net/Net/WebSockets/TextMessageEventArgs.cs b/src/Discord.Net/Net/WebSockets/TextMessageEventArgs.cs new file mode 100644 index 000000000..e4e186044 --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/TextMessageEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Net.WebSockets +{ + public class TextMessageEventArgs : EventArgs + { + public string Message { get; } + + public TextMessageEventArgs(string msg) { Message = msg; } + } +} diff --git a/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs new file mode 100644 index 000000000..ab41404b9 --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.Net.WebSockets +{ + public delegate IWebSocketClient WebSocketProvider(string baseUrl); +} diff --git a/src/Discord.Net/Properties/AssemblyInfo.cs b/src/Discord.Net/Properties/AssemblyInfo.cs index 26e8c428e..7dcbdb315 100644 --- a/src/Discord.Net/Properties/AssemblyInfo.cs +++ b/src/Discord.Net/Properties/AssemblyInfo.cs @@ -1,16 +1,19 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -[assembly: AssemblyTitle("Discord.Net")] -[assembly: AssemblyProduct("Discord.Net")] -[assembly: AssemblyDescription("An unofficial .Net API wrapper for the Discord client.")] -[assembly: AssemblyCompany("RogueException")] -[assembly: AssemblyCopyright("Copyright © RogueException 2016")] +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Discord.Net.Core")] +[assembly: AssemblyTrademark("")] +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] -[assembly: Guid("62ea817d-c945-4100-ba21-9dfb139d2868")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: AssemblyInformationalVersion("1.0.0-alpha1")] +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("91e9e7bd-75c9-4e98-84aa-2c271922e5c2")] diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index 8cd35dd23..90ab61ea1 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/Rest/DiscordClient.cs @@ -10,25 +10,28 @@ using System.Threading.Tasks; namespace Discord.Rest { + //TODO: Docstrings + //TODO: Log Internal/External REST Rate Limits, 502s + //TODO: Log Logins/Logouts public sealed class DiscordClient : IDiscordClient, IDisposable { public event EventHandler Log; public event EventHandler LoggedIn, LoggedOut; + private readonly Logger _discordLogger, _restLogger; private readonly SemaphoreSlim _connectionLock; private readonly RestClientProvider _restClientProvider; private readonly LogManager _log; private CancellationTokenSource _cancelTokenSource; private bool _isDisposed; - private string _userAgent; private SelfUser _currentUser; public bool IsLoggedIn { get; private set; } - public API.DiscordAPIClient APIClient { get; private set; } + public API.DiscordApiClient ApiClient { get; private set; } - public TokenType AuthTokenType => APIClient.AuthTokenType; - public IRestClient RestClient => APIClient.RestClient; - public IRequestQueue RequestQueue => APIClient.RequestQueue; + public TokenType AuthTokenType => ApiClient.AuthTokenType; + public IRestClient RestClient => ApiClient.RestClient; + public IRequestQueue RequestQueue => ApiClient.RequestQueue; public DiscordClient(DiscordConfig config = null) { @@ -37,12 +40,14 @@ namespace Discord.Rest _restClientProvider = config.RestClientProvider; - _connectionLock = new SemaphoreSlim(1, 1); _log = new LogManager(config.LogLevel); - _userAgent = DiscordConfig.UserAgent; - APIClient = new API.DiscordAPIClient(_restClientProvider); + _log.Message += (s, e) => Log.Raise(this, e); + _discordLogger = _log.CreateLogger("Discord"); + _restLogger = _log.CreateLogger("Rest"); - _log.Message += (s,e) => Log.Raise(this, e); + _connectionLock = new SemaphoreSlim(1, 1); + ApiClient = new API.DiscordApiClient(_restClientProvider); + ApiClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); } public async Task Login(string email, string password) @@ -72,7 +77,7 @@ namespace Discord.Rest _cancelTokenSource = new CancellationTokenSource(); var args = new LoginParams { Email = email, Password = password }; - await APIClient.Login(args, _cancelTokenSource.Token).ConfigureAwait(false); + await ApiClient.Login(args, _cancelTokenSource.Token).ConfigureAwait(false); await CompleteLogin(false).ConfigureAwait(false); } catch { await LogoutInternal().ConfigureAwait(false); throw; } @@ -85,22 +90,20 @@ namespace Discord.Rest { _cancelTokenSource = new CancellationTokenSource(); - await APIClient.Login(tokenType, token, _cancelTokenSource.Token).ConfigureAwait(false); + await ApiClient.Login(tokenType, token, _cancelTokenSource.Token).ConfigureAwait(false); await CompleteLogin(validateToken).ConfigureAwait(false); } catch { await LogoutInternal().ConfigureAwait(false); throw; } } private async Task CompleteLogin(bool validateToken) { - APIClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); - if (validateToken) { try { - await APIClient.ValidateToken().ConfigureAwait(false); + await ApiClient.ValidateToken().ConfigureAwait(false); } - catch { await APIClient.Logout().ConfigureAwait(false); } + catch { await ApiClient.Logout().ConfigureAwait(false); } } IsLoggedIn = true; @@ -127,7 +130,7 @@ namespace Discord.Rest catch { } } - await APIClient.Logout().ConfigureAwait(false); + await ApiClient.Logout().ConfigureAwait(false); _currentUser = null; if (wasLoggedIn) @@ -139,18 +142,18 @@ namespace Discord.Rest public async Task> GetConnections() { - var models = await APIClient.GetCurrentUserConnections().ConfigureAwait(false); + var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); return models.Select(x => new Connection(x)); } public async Task GetChannel(ulong id) { - var model = await APIClient.GetChannel(id).ConfigureAwait(false); + var model = await ApiClient.GetChannel(id).ConfigureAwait(false); if (model != null) { if (model.GuildId != null) { - var guildModel = await APIClient.GetGuild(model.GuildId.Value).ConfigureAwait(false); + var guildModel = await ApiClient.GetGuild(model.GuildId.Value).ConfigureAwait(false); if (guildModel != null) { var guild = new Guild(this, guildModel); @@ -164,13 +167,13 @@ namespace Discord.Rest } public async Task> GetDMChannels() { - var models = await APIClient.GetCurrentUserDMs().ConfigureAwait(false); + var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); return models.Select(x => new DMChannel(this, x)); } public async Task GetInvite(string inviteIdOrXkcd) { - var model = await APIClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); + var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); if (model != null) return new Invite(this, model); return null; @@ -178,41 +181,41 @@ namespace Discord.Rest public async Task GetGuild(ulong id) { - var model = await APIClient.GetGuild(id).ConfigureAwait(false); + var model = await ApiClient.GetGuild(id).ConfigureAwait(false); if (model != null) return new Guild(this, model); return null; } public async Task GetGuildEmbed(ulong id) { - var model = await APIClient.GetGuildEmbed(id).ConfigureAwait(false); + var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); if (model != null) return new GuildEmbed(model); return null; } public async Task> GetGuilds() { - var models = await APIClient.GetCurrentUserGuilds().ConfigureAwait(false); + var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); return models.Select(x => new UserGuild(this, x)); } public async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) { var args = new CreateGuildParams(); - var model = await APIClient.CreateGuild(args).ConfigureAwait(false); + var model = await ApiClient.CreateGuild(args).ConfigureAwait(false); return new Guild(this, model); } public async Task GetUser(ulong id) { - var model = await APIClient.GetUser(id).ConfigureAwait(false); + var model = await ApiClient.GetUser(id).ConfigureAwait(false); if (model != null) return new PublicUser(this, model); return null; } public async Task GetUser(string username, ushort discriminator) { - var model = await APIClient.GetUser(username, discriminator).ConfigureAwait(false); + var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); if (model != null) return new PublicUser(this, model); return null; @@ -222,7 +225,7 @@ namespace Discord.Rest var user = _currentUser; if (user == null) { - var model = await APIClient.GetCurrentUser().ConfigureAwait(false); + var model = await ApiClient.GetCurrentUser().ConfigureAwait(false); user = new SelfUser(this, model); _currentUser = user; } @@ -230,25 +233,20 @@ namespace Discord.Rest } public async Task> QueryUsers(string query, int limit) { - var models = await APIClient.QueryUsers(query, limit).ConfigureAwait(false); + var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); return models.Select(x => new PublicUser(this, x)); } public async Task> GetVoiceRegions() { - var models = await APIClient.GetVoiceRegions().ConfigureAwait(false); + var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)); } public async Task GetVoiceRegion(string id) { - var models = await APIClient.GetVoiceRegions().ConfigureAwait(false); + var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); } - public async Task GetOptimalVoiceRegion() - { - var models = await APIClient.GetVoiceRegions().ConfigureAwait(false); - return models.Select(x => new VoiceRegion(x)).Where(x => x.IsOptimal).FirstOrDefault(); - } void Dispose(bool disposing) { @@ -261,7 +259,7 @@ namespace Discord.Rest } public void Dispose() => Dispose(true); - API.DiscordAPIClient IDiscordClient.APIClient => APIClient; + API.DiscordApiClient IDiscordClient.ApiClient => ApiClient; async Task IDiscordClient.GetChannel(ulong id) => await GetChannel(id).ConfigureAwait(false); @@ -289,7 +287,5 @@ namespace Discord.Rest => await GetVoiceRegions().ConfigureAwait(false); async Task IDiscordClient.GetVoiceRegion(string id) => await GetVoiceRegion(id).ConfigureAwait(false); - async Task IDiscordClient.GetOptimalVoiceRegion() - => await GetOptimalVoiceRegion().ConfigureAwait(false); } } diff --git a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs index b62bbc6f6..0efa29da3 100644 --- a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs @@ -18,7 +18,7 @@ namespace Discord.Rest internal DiscordClient Discord { get; } /// - public DMUser Recipient { get; private set; } + public User Recipient { get; private set; } /// public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); @@ -33,13 +33,13 @@ namespace Discord.Rest private void Update(Model model) { if (Recipient == null) - Recipient = new DMUser(this, model.Recipient); + Recipient = new PublicUser(Discord, model.Recipient); else Recipient.Update(model.Recipient); } /// - public async Task GetUser(ulong id) + public async Task GetUser(ulong id) { var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); if (id == Recipient.Id) @@ -50,24 +50,24 @@ namespace Discord.Rest return null; } /// - public async Task> GetUsers() + public async Task> GetUsers() { var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); - return ImmutableArray.Create(currentUser, Recipient); + return ImmutableArray.Create(currentUser, Recipient); } /// public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.APIClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, x)); } /// public async Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.APIClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, x)); } @@ -75,7 +75,7 @@ namespace Discord.Rest public async Task SendMessage(string text, bool isTTS = false) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.CreateDMMessage(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false); return new Message(this, model); } /// @@ -85,7 +85,7 @@ namespace Discord.Rest using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.UploadDMFile(Id, file, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadDMFile(Id, file, args).ConfigureAwait(false); return new Message(this, model); } } @@ -93,32 +93,32 @@ namespace Discord.Rest public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); return new Message(this, model); } /// public async Task DeleteMessages(IEnumerable messages) { - await Discord.APIClient.DeleteDMMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } /// public async Task TriggerTyping() { - await Discord.APIClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); } /// public async Task Close() { - await Discord.APIClient.DeleteChannel(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); } /// public async Task Update() { - var model = await Discord.APIClient.GetChannel(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); Update(model); } @@ -126,7 +126,7 @@ namespace Discord.Rest public override string ToString() => '@' + Recipient.ToString(); private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - IDMUser IDMChannel.Recipient => Recipient; + IUser IDMChannel.Recipient => Recipient; IEnumerable IMessageChannel.CachedMessages => Array.Empty(); async Task> IChannel.GetUsers() diff --git a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs index bdb92ffce..66e0abe19 100644 --- a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs @@ -55,7 +55,7 @@ namespace Discord.Rest var args = new ModifyGuildChannelParams(); func(args); - var model = await Discord.APIClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); Update(model); } @@ -78,7 +78,7 @@ namespace Discord.Rest /// Downloads a collection of all invites to this channel. public async Task> GetInvites() { - var models = await Discord.APIClient.GetChannelInvites(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); return models.Select(x => new InviteMetadata(Discord, x)); } @@ -86,20 +86,20 @@ namespace Discord.Rest public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.APIClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); _overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }); } /// public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.APIClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); _overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }); } /// public async Task RemovePermissionOverwrite(IUser user) { - await Discord.APIClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); Overwrite value; _overwrites.TryRemove(user.Id, out value); @@ -107,7 +107,7 @@ namespace Discord.Rest /// public async Task RemovePermissionOverwrite(IRole role) { - await Discord.APIClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); Overwrite value; _overwrites.TryRemove(role.Id, out value); @@ -127,19 +127,19 @@ namespace Discord.Rest Temporary = isTemporary, XkcdPass = withXkcd }; - var model = await Discord.APIClient.CreateChannelInvite(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); return new InviteMetadata(Discord, model); } /// public async Task Delete() { - await Discord.APIClient.DeleteChannel(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); } /// public async Task Update() { - var model = await Discord.APIClient.GetChannel(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); Update(model); } diff --git a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs index ee579ae0c..4c171bea2 100644 --- a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs @@ -35,7 +35,7 @@ namespace Discord.Rest var args = new ModifyTextChannelParams(); func(args); - var model = await Discord.APIClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); Update(model); } @@ -64,14 +64,14 @@ namespace Discord.Rest public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.APIClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, x)); } /// public async Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.APIClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, x)); } @@ -79,7 +79,7 @@ namespace Discord.Rest public async Task SendMessage(string text, bool isTTS = false) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); return new Message(this, model); } /// @@ -89,7 +89,7 @@ namespace Discord.Rest using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); return new Message(this, model); } } @@ -97,20 +97,20 @@ namespace Discord.Rest public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); return new Message(this, model); } /// public async Task DeleteMessages(IEnumerable messages) { - await Discord.APIClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } /// public async Task TriggerTyping() { - await Discord.APIClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); } private string DebuggerDisplay => $"{Name} ({Id}, Text)"; diff --git a/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs index a2c6c8c49..208e31e49 100644 --- a/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs @@ -30,7 +30,7 @@ namespace Discord.Rest var args = new ModifyVoiceChannelParams(); func(args); - var model = await Discord.APIClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); Update(model); } diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs index 87d15108c..936a0d35c 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs @@ -115,7 +115,7 @@ namespace Discord.Rest /// public async Task Update() { - var response = await Discord.APIClient.GetGuild(Id).ConfigureAwait(false); + var response = await Discord.ApiClient.GetGuild(Id).ConfigureAwait(false); Update(response); } /// @@ -125,7 +125,7 @@ namespace Discord.Rest var args = new ModifyGuildParams(); func(args); - var model = await Discord.APIClient.ModifyGuild(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); Update(model); } /// @@ -135,35 +135,35 @@ namespace Discord.Rest var args = new ModifyGuildEmbedParams(); func(args); - var model = await Discord.APIClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); Update(model); } /// public async Task ModifyChannels(IEnumerable args) { - await Discord.APIClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); } /// public async Task ModifyRoles(IEnumerable args) { - var models = await Discord.APIClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); Update(models); } /// public async Task Leave() { - await Discord.APIClient.LeaveGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); } /// public async Task Delete() { - await Discord.APIClient.DeleteGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); } /// public async Task> GetBans() { - var models = await Discord.APIClient.GetGuildBans(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false); return models.Select(x => new PublicUser(Discord, x)); } /// @@ -171,24 +171,21 @@ namespace Discord.Rest /// public async Task AddBan(ulong userId, int pruneDays = 0) { - var args = new CreateGuildBanParams() - { - PruneDays = pruneDays - }; - await Discord.APIClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); + var args = new CreateGuildBanParams() { PruneDays = pruneDays }; + await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); } /// public Task RemoveBan(IUser user) => RemoveBan(user.Id); /// public async Task RemoveBan(ulong userId) { - await Discord.APIClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); + await Discord.ApiClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); } /// Gets the channel in this guild with the provided id, or null if not found. public async Task GetChannel(ulong id) { - var model = await Discord.APIClient.GetChannel(Id, id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannel(Id, id).ConfigureAwait(false); if (model != null) return ToChannel(model); return null; @@ -196,7 +193,7 @@ namespace Discord.Rest /// Gets a collection of all channels in this guild. public async Task> GetChannels() { - var models = await Discord.APIClient.GetGuildChannels(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildChannels(Id).ConfigureAwait(false); return models.Select(x => ToChannel(x)); } /// Creates a new text channel. @@ -205,7 +202,7 @@ namespace Discord.Rest if (name == null) throw new ArgumentNullException(nameof(name)); var args = new CreateGuildChannelParams() { Name = name, Type = ChannelType.Text }; - var model = await Discord.APIClient.CreateGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); return new TextChannel(this, model); } /// Creates a new voice channel. @@ -214,28 +211,28 @@ namespace Discord.Rest if (name == null) throw new ArgumentNullException(nameof(name)); var args = new CreateGuildChannelParams { Name = name, Type = ChannelType.Voice }; - var model = await Discord.APIClient.CreateGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); return new VoiceChannel(this, model); } /// Gets a collection of all integrations attached to this guild. public async Task> GetIntegrations() { - var models = await Discord.APIClient.GetGuildIntegrations(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildIntegrations(Id).ConfigureAwait(false); return models.Select(x => new GuildIntegration(this, x)); } /// Creates a new integration for this guild. public async Task CreateIntegration(ulong id, string type) { var args = new CreateGuildIntegrationParams { Id = id, Type = type }; - var model = await Discord.APIClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); return new GuildIntegration(this, model); } /// Gets a collection of all invites to this guild. public async Task> GetInvites() { - var models = await Discord.APIClient.GetGuildInvites(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildInvites(Id).ConfigureAwait(false); return models.Select(x => new InviteMetadata(Discord, x)); } /// Creates a new invite to this guild. @@ -251,7 +248,7 @@ namespace Discord.Rest Temporary = isTemporary, XkcdPass = withXkcd }; - var model = await Discord.APIClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); return new InviteMetadata(Discord, model); } @@ -269,7 +266,7 @@ namespace Discord.Rest { if (name == null) throw new ArgumentNullException(nameof(name)); - var model = await Discord.APIClient.CreateGuildRole(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildRole(Id).ConfigureAwait(false); var role = new Role(this, model); await role.Modify(x => @@ -287,20 +284,20 @@ namespace Discord.Rest public async Task> GetUsers() { var args = new GetGuildMembersParams(); - var models = await Discord.APIClient.GetGuildMembers(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); return models.Select(x => new GuildUser(this, x)); } /// Gets a paged collection of all users in this guild. public async Task> GetUsers(int limit, int offset) { var args = new GetGuildMembersParams { Limit = limit, Offset = offset }; - var models = await Discord.APIClient.GetGuildMembers(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); return models.Select(x => new GuildUser(this, x)); } /// Gets the user in this guild with the provided id, or null if not found. public async Task GetUser(ulong id) { - var model = await Discord.APIClient.GetGuildMember(Id, id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetGuildMember(Id, id).ConfigureAwait(false); if (model != null) return new GuildUser(this, model); return null; @@ -316,9 +313,9 @@ namespace Discord.Rest var args = new GuildPruneParams() { Days = days }; GetGuildPruneCountResponse model; if (simulate) - model = await Discord.APIClient.GetGuildPruneCount(Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.GetGuildPruneCount(Id, args).ConfigureAwait(false); else - model = await Discord.APIClient.BeginGuildPrune(Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.BeginGuildPrune(Id, args).ConfigureAwait(false); return model.Pruned; } diff --git a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs index c8dd61ede..e368cc8d7 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs @@ -60,7 +60,7 @@ namespace Discord.Rest /// public async Task Delete() { - await Discord.APIClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); } /// public async Task Modify(Action func) @@ -69,14 +69,14 @@ namespace Discord.Rest var args = new ModifyGuildIntegrationParams(); func(args); - var model = await Discord.APIClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); Update(model); } /// public async Task Sync() { - await Discord.APIClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs index 0679ccf21..ae5c31da3 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs @@ -42,12 +42,12 @@ namespace Discord /// public async Task Leave() { - await Discord.APIClient.LeaveGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); } /// public async Task Delete() { - await Discord.APIClient.DeleteGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net/Rest/Entities/Message.cs b/src/Discord.Net/Rest/Entities/Message.cs index 962c5af05..319394214 100644 --- a/src/Discord.Net/Rest/Entities/Message.cs +++ b/src/Discord.Net/Rest/Entities/Message.cs @@ -28,14 +28,14 @@ namespace Discord.Rest /// public IMessageChannel Channel { get; } /// - public User Author { get; } + public IUser Author { get; } /// public IReadOnlyList Attachments { get; private set; } /// public IReadOnlyList Embeds { get; private set; } /// - public IReadOnlyList MentionedUsers { get; private set; } + public IReadOnlyList MentionedUsers { get; private set; } /// public IReadOnlyList MentionedChannelIds { get; private set; } /// @@ -55,6 +55,10 @@ namespace Discord.Rest } private void Update(Model model) { + var guildChannel = Channel as GuildChannel; + var guild = guildChannel?.Guild; + var discord = Discord; + IsTTS = model.IsTextToSpeech; Timestamp = model.Timestamp; EditedTimestamp = model.EditedTimestamp; @@ -80,38 +84,32 @@ namespace Discord.Rest else Embeds = Array.Empty(); - if (model.Mentions.Length > 0) + if (guildChannel != null && model.Mentions.Length > 0) { - var discord = Discord; - var builder = ImmutableArray.CreateBuilder(model.Mentions.Length); + var mentions = new PublicUser[model.Mentions.Length]; for (int i = 0; i < model.Mentions.Length; i++) - builder.Add(new PublicUser(discord, model.Mentions[i])); - MentionedUsers = builder.ToArray(); + mentions[i] = new PublicUser(discord, model.Mentions[i]); + MentionedUsers = ImmutableArray.Create(mentions); } else MentionedUsers = Array.Empty(); - MentionedChannelIds = MentionUtils.GetChannelMentions(model.Content); - MentionedRoleIds = MentionUtils.GetRoleMentions(model.Content); - if (model.IsMentioningEveryone) + + if (guildChannel != null) { - ulong? guildId = (Channel as IGuildChannel)?.Guild.Id; - if (guildId != null) - { - if (MentionedRoleIds.Count == 0) - MentionedRoleIds = ImmutableArray.Create(guildId.Value); - else - { - var builder = ImmutableArray.CreateBuilder(MentionedRoleIds.Count + 1); - builder.AddRange(MentionedRoleIds); - builder.Add(guildId.Value); - MentionedRoleIds = builder.ToImmutable(); - } - } + MentionedChannelIds = MentionUtils.GetChannelMentions(model.Content); + + var mentionedRoleIds = MentionUtils.GetRoleMentions(model.Content); + if (model.IsMentioningEveryone) + mentionedRoleIds = mentionedRoleIds.Add(guildChannel.Guild.EveryoneRole.Id); + MentionedRoleIds = mentionedRoleIds; + } + else + { + MentionedChannelIds = Array.Empty(); + MentionedRoleIds = Array.Empty(); } Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); - - Author.Update(model.Author); } /// @@ -125,9 +123,9 @@ namespace Discord.Rest Model model; if (guildChannel != null) - model = await Discord.APIClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); else - model = await Discord.APIClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); Update(model); } @@ -136,18 +134,15 @@ namespace Discord.Rest { var guildChannel = Channel as GuildChannel; if (guildChannel != null) - await Discord.APIClient.DeleteMessage(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteMessage(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); else - await Discord.APIClient.DeleteDMMessage(Channel.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteDMMessage(Channel.Id, Id).ConfigureAwait(false); } public override string ToString() => Text; private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; IUser IMessage.Author => Author; - IReadOnlyList IMessage.Attachments => Attachments; - IReadOnlyList IMessage.Embeds => Embeds; - IReadOnlyList IMessage.MentionedChannelIds => MentionedChannelIds; IReadOnlyList IMessage.MentionedUsers => MentionedUsers; } } diff --git a/src/Discord.Net/Rest/Entities/Role.cs b/src/Discord.Net/Rest/Entities/Role.cs index cd6789c47..20ed0940e 100644 --- a/src/Discord.Net/Rest/Entities/Role.cs +++ b/src/Discord.Net/Rest/Entities/Role.cs @@ -60,12 +60,12 @@ namespace Discord.Rest var args = new ModifyGuildRoleParams(); func(args); - var response = await Discord.APIClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); + var response = await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); Update(response); } /// Deletes this message. public async Task Delete() - => await Discord.APIClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); + => await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); /// public override string ToString() => Name; @@ -75,8 +75,8 @@ namespace Discord.Rest async Task> IRole.GetUsers() { - //A tad hacky, but it works - var models = await Discord.APIClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false); + //TODO: Rethink this, it isn't paginated or anything... + var models = await Discord.ApiClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false); return models.Where(x => x.Roles.Contains(Id)).Select(x => new GuildUser(Guild, x)); } } diff --git a/src/Discord.Net/Rest/Entities/Users/DMUser.cs b/src/Discord.Net/Rest/Entities/Users/DMUser.cs deleted file mode 100644 index 67bc534f3..000000000 --- a/src/Discord.Net/Rest/Entities/Users/DMUser.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Model = Discord.API.User; - -namespace Discord.Rest -{ - public class DMUser : User, IDMUser - { - /// - public DMChannel Channel { get; } - - internal override DiscordClient Discord => Channel.Discord; - - internal DMUser(DMChannel channel, Model model) - : base(model) - { - Channel = channel; - } - - IDMChannel IDMUser.Channel => Channel; - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs index 3f5779965..c48aec4bf 100644 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs @@ -55,23 +55,13 @@ namespace Discord.Rest public async Task Update() { - var model = await Discord.APIClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); Update(model); } - public bool HasRole(IRole role) - { - for (int i = 0; i < _roles.Length; i++) - { - if (_roles[i].Id == role.Id) - return true; - } - return false; - } - public async Task Kick() { - await Discord.APIClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); } public ChannelPermissions GetPermissions(IGuildChannel channel) @@ -91,13 +81,13 @@ namespace Discord.Rest if (isCurrentUser && args.Nickname.IsSpecified) { var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value }; - await Discord.APIClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); + await Discord.ApiClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); args.Nickname = new API.Optional(); //Remove } if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) { - await Discord.APIClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); if (args.Deaf.IsSpecified) IsDeaf = args.Deaf.Value; if (args.Mute.IsSpecified) diff --git a/src/Discord.Net/Rest/Entities/Users/SelfUser.cs b/src/Discord.Net/Rest/Entities/Users/SelfUser.cs index 503773382..a821b369b 100644 --- a/src/Discord.Net/Rest/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/SelfUser.cs @@ -30,7 +30,7 @@ namespace Discord.Rest /// public async Task Update() { - var model = await Discord.APIClient.GetCurrentUser().ConfigureAwait(false); + var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); Update(model); } @@ -41,7 +41,7 @@ namespace Discord.Rest var args = new ModifyCurrentUserParams(); func(args); - var model = await Discord.APIClient.ModifyCurrentUser(args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); Update(model); } } diff --git a/src/Discord.Net/Rest/Entities/Users/User.cs b/src/Discord.Net/Rest/Entities/Users/User.cs index 0469b19d3..26754fc18 100644 --- a/src/Discord.Net/Rest/Entities/Users/User.cs +++ b/src/Discord.Net/Rest/Entities/Users/User.cs @@ -45,10 +45,10 @@ namespace Discord.Rest Username = model.Username; } - public async Task CreateDMChannel() + protected virtual async Task CreateDMChannelInternal() { var args = new CreateDMChannelParams { RecipientId = Id }; - var model = await Discord.APIClient.CreateDMChannel(args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); return new DMChannel(Discord, model); } @@ -63,6 +63,6 @@ namespace Discord.Rest /// async Task IUser.CreateDMChannel() - => await CreateDMChannel().ConfigureAwait(false); + => await CreateDMChannelInternal().ConfigureAwait(false); } } diff --git a/src/Discord.Net/WebSocket/DiscordClient.cs b/src/Discord.Net/WebSocket/DiscordClient.cs index de342d2b2..99fba0107 100644 --- a/src/Discord.Net/WebSocket/DiscordClient.cs +++ b/src/Discord.Net/WebSocket/DiscordClient.cs @@ -1,139 +1,305 @@ -using System; +using Discord.API.Rest; +using Discord.Logging; +using Discord.Net.Rest; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; +using System.Linq; +using System.Threading; using System.Threading.Tasks; -using Discord.API; -using Discord.Net.Rest; namespace Discord.WebSocket { - public class DiscordClient : IDiscordClient + //TODO: Docstrings + //TODO: Log Logins/Logouts + public sealed class DiscordClient : IDiscordClient, IDisposable { - internal int MessageCacheSize { get; } = 100; + public event EventHandler Log; + public event EventHandler LoggedIn, LoggedOut; + public event EventHandler Connected, Disconnected; + public event EventHandler VoiceConnected, VoiceDisconnected; + + public event EventHandler ChannelCreated, ChannelDestroyed; + public event EventHandler ChannelUpdated; + public event EventHandler MessageReceived, MessageDeleted; + public event EventHandler MessageUpdated; + public event EventHandler RoleCreated, RoleDeleted; + public event EventHandler RoleUpdated; + public event EventHandler JoinedGuild, LeftGuild; + public event EventHandler GuildAvailable, GuildUnavailable; + public event EventHandler GuildUpdated; + public event EventHandler CurrentUserUpdated; + public event EventHandler UserJoined, UserLeft; + public event EventHandler UserBanned, UserUnbanned; + public event EventHandler UserUpdated; + public event EventHandler UserIsTyping; + + private readonly Logger _discordLogger, _gatewayLogger; + private readonly SemaphoreSlim _connectionLock; + private readonly RestClientProvider _restClientProvider; + private readonly LogManager _log; + private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; + private readonly bool _enablePreUpdateEvents; + private readonly int _largeThreshold; + private readonly int _totalShards; + private IReadOnlyDictionary _voiceRegions; + private CancellationTokenSource _cancelTokenSource; + private bool _isDisposed; + private SelfUser _currentUser; + private ConcurrentDictionary _guilds; + private ConcurrentDictionary _channels; + private ConcurrentDictionary _dmChannels; //Key = RecipientId + private ConcurrentDictionary _users; + + public int ShardId { get; } + public bool IsLoggedIn { get; private set; } + public API.DiscordApiClient ApiClient { get; private set; } + public SelfUser CurrentUser { get; private set; } + //public GatewaySocket GatewaySocket { get; private set; } + internal int MessageCacheSize { get; private set; } + internal bool UsePermissionCache { get; private set; } + + public TokenType AuthTokenType => ApiClient.AuthTokenType; + public IRestClient RestClient => ApiClient.RestClient; + public IRequestQueue RequestQueue => ApiClient.RequestQueue; + public IEnumerable Guilds => _guilds.Values; + public IEnumerable Channels => _channels.Values; + public IEnumerable DMChannels => _dmChannels.Values; + public IEnumerable VoiceRegions => _voiceRegions.Values; - public SelfUser CurrentUser + //public bool IsConnected => GatewaySocket.State == ConnectionState.Connected; + + public DiscordClient(DiscordSocketConfig config = null) { - get - { - throw new NotImplementedException(); - } + if (config == null) + config = new DiscordSocketConfig(); + + _restClientProvider = config.RestClientProvider; + ShardId = config.ShardId; + _totalShards = config.TotalShards; + + _connectionTimeout = config.ConnectionTimeout; + _reconnectDelay = config.ReconnectDelay; + _failedReconnectDelay = config.FailedReconnectDelay; + + MessageCacheSize = config.MessageCacheSize; + UsePermissionCache = config.UsePermissionsCache; + _enablePreUpdateEvents = config.EnablePreUpdateEvents; + _largeThreshold = config.LargeThreshold; + + _log = new LogManager(config.LogLevel); + _log.Message += (s, e) => Log.Raise(this, e); + _discordLogger = _log.CreateLogger("Discord"); + _gatewayLogger = _log.CreateLogger("Gateway"); + + _connectionLock = new SemaphoreSlim(1, 1); + ApiClient = new API.DiscordApiClient(_restClientProvider); + ApiClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); + + _channels = new ConcurrentDictionary(1, 100); + _dmChannels = new ConcurrentDictionary(1, 100); + _guilds = new ConcurrentDictionary(1, 25); + _users = new ConcurrentDictionary(1, 250); } - public TokenType AuthTokenType + public async Task Login(string email, string password) { - get + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { - throw new NotImplementedException(); + await LoginInternal(email, password).ConfigureAwait(false); } + finally { _connectionLock.Release(); } } - - public DiscordAPIClient APIClient + public async Task Login(TokenType tokenType, string token, bool validateToken = true) { - get + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { - throw new NotImplementedException(); + await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); } + finally { _connectionLock.Release(); } } - - public IRequestQueue RequestQueue + private async Task LoginInternal(string email, string password) { - get + if (IsLoggedIn) + await LogoutInternal().ConfigureAwait(false); + try { - throw new NotImplementedException(); + _cancelTokenSource = new CancellationTokenSource(); + + var args = new LoginParams { Email = email, Password = password }; + await ApiClient.Login(args, _cancelTokenSource.Token).ConfigureAwait(false); + await CompleteLogin(false).ConfigureAwait(false); } + catch { await LogoutInternal().ConfigureAwait(false); throw; } } - - public IRestClient RestClient + private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) { - get + if (IsLoggedIn) + await LogoutInternal().ConfigureAwait(false); + try { - throw new NotImplementedException(); + _cancelTokenSource = new CancellationTokenSource(); + + await ApiClient.Login(tokenType, token, _cancelTokenSource.Token).ConfigureAwait(false); + await CompleteLogin(validateToken).ConfigureAwait(false); } + catch { await LogoutInternal().ConfigureAwait(false); throw; } } - - public Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) + private async Task CompleteLogin(bool validateToken) { - throw new NotImplementedException(); - } + if (validateToken) + { + try + { + await ApiClient.ValidateToken().ConfigureAwait(false); + var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false); + _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); - public Task GetChannel(ulong id) - { - throw new NotImplementedException(); - } + } + catch { await ApiClient.Logout().ConfigureAwait(false); } + } - public Task> GetConnections() - { - throw new NotImplementedException(); + IsLoggedIn = true; + LoggedIn.Raise(this); } - public Task GetCurrentUser() + public async Task Logout() { - throw new NotImplementedException(); + _cancelTokenSource?.Cancel(); + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LogoutInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } } - - public Task> GetDMChannels() + private async Task LogoutInternal() { - throw new NotImplementedException(); - } + bool wasLoggedIn = IsLoggedIn; - public Task GetGuild(ulong id) - { - throw new NotImplementedException(); - } + if (_cancelTokenSource != null) + { + try { _cancelTokenSource.Cancel(false); } + catch { } + } - public Task> GetGuilds() - { - throw new NotImplementedException(); + await ApiClient.Logout().ConfigureAwait(false); + _channels.Clear(); + _dmChannels.Clear(); + _guilds.Clear(); + _users.Clear(); + _currentUser = null; + + if (wasLoggedIn) + { + IsLoggedIn = false; + LoggedOut.Raise(this); + } } - public Task GetInvite(string inviteIdOrXkcd) + public async Task> GetConnections() { - throw new NotImplementedException(); + var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); + return models.Select(x => new Connection(x)); } - public Task GetOptimalVoiceRegion() + public IChannel GetChannel(ulong id) { - throw new NotImplementedException(); + IChannel channel; + if (_channels.TryGetValue(id, out channel)) + return channel; + return null; } - public Task GetUser(ulong id) + public async Task GetInvite(string inviteIdOrXkcd) { - throw new NotImplementedException(); + var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); + if (model != null) + return new Invite(this, model); + return null; } - public Task GetUser(string username, ushort discriminator) + public Guild GetGuild(ulong id) { - throw new NotImplementedException(); + Guild guild; + if (_guilds.TryGetValue(id, out guild)) + return guild; + return null; } - - public Task GetVoiceRegion(string id) + public async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) { - throw new NotImplementedException(); + var args = new CreateGuildParams(); + var model = await ApiClient.CreateGuild(args).ConfigureAwait(false); + return new Guild(this, model); } - public Task> GetVoiceRegions() + public User GetUser(ulong id) { - throw new NotImplementedException(); + User user; + if (_users.TryGetValue(id, out user)) + return user; + return null; } - - public Task Login(string email, string password) + public User GetUser(string username, ushort discriminator) { - throw new NotImplementedException(); + return _users.Where(x => x.Value.Discriminator == discriminator && x.Value.Username == username).Select(x => x.Value).FirstOrDefault(); } - - public Task Login(TokenType tokenType, string token, bool validateToken = true) + public async Task> QueryUsers(string query, int limit) { - throw new NotImplementedException(); + var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); + return models.Select(x => new User(this, x)); } - public Task Logout() + public VoiceRegion GetVoiceRegion(string id) { - throw new NotImplementedException(); + VoiceRegion region; + if (_voiceRegions.TryGetValue(id, out region)) + return region; + return null; } - public Task> QueryUsers(string query, int limit) + void Dispose(bool disposing) { - throw new NotImplementedException(); + if (!_isDisposed) + { + if (disposing) + _cancelTokenSource.Dispose(); + _isDisposed = true; + } } + public void Dispose() => Dispose(true); + + API.DiscordApiClient IDiscordClient.ApiClient => ApiClient; + + Task IDiscordClient.GetChannel(ulong id) + => Task.FromResult(GetChannel(id)); + Task> IDiscordClient.GetDMChannels() + => Task.FromResult>(DMChannels); + async Task> IDiscordClient.GetConnections() + => await GetConnections().ConfigureAwait(false); + async Task IDiscordClient.GetInvite(string inviteIdOrXkcd) + => await GetInvite(inviteIdOrXkcd).ConfigureAwait(false); + Task IDiscordClient.GetGuild(ulong id) + => Task.FromResult(GetGuild(id)); + Task> IDiscordClient.GetGuilds() + => Task.FromResult>(Guilds); + async Task IDiscordClient.CreateGuild(string name, IVoiceRegion region, Stream jpegIcon) + => await CreateGuild(name, region, jpegIcon).ConfigureAwait(false); + Task IDiscordClient.GetUser(ulong id) + => Task.FromResult(GetUser(id)); + Task IDiscordClient.GetUser(string username, ushort discriminator) + => Task.FromResult(GetUser(username, discriminator)); + Task IDiscordClient.GetCurrentUser() + => Task.FromResult(CurrentUser); + async Task> IDiscordClient.QueryUsers(string query, int limit) + => await QueryUsers(query, limit).ConfigureAwait(false); + Task> IDiscordClient.GetVoiceRegions() + => Task.FromResult>(VoiceRegions); + Task IDiscordClient.GetVoiceRegion(string id) + => Task.FromResult(GetVoiceRegion(id)); } } diff --git a/src/Discord.Net/WebSocket/DiscordSocketConfig.cs b/src/Discord.Net/WebSocket/DiscordSocketConfig.cs new file mode 100644 index 000000000..abe76a0d7 --- /dev/null +++ b/src/Discord.Net/WebSocket/DiscordSocketConfig.cs @@ -0,0 +1,39 @@ +using Discord.Net.WebSockets; + +namespace Discord.WebSocket +{ + public class DiscordSocketConfig : DiscordConfig + { + /// Gets or sets the id for this shard. Must be less than TotalShards. + public int ShardId { get; set; } = 0; + /// Gets or sets the total number of shards for this application. + public int TotalShards { get; set; } = 1; + + /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. + public int ConnectionTimeout { get; set; } = 30000; + /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. + public int ReconnectDelay { get; set; } = 1000; + /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. + public int FailedReconnectDelay { get; set; } = 15000; + + /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. + public int MessageCacheSize { get; set; } = 100; + /// + /// Gets or sets whether the permissions cache should be used. + /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. + /// + public bool UsePermissionsCache { get; set; } = true; + /// Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. + public bool EnablePreUpdateEvents { get; set; } = true; + /// + /// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250. + /// Decreasing this may reduce CPU usage while increasing login time and network usage. + /// + public int LargeThreshold { get; set; } = 250; + + //Engines + + /// Gets or sets the provider used to generate new websocket connections. + public WebSocketProvider WebSocketProvider { get; set; } = null; + } +} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs b/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs new file mode 100644 index 000000000..b645d1c20 --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + //TODO: Look into Internal abstract pattern - can we get rid of this? + public abstract class Channel : IChannel + { + /// + public ulong Id { get; private set; } + public IEnumerable Users => GetUsersInternal(); + + /// + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); + + internal Channel(ulong id) + { + Id = id; + } + + /// + public User GetUser(ulong id) + => GetUserInternal(id); + + protected abstract User GetUserInternal(ulong id); + protected abstract IEnumerable GetUsersInternal(); + + Task> IChannel.GetUsers() + => Task.FromResult>(GetUsersInternal()); + Task> IChannel.GetUsers(int limit, int offset) + => Task.FromResult>(GetUsersInternal().Skip(offset).Take(limit)); + Task IChannel.GetUser(ulong id) + => Task.FromResult(GetUserInternal(id)); + } +} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs index abc5dd725..bb93566af 100644 --- a/src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs @@ -1,5 +1,4 @@ using Discord.API.Rest; -using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -11,41 +10,34 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class DMChannel : IDMChannel + public class DMChannel : Channel, IDMChannel { private readonly MessageCache _messages; - - /// - public ulong Id { get; } + internal DiscordClient Discord { get; } /// - public DMUser Recipient { get; private set; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); + public User Recipient { get; private set; } + /// - public IEnumerable Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); + public new IEnumerable Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); public IEnumerable CachedMessages => _messages.Messages; - internal DMChannel(DiscordClient discord, Model model) + internal DMChannel(DiscordClient discord, User recipient, Model model) + : base(model.Id) { - Id = model.Id; Discord = discord; + Recipient = recipient; _messages = new MessageCache(Discord, this); Update(model); } private void Update(Model model) { - if (Recipient == null) - Recipient = new DMUser(this, model.Recipient); - else - Recipient.Update(model.Recipient); + Recipient.Update(model.Recipient); } - /// - public IUser GetUser(ulong id) + protected override User GetUserInternal(ulong id) { if (id == Recipient.Id) return Recipient; @@ -54,6 +46,10 @@ namespace Discord.WebSocket else return null; } + protected override IEnumerable GetUsersInternal() + { + return Users; + } /// Gets the message from this channel's cache with the given id, or null if none was found. public Message GetCachedMessage(ulong id) @@ -75,8 +71,8 @@ namespace Discord.WebSocket public async Task SendMessage(string text, bool isTTS = false) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.CreateDMMessage(Id, args).ConfigureAwait(false); - return new Message(this, model); + var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false); + return new Message(this, GetUser(model.Id), model); } /// public async Task SendFile(string filePath, string text = null, bool isTTS = false) @@ -85,49 +81,49 @@ namespace Discord.WebSocket using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.UploadDMFile(Id, file, args).ConfigureAwait(false); - return new Message(this, model); + var model = await Discord.ApiClient.UploadDMFile(Id, file, args).ConfigureAwait(false); + return new Message(this, GetUser(model.Id), model); } } /// public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); - return new Message(this, model); + var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); + return new Message(this, GetUser(model.Id), model); } /// public async Task DeleteMessages(IEnumerable messages) { - await Discord.APIClient.DeleteDMMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } /// public async Task TriggerTyping() { - await Discord.APIClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); } /// public async Task Close() { - await Discord.APIClient.DeleteChannel(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); } /// public override string ToString() => '@' + Recipient.ToString(); private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - IDMUser IDMChannel.Recipient => Recipient; + IUser IDMChannel.Recipient => Recipient; IEnumerable IMessageChannel.CachedMessages => CachedMessages; Task> IChannel.GetUsers() - => Task.FromResult(Users); + => Task.FromResult>(Users); Task> IChannel.GetUsers(int limit, int offset) - => Task.FromResult(Users.Skip(offset).Take(limit)); + => Task.FromResult>(Users.Skip(offset).Take(limit)); Task IChannel.GetUser(ulong id) - => Task.FromResult(GetUser(id)); + => Task.FromResult(GetUser(id)); Task IMessageChannel.GetCachedMessage(ulong id) => Task.FromResult(GetCachedMessage(id)); async Task> IMessageChannel.GetMessages(int limit) diff --git a/src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs index 5d8a6db68..db571577a 100644 --- a/src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs @@ -8,13 +8,11 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { - public abstract class GuildChannel : IGuildChannel + public abstract class GuildChannel : Channel, IGuildChannel { private ConcurrentDictionary _overwrites; internal PermissionsCache _permissions; - - /// - public ulong Id { get; } + /// Gets the guild this channel is a member of. public Guild Guild { get; } @@ -22,17 +20,15 @@ namespace Discord.WebSocket public string Name { get; private set; } /// public int Position { get; private set; } - public abstract IEnumerable Users { get; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); + public new abstract IEnumerable Users { get; } + /// public IReadOnlyDictionary PermissionOverwrites => _overwrites; internal DiscordClient Discord => Guild.Discord; internal GuildChannel(Guild guild, Model model) + : base(model.Id) { - Id = model.Id; Guild = guild; Update(model); @@ -57,11 +53,19 @@ namespace Discord.WebSocket var args = new ModifyGuildChannelParams(); func(args); - await Discord.APIClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); } /// Gets a user in this channel with the given id. - public abstract GuildUser GetUser(ulong id); + public new abstract GuildUser GetUser(ulong id); + protected override User GetUserInternal(ulong id) + { + return GetUser(id).GlobalUser; + } + protected override IEnumerable GetUsersInternal() + { + return Users.Select(x => x.GlobalUser); + } /// public OverwritePermissions? GetPermissionOverwrite(IUser user) @@ -82,7 +86,7 @@ namespace Discord.WebSocket /// Downloads a collection of all invites to this channel. public async Task> GetInvites() { - var models = await Discord.APIClient.GetChannelInvites(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); return models.Select(x => new InviteMetadata(Discord, x)); } @@ -90,23 +94,23 @@ namespace Discord.WebSocket public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.APIClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); } /// public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.APIClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); } /// public async Task RemovePermissionOverwrite(IUser user) { - await Discord.APIClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); } /// public async Task RemovePermissionOverwrite(IRole role) { - await Discord.APIClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); } /// Creates a new invite to this channel. @@ -123,14 +127,14 @@ namespace Discord.WebSocket Temporary = isTemporary, XkcdPass = withXkcd }; - var model = await Discord.APIClient.CreateChannelInvite(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); return new InviteMetadata(Discord, model); } /// public async Task Delete() { - await Discord.APIClient.DeleteChannel(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); } /// diff --git a/src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs index 760fd6c84..ade45276e 100644 --- a/src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs @@ -42,7 +42,7 @@ namespace Discord.WebSocket var args = new ModifyTextChannelParams(); func(args); - await Discord.APIClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); } /// Gets the message from this channel's cache with the given id, or null if none was found. @@ -73,8 +73,8 @@ namespace Discord.WebSocket public async Task SendMessage(string text, bool isTTS = false) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); - return new Message(this, model); + var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); + return new Message(this, GetUser(model.Id), model); } /// public async Task SendFile(string filePath, string text = null, bool isTTS = false) @@ -83,28 +83,28 @@ namespace Discord.WebSocket using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); - return new Message(this, model); + var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); + return new Message(this, GetUser(model.Author.Id), model); } } /// public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.APIClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); - return new Message(this, model); + var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); + return new Message(this, GetUser(model.Author.Id), model); } /// public async Task DeleteMessages(IEnumerable messages) { - await Discord.APIClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } /// public async Task TriggerTyping() { - await Discord.APIClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); } private string DebuggerDisplay => $"{Name} ({Id}, Text)"; diff --git a/src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs index d9cc49500..eec317352 100644 --- a/src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs @@ -34,7 +34,7 @@ namespace Discord.WebSocket var args = new ModifyVoiceChannelParams(); func(args); - await Discord.APIClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); } public override GuildUser GetUser(ulong id) diff --git a/src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs b/src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs index 8ccb68fe3..1b0f87f2a 100644 --- a/src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs @@ -6,18 +6,21 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Guild; -using EmbedModel = Discord.API.GuildEmbed; -using RoleModel = Discord.API.Role; using System.Diagnostics; namespace Discord.WebSocket { /// Represents a Discord guild (called a server in the official client). [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Guild : IGuild + public class Guild : IGuild, IUserGuild { + private ConcurrentDictionary _channels; + private ConcurrentDictionary _members; private ConcurrentDictionary _roles; + private ulong _ownerId; + private ulong? _afkChannelId, _embedChannelId; private string _iconId, _splashId; + private int _userCount; /// public ulong Id { get; } @@ -31,34 +34,52 @@ namespace Discord.WebSocket public bool IsEmbeddable { get; private set; } /// public int VerificationLevel { get; private set; } - public int UserCount { get; private set; } - - /// - public ulong? AFKChannelId { get; private set; } - /// - public ulong? EmbedChannelId { get; private set; } - /// - public ulong OwnerId { get; private set; } + /// - public string VoiceRegionId { get; private set; } + public VoiceRegion VoiceRegion { get; private set; } /// public IReadOnlyList Emojis { get; private set; } /// public IReadOnlyList Features { get; private set; } - + /// public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); + /// public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); /// public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); - /// - public ulong DefaultChannelId => Id; - /// + + /// Gets the number of channels in this guild. + public int ChannelCount => _channels.Count; + /// Gets the number of roles in this guild. + public int RoleCount => _roles.Count; + /// Gets the number of users in this guild. + public int UserCount => _userCount; + /// Gets the number of users downloaded for this guild so far. + internal int CurrentUserCount => _members.Count; + + /// Gets the the role representing all users in a guild. public Role EveryoneRole => GetRole(Id); + public GuildUser CurrentUser => GetUser(Discord.CurrentUser.Id); + /// Gets the user that created this guild. + public GuildUser Owner => GetUser(_ownerId); + /// Gets the default channel for this guild. + public TextChannel DefaultChannel => GetChannel(Id) as TextChannel; + /// Gets the AFK voice channel for this guild. + public VoiceChannel AFKChannel => GetChannel(_afkChannelId.GetValueOrDefault(0)) as VoiceChannel; + /// Gets the embed channel for this guild. + public IChannel EmbedChannel => GetChannel(_embedChannelId.GetValueOrDefault(0)); //TODO: Is this text or voice? + /// Gets a collection of all channels in this guild. + public IEnumerable Channels => _channels.Select(x => x.Value); + /// Gets a collection of text channels in this guild. + public IEnumerable TextChannels => _channels.Select(x => x.Value).OfType(); + /// Gets a collection of voice channels in this guild. + public IEnumerable VoiceChannels => _channels.Select(x => x.Value).OfType(); /// Gets a collection of all roles in this guild. public IEnumerable Roles => _roles?.Select(x => x.Value) ?? Enumerable.Empty(); - public IEnumerable Users => Array.Empty(); + /// Gets a collection of all users in this guild. + public IEnumerable Users => _members.Select(x => x.Value); internal Guild(DiscordClient discord, Model model) { @@ -69,15 +90,15 @@ namespace Discord.WebSocket } private void Update(Model model) { - AFKChannelId = model.AFKChannelId; + _afkChannelId = model.AFKChannelId; AFKTimeout = model.AFKTimeout; - EmbedChannelId = model.EmbedChannelId; + _embedChannelId = model.EmbedChannelId; IsEmbeddable = model.EmbedEnabled; Features = model.Features; _iconId = model.Icon; Name = model.Name; - OwnerId = model.OwnerId; - VoiceRegionId = model.Region; + _ownerId = model.OwnerId; + VoiceRegion = Discord.GetVoiceRegion(model.Region); _splashId = model.Splash; VerificationLevel = model.VerificationLevel; @@ -99,20 +120,6 @@ namespace Discord.WebSocket } _roles = roles; } - private void Update(EmbedModel model) - { - IsEmbeddable = model.Enabled; - EmbedChannelId = model.ChannelId; - } - private void Update(IEnumerable models) - { - Role role; - foreach (var model in models) - { - if (_roles.TryGetValue(model.Id, out role)) - role.Update(model); - } - } /// public async Task Modify(Action func) @@ -121,7 +128,7 @@ namespace Discord.WebSocket var args = new ModifyGuildParams(); func(args); - await Discord.APIClient.ModifyGuild(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); } /// public async Task ModifyEmbed(Action func) @@ -130,75 +137,66 @@ namespace Discord.WebSocket var args = new ModifyGuildEmbedParams(); func(args); - await Discord.APIClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); } /// public async Task ModifyChannels(IEnumerable args) { - await Discord.APIClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); } /// public async Task ModifyRoles(IEnumerable args) { - await Discord.APIClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); } /// public async Task Leave() { - await Discord.APIClient.LeaveGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); } /// public async Task Delete() { - await Discord.APIClient.DeleteGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); } /// public async Task> GetBans() { - var models = await Discord.APIClient.GetGuildBans(Id).ConfigureAwait(false); - return models.Select(x => new PublicUser(Discord, x)); + var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false); + return models.Select(x => new User(Discord, x)); } /// public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays); /// public async Task AddBan(ulong userId, int pruneDays = 0) { - var args = new CreateGuildBanParams() - { - PruneDays = pruneDays - }; - await Discord.APIClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); + var args = new CreateGuildBanParams() { PruneDays = pruneDays }; + await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); } /// public Task RemoveBan(IUser user) => RemoveBan(user.Id); /// public async Task RemoveBan(ulong userId) { - await Discord.APIClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); + await Discord.ApiClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); } /// Gets the channel in this guild with the provided id, or null if not found. - public async Task GetChannel(ulong id) + public GuildChannel GetChannel(ulong id) { - var model = await Discord.APIClient.GetChannel(Id, id).ConfigureAwait(false); - if (model != null) - return ToChannel(model); + GuildChannel channel; + if (_channels.TryGetValue(id, out channel)) + return channel; return null; } - /// Gets a collection of all channels in this guild. - public async Task> GetChannels() - { - var models = await Discord.APIClient.GetGuildChannels(Id).ConfigureAwait(false); - return models.Select(x => ToChannel(x)); - } /// Creates a new text channel. public async Task CreateTextChannel(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); var args = new CreateGuildChannelParams() { Name = name, Type = ChannelType.Text }; - var model = await Discord.APIClient.CreateGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); return new TextChannel(this, model); } /// Creates a new voice channel. @@ -207,28 +205,22 @@ namespace Discord.WebSocket if (name == null) throw new ArgumentNullException(nameof(name)); var args = new CreateGuildChannelParams { Name = name, Type = ChannelType.Voice }; - var model = await Discord.APIClient.CreateGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); return new VoiceChannel(this, model); } - - /// Gets a collection of all integrations attached to this guild. - public async Task> GetIntegrations() - { - var models = await Discord.APIClient.GetGuildIntegrations(Id).ConfigureAwait(false); - return models.Select(x => new GuildIntegration(this, x)); - } + /// Creates a new integration for this guild. public async Task CreateIntegration(ulong id, string type) { var args = new CreateGuildIntegrationParams { Id = id, Type = type }; - var model = await Discord.APIClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); return new GuildIntegration(this, model); } /// Gets a collection of all invites to this guild. public async Task> GetInvites() { - var models = await Discord.APIClient.GetGuildInvites(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildInvites(Id).ConfigureAwait(false); return models.Select(x => new InviteMetadata(Discord, x)); } /// Creates a new invite to this guild. @@ -244,7 +236,7 @@ namespace Discord.WebSocket Temporary = isTemporary, XkcdPass = withXkcd }; - var model = await Discord.APIClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); return new InviteMetadata(Discord, model); } @@ -262,7 +254,7 @@ namespace Discord.WebSocket { if (name == null) throw new ArgumentNullException(nameof(name)); - var model = await Discord.APIClient.CreateGuildRole(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildRole(Id).ConfigureAwait(false); var role = new Role(this, model); await role.Modify(x => @@ -275,43 +267,23 @@ namespace Discord.WebSocket return role; } - - /// Gets a collection of all users in this guild. - public async Task> GetUsers() - { - var args = new GetGuildMembersParams(); - var models = await Discord.APIClient.GetGuildMembers(Id, args).ConfigureAwait(false); - return models.Select(x => new GuildUser(this, x)); - } - /// Gets a paged collection of all users in this guild. - public async Task> GetUsers(int limit, int offset) - { - var args = new GetGuildMembersParams { Limit = limit, Offset = offset }; - var models = await Discord.APIClient.GetGuildMembers(Id, args).ConfigureAwait(false); - return models.Select(x => new GuildUser(this, x)); - } + /// Gets the user in this guild with the provided id, or null if not found. - public async Task GetUser(ulong id) + public GuildUser GetUser(ulong id) { - var model = await Discord.APIClient.GetGuildMember(Id, id).ConfigureAwait(false); - if (model != null) - return new GuildUser(this, model); + GuildUser user; + if (_members.TryGetValue(id, out user)) + return user; return null; } - /// Gets a the current user. - public async Task GetCurrentUser() - { - var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); - return await GetUser(currentUser.Id).ConfigureAwait(false); - } public async Task PruneUsers(int days = 30, bool simulate = false) { var args = new GuildPruneParams() { Days = days }; GetGuildPruneCountResponse model; if (simulate) - model = await Discord.APIClient.GetGuildPruneCount(Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.GetGuildPruneCount(Id, args).ConfigureAwait(false); else - model = await Discord.APIClient.BeginGuildPrune(Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.BeginGuildPrune(Id, args).ConfigureAwait(false); return model.Pruned; } @@ -331,15 +303,22 @@ namespace Discord.WebSocket private string DebuggerDisplay => $"{Name} ({Id})"; IEnumerable IGuild.Emojis => Emojis; - ulong IGuild.EveryoneRoleId => EveryoneRole.Id; IEnumerable IGuild.Features => Features; + ulong? IGuild.AFKChannelId => _afkChannelId; + ulong IGuild.DefaultChannelId => Id; + ulong? IGuild.EmbedChannelId => _embedChannelId; + ulong IGuild.EveryoneRoleId => EveryoneRole.Id; + ulong IGuild.OwnerId => _ownerId; + string IGuild.VoiceRegionId => VoiceRegion.Id; + bool IUserGuild.IsOwner => CurrentUser.Id == _ownerId; + GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; async Task> IGuild.GetBans() => await GetBans().ConfigureAwait(false); - async Task IGuild.GetChannel(ulong id) - => await GetChannel(id).ConfigureAwait(false); - async Task> IGuild.GetChannels() - => await GetChannels().ConfigureAwait(false); + Task IGuild.GetChannel(ulong id) + => Task.FromResult(GetChannel(id)); + Task> IGuild.GetChannels() + => Task.FromResult>(Channels); async Task IGuild.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); async Task IGuild.CreateRole(string name, GuildPermissions? permissions, Color? color, bool isHoisted) @@ -354,12 +333,12 @@ namespace Discord.WebSocket => Task.FromResult(GetRole(id)); Task> IGuild.GetRoles() => Task.FromResult>(Roles); - async Task IGuild.GetUser(ulong id) - => await GetUser(id).ConfigureAwait(false); - async Task IGuild.GetCurrentUser() - => await GetCurrentUser().ConfigureAwait(false); - async Task> IGuild.GetUsers() - => await GetUsers().ConfigureAwait(false); + Task IGuild.GetUser(ulong id) + => Task.FromResult(GetUser(id)); + Task IGuild.GetCurrentUser() + => Task.FromResult(CurrentUser); + Task> IGuild.GetUsers() + => Task.FromResult>(Users); Task IUpdateable.Update() => Task.CompletedTask; } diff --git a/src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs index fbb810e90..61610a0ae 100644 --- a/src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs @@ -31,7 +31,7 @@ namespace Discord.WebSocket /// public Role Role { get; private set; } /// - public User User { get; private set; } + public GuildUser User { get; private set; } /// public IntegrationAccount Account { get; private set; } internal DiscordClient Discord => Guild.Discord; @@ -54,13 +54,13 @@ namespace Discord.WebSocket SyncedAt = model.SyncedAt; Role = Guild.GetRole(model.RoleId); - User = new PublicUser(Discord, model.User); + User = Guild.GetUser(model.User.Id); } /// public async Task Delete() { - await Discord.APIClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); } /// public async Task Modify(Action func) @@ -69,12 +69,12 @@ namespace Discord.WebSocket var args = new ModifyGuildIntegrationParams(); func(args); - await Discord.APIClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); } /// public async Task Sync() { - await Discord.APIClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net/WebSocket/Entities/Message.cs b/src/Discord.Net/WebSocket/Entities/Message.cs index dcd1dbf36..af42298f8 100644 --- a/src/Discord.Net/WebSocket/Entities/Message.cs +++ b/src/Discord.Net/WebSocket/Entities/Message.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Message; namespace Discord.WebSocket { + //TODO: Support mention_roles [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Message : IMessage { @@ -28,33 +30,36 @@ namespace Discord.WebSocket /// public IMessageChannel Channel { get; } /// - public User Author { get; } + public IUser Author { get; } /// public IReadOnlyList Attachments { get; private set; } /// public IReadOnlyList Embeds { get; private set; } /// - public IReadOnlyList MentionedUsers { get; private set; } + public IReadOnlyList MentionedUsers { get; private set; } /// - public IReadOnlyList MentionedChannelIds { get; private set; } + public IReadOnlyList MentionedChannels { get; private set; } /// - public IReadOnlyList MentionedRoleIds { get; private set; } + public IReadOnlyList MentionedRoles { get; private set; } /// public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord; - internal Message(IMessageChannel channel, Model model) + internal Message(IMessageChannel channel, IUser author, Model model) { Id = model.Id; Channel = channel; - Author = new PublicUser(Discord, model.Author); + Author = author; Update(model); } private void Update(Model model) { + var guildChannel = Channel as GuildChannel; + var guild = guildChannel?.Guild; + IsTTS = model.IsTextToSpeech; Timestamp = model.Timestamp; EditedTimestamp = model.EditedTimestamp; @@ -80,38 +85,38 @@ namespace Discord.WebSocket else Embeds = Array.Empty(); - if (model.Mentions.Length > 0) + if (guildChannel != null && model.Mentions.Length > 0) { - var discord = Discord; - var builder = ImmutableArray.CreateBuilder(model.Mentions.Length); + var builder = ImmutableArray.CreateBuilder(model.Mentions.Length); for (int i = 0; i < model.Mentions.Length; i++) - builder.Add(new PublicUser(discord, model.Mentions[i])); + { + var user = guild.GetUser(model.Mentions[i].Id); + if (user != null) + builder.Add(user); + } MentionedUsers = builder.ToArray(); } else - MentionedUsers = Array.Empty(); - MentionedChannelIds = MentionUtils.GetChannelMentions(model.Content); - MentionedRoleIds = MentionUtils.GetRoleMentions(model.Content); - if (model.IsMentioningEveryone) + MentionedUsers = Array.Empty(); + + if (guildChannel != null/* && model.Content != null*/) { - ulong? guildId = (Channel as IGuildChannel)?.Guild.Id; - if (guildId != null) - { - if (MentionedRoleIds.Count == 0) - MentionedRoleIds = ImmutableArray.Create(guildId.Value); - else - { - var builder = ImmutableArray.CreateBuilder(MentionedRoleIds.Count + 1); - builder.AddRange(MentionedRoleIds); - builder.Add(guildId.Value); - MentionedRoleIds = builder.ToImmutable(); - } - } + MentionedChannels = MentionUtils.GetChannelMentions(model.Content).Select(x => guild.GetChannel(x)).Where(x => x != null).ToImmutableArray(); + + var mentionedRoles = MentionUtils.GetRoleMentions(model.Content).Select(x => guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); + if (model.IsMentioningEveryone) + mentionedRoles = mentionedRoles.Add(guild.EveryoneRole); + MentionedRoles = mentionedRoles; + } + else + { + MentionedChannels = Array.Empty(); + MentionedRoles = Array.Empty(); } Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); - Author.Update(model.Author); + //Author.Update(model.Author); //TODO: Uncomment this somehow } /// @@ -124,9 +129,9 @@ namespace Discord.WebSocket var guildChannel = Channel as GuildChannel; if (guildChannel != null) - await Discord.APIClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); else - await Discord.APIClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); } /// @@ -134,18 +139,17 @@ namespace Discord.WebSocket { var guildChannel = Channel as GuildChannel; if (guildChannel != null) - await Discord.APIClient.DeleteMessage(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteMessage(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); else - await Discord.APIClient.DeleteDMMessage(Channel.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteDMMessage(Channel.Id, Id).ConfigureAwait(false); } public override string ToString() => Text; private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; IUser IMessage.Author => Author; - IReadOnlyList IMessage.Attachments => Attachments; - IReadOnlyList IMessage.Embeds => Embeds; - IReadOnlyList IMessage.MentionedChannelIds => MentionedChannelIds; IReadOnlyList IMessage.MentionedUsers => MentionedUsers; + IReadOnlyList IMessage.MentionedChannelIds => MentionedChannels.Select(x => x.Id).ToImmutableArray(); + IReadOnlyList IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); } } diff --git a/src/Discord.Net/WebSocket/Entities/Role.cs b/src/Discord.Net/WebSocket/Entities/Role.cs index 24af5f1d8..52bef2b1e 100644 --- a/src/Discord.Net/WebSocket/Entities/Role.cs +++ b/src/Discord.Net/WebSocket/Entities/Role.cs @@ -35,6 +35,7 @@ namespace Discord.WebSocket public bool IsEveryone => Id == Guild.Id; /// public string Mention => MentionUtils.Mention(this); + public IEnumerable Users => Guild.Users.Where(x => x.Roles.Any(y => y.Id == Id)); internal DiscordClient Discord => Guild.Discord; internal Role(Guild guild, Model model) @@ -60,23 +61,19 @@ namespace Discord.WebSocket var args = new ModifyGuildRoleParams(); func(args); - await Discord.APIClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); } /// Deletes this message. public async Task Delete() - => await Discord.APIClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); + => await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; ulong IRole.GuildId => Guild.Id; - - async Task> IRole.GetUsers() - { - //A tad hacky, but it works - var models = await Discord.APIClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false); - return models.Where(x => x.Roles.Contains(Id)).Select(x => new GuildUser(Guild, x)); - } + + Task> IRole.GetUsers() + => Task.FromResult>(Users); } } diff --git a/src/Discord.Net/WebSocket/Entities/Users/DMUser.cs b/src/Discord.Net/WebSocket/Entities/Users/DMUser.cs deleted file mode 100644 index 4b300ce76..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/DMUser.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Model = Discord.API.User; - -namespace Discord.WebSocket -{ - public class DMUser : User, IDMUser - { - /// - public DMChannel Channel { get; } - - internal override DiscordClient Discord => Channel.Discord; - - internal DMUser(DMChannel channel, Model model) - : base(model) - { - Channel = channel; - } - - IDMChannel IDMUser.Channel => Channel; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs b/src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs index 0dde4a8c5..c0caec225 100644 --- a/src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs @@ -8,11 +8,12 @@ using Model = Discord.API.GuildMember; namespace Discord.WebSocket { - public class GuildUser : User, IGuildUser + public class GuildUser : IGuildUser { private ImmutableArray _roles; public Guild Guild { get; } + public User GlobalUser { get; } /// public bool IsDeaf { get; private set; } @@ -23,18 +24,38 @@ namespace Discord.WebSocket /// public string Nickname { get; private set; } /// + public UserStatus Status { get; private set; } + /// + public Game? CurrentGame { get; private set; } + /// public VoiceChannel VoiceChannel { get; private set; } + /// public GuildPermissions GuildPermissions { get; private set; } /// public IReadOnlyList Roles => _roles; - internal override DiscordClient Discord => Guild.Discord; + /// + public string AvatarUrl => GlobalUser.AvatarUrl; + /// + public ushort Discriminator => GlobalUser.Discriminator; + /// + public bool IsBot => GlobalUser.IsBot; + /// + public string Username => GlobalUser.Username; + /// + public DateTime CreatedAt => GlobalUser.CreatedAt; + /// + public ulong Id => GlobalUser.Id; + /// + public string Mention => GlobalUser.Mention; + internal DiscordClient Discord => Guild.Discord; - internal GuildUser(Guild guild, Model model) - : base(model.User) + internal GuildUser(User globalUser, Guild guild, Model model) { + GlobalUser = globalUser; Guild = guild; + globalUser.Update(model.User); Update(model); } internal void Update(Model model) @@ -57,31 +78,6 @@ namespace Discord.WebSocket GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); } - public bool HasRole(IRole role) - { - for (int i = 0; i < _roles.Length; i++) - { - if (_roles[i].Id == role.Id) - return true; - } - return false; - } - - public async Task Kick() - { - await Discord.APIClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); - } - - public GuildPermissions GetGuildPermissions() - { - return new GuildPermissions(Permissions.ResolveGuild(this)); - } - public ChannelPermissions GetPermissions(IGuildChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); - } - public async Task Modify(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -89,17 +85,17 @@ namespace Discord.WebSocket var args = new ModifyGuildMemberParams(); func(args); - bool isCurrentUser = (await Discord.GetCurrentUser().ConfigureAwait(false)).Id == Id; + bool isCurrentUser = Discord.CurrentUser.Id == Id; if (isCurrentUser && args.Nickname.IsSpecified) { var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value }; - await Discord.APIClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); + await Discord.ApiClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); args.Nickname = new API.Optional(); //Remove } if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) { - await Discord.APIClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); if (args.Deaf.IsSpecified) IsDeaf = args.Deaf.Value; if (args.Mute.IsSpecified) @@ -111,6 +107,26 @@ namespace Discord.WebSocket } } + public async Task Kick() + { + await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); + } + + public GuildPermissions GetGuildPermissions() + { + return new GuildPermissions(Permissions.ResolveGuild(this)); + } + public ChannelPermissions GetPermissions(IGuildChannel channel) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); + } + + public async Task CreateDMChannel() + { + return await GlobalUser.CreateDMChannel().ConfigureAwait(false); + } + IGuild IGuildUser.Guild => Guild; IReadOnlyList IGuildUser.Roles => Roles; @@ -118,7 +134,10 @@ namespace Discord.WebSocket ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => GetPermissions(channel); + async Task IUser.CreateDMChannel() + => await CreateDMChannel().ConfigureAwait(false); Task IUpdateable.Update() => Task.CompletedTask; + } } diff --git a/src/Discord.Net/WebSocket/Entities/Users/PublicUser.cs b/src/Discord.Net/WebSocket/Entities/Users/PublicUser.cs deleted file mode 100644 index f1816077e..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/PublicUser.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Model = Discord.API.User; - -namespace Discord.WebSocket -{ - public class PublicUser : User - { - internal override DiscordClient Discord { get; } - - internal PublicUser(DiscordClient discord, Model model) - : base(model) - { - Discord = discord; - } - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs index 9b97cf58e..8b8a86788 100644 --- a/src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs @@ -7,17 +7,14 @@ namespace Discord.WebSocket { public class SelfUser : User, ISelfUser { - internal override DiscordClient Discord { get; } - /// public string Email { get; private set; } /// public bool IsVerified { get; private set; } internal SelfUser(DiscordClient discord, Model model) - : base(model) + : base(discord, model) { - Discord = discord; } internal override void Update(Model model) { @@ -34,7 +31,7 @@ namespace Discord.WebSocket var args = new ModifyCurrentUserParams(); func(args); - await Discord.APIClient.ModifyCurrentUser(args).ConfigureAwait(false); + await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); } Task IUpdateable.Update() diff --git a/src/Discord.Net/WebSocket/Entities/Users/User.cs b/src/Discord.Net/WebSocket/Entities/Users/User.cs index f43465544..1101dd961 100644 --- a/src/Discord.Net/WebSocket/Entities/Users/User.cs +++ b/src/Discord.Net/WebSocket/Entities/Users/User.cs @@ -6,14 +6,15 @@ using Model = Discord.API.User; namespace Discord.WebSocket { + //TODO: Unload when there are no more references via DMUser or GuildUser [DebuggerDisplay("{DebuggerDisplay,nq}")] - public abstract class User : IUser + public class User : IUser { private string _avatarId; /// public ulong Id { get; } - internal abstract DiscordClient Discord { get; } + internal DiscordClient Discord { get; } /// public ushort Discriminator { get; private set; } @@ -21,6 +22,8 @@ namespace Discord.WebSocket public bool IsBot { get; private set; } /// public string Username { get; private set; } + /// + public DMChannel DMChannel { get; private set; } /// public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); @@ -31,8 +34,9 @@ namespace Discord.WebSocket /// public string NicknameMention => MentionUtils.Mention(this, true); - internal User(Model model) + internal User(DiscordClient discord, Model model) { + Discord = discord; Id = model.Id; Update(model); @@ -47,10 +51,16 @@ namespace Discord.WebSocket public async Task CreateDMChannel() { - var args = new CreateDMChannelParams { RecipientId = Id }; - var model = await Discord.APIClient.CreateDMChannel(args).ConfigureAwait(false); + var channel = DMChannel; + if (channel == null) + { + var args = new CreateDMChannelParams { RecipientId = Id }; + var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); - return new DMChannel(Discord, model); + channel = new DMChannel(Discord, this, model); + DMChannel = channel; + } + return channel; } public override string ToString() => $"{Username}#{Discriminator}"; diff --git a/src/Discord.Net/WebSocket/Events/ChannelEventArgs.cs b/src/Discord.Net/WebSocket/Events/ChannelEventArgs.cs new file mode 100644 index 000000000..4689e44bc --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/ChannelEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.WebSocket +{ + public class ChannelEventArgs : EventArgs + { + public IChannel Channel { get; } + + public ChannelEventArgs(IChannel channel) + { + Channel = channel; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/ChannelUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/ChannelUpdatedEventArgs.cs new file mode 100644 index 000000000..24d001bc3 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/ChannelUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord.WebSocket +{ + public class ChannelUpdatedEventArgs : ChannelEventArgs + { + public IChannel Before { get; } + public IChannel After => Channel; + + public ChannelUpdatedEventArgs(IChannel before, IChannel after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/CurrentUserEventArgs.cs b/src/Discord.Net/WebSocket/Events/CurrentUserEventArgs.cs new file mode 100644 index 000000000..97d352c31 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/CurrentUserEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.WebSocket +{ + public class CurrentUserEventArgs : EventArgs + { + public SelfUser CurrentUser { get; } + + public CurrentUserEventArgs(SelfUser currentUser) + { + CurrentUser = currentUser; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/CurrentUserUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/CurrentUserUpdatedEventArgs.cs new file mode 100644 index 000000000..167278089 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/CurrentUserUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord.WebSocket +{ + public class CurrentUserUpdatedEventArgs : CurrentUserEventArgs + { + public SelfUser Before { get; } + public SelfUser After => CurrentUser; + + public CurrentUserUpdatedEventArgs(SelfUser before, SelfUser after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/DisconnectedEventArgs.cs b/src/Discord.Net/WebSocket/Events/DisconnectedEventArgs.cs new file mode 100644 index 000000000..d9120b243 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/DisconnectedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord.WebSocket +{ + public class DisconnectedEventArgs : EventArgs + { + public bool WasUnexpected { get; } + public Exception Exception { get; } + + public DisconnectedEventArgs(bool wasUnexpected, Exception exception = null) + { + WasUnexpected = wasUnexpected; + Exception = exception; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/GuildEventArgs.cs b/src/Discord.Net/WebSocket/Events/GuildEventArgs.cs new file mode 100644 index 000000000..8c9d4dc20 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/GuildEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.WebSocket +{ + public class GuildEventArgs : EventArgs + { + public Guild Guild { get; } + + public GuildEventArgs(Guild guild) + { + Guild = guild; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/GuildUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/GuildUpdatedEventArgs.cs new file mode 100644 index 000000000..03a7f0f75 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/GuildUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord.WebSocket +{ + public class GuildUpdatedEventArgs : GuildEventArgs + { + public Guild Before { get; } + public Guild After => Guild; + + public GuildUpdatedEventArgs(Guild before, Guild after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/MessageEventArgs.cs b/src/Discord.Net/WebSocket/Events/MessageEventArgs.cs new file mode 100644 index 000000000..a1bea179a --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/MessageEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.WebSocket +{ + public class MessageEventArgs : EventArgs + { + public Message Message { get; } + + public MessageEventArgs(Message message) + { + Message = message; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/MessageUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/MessageUpdatedEventArgs.cs new file mode 100644 index 000000000..14b41707d --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/MessageUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord.WebSocket +{ + public class MessageUpdatedEventArgs : MessageEventArgs + { + public Message Before { get; } + public Message After => Message; + + public MessageUpdatedEventArgs(Message before, Message after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/RoleEventArgs.cs b/src/Discord.Net/WebSocket/Events/RoleEventArgs.cs new file mode 100644 index 000000000..902eea1e8 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/RoleEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.WebSocket +{ + public class RoleEventArgs : EventArgs + { + public Role Role { get; } + + public RoleEventArgs(Role role) + { + Role = role; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/RoleUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/RoleUpdatedEventArgs.cs new file mode 100644 index 000000000..45c6f05a9 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/RoleUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord.WebSocket +{ + public class RoleUpdatedEventArgs : RoleEventArgs + { + public Role Before { get; } + public Role After => Role; + + public RoleUpdatedEventArgs(Role before, Role after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/TypingEventArgs.cs b/src/Discord.Net/WebSocket/Events/TypingEventArgs.cs new file mode 100644 index 000000000..a3f45d452 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/TypingEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord.WebSocket +{ + public class TypingEventArgs : EventArgs + { + public IMessageChannel Channel { get; } + public IUser User { get; } + + public TypingEventArgs(IMessageChannel channel, IUser user) + { + Channel = channel; + User = user; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/UserEventArgs.cs b/src/Discord.Net/WebSocket/Events/UserEventArgs.cs new file mode 100644 index 000000000..8e31a58aa --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/UserEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.WebSocket +{ + public class UserEventArgs : EventArgs + { + public IUser User { get; } + + public UserEventArgs(IUser user) + { + User = user; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/UserUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/UserUpdatedEventArgs.cs new file mode 100644 index 000000000..298b0d427 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/UserUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord.WebSocket +{ + public class UserUpdatedEventArgs : UserEventArgs + { + public IUser Before { get; } + public IUser After => User; + + public UserUpdatedEventArgs(IUser before, IUser after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/WebSocket/Events/VoiceChannelEventArgs.cs b/src/Discord.Net/WebSocket/Events/VoiceChannelEventArgs.cs new file mode 100644 index 000000000..1b10f3cd7 --- /dev/null +++ b/src/Discord.Net/WebSocket/Events/VoiceChannelEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.WebSocket +{ + public class VoiceChannelEventArgs : EventArgs + { + public VoiceChannel Channel { get; } + + public VoiceChannelEventArgs(VoiceChannel channel) + { + Channel = channel; + } + } +} diff --git a/src/Discord.Net/WebSocket/MessageCache.cs b/src/Discord.Net/WebSocket/MessageCache.cs index d81fb3498..5051efc3a 100644 --- a/src/Discord.Net/WebSocket/MessageCache.cs +++ b/src/Discord.Net/WebSocket/MessageCache.cs @@ -99,8 +99,9 @@ namespace Discord.WebSocket RelativeDirection = dir, RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id }; - var downloadedMessages = await _discord.APIClient.GetChannelMessages(_channel.Id, args).ConfigureAwait(false); - return cachedMessages.AsEnumerable().Concat(downloadedMessages.Select(x => new Message(_channel, x))).ToImmutableArray(); + var downloadedMessages = await _discord.ApiClient.GetChannelMessages(_channel.Id, args).ConfigureAwait(false); + //TODO: Ugly channel cast + return cachedMessages.AsEnumerable().Concat(downloadedMessages.Select(x => new Message(_channel, (_channel as Channel).GetUser(x.Id), x))).ToImmutableArray(); } } } diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index c47cb1411..947633661 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -1,14 +1,38 @@ -{ +{ + "version": "1.0.0-dev", + "description": "A Discord.Net extension adding voice support.", + "authors": [ "RogueException" ], + + "buildOptions": { + "allowUnsafe": true, + "warningsAsErrors": false + }, + + "packOptions": { + "tags": [ "discord", "discordapp" ], + "licenseUrl": "http://opensource.org/licenses/MIT", + "projectUrl": "https://github.com/RogueException/Discord.Net", + "repository": { + "type": "git", + "url": "git://github.com/RogueException/Discord.Net" + } + }, + "dependencies": { + "NETStandard.Library": "1.5.0-rc2-24027", "Newtonsoft.Json": "8.0.3", - "System.Collections.Immutable": "1.1.37" + "System.Collections.Immutable": "1.2.0-rc2-24027", + "System.Net.Websockets.Client": "4.0.0-rc2-24027", + "System.Runtime.Serialization.Primitives": "4.1.1-rc2-24027" }, "frameworks": { - "net461": { } - }, - - "runtimes": { - "win": { } + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] + } } -} \ No newline at end of file +}