From 4678544fed606df2c427f523577b0b91159b7c04 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 4 Oct 2016 07:32:26 -0300 Subject: [PATCH] Added remaining gateway events, added IAudioChannel, added CacheModes --- src/Discord.Net.Commands/project.json | 4 +- src/Discord.Net.Core/Entities/CacheMode.cs | 8 + .../Entities/Channels/IAudioChannel.cs | 6 + .../Entities/Channels/IChannel.cs | 7 +- .../Entities/Channels/IGroupChannel.cs | 10 +- .../Entities/Channels/IGuildChannel.cs | 11 +- .../Entities/Channels/IMessageChannel.cs | 11 +- .../Entities/Channels/IVoiceChannel.cs | 2 +- src/Discord.Net.Core/Entities/Guilds/IBan.cs | 7 +- .../Entities/Guilds/IGuild.cs | 14 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 4 +- .../Extensions/CollectionExtensions.cs | 0 .../TaskCompletionSourceExtensions.cs | 8 + src/Discord.Net.Core/RequestOptions.cs | 2 +- .../Utils/ConcurrentHashSet.cs | 5 + .../Utils/Extensions/Permissions.cs | 152 ------ src/Discord.Net.Core/Utils/MentionsHelper.cs | 5 +- .../Entities/Channels/IRestAudioChannel.cs | 6 + .../Entities/Channels/IRestMessageChannel.cs | 25 + .../Entities/Channels/IRestPrivateChannel.cs | 9 + .../Entities/Channels/RestChannel.cs | 25 +- .../Entities/Channels/RestDMChannel.cs | 50 +- .../Entities/Channels/RestGroupChannel.cs | 48 +- .../Entities/Channels/RestGuildChannel.cs | 18 +- .../Entities/Channels/RestTextChannel.cs | 52 ++- .../Entities/Channels/RestVoiceChannel.cs | 6 +- .../Entities/Guilds/RestGuild.cs | 59 ++- .../Entities/Users/RestUser.cs | 12 +- .../Entities/Users/UserHelper.cs | 2 +- .../Entities/{ => Guilds}/RpcGuild.cs | 0 .../Audio/AudioClient.cs | 6 +- src/Discord.Net.WebSocket/ClientState.cs | 6 +- .../DiscordSocketClient.Events.cs | 104 ++--- .../DiscordSocketClient.cs | 379 ++++++++------- .../Entities/Channels/ISocketAudioChannel.cs | 6 + .../Channels/ISocketMessageChannel.cs | 32 ++ .../Channels/ISocketPrivateChannel.cs | 9 + .../Entities/Channels/SocketChannel.cs | 32 +- .../Entities/Channels/SocketChannelHelper.cs | 81 ++++ .../Entities/Channels/SocketDMChannel.cs | 119 ++--- .../Entities/Channels/SocketGroupChannel.cs | 161 +++++-- .../Entities/Channels/SocketGuildChannel.cs | 61 +-- .../Entities/Channels/SocketTextChannel.cs | 118 +++-- .../Entities/Channels/SocketVoiceChannel.cs | 38 +- .../Entities/Guilds/SocketGuild.cs | 434 ++++++++++++++++-- .../Entities/Messages/MessageCache.cs | 7 +- .../Entities/Messages/SocketMessage.cs | 19 +- .../Entities/Messages/SocketSystemMessage.cs | 15 +- .../Entities/Messages/SocketUserMessage.cs | 14 +- .../Entities/Roles/SocketRole.cs | 58 +++ .../Entities/Users/SocketGlobalUser.cs | 50 +- .../Entities/Users/SocketGroupUser.cs | 26 +- .../Entities/Users/SocketGuildUser.cs | 69 ++- .../Entities/Users/SocketPresence.cs | 6 +- .../Entities/Users/SocketSelfUser.cs | 30 +- .../Entities/Users/SocketSimpleUser.cs | 34 ++ .../Entities/Users/SocketUser.cs | 44 +- .../Entities/Users/SocketVoiceState.cs | 2 +- 58 files changed, 1679 insertions(+), 849 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/CacheMode.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs rename src/Discord.Net.Core/{Utils => }/Extensions/CollectionExtensions.cs (100%) rename src/Discord.Net.Core/{Utils => }/Extensions/TaskCompletionSourceExtensions.cs (58%) delete mode 100644 src/Discord.Net.Core/Utils/Extensions/Permissions.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs rename src/Discord.Net.Rpc/Entities/{ => Guilds}/RpcGuild.cs (100%) create mode 100644 src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json index 6505ea3c8..c0449bf35 100644 --- a/src/Discord.Net.Commands/project.json +++ b/src/Discord.Net.Commands/project.json @@ -30,7 +30,9 @@ }, "dependencies": { - "Discord.Net": "1.0.0-*" + "Discord.Net.Core": { + "target": "project" + } }, "frameworks": { diff --git a/src/Discord.Net.Core/Entities/CacheMode.cs b/src/Discord.Net.Core/Entities/CacheMode.cs new file mode 100644 index 000000000..a047bd616 --- /dev/null +++ b/src/Discord.Net.Core/Entities/CacheMode.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + public enum CacheMode + { + AllowDownload, + CacheOnly + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs new file mode 100644 index 000000000..9b8074efb --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -0,0 +1,6 @@ +namespace Discord +{ + public interface IAudioChannel + { + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index e3990204c..c81911a45 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -5,13 +5,10 @@ namespace Discord { public interface IChannel : ISnowflakeEntity { - IReadOnlyCollection CachedUsers { get; } - /// Gets a collection of all users in this channel. - IAsyncEnumerable> GetUsersAsync(); + IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload); /// Gets a user in this channel with the provided id. - Task GetUserAsync(ulong id); - IUser GetCachedUser(ulong id); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs index dafc997a7..23f5b4784 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs @@ -1,15 +1,9 @@ -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { - public interface IGroupChannel : IMessageChannel, IPrivateChannel + public interface IGroupChannel : IMessageChannel, IPrivateChannel, IAudioChannel { - ///// Adds a user to this group. - //Task AddUserAsync(IUser user); - - //new IReadOnlyCollection CachedUsers { get; } - /// Leaves this group. Task LeaveAsync(); } diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 0927c2b79..085771fbf 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -14,7 +14,8 @@ namespace Discord /// Gets the id of the guild this channel is a member of. ulong GuildId { get; } - new IReadOnlyCollection CachedUsers { get; } + /// Gets a collection of permission overwrites for this channel. + IReadOnlyCollection PermissionOverwrites { get; } /// Creates a new invite to this channel. /// The time (in seconds) until the invite expires. Set to null to never expire. @@ -23,9 +24,6 @@ namespace Discord Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false); /// Returns a collection of all invites to this channel. Task> GetInvitesAsync(); - - /// Gets a collection of permission overwrites for this channel. - IReadOnlyCollection PermissionOverwrites { get; } /// Modifies this guild channel. Task ModifyAsync(Action func); @@ -44,9 +42,8 @@ namespace Discord Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions); /// Gets a collection of all users in this channel. - new IAsyncEnumerable> GetUsersAsync(); + new IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload); /// Gets a user in this channel with the provided id. - new Task GetUserAsync(ulong id); - new IGuildUser GetCachedUser(ulong id); + new Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index e8089d2af..b8333c64a 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -7,9 +7,6 @@ namespace Discord { public interface IMessageChannel : IChannel { - /// Gets all messages in this channel's cache. - IReadOnlyCollection CachedMessages { get; } - /// Sends a message to this message channel. Task SendMessageAsync(string text, bool isTTS = false); /// Sends a file to this text channel, with an optional caption. @@ -18,13 +15,11 @@ namespace Discord Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false); /// Gets a message from this message channel with the given id, or null if not found. - Task GetMessageAsync(ulong id); - /// Gets the message from this channel's cache with the given id, or null if not found. - IMessage GetCachedMessage(ulong id); + Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); /// Gets the last N messages from this message channel. - IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch); + IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload); /// Gets a collection of messages in this channel. - IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload); /// Gets a collection of pinned messages in this channel. Task> GetPinnedMessagesAsync(); /// Bulk deletes multiple messages. diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 5f6e8c817..75f6a0190 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace Discord { - public interface IVoiceChannel : IGuildChannel + public interface IVoiceChannel : IGuildChannel, IAudioChannel { /// Gets the bitrate, in bits per second, clients in this voice channel are requested to use. int Bitrate { get; } diff --git a/src/Discord.Net.Core/Entities/Guilds/IBan.cs b/src/Discord.Net.Core/Entities/Guilds/IBan.cs index aee2cc198..05ab0c00f 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IBan.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IBan.cs @@ -1,10 +1,5 @@ -//using Discord.Rest; -using System.Diagnostics; -using Model = Discord.API.Ban; - -namespace Discord +namespace Discord { - public interface IBan { IUser User { get; } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 489f9e8f5..414a7cc74 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -41,7 +41,6 @@ namespace Discord ulong OwnerId { get; } /// Gets the id of the region hosting this guild's voice channels. string VoiceRegionId { get; } - /// Gets the IAudioClient currently associated with this guild. IAudioClient AudioClient { get; } /// Gets the built-in role containing all users in this guild. @@ -52,7 +51,6 @@ namespace Discord IReadOnlyCollection Features { get; } /// Gets a collection of all roles in this guild. IReadOnlyCollection Roles { get; } - IReadOnlyCollection CachedUsers { get; } /// Modifies this guild. Task ModifyAsync(Action func); @@ -77,10 +75,9 @@ namespace Discord Task RemoveBanAsync(ulong userId); /// Gets a collection of all channels in this guild. - Task> GetChannelsAsync(); + Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload); /// Gets the channel in this guild with the provided id, or null if not found. - Task GetChannelAsync(ulong id); - IGuildChannel GetCachedChannel(ulong id); + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); /// Creates a new text channel. Task CreateTextChannelAsync(string name); /// Creates a new voice channel. @@ -98,12 +95,11 @@ namespace Discord Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false); /// Gets a collection of all users in this guild. - Task> GetUsersAsync(); + Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload); //TODO: shouldnt this be paged? /// Gets the user in this guild with the provided id, or null if not found. - Task GetUserAsync(ulong id); - IGuildUser GetCachedUser(ulong id); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); /// Gets the current user for this guild. - Task GetCurrentUserAsync(); + Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload); /// Downloads all users for this guild if the current list is incomplete. Task DownloadUsersAsync(); /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 30aa974cf..43f441e7e 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -17,8 +17,8 @@ namespace Discord /// Gets the username for this user. string Username { get; } - /// Returns a private message channel to this user, returning null if one does not exist or is not cached. - IDMChannel GetCachedDMChannel(); + /// Returns a private message channel to this user, creating one if it does not already exist. + Task GetDMChannelAsync(CacheMode mode = CacheMode.AllowDownload); /// Returns a private message channel to this user, creating one if it does not already exist. Task CreateDMChannelAsync(); } diff --git a/src/Discord.Net.Core/Utils/Extensions/CollectionExtensions.cs b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs similarity index 100% rename from src/Discord.Net.Core/Utils/Extensions/CollectionExtensions.cs rename to src/Discord.Net.Core/Extensions/CollectionExtensions.cs diff --git a/src/Discord.Net.Core/Utils/Extensions/TaskCompletionSourceExtensions.cs b/src/Discord.Net.Core/Extensions/TaskCompletionSourceExtensions.cs similarity index 58% rename from src/Discord.Net.Core/Utils/Extensions/TaskCompletionSourceExtensions.cs rename to src/Discord.Net.Core/Extensions/TaskCompletionSourceExtensions.cs index be7c42e9c..a5a715b4c 100644 --- a/src/Discord.Net.Core/Utils/Extensions/TaskCompletionSourceExtensions.cs +++ b/src/Discord.Net.Core/Extensions/TaskCompletionSourceExtensions.cs @@ -5,10 +5,18 @@ namespace Discord { internal static class TaskCompletionSourceExtensions { + public static Task SetResultAsync(this TaskCompletionSource source, T result) + => Task.Run(() => source.SetResult(result)); public static Task TrySetResultAsync(this TaskCompletionSource source, T result) => Task.Run(() => source.TrySetResult(result)); + + public static Task SetExceptionAsync(this TaskCompletionSource source, Exception ex) + => Task.Run(() => source.SetException(ex)); public static Task TrySetExceptionAsync(this TaskCompletionSource source, Exception ex) => Task.Run(() => source.TrySetException(ex)); + + public static Task SetCanceledAsync(this TaskCompletionSource source) + => Task.Run(() => source.SetCanceled()); public static Task TrySetCanceledAsync(this TaskCompletionSource source) => Task.Run(() => source.TrySetCanceled()); } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 452b75444..9c9986b29 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -11,7 +11,7 @@ internal bool IgnoreState { get; set; } - public static RequestOptions CreateOrClone(RequestOptions options) + internal static RequestOptions CreateOrClone(RequestOptions options) { if (options == null) return new RequestOptions(); diff --git a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs index 1805649a9..846ad6348 100644 --- a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs +++ b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -9,6 +9,11 @@ namespace Discord { //Based on https://github.com/dotnet/corefx/blob/master/src/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs //Copyright (c) .NET Foundation and Contributors + public static class ConcurrentHashSet + { + public static int DefaultConcurrencyLevel => PlatformHelper.ProcessorCount; + } + [DebuggerDisplay("Count = {Count}")] internal class ConcurrentHashSet : IReadOnlyCollection { diff --git a/src/Discord.Net.Core/Utils/Extensions/Permissions.cs b/src/Discord.Net.Core/Utils/Extensions/Permissions.cs deleted file mode 100644 index b8593a31d..000000000 --- a/src/Discord.Net.Core/Utils/Extensions/Permissions.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Discord.Extensions -{ - internal static class Permissions - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(ulong allow, ulong deny, ChannelPermission bit) - => GetValue(allow, deny, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(ulong allow, ulong deny, GuildPermission bit) - => GetValue(allow, deny, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(ulong allow, ulong deny, byte bit) - { - if (HasBit(allow, bit)) - return PermValue.Allow; - else if (HasBit(deny, bit)) - return PermValue.Deny; - else - return PermValue.Inherit; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(ulong value, ChannelPermission bit) - => GetValue(value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(ulong value, GuildPermission bit) - => GetValue(value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(ulong value, byte bit) => HasBit(value, bit); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong rawValue, bool? value, ChannelPermission bit) - => SetValue(ref rawValue, value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong rawValue, bool? value, GuildPermission bit) - => SetValue(ref rawValue, value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong rawValue, bool? value, byte bit) - { - if (value.HasValue) - { - if (value == true) - SetBit(ref rawValue, bit); - else - UnsetBit(ref rawValue, bit); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ChannelPermission bit) - => SetValue(ref allow, ref deny, value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, GuildPermission bit) - => SetValue(ref allow, ref deny, value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, byte bit) - { - if (value.HasValue) - { - switch (value) - { - case PermValue.Allow: - SetBit(ref allow, bit); - UnsetBit(ref deny, bit); - break; - case PermValue.Deny: - UnsetBit(ref allow, bit); - SetBit(ref deny, bit); - break; - default: - UnsetBit(ref allow, bit); - UnsetBit(ref deny, bit); - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasBit(ulong value, byte bit) => (value & (1U << bit)) != 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetBit(ref ulong value, byte bit) => value |= (1U << bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnsetBit(ref ulong value, byte bit) => value &= ~(1U << bit); - - public static ulong ResolveGuild(IGuild guild, IGuildUser user) - { - ulong resolvedPermissions = 0; - - if (user.Id == guild.OwnerId) - resolvedPermissions = GuildPermissions.All.RawValue; //Owners always have all permissions - else - { - foreach (var role in user.RoleIds) - resolvedPermissions |= guild.GetRole(role).Permissions.RawValue; - if (GetValue(resolvedPermissions, GuildPermission.Administrator)) - resolvedPermissions = GuildPermissions.All.RawValue; //Administrators always have all permissions - } - return resolvedPermissions; - } - - /*public static ulong ResolveChannel(IGuildUser user, IGuildChannel channel) - { - return ResolveChannel(user, channel, ResolveGuild(user)); - }*/ - public static ulong ResolveChannel(IGuild guild, IGuildChannel channel, IGuildUser user, ulong guildPermissions) - { - ulong resolvedPermissions = 0; - - ulong mask = ChannelPermissions.All(channel).RawValue; - if (/*user.Id == user.Guild.OwnerId || */GetValue(guildPermissions, GuildPermission.Administrator)) - resolvedPermissions = mask; //Owners and administrators always have all permissions - else - { - //Start with this user's guild permissions - resolvedPermissions = guildPermissions; - - OverwritePermissions? perms; - var roleIds = user.RoleIds; - if (roleIds.Count > 0) - { - ulong deniedPermissions = 0UL, allowedPermissions = 0UL; - foreach (var roleId in roleIds) - { - perms = channel.GetPermissionOverwrite(guild.GetRole(roleId)); - if (perms != null) - { - deniedPermissions |= perms.Value.DenyValue; - allowedPermissions |= perms.Value.AllowValue; - } - } - resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; - } - perms = channel.GetPermissionOverwrite(user); - if (perms != null) - resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; - - //TODO: C#7 Typeswitch candidate - 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 - resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) - } - - return resolvedPermissions; - } - } -} diff --git a/src/Discord.Net.Core/Utils/MentionsHelper.cs b/src/Discord.Net.Core/Utils/MentionsHelper.cs index 58c0fe16d..19c0fd511 100644 --- a/src/Discord.Net.Core/Utils/MentionsHelper.cs +++ b/src/Discord.Net.Core/Utils/MentionsHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace Discord { @@ -34,7 +35,7 @@ namespace Discord { if (userMention.Id == id) { - user = channel?.GetCachedUser(id) as TUser; + user = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult() as TUser; if (user == null) //User not found, fallback to basic mention info user = userMention; break; @@ -136,7 +137,7 @@ namespace Discord return ""; case ChannelMentionHandling.Name: IGuildChannel channel = null; - channel = guild.GetCachedChannel(id); + channel = guild.GetChannelAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); if (channel != null) return $"#{channel.Name}"; else diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs new file mode 100644 index 000000000..2c3341537 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs @@ -0,0 +1,6 @@ +namespace Discord.Rest +{ + public interface IRestAudioChannel : IAudioChannel + { + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs new file mode 100644 index 000000000..e15311f02 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + public interface IRestMessageChannel : IMessageChannel + { + /// Sends a message to this message channel. + new Task SendMessageAsync(string text, bool isTTS = false); + /// Sends a file to this text channel, with an optional caption. + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false); + /// Sends a file to this text channel, with an optional caption. + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false); + + /// Gets a message from this message channel with the given id, or null if not found. + Task GetMessageAsync(ulong id); + /// Gets the last N messages from this message channel. + IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of messages in this channel. + IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of pinned messages in this channel. + new Task> GetPinnedMessagesAsync(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs new file mode 100644 index 000000000..a6939f81e --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord.Rest +{ + public interface IRestPrivateChannel : IPrivateChannel + { + new IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 868bc7e37..bd2584dce 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -23,6 +23,17 @@ namespace Discord.Rest return RestTextChannel.Create(discord, model); case ChannelType.Voice: return RestVoiceChannel.Create(discord, model); + case ChannelType.DM: + case ChannelType.Group: + return CreatePrivate(discord, model) as RestChannel; + default: + throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); + } + } + internal static IRestPrivateChannel CreatePrivate(BaseDiscordClient discord, Model model) + { + switch (model.Type) + { case ChannelType.DM: return RestDMChannel.Create(discord, model); case ChannelType.Group: @@ -35,14 +46,10 @@ namespace Discord.Rest public abstract Task UpdateAsync(); - //IChannel - IReadOnlyCollection IChannel.CachedUsers => ImmutableArray.Create(); - - IUser IChannel.GetCachedUser(ulong id) - => null; - Task IChannel.GetUserAsync(ulong id) - => Task.FromResult(null); - IAsyncEnumerable> IChannel.GetUsersAsync() - => ImmutableArray.Create>().ToAsyncEnumerable(); + //IChannel + Task IChannel.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(null); //Overriden + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode) + => ImmutableArray.Create>().ToAsyncEnumerable(); //Overriden } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 6e7451f22..3f399da57 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -10,7 +10,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestDMChannel : RestChannel, IDMChannel, IUpdateable + public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel, IUpdateable { public RestUser CurrentUser { get; private set; } public RestUser Recipient { get; private set; } @@ -75,23 +75,39 @@ namespace Discord.Rest public override string ToString() => $"@{Recipient}"; private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - + + //IDMChannel IUser IDMChannel.Recipient => Recipient; + //IRestPrivateChannel + IReadOnlyCollection IRestPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + //IPrivateChannel IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); //IMessageChannel - IReadOnlyCollection IMessageChannel.CachedMessages => ImmutableArray.Create(); - IMessage IMessageChannel.GetCachedMessage(ulong id) => null; - - async Task IMessageChannel.GetMessageAsync(ulong id) - => await GetMessageAsync(id); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit) - => GetMessagesAsync(limit); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - => GetMessagesAsync(fromMessageId, dir, limit); + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit); + else + return ImmutableArray.Create>().ToAsyncEnumerable(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit); + else + return ImmutableArray.Create>().ToAsyncEnumerable(); + } async Task> IMessageChannel.GetPinnedMessagesAsync() => await GetPinnedMessagesAsync().ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) @@ -103,14 +119,10 @@ namespace Discord.Rest IDisposable IMessageChannel.EnterTypingState() => EnterTypingState(); - //IChannel - IReadOnlyCollection IChannel.CachedUsers => Users; - - IUser IChannel.GetCachedUser(ulong id) - => GetUser(id); - Task IChannel.GetUserAsync(ulong id) + //IChannel + Task IChannel.GetUserAsync(ulong id, CacheMode mode) => Task.FromResult(GetUser(id)); - IAsyncEnumerable> IChannel.GetUsersAsync() - => ImmutableArray.Create>().ToAsyncEnumerable(); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 0324ba1fe..7e0806270 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -10,11 +10,11 @@ using Model = Discord.API.Channel; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestGroupChannel : RestChannel, IGroupChannel, IUpdateable + public class RestGroupChannel : RestChannel, IGroupChannel, IRestPrivateChannel, IRestMessageChannel, IRestAudioChannel, IUpdateable { private string _iconId; private ImmutableDictionary _users; - + public string Name { get; private set; } public IReadOnlyCollection Users => _users.ToReadOnlyCollection(); @@ -86,20 +86,34 @@ namespace Discord.Rest public IDisposable EnterTypingState() => ChannelHelper.EnterTypingState(this, Discord); + //ISocketPrivateChannel + IReadOnlyCollection IRestPrivateChannel.Recipients => Recipients; + //IPrivateChannel IReadOnlyCollection IPrivateChannel.Recipients => Recipients; //IMessageChannel - IReadOnlyCollection IMessageChannel.CachedMessages => ImmutableArray.Create(); - - IMessage IMessageChannel.GetCachedMessage(ulong id) - => null; - async Task IMessageChannel.GetMessageAsync(ulong id) - => await GetMessageAsync(id); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit) - => GetMessagesAsync(limit); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - => GetMessagesAsync(fromMessageId, dir, limit); + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit); + else + return ImmutableArray.Create>().ToAsyncEnumerable(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit); + else + return ImmutableArray.Create>().ToAsyncEnumerable(); + } async Task> IMessageChannel.GetPinnedMessagesAsync() => await GetPinnedMessagesAsync(); @@ -112,14 +126,10 @@ namespace Discord.Rest IDisposable IMessageChannel.EnterTypingState() => EnterTypingState(); - //IChannel - IReadOnlyCollection IChannel.CachedUsers => Users; - - IUser IChannel.GetCachedUser(ulong id) - => GetUser(id); - Task IChannel.GetUserAsync(ulong id) + //IChannel + Task IChannel.GetUserAsync(ulong id, CacheMode mode) => Task.FromResult(GetUser(id)); - IAsyncEnumerable> IChannel.GetUsersAsync() + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index d6a4143d3..7d7547713 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -135,24 +135,16 @@ namespace Discord.Rest => await RemovePermissionOverwriteAsync(role); async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user) => await RemovePermissionOverwriteAsync(user); - - IReadOnlyCollection IGuildChannel.CachedUsers - => ImmutableArray.Create(); - IAsyncEnumerable> IGuildChannel.GetUsersAsync() + + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode) => ImmutableArray.Create>().ToAsyncEnumerable(); //Overriden in Text/Voice //TODO: Does this actually override? - Task IGuildChannel.GetUserAsync(ulong id) + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode) => Task.FromResult(null); //Overriden in Text/Voice //TODO: Does this actually override? - IGuildUser IGuildChannel.GetCachedUser(ulong id) - => null; //IChannel - IReadOnlyCollection IChannel.CachedUsers - => ImmutableArray.Create(); - IUser IChannel.GetCachedUser(ulong id) - => null; - IAsyncEnumerable> IChannel.GetUsersAsync() + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode) => ImmutableArray.Create>().ToAsyncEnumerable(); //Overriden in Text/Voice //TODO: Does this actually override? - Task IChannel.GetUserAsync(ulong id) + Task IChannel.GetUserAsync(ulong id, CacheMode mode) => Task.FromResult(null); //Overriden in Text/Voice //TODO: Does this actually override? } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 6e15825e0..3e660b53b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -4,13 +4,14 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestTextChannel : RestGuildChannel, ITextChannel + public class RestTextChannel : RestGuildChannel, IRestMessageChannel, ITextChannel { public string Topic { get; private set; } @@ -67,22 +68,43 @@ namespace Discord.Rest => ChannelHelper.EnterTypingState(this, Discord); //IGuildChannel - async Task IGuildChannel.GetUserAsync(ulong id) - => await GetUserAsync(id); - IAsyncEnumerable> IGuildChannel.GetUsersAsync() - => GetUsersAsync(); + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetUserAsync(id); + else + return null; + } + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return GetUsersAsync(); + else + return ImmutableArray.Create>().ToAsyncEnumerable(); + } //IMessageChannel - IReadOnlyCollection IMessageChannel.CachedMessages - => ImmutableArray.Create(); - IMessage IMessageChannel.GetCachedMessage(ulong id) - => null; - async Task IMessageChannel.GetMessageAsync(ulong id) - => await GetMessageAsync(id); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit) - => GetMessagesAsync(limit); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - => GetMessagesAsync(fromMessageId, dir, limit); + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit); + else + return ImmutableArray.Create>().ToAsyncEnumerable(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit); + else + return ImmutableArray.Create>().ToAsyncEnumerable(); + } async Task> IMessageChannel.GetPinnedMessagesAsync() => await GetPinnedMessagesAsync(); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 4653ee381..1ff7c3075 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -11,7 +11,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestVoiceChannel : RestGuildChannel, IVoiceChannel + public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel { public int Bitrate { get; private set; } public int UserLimit { get; private set; } @@ -41,9 +41,9 @@ namespace Discord.Rest Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } //IGuildChannel - Task IGuildChannel.GetUserAsync(ulong id) + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode) => Task.FromResult(null); - IAsyncEnumerable> IGuildChannel.GetUsersAsync() + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode) => ImmutableArray.Create>().ToAsyncEnumerable(); } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 601329b5c..ae61187ab 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Discord.API.Rest; using Model = Discord.API.Guild; +using System.Linq; namespace Discord.Rest { @@ -149,7 +150,7 @@ namespace Discord.Rest return null; } - public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false) + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false) { var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted); _roles = _roles.Add(role.Id, role); @@ -157,8 +158,8 @@ namespace Discord.Rest } //Users - public Task> GetUsersAsync() - => GuildHelper.GetUsersAsync(this, Discord); + public IAsyncEnumerable> GetUsersAsync() + => GuildHelper.GetUsersAsync(this, Discord).ToAsyncEnumerable(); public Task GetUserAsync(ulong id) => GuildHelper.GetUserAsync(this, Discord, id); public Task GetCurrentUserAsync() @@ -170,19 +171,26 @@ namespace Discord.Rest //IGuild bool IGuild.Available => true; IAudioClient IGuild.AudioClient => null; - IReadOnlyCollection IGuild.CachedUsers => ImmutableArray.Create(); IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Roles => Roles; async Task> IGuild.GetBansAsync() => await GetBansAsync(); - async Task> IGuild.GetChannelsAsync() - => await GetChannelsAsync(); - async Task IGuild.GetChannelAsync(ulong id) - => await GetChannelAsync(id); - IGuildChannel IGuild.GetCachedChannel(ulong id) - => null; + async Task> IGuild.GetChannelsAsync(CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetChannelsAsync(); + else + return ImmutableArray.Create(); + } + async Task IGuild.GetChannelAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetChannelAsync(id); + else + return null; + } async Task IGuild.CreateTextChannelAsync(string name) => await CreateTextChannelAsync(name); async Task IGuild.CreateVoiceChannelAsync(string name) @@ -198,15 +206,30 @@ namespace Discord.Rest IRole IGuild.GetRole(ulong id) => GetRole(id); + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted) + => await CreateRoleAsync(name, permissions, color, isHoisted); - async Task> IGuild.GetUsersAsync() - => await GetUsersAsync(); - async Task IGuild.GetUserAsync(ulong id) - => await GetUserAsync(id); - IGuildUser IGuild.GetCachedUser(ulong id) - => null; - async Task IGuild.GetCurrentUserAsync() - => await GetCurrentUserAsync(); + async Task IGuild.GetUserAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetUserAsync(id); + else + return null; + } + async Task IGuild.GetCurrentUserAsync(CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetCurrentUserAsync(); + else + return null; + } + async Task> IGuild.GetUsersAsync(CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return (await GetUsersAsync().Flatten()).ToImmutableArray(); + else + return ImmutableArray.Create(); + } Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 6a42de397..5d704e590 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; @@ -42,10 +43,13 @@ namespace Discord.Rest public virtual async Task UpdateAsync() => Update(await UserHelper.GetAsync(this, Discord)); - - public Task CreateDMChannelAsync() + + public Task CreateDMChannelAsync() => UserHelper.CreateDMChannelAsync(this, Discord); - IDMChannel IUser.GetCachedDMChannel() => null; + Task IUser.GetDMChannelAsync(CacheMode mode) + => Task.FromResult(null); + async Task IUser.CreateDMChannelAsync() + => await CreateDMChannelAsync(); } } diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index ddf8055d9..012627283 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -44,7 +44,7 @@ namespace Discord.Rest await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id); } - public static async Task CreateDMChannelAsync(IUser user, BaseDiscordClient client) + public static async Task CreateDMChannelAsync(IUser user, BaseDiscordClient client) { var args = new CreateDMChannelParams(user.Id); return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args)); diff --git a/src/Discord.Net.Rpc/Entities/RpcGuild.cs b/src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/RpcGuild.cs rename to src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 202c65da9..5bf686dc4 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; namespace Discord.Audio { - internal class AudioClient : IAudioClient, IDisposable + public class AudioClient : IAudioClient, IDisposable { public event Func Connected { @@ -56,7 +56,7 @@ namespace Discord.Audio private DiscordSocketClient Discord => Guild.Discord; /// Creates a new REST/WebSocket discord client. - public AudioClient(SocketGuild guild, int id) + internal AudioClient(SocketGuild guild, int id) { Guild = guild; @@ -90,7 +90,7 @@ namespace Discord.Audio } /// - public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) + internal async Task ConnectAsync(string url, ulong userId, string sessionId, string token) { await _connectionLock.WaitAsync().ConfigureAwait(false); try diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index 11d952842..13beb0ffb 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -24,9 +24,9 @@ namespace Discord.WebSocket internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); - internal IReadOnlyCollection PrivateChannels => - _dmChannels.Select(x => x.Value as IPrivateChannel).Concat( - _groupChannels.Select(x => GetChannel(x) as IPrivateChannel)) + internal IReadOnlyCollection PrivateChannels => + _dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat( + _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); public ClientState(int guildCount, int dmChannelCount) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index 04aadaa3f..3c9bf4fba 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -33,170 +33,170 @@ namespace Discord.WebSocket private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); //Channels - public event Func ChannelCreated + public event Func ChannelCreated { add { _channelCreatedEvent.Add(value); } remove { _channelCreatedEvent.Remove(value); } } - private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); - public event Func ChannelDestroyed + private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + public event Func ChannelDestroyed { add { _channelDestroyedEvent.Add(value); } remove { _channelDestroyedEvent.Remove(value); } } - private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); - public event Func ChannelUpdated + private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); + public event Func ChannelUpdated { add { _channelUpdatedEvent.Add(value); } remove { _channelUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); //Messages - public event Func MessageReceived + public event Func MessageReceived { add { _messageReceivedEvent.Add(value); } remove { _messageReceivedEvent.Remove(value); } } - private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); - public event Func, Task> MessageDeleted + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func, Task> MessageDeleted { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } } - private readonly AsyncEvent, Task>> _messageDeletedEvent = new AsyncEvent, Task>>(); - public event Func, IMessage, Task> MessageUpdated + private readonly AsyncEvent, Task>> _messageDeletedEvent = new AsyncEvent, Task>>(); + public event Func, SocketMessage, Task> MessageUpdated { add { _messageUpdatedEvent.Add(value); } remove { _messageUpdatedEvent.Remove(value); } } - private readonly AsyncEvent, IMessage, Task>> _messageUpdatedEvent = new AsyncEvent, IMessage, Task>>(); + private readonly AsyncEvent, SocketMessage, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, Task>>(); //Roles - public event Func RoleCreated + public event Func RoleCreated { add { _roleCreatedEvent.Add(value); } remove { _roleCreatedEvent.Remove(value); } } - private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); - public event Func RoleDeleted + private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); + public event Func RoleDeleted { add { _roleDeletedEvent.Add(value); } remove { _roleDeletedEvent.Remove(value); } } - private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); - public event Func RoleUpdated + private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); + public event Func RoleUpdated { add { _roleUpdatedEvent.Add(value); } remove { _roleUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); //Guilds - public event Func JoinedGuild + public event Func JoinedGuild { add { _joinedGuildEvent.Add(value); } remove { _joinedGuildEvent.Remove(value); } } - private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); - public event Func LeftGuild + private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); + public event Func LeftGuild { add { _leftGuildEvent.Add(value); } remove { _leftGuildEvent.Remove(value); } } - private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); - public event Func GuildAvailable + private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); + public event Func GuildAvailable { add { _guildAvailableEvent.Add(value); } remove { _guildAvailableEvent.Remove(value); } } - private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); - public event Func GuildUnavailable + private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); + public event Func GuildUnavailable { add { _guildUnavailableEvent.Add(value); } remove { _guildUnavailableEvent.Remove(value); } } - private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); - public event Func GuildMembersDownloaded + private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); + public event Func GuildMembersDownloaded { add { _guildMembersDownloadedEvent.Add(value); } remove { _guildMembersDownloadedEvent.Remove(value); } } - private AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); - public event Func GuildUpdated + private AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); + public event Func GuildUpdated { add { _guildUpdatedEvent.Add(value); } remove { _guildUpdatedEvent.Remove(value); } } - private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); //Users - public event Func UserJoined + public event Func UserJoined { add { _userJoinedEvent.Add(value); } remove { _userJoinedEvent.Remove(value); } } - private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); - public event Func UserLeft + private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); + public event Func UserLeft { add { _userLeftEvent.Add(value); } remove { _userLeftEvent.Remove(value); } } - private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); - public event Func UserBanned + private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); + public event Func UserBanned { add { _userBannedEvent.Add(value); } remove { _userBannedEvent.Remove(value); } } - private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); - public event Func UserUnbanned + private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); + public event Func UserUnbanned { add { _userUnbannedEvent.Add(value); } remove { _userUnbannedEvent.Remove(value); } } - private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); - public event Func UserUpdated + private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); + public event Func UserUpdated { add { _userUpdatedEvent.Add(value); } remove { _userUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); - public event Func UserPresenceUpdated + private readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); + public event Func UserPresenceUpdated { add { _userPresenceUpdatedEvent.Add(value); } remove { _userPresenceUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _userPresenceUpdatedEvent = new AsyncEvent>(); - public event Func UserVoiceStateUpdated + private readonly AsyncEvent> _userPresenceUpdatedEvent = new AsyncEvent>(); + public event Func UserVoiceStateUpdated { add { _userVoiceStateUpdatedEvent.Add(value); } remove { _userVoiceStateUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); - public event Func CurrentUserUpdated + private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); + public event Func CurrentUserUpdated { add { _selfUpdatedEvent.Add(value); } remove { _selfUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); - public event Func UserIsTyping + private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); + public event Func UserIsTyping { add { _userIsTypingEvent.Add(value); } remove { _userIsTypingEvent.Remove(value); } } - private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); - public event Func RecipientAdded + private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + public event Func RecipientAdded { add { _recipientAddedEvent.Add(value); } remove { _recipientAddedEvent.Remove(value); } } - private readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); - public event Func RecipientRemoved + private readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); + public event Func RecipientRemoved { add { _recipientRemovedEvent.Add(value); } remove { _recipientRemovedEvent.Remove(value); } } - private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index c8f1a4535..03831818a 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -54,8 +54,8 @@ namespace Discord.WebSocket internal WebSocketProvider WebSocketProvider { get; private set; } public new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; - public new SocketSelfUser CurrentUser => base.CurrentUser as SocketSelfUser; - public IReadOnlyCollection PrivateChannels => State.PrivateChannels; + public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } + public IReadOnlyCollection PrivateChannels => State.PrivateChannels; internal IReadOnlyCollection Guilds => State.Guilds; /// Creates a new REST/WebSocket discord client. @@ -167,14 +167,23 @@ namespace Discord.WebSocket connectTask.TrySetException(new TimeoutException()); }); + await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); await ApiClient.ConnectAsync().ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false); await _connectedEvent.InvokeAsync().ConfigureAwait(false); if (_sessionId != null) + { + await _gatewayLogger.DebugAsync("Resuming").ConfigureAwait(false); await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); + } else - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards:TotalShards).ConfigureAwait(false); + { + await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + } + await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); if (!isReconnecting) _canReconnect = true; @@ -216,32 +225,33 @@ namespace Discord.WebSocket ConnectionState = ConnectionState.Disconnecting; await _gatewayLogger.InfoAsync("Disconnecting").ConfigureAwait(false); - await _gatewayLogger.DebugAsync("Disconnecting - CancelToken").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Cancelling current tasks").ConfigureAwait(false); //Signal tasks to complete try { _cancelToken.Cancel(); } catch { } - await _gatewayLogger.DebugAsync("Disconnecting - ApiClient").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); //Disconnect from server await ApiClient.DisconnectAsync().ConfigureAwait(false); //Wait for tasks to complete - await _gatewayLogger.DebugAsync("Disconnecting - Heartbeat").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); var heartbeatTask = _heartbeatTask; if (heartbeatTask != null) await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; - await _gatewayLogger.DebugAsync("Disconnecting - Guild Downloader").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false); var guildDownloadTask = _guildDownloadTask; if (guildDownloadTask != null) await guildDownloadTask.ConfigureAwait(false); _guildDownloadTask = null; //Clear large guild queue - await _gatewayLogger.DebugAsync("Disconnecting - Clean Large Guilds").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Clearing large guild queue").ConfigureAwait(false); while (_largeGuilds.TryDequeue(out guildId)) { } //Raise virtual GUILD_UNAVAILABLEs + await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); foreach (var guild in State.Guilds) { if (guild._available) @@ -318,7 +328,6 @@ namespace Discord.WebSocket public Task GetApplicationInfoAsync() => ClientHelper.GetApplicationInfoAsync(this); - /// public SocketGuild GetGuild(ulong id) { @@ -329,7 +338,7 @@ namespace Discord.WebSocket => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); /// - public IChannel GetChannel(ulong id) + public SocketChannel GetChannel(ulong id) { return State.GetChannel(id); } @@ -343,15 +352,38 @@ namespace Discord.WebSocket => ClientHelper.GetInviteAsync(this, inviteId); /// - public IUser GetUser(ulong id) + public SocketUser GetUser(ulong id) { return State.GetUser(id); } /// - public IUser GetUser(string username, string discriminator) + public SocketUser GetUser(string username, string discriminator) { return State.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); } + internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) + { + return state.GetOrAddUser(model.Id, x => + { + var user = SocketGlobalUser.Create(this, state, model); + user.GlobalUser.AddRef(); + return user; + }); + } + internal SocketGlobalUser GetOrCreateSelfUser(ClientState state, Discord.API.User model) + { + return state.GetOrAddUser(model.Id, x => + { + var user = SocketGlobalUser.Create(this, state, model); + user.GlobalUser.AddRef(); + user.Presence = new SocketPresence(null, UserStatus.Online); + return user; + }); + } + internal void RemoveUser(ulong id) + { + State.RemoveUser(id); + } /// public RestVoiceRegion GetVoiceRegion(string id) @@ -363,8 +395,8 @@ namespace Discord.WebSocket } /// Downloads the users list for all large guilds. - /*public Task DownloadAllUsersAsync() - => DownloadUsersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); + public Task DownloadAllUsersAsync() + => DownloadUsersAsync(State.Guilds.Where(x => !x.HasAllMembers)); /// Downloads the users list for the provided guilds, if they don't have a complete list. public Task DownloadUsersAsync(IEnumerable guilds) => DownloadUsersAsync(guilds.Select(x => x as SocketGuild).Where(x => x != null)); @@ -414,7 +446,7 @@ namespace Discord.WebSocket else await Task.WhenAll(batchTasks).ConfigureAwait(false); } - }*/ + } private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) { @@ -486,26 +518,26 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var dataStore = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); + var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); - var currentUser = SocketSelfUser.Create(this, data.User); + var currentUser = SocketSelfUser.Create(this, state, data.User); int unavailableGuilds = 0; - /*for (int i = 0; i < data.Guilds.Length; i++) + for (int i = 0; i < data.Guilds.Length; i++) { var model = data.Guilds[i]; - var guild = AddGuild(model, dataStore); + var guild = AddGuild(model, state); if (!guild._available || ApiClient.AuthTokenType == TokenType.User) unavailableGuilds++; else await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); } for (int i = 0; i < data.PrivateChannels.Length; i++) - AddPrivateChannel(data.PrivateChannels[i], dataStore);*/ + AddPrivateChannel(data.PrivateChannels[i], state); _sessionId = data.SessionId; - base.CurrentUser = currentUser; _unavailableGuilds = unavailableGuilds; - State = dataStore; + CurrentUser = currentUser; + State = state; } catch (Exception ex) { @@ -525,14 +557,14 @@ namespace Discord.WebSocket await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); } break; - /*case "RESUMED": + case "RESUMED": { await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete //Notify the client that these guilds are available again - foreach (var guild in DataStore.Guilds) + foreach (var guild in State.Guilds) { if (guild._available) await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); @@ -553,10 +585,10 @@ namespace Discord.WebSocket _lastGuildAvailableTime = Environment.TickCount; await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); - var guild = DataStore.GetGuild(data.Id); + var guild = State.GetGuild(data.Id); if (guild != null) { - guild.Update(data, UpdateSource.WebSocket, DataStore); + guild.Update(State, data); var unavailableGuilds = _unavailableGuilds; if (unavailableGuilds != 0) @@ -573,7 +605,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); - var guild = AddGuild(data, DataStore); + var guild = AddGuild(data, State); if (guild != null) { if (ApiClient.AuthTokenType == TokenType.User) @@ -593,11 +625,11 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.Id); + var guild = State.GetGuild(data.Id); if (guild != null) { var before = guild.Clone(); - guild.Update(data, UpdateSource.WebSocket); + guild.Update(State, data); await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); } else @@ -612,11 +644,11 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var before = guild.Clone(); - guild.Update(data, UpdateSource.WebSocket); + guild.Update(State, data); await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); } else @@ -626,20 +658,15 @@ namespace Discord.WebSocket } } return; - case "GUILD_INTEGRATIONS_UPDATE": - { - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); - } - return; case "GUILD_SYNC": { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.Id); + var guild = State.GetGuild(data.Id); if (guild != null) { var before = guild.Clone(); - guild.Update(data, UpdateSource.WebSocket, DataStore); + guild.Update(State, data); //This is treated as an extension of GUILD_AVAILABLE _unavailableGuilds--; _lastGuildAvailableTime = Environment.TickCount; @@ -661,11 +688,9 @@ namespace Discord.WebSocket type = "GUILD_UNAVAILABLE"; await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); - var guild = DataStore.GetGuild(data.Id); + var guild = State.GetGuild(data.Id); if (guild != null) { - foreach (var member in guild.Members) - member.User.RemoveRef(this); await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); _unavailableGuilds++; } @@ -700,14 +725,13 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - ISocketChannel channel = null; + SocketChannel channel = null; if (data.GuildId.IsSpecified) { - var guild = DataStore.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); if (guild != null) { - guild.AddChannel(data, DataStore); - channel = guild.AddChannel(data, DataStore); + channel = guild.AddChannel(State, data); if (!guild.IsSynced) { @@ -722,7 +746,7 @@ namespace Discord.WebSocket } } else - channel = AddPrivateChannel(data, DataStore); + channel = AddPrivateChannel(data, State) as SocketChannel; if (channel != null) await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); @@ -733,13 +757,13 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.Id); + var channel = State.GetChannel(data.Id); if (channel != null) { var before = channel.Clone(); - channel.Update(data, UpdateSource.WebSocket); + channel.Update(State, data); - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) { await _gatewayLogger.DebugAsync("Ignored CHANNEL_UPDATE, guild is not synced yet.").ConfigureAwait(false); return; @@ -758,14 +782,14 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); - ISocketChannel channel = null; + SocketChannel channel = null; var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.IsSpecified) { - var guild = DataStore.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); if (guild != null) { - channel = guild.RemoveChannel(data.Id); + channel = guild.RemoveChannel(State, data.Id); if (!guild.IsSynced) { @@ -780,7 +804,7 @@ namespace Discord.WebSocket } } else - channel = RemovePrivateChannel(data.Id); + channel = RemovePrivateChannel(data.Id) as SocketChannel; if (channel != null) await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); @@ -798,10 +822,10 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { - var user = guild.AddOrUpdateUser(data, DataStore); + var user = guild.AddOrUpdateUser(data); guild.MemberCount++; if (!guild.IsSynced) @@ -824,7 +848,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var user = guild.GetUser(data.User.Id); @@ -838,7 +862,7 @@ namespace Discord.WebSocket if (user != null) { var before = user.Clone(); - user.Update(data, UpdateSource.WebSocket); + user.Update(State, data); await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); } else @@ -865,7 +889,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var user = guild.RemoveUser(data.User.Id); @@ -878,10 +902,7 @@ namespace Discord.WebSocket } if (user != null) - { - user.User.RemoveRef(this); await _userLeftEvent.InvokeAsync(user).ConfigureAwait(false); - } else { if (!guild.HasAllMembers) @@ -906,15 +927,15 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { foreach (var memberModel in data.Members) - guild.AddOrUpdateUser(memberModel, DataStore); + guild.AddOrUpdateUser(memberModel); if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there { - guild.CompleteDownloadMembers(); + guild.CompleteDownloadUsers(); await _guildMembersDownloadedEvent.InvokeAsync(guild).ConfigureAwait(false); } } @@ -930,10 +951,10 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as SocketGroupChannel; + var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; if (channel != null) { - var user = channel.AddUser(data.User, DataStore); + var user = channel.AddUser(data.User); await _recipientAddedEvent.InvokeAsync(user).ConfigureAwait(false); } else @@ -948,15 +969,12 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as SocketGroupChannel; + var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; if (channel != null) { var user = channel.RemoveUser(data.User.Id); if (user != null) - { - user.User.RemoveRef(this); await _recipientRemovedEvent.InvokeAsync(user).ConfigureAwait(false); - } else { await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_REMOVE referenced an unknown user.").ConfigureAwait(false); @@ -977,7 +995,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var role = guild.AddRole(data.Role); @@ -1001,14 +1019,14 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var role = guild.GetRole(data.Role.Id); if (role != null) { var before = role.Clone(); - role.Update(data.Role, UpdateSource.WebSocket); + role.Update(State, data.Role); if (!guild.IsSynced) { @@ -1036,7 +1054,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var role = guild.RemoveRole(data.RoleId); @@ -1070,7 +1088,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { if (!guild.IsSynced) @@ -1078,8 +1096,8 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_ADD, guild is not synced yet.").ConfigureAwait(false); return; } - - await _userBannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false); + + await _userBannedEvent.InvokeAsync(SocketSimpleUser.Create(this, State, data.User), guild).ConfigureAwait(false); } else { @@ -1093,7 +1111,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { if (!guild.IsSynced) @@ -1102,7 +1120,10 @@ namespace Discord.WebSocket return; } - await _userUnbannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false); + SocketUser user = State.GetUser(data.User.Id); + if (user == null) + user = SocketSimpleUser.Create(this, State, data.User); + await _userUnbannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); } else { @@ -1118,20 +1139,28 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) { await _gatewayLogger.DebugAsync("Ignored MESSAGE_CREATE, guild is not synced yet.").ConfigureAwait(false); return; } - var author = channel.GetUser(data.Author.Value.Id, true); + SocketUser author; + if (guild != null) + author = guild.GetUser(data.Author.Value.Id); + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + if (author == null) + author = SocketSimpleUser.Create(this, State, data.Author.Value); if (author != null) { - var msg = channel.AddMessage(author, data); + var msg = SocketMessage.Create(this, State, author, data); + SocketChannelHelper.AddMessage(channel, this, msg); await _messageReceivedEvent.InvokeAsync(msg).ConfigureAwait(false); } else @@ -1152,38 +1181,41 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) { await _gatewayLogger.DebugAsync("Ignored MESSAGE_UPDATE, guild is not synced yet.").ConfigureAwait(false); return; } - IMessage before = null, after = null; - ISocketMessage cachedMsg = channel.GetMessage(data.Id); + SocketMessage before = null, after = null; + SocketMessage cachedMsg = channel.GetCachedMessage(data.Id); if (cachedMsg != null) { before = cachedMsg.Clone(); - cachedMsg.Update(data, UpdateSource.WebSocket); + cachedMsg.Update(State, data); after = cachedMsg; } else if (data.Author.IsSpecified) { //Edited message isnt in cache, create a detached one - var author = channel.GetUser(data.Author.Value.Id, true); - if (author != null) - after = channel.CreateMessage(author, data); - } - if (after != null) - await _messageUpdatedEvent.InvokeAsync(Optional.Create(before), after).ConfigureAwait(false); - { - if (before != null) - await _messageUpdatedEvent.InvokeAsync(Optional.Create(before), after).ConfigureAwait(false); + SocketUser author; + if (guild != null) + author = guild.GetUser(data.Author.Value.Id); else - await _messageUpdatedEvent.InvokeAsync(Optional.Create(), after).ConfigureAwait(false); + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + if (author == null) + author = SocketSimpleUser.Create(this, State, data.Author.Value); + + after = SocketMessage.Create(this, State, author, data); } + if (before != null) + await _messageUpdatedEvent.InvokeAsync(before, after).ConfigureAwait(false); + else + await _messageUpdatedEvent.InvokeAsync(Optional.Create(), after).ConfigureAwait(false); } else { @@ -1197,20 +1229,20 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) { await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE, guild is not synced yet.").ConfigureAwait(false); return; } - var msg = channel.RemoveMessage(data.Id); + var msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); if (msg != null) - await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create(msg)).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(data.Id, msg).ConfigureAwait(false); else - await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create()).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create()).ConfigureAwait(false); } else { @@ -1224,10 +1256,10 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) { await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE_BULK, guild is not synced yet.").ConfigureAwait(false); return; @@ -1235,11 +1267,11 @@ namespace Discord.WebSocket foreach (var id in data.Ids) { - var msg = channel.RemoveMessage(id); + var msg = SocketChannelHelper.RemoveMessage(channel, this, id); if (msg != null) - await _messageDeletedEvent.InvokeAsync(id, Optional.Create(msg)).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(id, msg).ConfigureAwait(false); else - await _messageDeletedEvent.InvokeAsync(id, Optional.Create()).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(id, Optional.Create()).ConfigureAwait(false); } } else @@ -1258,39 +1290,43 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.IsSpecified) { - var guild = DataStore.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); if (guild == null) { await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); break; } - if (!guild.IsSynced) { await _gatewayLogger.DebugAsync("Ignored PRESENCE_UPDATE, guild is not synced yet.").ConfigureAwait(false); return; } - IPresence before; - var user = guild.GetUser(data.User.Id); + SocketPresence before; + SocketUser user = guild.GetUser(data.User.Id); if (user != null) { before = user.Presence.Clone(); - user.Update(data, UpdateSource.WebSocket); + user.Update(State, data); } else { before = new SocketPresence(null, UserStatus.Offline); - user = guild.AddOrUpdateUser(data, DataStore); + user = guild.AddOrUpdateUser(data); } - await _userPresenceUpdatedEvent.InvokeAsync(user, before, user).ConfigureAwait(false); + await _userPresenceUpdatedEvent.InvokeAsync(user, before, user.Presence).ConfigureAwait(false); } else { - var channel = DataStore.GetDMChannel(data.User.Id); + var channel = State.GetChannel(data.User.Id); if (channel != null) - channel.Recipient.Update(data, UpdateSource.WebSocket); + { + var user = channel.GetUser(data.User.Id); + var before = user.Presence.Clone(); + user.Update(State, data); + await _userPresenceUpdatedEvent.InvokeAsync(user, before, user.Presence).ConfigureAwait(false); + } } } break; @@ -1299,16 +1335,16 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) { await _gatewayLogger.DebugAsync("Ignored TYPING_START, guild is not synced yet.").ConfigureAwait(false); return; } - var user = channel.GetUser(data.UserId, true); + var user = (channel as SocketChannel).GetUser(data.UserId); if (user != null) await _userIsTypingEvent.InvokeAsync(user, channel).ConfigureAwait(false); } @@ -1324,7 +1360,7 @@ namespace Discord.WebSocket if (data.Id == CurrentUser.Id) { var before = CurrentUser.Clone(); - CurrentUser.Update(data, UpdateSource.WebSocket); + CurrentUser.Update(State, data); await _selfUpdatedEvent.InvokeAsync(before, CurrentUser).ConfigureAwait(false); } else @@ -1343,56 +1379,53 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.HasValue) { - ISocketUser user; + SocketUser user; SocketVoiceState before, after; if (data.GuildId != null) { - var guild = DataStore.GetGuild(data.GuildId.Value); - if (guild != null) + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) { - if (!guild.IsSynced) - { - await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; + } + else if (!guild.IsSynced) + { + await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false); + return; + } - if (data.ChannelId != null) - { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); - after = guild.AddOrUpdateVoiceState(data, DataStore); - if (data.UserId == _currentUser.Id) - { - var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); - } - } - else + if (data.ChannelId != null) + { + before = guild.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); + after = guild.AddOrUpdateVoiceState(State, data); + if (data.UserId == CurrentUser.Id) { - before = guild.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); - after = new SocketVoiceState(null, data); + var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); } - - user = guild.GetUser(data.UserId); } else { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - return; + before = guild.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); + after = SocketVoiceState.Create(null, data); } + + user = guild.GetUser(data.UserId); } else { - var groupChannel = DataStore.GetChannel(data.ChannelId.Value) as SocketGroupChannel; + var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; if (groupChannel != null) { if (data.ChannelId != null) { before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); - after = groupChannel.AddOrUpdateVoiceState(data, DataStore); + after = groupChannel.AddOrUpdateVoiceState(State, data); } else { before = groupChannel.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); - after = new SocketVoiceState(null, data); + after = SocketVoiceState.Create(null, data); } user = groupChannel.GetUser(data.UserId); } @@ -1419,7 +1452,7 @@ namespace Discord.WebSocket if (AudioMode != AudioMode.Disabled) { var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); @@ -1431,8 +1464,7 @@ namespace Discord.WebSocket return; } } - - return;*/ + return; //Ignored (User only) case "CHANNEL_PINS_ACK": @@ -1441,12 +1473,15 @@ namespace Discord.WebSocket case "CHANNEL_PINS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_UPDATE)"); break; - case "USER_SETTINGS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); - return; case "MESSAGE_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); return; + case "GUILD_INTEGRATIONS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); + return; + case "USER_SETTINGS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); + return; //Others default: @@ -1532,6 +1567,42 @@ namespace Discord.WebSocket await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); } + internal SocketGuild AddGuild(ExtendedGuild model, ClientState state) + { + var guild = SocketGuild.Create(this, state, model); + state.AddGuild(guild); + if (model.Large) + _largeGuilds.Enqueue(model.Id); + return guild; + } + internal SocketGuild RemoveGuild(ulong id) + { + var guild = State.RemoveGuild(id); + if (guild != null) + { + foreach (var channel in guild.Channels) + State.RemoveChannel(id); + foreach (var user in guild.Users) + user.GlobalUser.RemoveRef(this); + } + return guild; + } + + internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) + { + return SocketChannel.CreatePrivate(this, state, model); + } + internal ISocketPrivateChannel RemovePrivateChannel(ulong id) + { + var channel = State.RemoveChannel(id) as ISocketPrivateChannel; + if (channel != null) + { + foreach (var recipient in channel.Recipients) + recipient.GlobalUser.RemoveRef(this); + } + return channel; + } + //IDiscordClient DiscordRestApiClient IDiscordClient.ApiClient => ApiClient; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs new file mode 100644 index 000000000..7056a4df5 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -0,0 +1,6 @@ +namespace Discord.WebSocket +{ + public interface ISocketAudioChannel : IAudioChannel + { + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs new file mode 100644 index 000000000..a555754d2 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -0,0 +1,32 @@ +using Discord.Rest; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public interface ISocketMessageChannel : IMessageChannel + { + /// Gets all messages in this channel's cache. + IReadOnlyCollection CachedMessages { get; } + + /// Sends a message to this message channel. + new Task SendMessageAsync(string text, bool isTTS = false); + /// Sends a file to this text channel, with an optional caption. + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false); + /// Sends a file to this text channel, with an optional caption. + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false); + + SocketMessage GetCachedMessage(ulong id); + /// Gets a message from this message channel with the given id, or null if not found. + Task GetMessageAsync(ulong id); + /// Gets the last N messages from this message channel. + Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of messages in this channel. + Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of messages in this channel. + Task> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of pinned messages in this channel. + new Task> GetPinnedMessagesAsync(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs new file mode 100644 index 000000000..4e91673dd --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord.WebSocket +{ + public interface ISocketPrivateChannel : IPrivateChannel + { + new IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index c9f38caec..07f9f5073 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -11,35 +11,37 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public abstract class SocketChannel : SocketEntity, IChannel { + public IReadOnlyCollection Users => GetUsersInternal(); + internal SocketChannel(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketChannel Create(DiscordSocketClient discord, Model model) + internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model) { switch (model.Type) { - case ChannelType.Text: - return SocketTextChannel.Create(discord, model); - case ChannelType.Voice: - return SocketVoiceChannel.Create(discord, model); case ChannelType.DM: - return SocketDMChannel.Create(discord, model); + return SocketDMChannel.Create(discord, state, model); case ChannelType.Group: - return SocketGroupChannel.Create(discord, model); + return SocketGroupChannel.Create(discord, state, model); default: throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); } } + internal abstract void Update(ClientState state, Model model); + + //User + public SocketUser GetUser(ulong id) => GetUserInternal(id); + internal abstract SocketUser GetUserInternal(ulong id); + internal abstract IReadOnlyCollection GetUsersInternal(); - //IChannel - IReadOnlyCollection IChannel.CachedUsers => ImmutableArray.Create(); + internal SocketChannel Clone() => MemberwiseClone() as SocketChannel; - IUser IChannel.GetCachedUser(ulong id) - => null; - Task IChannel.GetUserAsync(ulong id) - => Task.FromResult(null); - IAsyncEnumerable> IChannel.GetUsersAsync() - => ImmutableArray.Create>().ToAsyncEnumerable(); + //IChannel + Task IChannel.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(null); //Overridden + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode) + => ImmutableArray.Create>().ToAsyncEnumerable(); //Overridden } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs new file mode 100644 index 000000000..856acdc73 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -0,0 +1,81 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal static class SocketChannelHelper + { + public static async Task> GetMessagesAsync(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, + ulong? fromMessageId, Direction dir, int limit) + { + if (messages == null) //Cache disabled + { + var msgs = await ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit).Flatten(); + return msgs.ToImmutableArray(); + } + + var cachedMessages = messages.GetMany(fromMessageId, dir, limit); + limit -= cachedMessages.Count; + if (limit == 0) + return cachedMessages; + + if (dir == Direction.Before) + fromMessageId = cachedMessages.Min(x => x.Id); + else + fromMessageId = cachedMessages.Max(x => x.Id); + var downloadedMessages = await ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit).Flatten(); + return cachedMessages.Concat(downloadedMessages).ToImmutableArray(); + } + + public static IAsyncEnumerable> PagedGetMessagesAsync(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, + ulong? fromMessageId, Direction dir, int limit) + { + if (messages == null) //Cache disabled + return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit); + + var cachedMessages = messages.GetMany(fromMessageId, dir, limit); + var result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable>(); + limit -= cachedMessages.Count; + if (limit == 0) + return result; + + if (dir == Direction.Before) + fromMessageId = cachedMessages.Min(x => x.Id); + else + fromMessageId = cachedMessages.Max(x => x.Id); + var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit); + return result.Concat(downloadedMessages); + } + + public static void AddMessage(ISocketMessageChannel channel, DiscordSocketClient discord, + SocketMessage msg) + { + //C#7 Candidate for pattern matching + if (channel is SocketDMChannel) + (channel as SocketDMChannel).AddMessage(msg); + else if (channel is SocketGroupChannel) + (channel as SocketDMChannel).AddMessage(msg); + else if (channel is SocketTextChannel) + (channel as SocketDMChannel).AddMessage(msg); + else + throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + } + public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord, + ulong id) + { + //C#7 Candidate for pattern matching + if (channel is SocketDMChannel) + return (channel as SocketDMChannel).RemoveMessage(id); + else if (channel is SocketGroupChannel) + return (channel as SocketDMChannel).RemoveMessage(id); + else if (channel is SocketTextChannel) + return (channel as SocketDMChannel).RemoveMessage(id); + else + throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 6b87fbae2..0a747b446 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -12,60 +12,52 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketDMChannel : SocketChannel, IDMChannel + public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, ISocketMessageChannel { private readonly MessageCache _messages; public SocketUser Recipient { get; private set; } - public IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + public new IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); - internal SocketDMChannel(DiscordSocketClient discord, ulong id, ulong recipientId) + internal SocketDMChannel(DiscordSocketClient discord, ulong id, SocketGlobalUser recipient) : base(discord, id) { - Recipient = new SocketUser(Discord, recipientId); + Recipient = recipient; if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord, this); } - internal new static SocketDMChannel Create(DiscordSocketClient discord, Model model) + internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, Model model) { - var entity = new SocketDMChannel(discord, model.Id, model.Recipients.Value[0].Id); - entity.Update(model); + var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateUser(state, model.Recipients.Value[0])); + entity.Update(state, model); return entity; } - internal void Update(Model model) + internal override void Update(ClientState state, Model model) { - Recipient.Update(model.Recipients.Value[0]); + Recipient.Update(state, model.Recipients.Value[0]); } public Task CloseAsync() => ChannelHelper.DeleteAsync(this, Discord); - public SocketUser GetUser(ulong id) - { - if (id == Recipient.Id) - return Recipient; - else if (id == Discord.CurrentUser.Id) - return Discord.CurrentUser as SocketSelfUser; - else - return null; - } - - public SocketMessage GetMessage(ulong id) + //Messages + public SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); - public async Task GetMessageAsync(ulong id, bool allowDownload = true) + public async Task GetMessageAsync(ulong id) { IMessage msg = _messages?.Get(id); - if (msg == null && allowDownload) + if (msg == null) msg = await ChannelHelper.GetMessageAsync(this, Discord, id); return msg; } - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, limit: limit); - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit); + public Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit); + public Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit); + public Task> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit); public Task> GetPinnedMessagesAsync() => ChannelHelper.GetPinnedMessagesAsync(this, Discord); @@ -82,38 +74,61 @@ namespace Discord.WebSocket public IDisposable EnterTypingState() => ChannelHelper.EnterTypingState(this, Discord); - internal SocketMessage AddMessage(SocketUser author, MessageModel model) - { - var msg = SocketMessage.Create(Discord, author, model); - _messages.Add(msg); - return msg; - } + internal void AddMessage(SocketMessage msg) + => _messages.Add(msg); internal SocketMessage RemoveMessage(ulong id) + => _messages.Remove(id); + + //Users + public new SocketUser GetUser(ulong id) { - return _messages.Remove(id); + if (id == Recipient.Id) + return Recipient; + else if (id == Discord.CurrentUser.Id) + return Discord.CurrentUser as SocketSelfUser; + else + return null; } - public SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; - public override string ToString() => $"@{Recipient}"; private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; + internal new SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; + + //SocketChannel + internal override IReadOnlyCollection GetUsersInternal() => Users; + internal override SocketUser GetUserInternal(ulong id) => GetUser(id); //IDMChannel IUser IDMChannel.Recipient => Recipient; + //ISocketPrivateChannel + IReadOnlyCollection ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + //IPrivateChannel IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); //IMessageChannel - IReadOnlyCollection IMessageChannel.CachedMessages => ImmutableArray.Create(); - IMessage IMessageChannel.GetCachedMessage(ulong id) => null; - - async Task IMessageChannel.GetMessageAsync(ulong id) - => await GetMessageAsync(id); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit) - => GetMessagesAsync(limit); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - => GetMessagesAsync(fromMessageId, dir, limit); + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id); + else + return GetCachedMessage(id); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return SocketChannelHelper.PagedGetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit); + else + return ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return SocketChannelHelper.PagedGetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit); + else + return ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); + } async Task> IMessageChannel.GetPinnedMessagesAsync() => await GetPinnedMessagesAsync().ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) @@ -125,14 +140,10 @@ namespace Discord.WebSocket IDisposable IMessageChannel.EnterTypingState() => EnterTypingState(); - //IChannel - IReadOnlyCollection IChannel.CachedUsers => Users; - - IUser IChannel.GetCachedUser(ulong id) - => GetUser(id); - Task IChannel.GetUserAsync(ulong id) + //IChannel + Task IChannel.GetUserAsync(ulong id, CacheMode mode) => Task.FromResult(GetUser(id)); - IAsyncEnumerable> IChannel.GetUsersAsync() - => ImmutableArray.Create>().ToAsyncEnumerable(); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 7dc81fbb6..346f303cd 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using MessageModel = Discord.API.Message; using Model = Discord.API.Channel; using UserModel = Discord.API.User; using VoiceStateModel = Discord.API.VoiceState; @@ -15,7 +14,7 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketGroupChannel : SocketChannel, IGroupChannel + public class SocketGroupChannel : SocketChannel, IGroupChannel, ISocketPrivateChannel, ISocketMessageChannel, ISocketAudioChannel { private readonly MessageCache _messages; @@ -25,7 +24,8 @@ namespace Discord.WebSocket public string Name { get; private set; } - public IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + public new IReadOnlyCollection Users => _users.ToReadOnlyCollection(); public IReadOnlyCollection Recipients => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); @@ -37,13 +37,13 @@ namespace Discord.WebSocket _voiceStates = new ConcurrentDictionary(1, 5); _users = new ConcurrentDictionary(1, 5); } - internal new static SocketGroupChannel Create(DiscordSocketClient discord, Model model) + internal static SocketGroupChannel Create(DiscordSocketClient discord, ClientState state, Model model) { var entity = new SocketGroupChannel(discord, model.Id); - entity.Update(model); + entity.Update(state, model); return entity; } - internal void Update(Model model) + internal override void Update(ClientState state, Model model) { if (model.Name.IsSpecified) Name = model.Name.Value; @@ -51,37 +51,35 @@ namespace Discord.WebSocket _iconId = model.Icon.Value; if (model.Recipients.IsSpecified) - UpdateUsers(model.Recipients.Value); + UpdateUsers(state, model.Recipients.Value); } - internal virtual void UpdateUsers(API.User[] models) + internal virtual void UpdateUsers(ClientState state, UserModel[] models) { var users = new ConcurrentDictionary(1, (int)(models.Length * 1.05)); for (int i = 0; i < models.Length; i++) - users[models[i].Id] = SocketGroupUser.Create(Discord, models[i]); + users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); _users = users; } - - public async Task UpdateAsync() - => Update(await ChannelHelper.GetAsync(this, Discord)); + public Task LeaveAsync() => ChannelHelper.DeleteAsync(this, Discord); - public SocketGroupUser GetUser(ulong id) + //Messages + public SocketMessage GetCachedMessage(ulong id) + => _messages?.Get(id); + public async Task GetMessageAsync(ulong id) { - SocketGroupUser user; - if (_users.TryGetValue(id, out user)) - return user; - return null; + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id); + return msg; } - - public Task GetMessageAsync(ulong id) - => ChannelHelper.GetMessageAsync(this, Discord, id); - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, limit: limit); - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit); + public Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit); + public Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit); + public Task> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit); public Task> GetPinnedMessagesAsync() => ChannelHelper.GetPinnedMessagesAsync(this, Discord); @@ -98,20 +96,101 @@ namespace Discord.WebSocket public IDisposable EnterTypingState() => ChannelHelper.EnterTypingState(this, Discord); + internal void AddMessage(SocketMessage msg) + => _messages.Add(msg); + internal SocketMessage RemoveMessage(ulong id) + => _messages.Remove(id); + + //Users + public new SocketGroupUser GetUser(ulong id) + { + SocketGroupUser user; + if (_users.TryGetValue(id, out user)) + return user; + return null; + } + internal SocketGroupUser AddUser(UserModel model) + { + SocketGroupUser user; + if (_users.TryGetValue(model.Id, out user)) + return user as SocketGroupUser; + else + { + var privateUser = SocketGroupUser.Create(this, Discord.State, model); + _users[privateUser.Id] = privateUser; + return privateUser; + } + } + internal SocketGroupUser RemoveUser(ulong id) + { + SocketGroupUser user; + if (_users.TryRemove(id, out user)) + { + user.GlobalUser.RemoveRef(Discord); + return user as SocketGroupUser; + } + return null; + } + + //Voice States + internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) + { + var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; + var voiceState = SocketVoiceState.Create(voiceChannel, model); + _voiceStates[model.UserId] = voiceState; + return voiceState; + } + internal SocketVoiceState? GetVoiceState(ulong id) + { + SocketVoiceState voiceState; + if (_voiceStates.TryGetValue(id, out voiceState)) + return voiceState; + return null; + } + internal SocketVoiceState? RemoveVoiceState(ulong id) + { + SocketVoiceState voiceState; + if (_voiceStates.TryRemove(id, out voiceState)) + return voiceState; + return null; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, Group)"; + internal new SocketGroupChannel Clone() => MemberwiseClone() as SocketGroupChannel; + + //SocketChannel + internal override IReadOnlyCollection GetUsersInternal() => Users; + internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + + //ISocketPrivateChannel + IReadOnlyCollection ISocketPrivateChannel.Recipients => Recipients; + //IPrivateChannel IReadOnlyCollection IPrivateChannel.Recipients => Recipients; //IMessageChannel - IReadOnlyCollection IMessageChannel.CachedMessages => ImmutableArray.Create(); - - IMessage IMessageChannel.GetCachedMessage(ulong id) - => null; - async Task IMessageChannel.GetMessageAsync(ulong id) - => await GetMessageAsync(id); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit) - => GetMessagesAsync(limit); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - => GetMessagesAsync(fromMessageId, dir, limit); + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id); + else + return GetCachedMessage(id); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return SocketChannelHelper.PagedGetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit); + else + return ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return SocketChannelHelper.PagedGetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit); + else + return ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); + } async Task> IMessageChannel.GetPinnedMessagesAsync() => await GetPinnedMessagesAsync(); @@ -124,14 +203,10 @@ namespace Discord.WebSocket IDisposable IMessageChannel.EnterTypingState() => EnterTypingState(); - //IChannel - IReadOnlyCollection IChannel.CachedUsers => Users; - - IUser IChannel.GetCachedUser(ulong id) - => GetUser(id); - Task IChannel.GetUserAsync(ulong id) + //IChannel + Task IChannel.GetUserAsync(ulong id, CacheMode mode) => Task.FromResult(GetUser(id)); - IAsyncEnumerable> IChannel.GetUsersAsync() + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 8821ff616..c3a33c3ea 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -15,31 +15,31 @@ namespace Discord.WebSocket { private ImmutableArray _overwrites; - public IReadOnlyCollection PermissionOverwrites => _overwrites; - - public ulong GuildId { get; } - + public SocketGuild Guild { get; } public string Name { get; private set; } public int Position { get; private set; } - internal SocketGuildChannel(DiscordSocketClient discord, ulong id, ulong guildId) + public IReadOnlyCollection PermissionOverwrites => _overwrites; + public new abstract IReadOnlyCollection Users { get; } + + internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id) { - GuildId = guildId; + Guild = guild; } - internal new static SocketGuildChannel Create(DiscordSocketClient discord, Model model) + internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, Model model) { switch (model.Type) { case ChannelType.Text: - return SocketTextChannel.Create(discord, model); + return SocketTextChannel.Create(guild, state, model); case ChannelType.Voice: - return SocketVoiceChannel.Create(discord, model); + return SocketVoiceChannel.Create(guild, state, model); default: throw new InvalidOperationException("Unknown guild channel type"); } } - internal virtual void Update(Model model) + internal override void Update(ClientState state, Model model) { Name = model.Name.Value; Position = model.Position.Value; @@ -50,9 +50,7 @@ namespace Discord.WebSocket newOverwrites.Add(new Overwrite(overwrites[i])); _overwrites = newOverwrites.ToImmutable(); } - - public async Task UpdateAsync() - => Update(await ChannelHelper.GetAsync(this, Discord)); + public Task ModifyAsync(Action func) => ChannelHelper.ModifyAsync(this, Discord, func); public Task DeleteAsync() @@ -118,7 +116,18 @@ namespace Discord.WebSocket public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true) => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary); + public new abstract SocketGuildUser GetUser(ulong id); + + public override string ToString() => Name; + internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; + + //SocketChannel + internal override IReadOnlyCollection GetUsersInternal() => Users; + internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + //IGuildChannel + ulong IGuildChannel.GuildId => Guild.Id; + async Task> IGuildChannel.GetInvitesAsync() => await GetInvitesAsync(); async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary) @@ -136,24 +145,16 @@ namespace Discord.WebSocket => await RemovePermissionOverwriteAsync(role); async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user) => await RemovePermissionOverwriteAsync(user); - - IReadOnlyCollection IGuildChannel.CachedUsers - => ImmutableArray.Create(); - IAsyncEnumerable> IGuildChannel.GetUsersAsync() - => ImmutableArray.Create>().ToAsyncEnumerable(); //Overriden in Text/Voice //TODO: Does this actually override? - Task IGuildChannel.GetUserAsync(ulong id) - => Task.FromResult(null); //Overriden in Text/Voice //TODO: Does this actually override? - IGuildUser IGuildChannel.GetCachedUser(ulong id) - => null; + + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(GetUser(id)); //IChannel - IReadOnlyCollection IChannel.CachedUsers - => ImmutableArray.Create(); - IUser IChannel.GetCachedUser(ulong id) - => null; - IAsyncEnumerable> IChannel.GetUsersAsync() - => ImmutableArray.Create>().ToAsyncEnumerable(); //Overriden in Text/Voice //TODO: Does this actually override? - Task IChannel.GetUserAsync(ulong id) - => Task.FromResult(null); //Overriden in Text/Voice //TODO: Does this actually override? + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overriden in Text/Voice //TODO: Does this actually override? + Task IChannel.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(GetUser(id)); //Overriden in Text/Voice //TODO: Does this actually override? } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 39a392ef2..c1f5ca828 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -13,29 +13,34 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketTextChannel : SocketGuildChannel, ITextChannel + public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessageChannel { private readonly MessageCache _messages; public string Topic { get; private set; } public string Mention => MentionUtils.MentionChannel(Id); - - internal SocketTextChannel(DiscordSocketClient discord, ulong id, ulong guildId) - : base(discord, id, guildId) + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + public override IReadOnlyCollection Users + => Guild.Users.Where(x => Permissions.GetValue( + Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), + ChannelPermission.ReadMessages)).ToImmutableArray(); + + internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) { if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord, this); } - internal new static SocketTextChannel Create(DiscordSocketClient discord, Model model) + internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketTextChannel(discord, model.Id, model.GuildId.Value); - entity.Update(model); + var entity = new SocketTextChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); return entity; } - internal override void Update(Model model) + internal override void Update(ClientState state, Model model) { - base.Update(model); + base.Update(state, model); Topic = model.Topic.Value; } @@ -43,19 +48,22 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func) => ChannelHelper.ModifyAsync(this, Discord, func); - public Task GetUserAsync(ulong id) - => ChannelHelper.GetUserAsync(this, Discord, id); - public IAsyncEnumerable> GetUsersAsync() - => ChannelHelper.GetUsersAsync(this, Discord); - - public Task GetMessageAsync(ulong id) - => ChannelHelper.GetMessageAsync(this, Discord, id); - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, limit: limit); - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit); + //Messages + public SocketMessage GetCachedMessage(ulong id) + => _messages?.Get(id); + public async Task GetMessageAsync(ulong id) + { + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id); + return msg; + } + public Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit); + public Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit); + public Task> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit); public Task> GetPinnedMessagesAsync() => ChannelHelper.GetPinnedMessagesAsync(this, Discord); @@ -72,36 +80,56 @@ namespace Discord.WebSocket public IDisposable EnterTypingState() => ChannelHelper.EnterTypingState(this, Discord); - internal SocketMessage AddMessage(SocketUser author, MessageModel model) - { - var msg = SocketMessage.Create(Discord, author, model); - _messages.Add(msg); - return msg; - } + internal void AddMessage(SocketMessage msg) + => _messages.Add(msg); internal SocketMessage RemoveMessage(ulong id) + => _messages.Remove(id); + + //Users + public override SocketGuildUser GetUser(ulong id) { - return _messages.Remove(id); + var user = Guild.GetUser(id); + if (user != null) + { + var guildPerms = Permissions.ResolveGuild(Guild, user); + var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); + if (Permissions.GetValue(channelPerms, ChannelPermission.ReadMessages)) + return user; + } + return null; } - - public override string ToString() => Name; - private string DebuggerDisplay => $"@{Name} ({Id}, Text)"; + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; //IGuildChannel - async Task IGuildChannel.GetUserAsync(ulong id) - => await GetUserAsync(id); - IAsyncEnumerable> IGuildChannel.GetUsersAsync() - => GetUsersAsync(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //IMessageChannel - IReadOnlyCollection IMessageChannel.CachedMessages => ImmutableArray.Create(); - IMessage IMessageChannel.GetCachedMessage(ulong id) => null; - - async Task IMessageChannel.GetMessageAsync(ulong id) - => await GetMessageAsync(id); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit) - => GetMessagesAsync(limit); - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - => GetMessagesAsync(fromMessageId, dir, limit); + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id); + else + throw new NotImplementedException(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return SocketChannelHelper.PagedGetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit); + else + return ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return SocketChannelHelper.PagedGetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit); + else + return ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); + } async Task> IMessageChannel.GetPinnedMessagesAsync() => await GetPinnedMessagesAsync().ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 808f07e58..62f46cf60 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -12,24 +12,27 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel + public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel { public int Bitrate { get; private set; } public int UserLimit { get; private set; } - internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, ulong guildId) - : base(discord, id, guildId) + public override IReadOnlyCollection Users + => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + + internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) { } - internal new static SocketVoiceChannel Create(DiscordSocketClient discord, Model model) + internal new static SocketVoiceChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketVoiceChannel(discord, model.Id, model.GuildId.Value); - entity.Update(model); + var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); return entity; } - internal override void Update(Model model) + internal override void Update(ClientState state, Model model) { - base.Update(model); + base.Update(state, model); Bitrate = model.Bitrate.Value; UserLimit = model.UserLimit.Value; @@ -38,13 +41,24 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func) => ChannelHelper.ModifyAsync(this, Discord, func); + public override SocketGuildUser GetUser(ulong id) + { + var user = Guild.GetUser(id); + if (user?.VoiceChannel?.Id == Id) + return user; + return null; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; + //IVoiceChannel Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } //IGuildChannel - Task IGuildChannel.GetUserAsync(ulong id) - => Task.FromResult(null); - IAsyncEnumerable> IGuildChannel.GetUsersAsync() - => ImmutableArray.Create>().ToAsyncEnumerable(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 956596ace..45c1a588d 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -22,7 +22,14 @@ namespace Discord.WebSocket { public class SocketGuild : SocketEntity, IGuild { - private ImmutableDictionary _roles; + private readonly SemaphoreSlim _audioLock; + private TaskCompletionSource _syncPromise, _downloaderPromise; + private TaskCompletionSource _audioConnectPromise; + private ConcurrentHashSet _channels; + private ConcurrentDictionary _members; + private ConcurrentDictionary _roles; + private ConcurrentDictionary _voiceStates; + private ConcurrentDictionary _cachedPresences; private ImmutableArray _emojis; private ImmutableArray _features; internal bool _available; @@ -33,6 +40,9 @@ namespace Discord.WebSocket public VerificationLevel VerificationLevel { get; private set; } public MfaLevel MfaLevel { get; private set; } public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } + public int MemberCount { get; set; } + public int DownloadedMemberCount { get; private set; } + public AudioClient AudioClient { get; private set; } public ulong? AFKChannelId { get; private set; } public ulong? EmbedChannelId { get; private set; } @@ -44,24 +54,117 @@ namespace Discord.WebSocket public ulong DefaultChannelId => Id; public string IconUrl => API.CDN.GetGuildIconUrl(Id, IconId); public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, SplashId); - public bool IsSynced => false; + public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; + public bool IsSynced => _syncPromise.Task.IsCompleted; + public Task SyncPromise => _syncPromise.Task; + public Task DownloaderPromise => _downloaderPromise.Task; - public RestRole EveryoneRole => GetRole(Id); - public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + public SocketRole EveryoneRole => GetRole(Id); + public IReadOnlyCollection Channels + { + get + { + var channels = _channels; + var state = Discord.State; + return channels.Select(x => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); + } + } public IReadOnlyCollection Emojis => _emojis; public IReadOnlyCollection Features => _features; + public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); + public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + public IReadOnlyCollection VoiceStates => _voiceStates.ToReadOnlyCollection(); internal SocketGuild(DiscordSocketClient client, ulong id) : base(client, id) { + _emojis = ImmutableArray.Create(); + _features = ImmutableArray.Create(); } - internal static SocketGuild Create(DiscordSocketClient discord, Model model) + internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) { var entity = new SocketGuild(discord, model.Id); - entity.Update(model); + entity.Update(state, model); return entity; } - internal void Update(Model model) + internal void Update(ClientState state, ExtendedModel model) + { + _available = !(model.Unavailable ?? false); + if (!_available) + { + if (_channels == null) + _channels = new ConcurrentHashSet(); + if (_members == null) + _members = new ConcurrentDictionary(); + if (_roles == null) + _roles = new ConcurrentDictionary(); + /*if (Emojis == null) + _emojis = ImmutableArray.Create(); + if (Features == null) + _features = ImmutableArray.Create();*/ + return; + } + + Update(state, model as Model); + + var channels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Channels.Length * 1.05)); + { + for (int i = 0; i < model.Channels.Length; i++) + { + var channel = SocketGuildChannel.Create(this, state, model.Channels[i]); + state.AddChannel(channel); + channels.TryAdd(channel.Id); + } + } + _channels = channels; + + var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); + { + for (int i = 0; i < model.Members.Length; i++) + { + var member = SocketGuildUser.Create(this, state, model.Members[i]); + members.TryAdd(member.Id, member); + } + DownloadedMemberCount = members.Count; + } + var cachedPresences = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Presences.Length * 1.05)); + { + for (int i = 0; i < model.Presences.Length; i++) + { + SocketGuildUser member; + if (_members.TryGetValue(model.Presences[i].User.Id, out member)) + member.Update(state, model.Presences[i]); + else + cachedPresences.TryAdd(model.Presences[i].User.Id, model.Presences[i]); + } + } + _members = members; + _cachedPresences = cachedPresences; + MemberCount = model.MemberCount; + + var voiceStates = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.VoiceStates.Length * 1.05)); + { + for (int i = 0; i < model.VoiceStates.Length; i++) + { + SocketVoiceChannel channel = null; + if (model.VoiceStates[i].ChannelId.HasValue) + channel = state.GetChannel(model.VoiceStates[i].ChannelId.Value) as SocketVoiceChannel; + var voiceState = SocketVoiceState.Create(channel, model.VoiceStates[i]); + voiceStates.TryAdd(model.VoiceStates[i].UserId, voiceState); + } + } + _voiceStates = voiceStates; + + _syncPromise = new TaskCompletionSource(); + _downloaderPromise = new TaskCompletionSource(); + if (Discord.ApiClient.AuthTokenType != TokenType.User) + { + var _ = _syncPromise.TrySetResultAsync(true); + if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true); + } + } + internal void Update(ClientState state, Model model) { AFKChannelId = model.AFKChannelId; EmbedChannelId = model.EmbedChannelId; @@ -81,7 +184,7 @@ namespace Discord.WebSocket var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) emojis.Add(Emoji.Create(model.Emojis[i])); - _emojis = emojis.ToImmutableArray(); + _emojis = emojis.ToImmutable(); } else _emojis = ImmutableArray.Create(); @@ -91,17 +194,52 @@ namespace Discord.WebSocket else _features = ImmutableArray.Create(); - var roles = ImmutableDictionary.CreateBuilder(); + var roles = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); if (model.Roles != null) { - throw new NotImplementedException(); + for (int i = 0; i < model.Roles.Length; i++) + { + var role = SocketRole.Create(this, state, model.Roles[i]); + roles.TryAdd(role.Id, role); + } } - _roles = roles.ToImmutable(); + _roles = roles; + } + internal void Update(ClientState state, GuildSyncModel model) + { + var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); + { + for (int i = 0; i < model.Members.Length; i++) + { + var member = SocketGuildUser.Create(this, state, model.Members[i]); + members.TryAdd(member.Id, member); + } + DownloadedMemberCount = members.Count; + } + var cachedPresences = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Presences.Length * 1.05)); + { + for (int i = 0; i < model.Presences.Length; i++) + { + SocketGuildUser member; + if (_members.TryGetValue(model.Presences[i].User.Id, out member)) + member.Update(state, model.Presences[i]); + else + cachedPresences.TryAdd(model.Presences[i].User.Id, model.Presences[i]); + } + } + _members = members; + _cachedPresences = cachedPresences; + } + + internal void Update(ClientState state, EmojiUpdateModel model) + { + var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + for (int i = 0; i < model.Emojis.Length; i++) + emojis.Add(Emoji.Create(model.Emojis[i])); + _emojis = emojis.ToImmutable(); } //General - public async Task UpdateAsync() - => Update(await Discord.ApiClient.GetGuildAsync(Id)); public Task DeleteAsync() => GuildHelper.DeleteAsync(this, Discord); @@ -132,14 +270,30 @@ namespace Discord.WebSocket => GuildHelper.RemoveBanAsync(this, Discord, userId); //Channels - public Task> GetChannelsAsync() - => GuildHelper.GetChannelsAsync(this, Discord); - public Task GetChannelAsync(ulong id) - => GuildHelper.GetChannelAsync(this, Discord, id); + public SocketGuildChannel GetChannel(ulong id) + { + var channel = Discord.State.GetChannel(id) as SocketGuildChannel; + if (channel?.Guild.Id == Id) + return channel; + return null; + } public Task CreateTextChannelAsync(string name) => GuildHelper.CreateTextChannelAsync(this, Discord, name); public Task CreateVoiceChannelAsync(string name) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name); + internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) + { + var channel = SocketGuildChannel.Create(this, state, model); + _channels.TryAdd(model.Id); + state.AddChannel(channel); + return channel; + } + internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) + { + if (_channels.TryRemove(id)) + return state.RemoveChannel(id) as SocketGuildChannel; + return null; + } //Integrations public Task> GetIntegrationsAsync() @@ -152,48 +306,238 @@ namespace Discord.WebSocket => GuildHelper.GetInvitesAsync(this, Discord); //Roles - public RestRole GetRole(ulong id) + public SocketRole GetRole(ulong id) { - RestRole value; + SocketRole value; if (_roles.TryGetValue(id, out value)) return value; return null; } - - public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false) + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false) + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted); + internal SocketRole AddRole(RoleModel model) { - var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted); - _roles = _roles.Add(role.Id, role); + var role = SocketRole.Create(this, Discord.State, model); + _roles[model.Id] = role; return role; } + internal SocketRole RemoveRole(ulong id) + { + SocketRole role; + if (_roles.TryRemove(id, out role)) + return role; + return null; + } //Users - public Task> GetUsersAsync() - => GuildHelper.GetUsersAsync(this, Discord); - public Task GetUserAsync(ulong id) - => GuildHelper.GetUserAsync(this, Discord, id); - public Task GetCurrentUserAsync() - => GuildHelper.GetUserAsync(this, Discord, Discord.CurrentUser.Id); - + public SocketGuildUser GetUser(ulong id) + { + SocketGuildUser member; + if (_members.TryGetValue(id, out member)) + return member; + return null; + } + public SocketGuildUser GetCurrentUser() + { + SocketGuildUser member; + if (_members.TryGetValue(Discord.CurrentUser.Id, out member)) + return member; + return null; + } public Task PruneUsersAsync(int days = 30, bool simulate = false) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate); + internal SocketGuildUser AddOrUpdateUser(MemberModel model) + { + SocketGuildUser member; + if (_members.TryGetValue(model.User.Id, out member)) + member.Update(Discord.State, model); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + _members[member.Id] = member; + DownloadedMemberCount++; + } + return member; + } + internal SocketGuildUser AddOrUpdateUser(PresenceModel model) + { + SocketGuildUser member; + if (_members.TryGetValue(model.User.Id, out member)) + member.Update(Discord.State, model); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + _members[member.Id] = member; + DownloadedMemberCount++; + } + return member; + } + internal SocketGuildUser RemoveUser(ulong id) + { + SocketGuildUser member; + if (_members.TryRemove(id, out member)) + { + DownloadedMemberCount--; + return member; + } + member.GlobalUser.RemoveRef(Discord); + return null; + } + + public async Task DownloadUsersAsync() + { + await Discord.DownloadUsersAsync(new[] { this }); + } + internal void CompleteDownloadUsers() + { + _downloaderPromise.TrySetResultAsync(true); + } + + //Voice States + internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) + { + var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; + var voiceState = SocketVoiceState.Create(voiceChannel, model); + _voiceStates[model.UserId] = voiceState; + return voiceState; + } + internal SocketVoiceState? GetVoiceState(ulong id) + { + SocketVoiceState voiceState; + if (_voiceStates.TryGetValue(id, out voiceState)) + return voiceState; + return null; + } + internal SocketVoiceState? RemoveVoiceState(ulong id) + { + SocketVoiceState voiceState; + if (_voiceStates.TryRemove(id, out voiceState)) + return voiceState; + return null; + } + + //Audio + public async Task DisconnectAudioAsync(AudioClient client = null) + { + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectAudioInternalAsync(client).ConfigureAwait(false); + } + finally + { + _audioLock.Release(); + } + } + private async Task DisconnectAudioInternalAsync(AudioClient client = null) + { + var oldClient = AudioClient; + if (oldClient != null) + { + if (client == null || oldClient == client) + { + _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection + _audioConnectPromise = null; + } + if (oldClient == client) + { + AudioClient = null; + await oldClient.DisconnectAsync().ConfigureAwait(false); + } + } + } + internal async Task FinishConnectAudio(int id, string url, string token) + { + var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value; + + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + if (AudioClient == null) + { + var audioClient = new AudioClient(this, id); + audioClient.Disconnected += async ex => + { + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + if (AudioClient == audioClient) //Only reconnect if we're still assigned as this guild's audio client + { + if (ex != null) + { + //Reconnect if we still have channel info. + //TODO: Is this threadsafe? Could channel data be deleted before we access it? + var voiceState2 = GetVoiceState(Discord.CurrentUser.Id); + if (voiceState2.HasValue) + { + var voiceChannelId = voiceState2.Value.VoiceChannel?.Id; + if (voiceChannelId != null) + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted); + } + } + else + { + try { AudioClient.Dispose(); } catch { } + AudioClient = null; + } + } + } + finally + { + _audioLock.Release(); + } + }; + AudioClient = audioClient; + } + await AudioClient.ConnectAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); + await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await DisconnectAudioAsync(); + } + catch (Exception e) + { + await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false); + await DisconnectAudioAsync(); + } + finally + { + _audioLock.Release(); + } + } + internal async Task FinishJoinAudioChannel() + { + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + if (AudioClient != null) + await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); + } + finally + { + _audioLock.Release(); + } + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; + //IGuild bool IGuild.Available => true; IAudioClient IGuild.AudioClient => null; - IReadOnlyCollection IGuild.CachedUsers => ImmutableArray.Create(); IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Roles => Roles; async Task> IGuild.GetBansAsync() => await GetBansAsync(); - async Task> IGuild.GetChannelsAsync() - => await GetChannelsAsync(); - async Task IGuild.GetChannelAsync(ulong id) - => await GetChannelAsync(id); - IGuildChannel IGuild.GetCachedChannel(ulong id) - => null; + Task> IGuild.GetChannelsAsync(CacheMode mode) + => Task.FromResult>(Channels); + Task IGuild.GetChannelAsync(ulong id, CacheMode mode) + => Task.FromResult(GetChannel(id)); async Task IGuild.CreateTextChannelAsync(string name) => await CreateTextChannelAsync(name); async Task IGuild.CreateVoiceChannelAsync(string name) @@ -209,15 +553,15 @@ namespace Discord.WebSocket IRole IGuild.GetRole(ulong id) => GetRole(id); + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted) + => await CreateRoleAsync(name, permissions, color, isHoisted); - async Task> IGuild.GetUsersAsync() - => await GetUsersAsync(); - async Task IGuild.GetUserAsync(ulong id) - => await GetUserAsync(id); - IGuildUser IGuild.GetCachedUser(ulong id) - => null; - async Task IGuild.GetCurrentUserAsync() - => await GetCurrentUserAsync(); + Task> IGuild.GetUsersAsync(CacheMode mode) + => Task.FromResult>(Users); + Task IGuild.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(GetUser(id)); + Task IGuild.GetCurrentUserAsync(CacheMode mode) + => Task.FromResult(GetCurrentUser()); Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs index d283f46b0..72e2b50ae 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -1,11 +1,8 @@ -using Discord.Rest; -using Discord.WebSocket; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading.Tasks; namespace Discord.WebSocket { @@ -51,7 +48,7 @@ namespace Discord.WebSocket return result; return null; } - public IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public IReadOnlyCollection GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); if (limit == 0) return ImmutableArray.Empty; diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 46271c9e6..7d97ee916 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -2,13 +2,12 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Threading.Tasks; using Model = Discord.API.Message; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public abstract class SocketMessage : SocketEntity, IMessage, IUpdateable + public abstract class SocketMessage : SocketEntity, IMessage { private long _timestampTicks; @@ -35,14 +34,14 @@ namespace Discord.WebSocket ChannelId = channelId; Author = author; } - internal static SocketMessage Create(DiscordSocketClient discord, SocketUser author, Model model) + internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, Model model) { if (model.Type == MessageType.Default) - return SocketUserMessage.Create(discord, author, model); + return SocketUserMessage.Create(discord, state, author, model); else - return SocketSystemMessage.Create(discord, author, model); + return SocketSystemMessage.Create(discord, state, author, model); } - internal virtual void Update(Model model) + internal virtual void Update(ClientState state, Model model) { if (model.Timestamp.IsSpecified) _timestampTicks = model.Timestamp.Value.UtcTicks; @@ -50,12 +49,8 @@ namespace Discord.WebSocket if (model.Content.IsSpecified) Content = model.Content.Value; } - - public async Task UpdateAsync() - { - var model = await Discord.ApiClient.GetChannelMessageAsync(ChannelId, Id).ConfigureAwait(false); - Update(model); - } + + internal SocketMessage Clone() => MemberwiseClone() as SocketMessage; //IMessage IUser IMessage.Author => Author; diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index 56a8bafaa..61bc9af1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -1,5 +1,4 @@ -using Discord.Rest; -using Model = Discord.API.Message; +using Model = Discord.API.Message; namespace Discord.WebSocket { @@ -11,17 +10,21 @@ namespace Discord.WebSocket : base(discord, id, channelId, author) { } - internal new static SocketSystemMessage Create(DiscordSocketClient discord, SocketUser author, Model model) + internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, Model model) { var entity = new SocketSystemMessage(discord, model.Id, model.ChannelId, author); - entity.Update(model); + entity.Update(state, model); return entity; } - internal override void Update(Model model) + internal override void Update(ClientState state, Model model) { - base.Update(model); + base.Update(state, model); Type = model.Type; } + + public override string ToString() => Content; + private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; + internal new SocketSystemMessage Clone() => MemberwiseClone() as SocketSystemMessage; } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 1f60ac8f2..003acecb2 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -8,7 +8,7 @@ using Model = Discord.API.Message; namespace Discord.WebSocket { - internal class SocketUserMessage : SocketMessage, IUserMessage + public class SocketUserMessage : SocketMessage, IUserMessage { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; @@ -32,16 +32,16 @@ namespace Discord.WebSocket : base(discord, id, channelId, author) { } - internal new static SocketUserMessage Create(DiscordSocketClient discord, SocketUser author, Model model) + internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, Model model) { var entity = new SocketUserMessage(discord, model.Id, model.ChannelId, author); - entity.Update(model); + entity.Update(state, model); return entity; } - internal override void Update(Model model) + internal override void Update(ClientState state, Model model) { - base.Update(model); + base.Update(state, model); if (model.IsTextToSpeech.IsSpecified) _isTTS = model.IsTextToSpeech.Value; @@ -129,5 +129,9 @@ namespace Discord.WebSocket text = MentionsHelper.ResolveEveryoneMentions(text, everyoneHandling); return text; } + + public override string ToString() => Content; + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")}"; + internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; } } diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs new file mode 100644 index 000000000..1cb2e2812 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -0,0 +1,58 @@ +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Role; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketRole : SocketEntity, IRole + { + public SocketGuild Guild { get; } + + public Color Color { get; private set; } + public bool IsHoisted { get; private set; } + public bool IsManaged { get; private set; } + public string Name { get; private set; } + public GuildPermissions Permissions { get; private set; } + public int Position { get; private set; } + + public bool IsEveryone => Id == Guild.Id; + public string Mention => MentionUtils.MentionRole(Id); + + internal SocketRole(SocketGuild guild, ulong id) + : base(guild.Discord, id) + { + Guild = guild; + } + internal static SocketRole Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketRole(guild, model.Id); + entity.Update(state, model); + return entity; + } + internal void Update(ClientState state, Model model) + { + Name = model.Name; + IsHoisted = model.Hoist; + IsManaged = model.Managed; + Position = model.Position; + Color = new Color(model.Color); + Permissions = new GuildPermissions(model.Permissions); + } + + public Task ModifyAsync(Action func) + => RoleHelper.ModifyAsync(this, Discord, func); + public Task DeleteAsync() + => RoleHelper.DeleteAsync(this, Discord); + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + internal SocketRole Clone() => MemberwiseClone() as SocketRole; + + //IRole + IGuild IRole.Guild => Guild; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 39340745c..d96993887 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,10 +1,56 @@ -namespace Discord.WebSocket +using Model = Discord.API.User; +using System.Collections.Concurrent; + +namespace Discord.WebSocket { internal class SocketGlobalUser : SocketUser { - internal SocketGlobalUser(DiscordSocketClient discord, ulong id) + public override bool IsBot { get; internal set; } + public override string Username { get; internal set; } + public override ushort DiscriminatorValue { get; internal set; } + public override string AvatarId { get; internal set; } + public SocketDMChannel DMChannel { get; internal set; } + + internal override SocketGlobalUser GlobalUser => this; + internal override SocketPresence Presence { get; set; } + + private readonly object _lockObj = new object(); + private ushort _references; + + private SocketGlobalUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } + internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientState state, Model model) + { + var entity = new SocketGlobalUser(discord, model.Id); + entity.Update(state, model); + return entity; + } + + internal void AddRef() + { + checked + { + lock (_lockObj) + _references++; + } + } + internal void RemoveRef(DiscordSocketClient discord) + { + lock (_lockObj) + { + if (--_references <= 0) + discord.RemoveUser(Id); + } + } + + internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; + + //Updates are only ever called from the gateway thread, thus threadsafe + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + } } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 2c2bfe74a..694d0ccb9 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -1,5 +1,4 @@ -using Discord.Rest; -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.User; namespace Discord.WebSocket @@ -7,17 +6,30 @@ namespace Discord.WebSocket [DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketGroupUser : SocketUser, IGroupUser { - internal SocketGroupUser(DiscordSocketClient discord, ulong id) - : base(discord, id) + public SocketGroupChannel Channel { get; } + internal override SocketGlobalUser GlobalUser { get; } + + public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + + internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) + : base(channel.Discord, globalUser.Id) { + Channel = channel; + GlobalUser = globalUser; } - internal new static SocketGroupUser Create(DiscordSocketClient discord, Model model) + internal static SocketGroupUser Create(SocketGroupChannel channel, ClientState state, Model model) { - var entity = new SocketGroupUser(discord, model.Id); - entity.Update(model); + var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); + entity.Update(state, model); return entity; } + internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; + //IVoiceState bool IVoiceState.IsDeafened => false; bool IVoiceState.IsMuted => false; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 33aea831c..f73b10a2e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -9,46 +9,76 @@ using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { - internal class SocketGuildUser : SocketUser, IGuildUser + public class SocketGuildUser : SocketUser, IGuildUser { private long? _joinedAtTicks; private ImmutableArray _roleIds; + internal override SocketGlobalUser GlobalUser { get; } + public SocketGuild Guild { get; } public string Nickname { get; private set; } - public ulong GuildId { get; private set; } + public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } public IReadOnlyCollection RoleIds => _roleIds; + public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); + public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; + public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; + public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; + public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; + public bool IsDeafened => VoiceState?.IsDeafened ?? false; + public bool IsMuted => VoiceState?.IsMuted ?? false; + public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; + public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - internal SocketGuildUser(DiscordSocketClient discord, ulong id) - : base(discord, id) + internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) + : base(guild.Discord, globalUser.Id) + { + Guild = guild; + GlobalUser = globalUser; + } + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, Model model) { + var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); + entity.Update(state, model); + return entity; } - internal static SocketGuildUser Create(DiscordSocketClient discord, Model model) + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) { - var entity = new SocketGuildUser(discord, model.User.Id); - entity.Update(model); + var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); + entity.Update(state, model); return entity; } - internal void Update(Model model) + internal void Update(ClientState state, Model model) { + base.Update(state, model.User); _joinedAtTicks = model.JoinedAt.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; UpdateRoles(model.Roles); } + internal override void Update(ClientState state, PresenceModel model) + { + base.Update(state, model); + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; + } private void UpdateRoles(ulong[] roleIds) { var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); - roles.Add(GuildId); + roles.Add(Guild.Id); for (int i = 0; i < roleIds.Length; i++) roles.Add(roleIds[i]); _roleIds = roles.ToImmutable(); } - - public override async Task UpdateAsync() - => Update(await UserHelper.GetAsync(this, Discord)); + public Task ModifyAsync(Action func) => UserHelper.ModifyAsync(this, Discord, func); public Task KickAsync() @@ -59,16 +89,17 @@ namespace Discord.WebSocket throw new NotImplementedException(); //TODO: Impl } + internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; + //IGuildUser + ulong IGuildUser.GuildId => Guild.Id; IReadOnlyCollection IGuildUser.RoleIds => RoleIds; + //IUser + Task IUser.GetDMChannelAsync(CacheMode mode) + => Task.FromResult(GlobalUser.DMChannel); + //IVoiceState - bool IVoiceState.IsDeafened => false; - bool IVoiceState.IsMuted => false; - bool IVoiceState.IsSelfDeafened => false; - bool IVoiceState.IsSelfMuted => false; - bool IVoiceState.IsSuppressed => false; - IVoiceChannel IVoiceState.VoiceChannel => null; - string IVoiceState.VoiceSessionId => null; + IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 01fd85345..edfe67e3f 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -3,7 +3,7 @@ namespace Discord.WebSocket { //TODO: C#7 Candidate for record type - internal struct SocketPresence : IPresence + public struct SocketPresence : IPresence { public Game? Game { get; } public UserStatus Status { get; } @@ -13,11 +13,11 @@ namespace Discord.WebSocket Game = game; Status = status; } - internal SocketPresence Create(Model model) + internal static SocketPresence Create(Model model) { return new SocketPresence(model.Game != null ? Discord.Game.Create(model.Game) : (Game?)null, model.Status); } - public SocketPresence Clone() => this; + internal SocketPresence Clone() => this; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index ccbbd4850..e859d7c0a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -11,21 +11,28 @@ namespace Discord.WebSocket public string Email { get; private set; } public bool IsVerified { get; private set; } public bool IsMfaEnabled { get; private set; } + internal override SocketGlobalUser GlobalUser { get; } - internal SocketSelfUser(DiscordSocketClient discord, ulong id) - : base(discord, id) + public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + + internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) + : base(discord, globalUser.Id) { - Status = UserStatus.Online; + GlobalUser = globalUser; } - internal new static SocketSelfUser Create(DiscordSocketClient discord, Model model) + internal static SocketSelfUser Create(DiscordSocketClient discord, ClientState state, Model model) { - var entity = new SocketSelfUser(discord, model.Id); - entity.Update(model); + var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); + entity.Update(state, model); return entity; } - internal override void Update(Model model) + internal override void Update(ClientState state, Model model) { - base.Update(model); + base.Update(state, model); if (model.Email.IsSpecified) Email = model.Email.Value; @@ -34,12 +41,13 @@ namespace Discord.WebSocket if (model.MfaEnabled.IsSpecified) IsMfaEnabled = model.MfaEnabled.Value; } - - public override async Task UpdateAsync() - => Update(await UserHelper.GetAsync(this, Discord)); + public Task ModifyAsync(Action func) => UserHelper.ModifyAsync(this, Discord, func); + internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; + + //ISelfUser Task ISelfUser.ModifyStatusAsync(Action func) { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs new file mode 100644 index 000000000..be2b279fc --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs @@ -0,0 +1,34 @@ +using System; +using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; + +namespace Discord.WebSocket +{ + public class SocketSimpleUser : SocketUser + { + public override bool IsBot { get; internal set; } + public override string Username { get; internal set; } + public override ushort DiscriminatorValue { get; internal set; } + public override string AvatarId { get; internal set; } + internal override SocketPresence Presence { get { return new SocketPresence(null, UserStatus.Offline); } set { } } + + internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + + internal SocketSimpleUser(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + internal static SocketSimpleUser Create(DiscordSocketClient discord, ClientState state, Model model) + { + var entity = new SocketSimpleUser(discord, model.Id); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, PresenceModel model) + { + } + + internal new SocketSimpleUser Clone() => MemberwiseClone() as SocketSimpleUser; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 5ab68d556..a9c419cf2 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -1,33 +1,30 @@ using Discord.Rest; using System.Threading.Tasks; using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { - public class SocketUser : SocketEntity, IUser + public abstract class SocketUser : SocketEntity, IUser { - public bool IsBot { get; private set; } - public string Username { get; private set; } - public ushort DiscriminatorValue { get; private set; } - public string AvatarId { get; private set; } + public abstract bool IsBot { get; internal set; } + public abstract string Username { get; internal set; } + public abstract ushort DiscriminatorValue { get; internal set; } + public abstract string AvatarId { get; internal set; } + internal abstract SocketGlobalUser GlobalUser { get; } + internal abstract SocketPresence Presence { get; set; } public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, AvatarId); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public virtual Game? Game => null; - public virtual UserStatus Status { get; internal set; } + public Game? Game => Presence.Game; + public UserStatus Status => Presence.Status; internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketUser Create(DiscordSocketClient discord, Model model) - { - var entity = new SocketUser(discord, model.Id); - entity.Update(model); - return entity; - } - internal virtual void Update(Model model) + internal virtual void Update(ClientState state, Model model) { if (model.Avatar.IsSpecified) AvatarId = model.Avatar.Value; @@ -38,13 +35,22 @@ namespace Discord.WebSocket if (model.Username.IsSpecified) Username = model.Username.Value; } + internal virtual void Update(ClientState state, PresenceModel model) + { + Presence = SocketPresence.Create(model); + } - public virtual async Task UpdateAsync() - => Update(await UserHelper.GetAsync(this, Discord)); - - public Task CreateDMChannelAsync() + public Task CreateDMChannelAsync() => UserHelper.CreateDMChannelAsync(this, Discord); - IDMChannel IUser.GetCachedDMChannel() => null; + public override string ToString() => $"{Username}#{Discriminator}"; + private string DebuggerDisplay => $"{Username}#{Discriminator} (Id{(IsBot ? ", Bot" : "")})"; + internal SocketUser Clone() => MemberwiseClone() as SocketUser; + + //IUser + Task IUser.GetDMChannelAsync(CacheMode mode) + => Task.FromResult(GlobalUser.DMChannel); + async Task IUser.CreateDMChannelAsync() + => await CreateDMChannelAsync(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs index 9813fb039..39d8e3ae3 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -47,7 +47,7 @@ namespace Discord.WebSocket return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); } - public SocketVoiceState Clone() => this; + internal SocketVoiceState Clone() => this; IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; }