diff --git a/Discord.Net.sln b/Discord.Net.sln index a24b833e3..b29a7bca5 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -15,8 +15,6 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Core", "src\Dis EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D-A636-4EAE-9AC1-4698B641B26E}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Discord.Net.Utils", "src\Discord.Net.Utils\Discord.Net.Utils.shproj", "{2B75119C-9893-4AAA-8D38-6176EEB09060}" -EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Rest", "src\Discord.Net.Rest\Discord.Net.Rest.xproj", "{BFC6DC28-0351-4573-926A-D4124244C04F}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.xproj", "{22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}" @@ -24,9 +22,6 @@ EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Rpc", "src\Discord.Net.Rpc\Discord.Net.Rpc.xproj", "{5688A353-121E-40A1-8BFA-B17B91FB48FB}" EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - src\Discord.Net.Utils\Discord.Net.Utils.projitems*{2b75119c-9893-4aaa-8d38-6176eeb09060}*SharedItemsImports = 13 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU diff --git a/src/Discord.Net.Core/AssemblyInfo.cs b/src/Discord.Net.Core/AssemblyInfo.cs new file mode 100644 index 000000000..0c0f48a50 --- /dev/null +++ b/src/Discord.Net.Core/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Rest")] +[assembly: InternalsVisibleTo("Discord.Net.Rpc")] +[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Commands")] +[assembly: InternalsVisibleTo("Discord.Net.Test")] \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Guilds/Emoji.cs b/src/Discord.Net.Core/Entities/Guilds/Emoji.cs index fc8b64fa4..ea9d176df 100644 --- a/src/Discord.Net.Core/Entities/Guilds/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Guilds/Emoji.cs @@ -14,7 +14,7 @@ namespace Discord public bool RequireColons { get; } public IReadOnlyList RoleIds { get; } - public Emoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) + private Emoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) { Id = id; Name = name; @@ -22,7 +22,7 @@ namespace Discord RequireColons = requireColons; RoleIds = roleIds; } - public static Emoji Create(Model model) + internal static Emoji Create(Model model) { return new Emoji(model.Id, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 4d4aa1ea8..8cc650ce6 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -7,12 +7,12 @@ namespace Discord public string Name { get; } public string Url { get; } - public EmbedProvider(string name, string url) + private EmbedProvider(string name, string url) { Name = name; Url = url; } - public static EmbedProvider Create(Model model) + internal static EmbedProvider Create(Model model) { return new EmbedProvider(model.Name, model.Url); } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 8e1a2a527..43a37548c 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -9,14 +9,14 @@ namespace Discord public int? Height { get; } public int? Width { get; } - public EmbedThumbnail(string url, string proxyUrl, int? height, int? width) + private EmbedThumbnail(string url, string proxyUrl, int? height, int? width) { Url = url; ProxyUrl = proxyUrl; Height = height; Width = width; } - public static EmbedThumbnail Create(Model model) + internal static EmbedThumbnail Create(Model model) { return new EmbedThumbnail(model.Url, model.ProxyUrl, model.Height.IsSpecified ? model.Height.Value : (int?)null, diff --git a/src/Discord.Net.Core/Entities/Users/Game.cs b/src/Discord.Net.Core/Entities/Users/Game.cs index 2a8645df3..5bed84ddb 100644 --- a/src/Discord.Net.Core/Entities/Users/Game.cs +++ b/src/Discord.Net.Core/Entities/Users/Game.cs @@ -16,9 +16,9 @@ namespace Discord StreamUrl = streamUrl; StreamType = type; } - public Game(string name) + private Game(string name) : this(name, null, StreamType.NotStreaming) { } - public static Game Create(Model model) + internal static Game Create(Model model) { return new Game(model.Name, model.StreamUrl.GetValueOrDefault(null), diff --git a/src/Discord.Net.Core/Entities/Users/UserStatus.cs b/src/Discord.Net.Core/Entities/Users/UserStatus.cs index d71ec3173..ea2c3680e 100644 --- a/src/Discord.Net.Core/Entities/Users/UserStatus.cs +++ b/src/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -5,6 +5,8 @@ Unknown, Online, Idle, + DoNotDisturb, + Invisible, Offline } } diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index d31f0ad0a..010d1b0dc 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -27,9 +27,7 @@ public static string Sanitize(string text) { foreach (string unsafeChar in SensitiveCharacters) - { text = text.Replace(unsafeChar, $"\\{unsafeChar}"); - } return text; } } diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index accd9080c..79ca30634 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -26,14 +26,12 @@ namespace Discord Task GetGuildAsync(ulong id); Task> GetGuildsAsync(); - Task> GetGuildSummariesAsync(); Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null); Task GetInviteAsync(string inviteId); Task GetUserAsync(ulong id); Task GetUserAsync(string username, string discriminator); - Task> QueryUsersAsync(string query, int limit); Task> GetVoiceRegionsAsync(); Task GetVoiceRegionAsync(string id); diff --git a/src/Discord.Net.Utils/Logging/LogManager.cs b/src/Discord.Net.Core/Logging/LogManager.cs similarity index 78% rename from src/Discord.Net.Utils/Logging/LogManager.cs rename to src/Discord.Net.Core/Logging/LogManager.cs index dfcc97d33..86b63f73f 100644 --- a/src/Discord.Net.Utils/Logging/LogManager.cs +++ b/src/Discord.Net.Core/Logging/LogManager.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading.Tasks; namespace Discord.Logging @@ -6,6 +7,7 @@ namespace Discord.Logging internal class LogManager { public LogSeverity Level { get; } + public Logger ClientLogger { get; } public event Func Message { add { _messageEvent.Add(value); } remove { _messageEvent.Remove(value); } } private readonly AsyncEvent> _messageEvent = new AsyncEvent>(); @@ -13,6 +15,7 @@ namespace Discord.Logging public LogManager(LogSeverity minSeverity) { Level = minSeverity; + ClientLogger = new Logger(this, "Discord"); } public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) @@ -67,5 +70,21 @@ namespace Discord.Logging => LogAsync(LogSeverity.Debug, source, ex); public Logger CreateLogger(string name) => new Logger(this, name); + + public async Task WriteInitialLog() + { + await ClientLogger.InfoAsync($"Discord.Net v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false); + await ClientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); + await ClientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); + } + private static string ToArchString(Architecture arch) + { + switch (arch) + { + case Architecture.X64: return "x64"; + case Architecture.X86: return "x86"; + default: return arch.ToString(); + } + } } } diff --git a/src/Discord.Net.Utils/Logging/Logger.cs b/src/Discord.Net.Core/Logging/Logger.cs similarity index 100% rename from src/Discord.Net.Utils/Logging/Logger.cs rename to src/Discord.Net.Core/Logging/Logger.cs diff --git a/src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs b/src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs index d2c25d3b8..3b4e1c0cb 100644 --- a/src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs +++ b/src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs @@ -19,6 +19,10 @@ namespace Discord.Net.Converters return UserStatus.Online; case "idle": return UserStatus.Idle; + case "dnd": + return UserStatus.DoNotDisturb; + case "invisible": + return UserStatus.Invisible; //Should never happen case "offline": return UserStatus.Offline; default: @@ -36,6 +40,12 @@ namespace Discord.Net.Converters case UserStatus.Idle: writer.WriteValue("idle"); break; + case UserStatus.DoNotDisturb: + writer.WriteValue("dnd"); + break; + case UserStatus.Invisible: + writer.WriteValue("invisible"); + break; case UserStatus.Offline: writer.WriteValue("offline"); break; diff --git a/src/Discord.Net.Utils/AsyncEvent.cs b/src/Discord.Net.Core/Utils/AsyncEvent.cs similarity index 100% rename from src/Discord.Net.Utils/AsyncEvent.cs rename to src/Discord.Net.Core/Utils/AsyncEvent.cs diff --git a/src/Discord.Net.Utils/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs similarity index 100% rename from src/Discord.Net.Utils/ConcurrentHashSet.cs rename to src/Discord.Net.Core/Utils/ConcurrentHashSet.cs diff --git a/src/Discord.Net.Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs similarity index 100% rename from src/Discord.Net.Utils/DateTimeUtils.cs rename to src/Discord.Net.Core/Utils/DateTimeUtils.cs diff --git a/src/Discord.Net.Utils/Extensions/CollectionExtensions.cs b/src/Discord.Net.Core/Utils/Extensions/CollectionExtensions.cs similarity index 100% rename from src/Discord.Net.Utils/Extensions/CollectionExtensions.cs rename to src/Discord.Net.Core/Utils/Extensions/CollectionExtensions.cs diff --git a/src/Discord.Net.Utils/Extensions/Permissions.cs b/src/Discord.Net.Core/Utils/Extensions/Permissions.cs similarity index 100% rename from src/Discord.Net.Utils/Extensions/Permissions.cs rename to src/Discord.Net.Core/Utils/Extensions/Permissions.cs diff --git a/src/Discord.Net.Utils/Extensions/TaskCompletionSourceExtensions.cs b/src/Discord.Net.Core/Utils/Extensions/TaskCompletionSourceExtensions.cs similarity index 100% rename from src/Discord.Net.Utils/Extensions/TaskCompletionSourceExtensions.cs rename to src/Discord.Net.Core/Utils/Extensions/TaskCompletionSourceExtensions.cs diff --git a/src/Discord.Net.Utils/MentionsHelper.cs b/src/Discord.Net.Core/Utils/MentionsHelper.cs similarity index 100% rename from src/Discord.Net.Utils/MentionsHelper.cs rename to src/Discord.Net.Core/Utils/MentionsHelper.cs diff --git a/src/Discord.Net.Utils/Paging/Page.cs b/src/Discord.Net.Core/Utils/Paging/Page.cs similarity index 100% rename from src/Discord.Net.Utils/Paging/Page.cs rename to src/Discord.Net.Core/Utils/Paging/Page.cs diff --git a/src/Discord.Net.Utils/Paging/PageInfo.cs b/src/Discord.Net.Core/Utils/Paging/PageInfo.cs similarity index 100% rename from src/Discord.Net.Utils/Paging/PageInfo.cs rename to src/Discord.Net.Core/Utils/Paging/PageInfo.cs diff --git a/src/Discord.Net.Utils/Paging/PagedEnumerator.cs b/src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs similarity index 100% rename from src/Discord.Net.Utils/Paging/PagedEnumerator.cs rename to src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs diff --git a/src/Discord.Net.Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs similarity index 100% rename from src/Discord.Net.Utils/Permissions.cs rename to src/Discord.Net.Core/Utils/Permissions.cs diff --git a/src/Discord.Net.Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs similarity index 100% rename from src/Discord.Net.Utils/Preconditions.cs rename to src/Discord.Net.Core/Utils/Preconditions.cs diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs new file mode 100644 index 000000000..ea53dbb51 --- /dev/null +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Rpc")] +[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Commands")] +[assembly: InternalsVisibleTo("Discord.Net.Test")] \ No newline at end of file diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs new file mode 100644 index 000000000..07868182d --- /dev/null +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -0,0 +1,118 @@ +using Discord.API.Rest; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal static class ClientHelper + { + //Applications + public static async Task GetApplicationInfoAsync(DiscordClient client) + { + var model = await client.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); + return RestApplication.Create(client, model); + } + + public static async Task GetChannelAsync(DiscordClient client, + ulong id) + { + var model = await client.ApiClient.GetChannelAsync(id).ConfigureAwait(false); + if (model != null) + return RestChannel.Create(client, model); + return null; + } + public static async Task> GetPrivateChannelsAsync(DiscordClient client) + { + var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + return models.Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); + } + + public static async Task> GetConnectionsAsync(DiscordClient client) + { + var models = await client.ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); + return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); + } + + public static async Task GetInviteAsync(DiscordClient client, + string inviteId) + { + var model = await client.ApiClient.GetInviteAsync(inviteId).ConfigureAwait(false); + if (model != null) + return RestInvite.Create(client, model); + return null; + } + + public static async Task GetGuildAsync(DiscordClient client, + ulong id) + { + var model = await client.ApiClient.GetGuildAsync(id).ConfigureAwait(false); + if (model != null) + return RestGuild.Create(client, model); + return null; + } + public static async Task GetGuildEmbedAsync(DiscordClient client, + ulong id) + { + var model = await client.ApiClient.GetGuildEmbedAsync(id).ConfigureAwait(false); + if (model != null) + return RestGuildEmbed.Create(model); + return null; + } + public static async Task> GetGuildSummariesAsync(DiscordClient client) + { + var models = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); + return models.Select(x => RestUserGuild.Create(client, x)).ToImmutableArray(); + } + public static async Task> GetGuildsAsync(DiscordClient client) + { + var summaryModels = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); + var guilds = ImmutableArray.CreateBuilder(summaryModels.Count); + foreach (var summaryModel in summaryModels) + { + var guildModel = await client.ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false); + if (guildModel != null) + guilds.Add(RestGuild.Create(client, guildModel)); + } + return guilds.ToImmutable(); + } + public static async Task CreateGuildAsync(DiscordClient client, + string name, IVoiceRegion region, Stream jpegIcon = null) + { + var args = new CreateGuildParams(name, region.Id); + var model = await client.ApiClient.CreateGuildAsync(args).ConfigureAwait(false); + return RestGuild.Create(client, model); + } + + public static async Task GetUserAsync(DiscordClient client, + ulong id) + { + var model = await client.ApiClient.GetUserAsync(id).ConfigureAwait(false); + if (model != null) + return RestUser.Create(client, model); + return null; + } + public static async Task GetUserAsync(DiscordClient client, + string username, string discriminator) + { + var model = await client.ApiClient.GetUserAsync(username, discriminator).ConfigureAwait(false); + if (model != null) + return RestUser.Create(client, model); + return null; + } + + public static async Task> GetVoiceRegionsAsync(DiscordClient client) + { + var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); + return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); + } + public static async Task GetVoiceRegionAsync(DiscordClient client, + string id) + { + var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); + return models.Select(x => RestVoiceRegion.Create(client, x)).Where(x => x.Id == id).FirstOrDefault(); + } + } +} diff --git a/src/Discord.Net.Rest/DiscordClient.cs b/src/Discord.Net.Rest/DiscordClient.cs new file mode 100644 index 000000000..5bf0f2c89 --- /dev/null +++ b/src/Discord.Net.Rest/DiscordClient.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Discord.Logging; +using System.Collections.Immutable; + +namespace Discord.Rest +{ + public abstract class DiscordClient : IDiscordClient + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + private readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + public event Func LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } + private readonly AsyncEvent> _loggedInEvent = new AsyncEvent>(); + public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } + private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); + + internal readonly Logger _restLogger, _queueLogger; + internal readonly SemaphoreSlim _connectionLock; + private bool _isFirstLogin; + private bool _isDisposed; + + public API.DiscordRestApiClient ApiClient { get; } + internal LogManager LogManager { get; } + public LoginState LoginState { get; private set; } + public ISelfUser CurrentUser { get; protected set; } + + /// Creates a new REST-only discord client. + internal DiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) + { + ApiClient = client; + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + + _connectionLock = new SemaphoreSlim(1, 1); + _restLogger = LogManager.CreateLogger("Rest"); + _queueLogger = LogManager.CreateLogger("Queue"); + _isFirstLogin = true; + + ApiClient.RequestQueue.RateLimitTriggered += async (id, bucket, millis) => + { + await _queueLogger.WarningAsync($"Rate limit triggered (id = \"{id ?? "null"}\")").ConfigureAwait(false); + if (bucket == null && id != null) + await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); + }; + ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); + } + + /// + public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternalAsync(tokenType, token).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LoginInternalAsync(TokenType tokenType, string token) + { + if (_isFirstLogin) + { + _isFirstLogin = false; + await LogManager.WriteInitialLog().ConfigureAwait(false); + } + + if (LoginState != LoginState.LoggedOut) + await LogoutInternalAsync().ConfigureAwait(false); + LoginState = LoginState.LoggingIn; + + try + { + await OnLoginAsync(tokenType, token).ConfigureAwait(false); + LoginState = LoginState.LoggedIn; + } + catch (Exception) + { + await LogoutInternalAsync().ConfigureAwait(false); + throw; + } + + await _loggedInEvent.InvokeAsync().ConfigureAwait(false); + } + protected virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.CompletedTask; } + + /// + public async Task LogoutAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LogoutInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LogoutInternalAsync() + { + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; + + await ApiClient.LogoutAsync().ConfigureAwait(false); + + await OnLogoutAsync().ConfigureAwait(false); + CurrentUser = null; + LoginState = LoginState.LoggedOut; + + await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); + } + protected virtual Task OnLogoutAsync() { return Task.CompletedTask; } + + internal virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + ApiClient.Dispose(); + _isDisposed = true; + } + } + /// + public void Dispose() => Dispose(true); + + //IDiscordClient + ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; + ISelfUser IDiscordClient.CurrentUser => CurrentUser; + + Task IDiscordClient.GetApplicationInfoAsync() { throw new NotSupportedException(); } + + Task IDiscordClient.GetChannelAsync(ulong id) + => Task.FromResult(null); + Task> IDiscordClient.GetPrivateChannelsAsync() + => Task.FromResult>(ImmutableArray.Create()); + + Task> IDiscordClient.GetConnectionsAsync() + => Task.FromResult>(ImmutableArray.Create()); + + Task IDiscordClient.GetInviteAsync(string inviteId) + => Task.FromResult(null); + + Task IDiscordClient.GetGuildAsync(ulong id) + => Task.FromResult(null); + Task> IDiscordClient.GetGuildsAsync() + => Task.FromResult>(ImmutableArray.Create()); + Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) { throw new NotSupportedException(); } + + Task IDiscordClient.GetUserAsync(ulong id) + => Task.FromResult(null); + Task IDiscordClient.GetUserAsync(string username, string discriminator) + => Task.FromResult(null); + + Task> IDiscordClient.GetVoiceRegionsAsync() + => Task.FromResult>(ImmutableArray.Create()); + Task IDiscordClient.GetVoiceRegionAsync(string id) + => Task.FromResult(null); + + Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } + Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } + + } +} diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index b1668250d..8bc3ca9c3 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -1,324 +1,105 @@ -using Discord.API.Rest; -using Discord.Net; -using Discord.Net.Queue; -using System; +using Discord.Net.Queue; using System.Collections.Generic; -using System.Collections.Immutable; using System.IO; -using System.Linq; -using System.Threading; using System.Threading.Tasks; -using System.Runtime.InteropServices; -using Discord.Logging; namespace Discord.Rest { - public class DiscordRestClient : IDiscordClient + public class DiscordRestClient : DiscordClient, IDiscordClient { - private readonly object _eventLock = new object(); + public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; - public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } - private readonly AsyncEvent> _logEvent = new AsyncEvent>(); - - public event Func LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } - private readonly AsyncEvent> _loggedInEvent = new AsyncEvent>(); - public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } - private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); - - internal readonly Logger _clientLogger, _restLogger, _queueLogger; - internal readonly SemaphoreSlim _connectionLock; - private bool _isFirstLogSub; - internal bool _isDisposed; - - public API.DiscordRestApiClient ApiClient { get; } - internal LogManager LogManager { get; } - public LoginState LoginState { get; private set; } - public RestSelfUser CurrentUser { get; private set; } - - /// Creates a new REST-only discord client. public DiscordRestClient() : this(new DiscordRestConfig()) { } - public DiscordRestClient(DiscordRestConfig config) : this(config, CreateApiClient(config)) { } - /// Creates a new REST-only discord client. - internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient client) - { - ApiClient = client; - LogManager = new LogManager(config.LogLevel); - LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); - _clientLogger = LogManager.CreateLogger("Client"); - _restLogger = LogManager.CreateLogger("Rest"); - _queueLogger = LogManager.CreateLogger("Queue"); - _isFirstLogSub = true; + public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } - _connectionLock = new SemaphoreSlim(1, 1); - - ApiClient.RequestQueue.RateLimitTriggered += async (id, bucket, millis) => - { - await _queueLogger.WarningAsync($"Rate limit triggered (id = \"{id ?? "null"}\")").ConfigureAwait(false); - if (bucket == null && id != null) - await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); - }; - ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); - } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, requestQueue: new RequestQueue()); - /// - public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LoginInternalAsync(tokenType, token).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task LoginInternalAsync(TokenType tokenType, string token) - { - if (_isFirstLogSub) - { - _isFirstLogSub = false; - await WriteInitialLog().ConfigureAwait(false); - } - - if (LoginState != LoginState.LoggedOut) - await LogoutInternalAsync().ConfigureAwait(false); - LoginState = LoginState.LoggingIn; - - try - { - await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); - CurrentUser = RestSelfUser.Create(this, ApiClient.CurrentUser); - - await OnLoginAsync(tokenType, token).ConfigureAwait(false); - LoginState = LoginState.LoggedIn; - } - catch (Exception) - { - await LogoutInternalAsync().ConfigureAwait(false); - throw; - } - - await _loggedInEvent.InvokeAsync().ConfigureAwait(false); - } - protected virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.CompletedTask; - - /// - public async Task LogoutAsync() + protected override async Task OnLoginAsync(TokenType tokenType, string token) { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LogoutInternalAsync().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } + await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); + base.CurrentUser = RestSelfUser.Create(this, ApiClient.CurrentUser); } - private async Task LogoutInternalAsync() - { - if (LoginState == LoginState.LoggedOut) return; - LoginState = LoginState.LoggingOut; - - await ApiClient.LogoutAsync().ConfigureAwait(false); - await OnLogoutAsync().ConfigureAwait(false); - - CurrentUser = null; - LoginState = LoginState.LoggedOut; - - await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); - } - protected virtual Task OnLogoutAsync() => Task.CompletedTask; /// - public async Task GetApplicationInfoAsync() - { - var model = await ApiClient.GetMyApplicationAsync().ConfigureAwait(false); - return RestApplication.Create(this, model); - } + public Task GetApplicationInfoAsync() + => ClientHelper.GetApplicationInfoAsync(this); /// - public virtual async Task GetChannelAsync(ulong id) - { - var model = await ApiClient.GetChannelAsync(id).ConfigureAwait(false); - if (model != null) - { - switch (model.Type) - { - case ChannelType.Text: - return RestTextChannel.Create(this, model); - case ChannelType.Voice: - return RestVoiceChannel.Create(this, model); - case ChannelType.DM: - return RestDMChannel.Create(this, model); - case ChannelType.Group: - return RestGroupChannel.Create(this, model); - default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } - } - return null; - } + public Task GetChannelAsync(ulong id) + => ClientHelper.GetChannelAsync(this, id); /// - public virtual async Task> GetPrivateChannelsAsync() - { - var models = await ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); - return models.Select(x => RestDMChannel.Create(this, x)).ToImmutableArray(); - } - + public Task> GetPrivateChannelsAsync() + => ClientHelper.GetPrivateChannelsAsync(this); + /// - public async Task> GetConnectionsAsync() - { - var models = await ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); - return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); - } + public Task> GetConnectionsAsync() + => ClientHelper.GetConnectionsAsync(this); /// - public virtual async Task GetInviteAsync(string inviteId) - { - var model = await ApiClient.GetInviteAsync(inviteId).ConfigureAwait(false); - if (model != null) - return RestInvite.Create(this, model); - return null; - } + public Task GetInviteAsync(string inviteId) + => ClientHelper.GetInviteAsync(this, inviteId); /// - public virtual async Task GetGuildAsync(ulong id) - { - var model = await ApiClient.GetGuildAsync(id).ConfigureAwait(false); - if (model != null) - return RestGuild.Create(this, model); - return null; - } + public Task GetGuildAsync(ulong id) + => ClientHelper.GetGuildAsync(this, id); /// - public virtual async Task GetGuildEmbedAsync(ulong id) - { - var model = await ApiClient.GetGuildEmbedAsync(id).ConfigureAwait(false); - if (model != null) - return RestGuildEmbed.Create(model); - return null; - } + public Task GetGuildEmbedAsync(ulong id) + => ClientHelper.GetGuildEmbedAsync(this, id); /// - public virtual async Task> GetGuildSummariesAsync() - { - var models = await ApiClient.GetMyGuildsAsync().ConfigureAwait(false); - return models.Select(x => RestUserGuild.Create(this, x)).ToImmutableArray(); - } + public Task> GetGuildSummariesAsync() + => ClientHelper.GetGuildSummariesAsync(this); /// - public virtual async Task> GetGuildsAsync() - { - var summaryModels = await ApiClient.GetMyGuildsAsync().ConfigureAwait(false); - var guilds = ImmutableArray.CreateBuilder(summaryModels.Count); - foreach (var summaryModel in summaryModels) - { - var guildModel = await ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false); - if (guildModel != null) - guilds.Add(RestGuild.Create(this, guildModel)); - } - return guilds.ToImmutable(); - } + public Task> GetGuildsAsync() + => ClientHelper.GetGuildsAsync(this); /// - public virtual async Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - { - var args = new CreateGuildParams(name, region.Id); - var model = await ApiClient.CreateGuildAsync(args).ConfigureAwait(false); - return RestGuild.Create(this, model); - } + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); /// - public virtual async Task GetUserAsync(ulong id) - { - var model = await ApiClient.GetUserAsync(id).ConfigureAwait(false); - if (model != null) - return RestUser.Create(this, model); - return null; - } + public Task GetUserAsync(ulong id) + => ClientHelper.GetUserAsync(this, id); /// - public virtual async Task GetUserAsync(string username, string discriminator) - { - var model = await ApiClient.GetUserAsync(username, discriminator).ConfigureAwait(false); - if (model != null) - return RestUser.Create(this, model); - return null; - } + public Task GetUserAsync(string username, string discriminator) + => ClientHelper.GetUserAsync(this, username, discriminator); /// - public virtual async Task> QueryUsersAsync(string query, int limit) - { - var models = await ApiClient.QueryUsersAsync(query, limit).ConfigureAwait(false); - return models.Select(x => RestUser.Create(this, x)).ToImmutableArray(); - } - - /// - public virtual async Task> GetVoiceRegionsAsync() - { - var models = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); - return models.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableArray(); - } - /// - public virtual async Task GetVoiceRegionAsync(string id) - { - var models = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); - return models.Select(x => RestVoiceRegion.Create(this, x)).Where(x => x.Id == id).FirstOrDefault(); - } - - internal virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - ApiClient.Dispose(); - _isDisposed = true; - } - } + public Task> GetVoiceRegionsAsync() + => ClientHelper.GetVoiceRegionsAsync(this); /// - public void Dispose() => Dispose(true); - - private async Task WriteInitialLog() - { - /*if (this is DiscordSocketClient) - await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordSocketConfig.GatewayEncoding})").ConfigureAwait(false); - else if (this is DiscordRpcClient) - await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, RPC API v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false);*/ - await _clientLogger.InfoAsync($"Discord.Net v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false); - await _clientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); - await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); - await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); - } - private static string ToArchString(Architecture arch) - { - switch (arch) - { - case Architecture.X64: return "x64"; - case Architecture.X86: return "x86"; - default: return arch.ToString(); - } - } + public Task GetVoiceRegionAsync(string id) + => ClientHelper.GetVoiceRegionAsync(this, id); //IDiscordClient - ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; - ISelfUser IDiscordClient.CurrentUser => CurrentUser; + async Task IDiscordClient.GetApplicationInfoAsync() + => await GetApplicationInfoAsync().ConfigureAwait(false); + + async Task IDiscordClient.GetChannelAsync(ulong id) + => await GetChannelAsync(id); + async Task> IDiscordClient.GetPrivateChannelsAsync() + => await GetPrivateChannelsAsync(); async Task> IDiscordClient.GetConnectionsAsync() => await GetConnectionsAsync().ConfigureAwait(false); + async Task IDiscordClient.GetInviteAsync(string inviteId) => await GetInviteAsync(inviteId).ConfigureAwait(false); + async Task IDiscordClient.GetGuildAsync(ulong id) => await GetGuildAsync(id).ConfigureAwait(false); - async Task> IDiscordClient.GetGuildSummariesAsync() - => await GetGuildSummariesAsync().ConfigureAwait(false); async Task> IDiscordClient.GetGuildsAsync() => await GetGuildsAsync().ConfigureAwait(false); async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + async Task IDiscordClient.GetUserAsync(ulong id) => await GetUserAsync(id).ConfigureAwait(false); async Task IDiscordClient.GetUserAsync(string username, string discriminator) => await GetUserAsync(username, discriminator).ConfigureAwait(false); - async Task> IDiscordClient.QueryUsersAsync(string query, int limit) - => await QueryUsersAsync(query, limit).ConfigureAwait(false); + async Task> IDiscordClient.GetVoiceRegionsAsync() => await GetVoiceRegionsAsync().ConfigureAwait(false); async Task IDiscordClient.GetVoiceRegionAsync(string id) => await GetVoiceRegionAsync(id).ConfigureAwait(false); - - Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } - Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 26512acaf..63f68049f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -12,33 +12,33 @@ namespace Discord.Rest internal static class ChannelHelper { //General - public static async Task GetAsync(IGuildChannel channel, DiscordRestClient client) + public static async Task GetAsync(IGuildChannel channel, DiscordClient client) { return await client.ApiClient.GetChannelAsync(channel.GuildId, channel.Id).ConfigureAwait(false); } - public static async Task GetAsync(IPrivateChannel channel, DiscordRestClient client) + public static async Task GetAsync(IPrivateChannel channel, DiscordClient client) { return await client.ApiClient.GetChannelAsync(channel.Id).ConfigureAwait(false); } - public static async Task DeleteAsync(IChannel channel, DiscordRestClient client) + public static async Task DeleteAsync(IChannel channel, DiscordClient client) { await client.ApiClient.DeleteChannelAsync(channel.Id).ConfigureAwait(false); } - public static async Task ModifyAsync(IGuildChannel channel, DiscordRestClient client, + public static async Task ModifyAsync(IGuildChannel channel, DiscordClient client, Action func) { var args = new ModifyGuildChannelParams(); func(args); await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args); } - public static async Task ModifyAsync(ITextChannel channel, DiscordRestClient client, + public static async Task ModifyAsync(ITextChannel channel, DiscordClient client, Action func) { var args = new ModifyTextChannelParams(); func(args); await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args); } - public static async Task ModifyAsync(IVoiceChannel channel, DiscordRestClient client, + public static async Task ModifyAsync(IVoiceChannel channel, DiscordClient client, Action func) { var args = new ModifyVoiceChannelParams(); @@ -47,12 +47,12 @@ namespace Discord.Rest } //Invites - public static async Task> GetInvitesAsync(IChannel channel, DiscordRestClient client) + public static async Task> GetInvitesAsync(IChannel channel, DiscordClient client) { var models = await client.ApiClient.GetChannelInvitesAsync(channel.Id); return models.Select(x => RestInviteMetadata.Create(client, x)).ToImmutableArray(); } - public static async Task CreateInviteAsync(IChannel channel, DiscordRestClient client, + public static async Task CreateInviteAsync(IChannel channel, DiscordClient client, int? maxAge, int? maxUses, bool isTemporary) { var args = new CreateChannelInviteParams { IsTemporary = isTemporary }; @@ -65,13 +65,13 @@ namespace Discord.Rest } //Messages - public static async Task GetMessageAsync(IChannel channel, DiscordRestClient client, + public static async Task GetMessageAsync(IChannel channel, DiscordClient client, ulong id) { var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id).ConfigureAwait(false); return RestMessage.Create(client, model); } - public static PagedAsyncEnumerable GetMessagesAsync(IChannel channel, DiscordRestClient client, + public static IAsyncEnumerable> GetMessagesAsync(IChannel channel, DiscordClient client, ulong? fromMessageId = null, Direction dir = Direction.Before, int limit = DiscordConfig.MaxMessagesPerBatch) { //TODO: Test this with Around direction @@ -102,13 +102,13 @@ namespace Discord.Rest count: (uint)limit ); } - public static async Task> GetPinnedMessagesAsync(IChannel channel, DiscordRestClient client) + public static async Task> GetPinnedMessagesAsync(IChannel channel, DiscordClient client) { var models = await client.ApiClient.GetPinsAsync(channel.Id).ConfigureAwait(false); return models.Select(x => RestMessage.Create(client, x)).ToImmutableArray(); } - public static async Task SendMessageAsync(IChannel channel, DiscordRestClient client, + public static async Task SendMessageAsync(IChannel channel, DiscordClient client, string text, bool isTTS) { var args = new CreateMessageParams(text) { IsTTS = isTTS }; @@ -116,14 +116,14 @@ namespace Discord.Rest return RestUserMessage.Create(client, model); } - public static Task SendFileAsync(IChannel channel, DiscordRestClient client, + public static Task SendFileAsync(IChannel channel, DiscordClient client, string filePath, string text, bool isTTS) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) return SendFileAsync(channel, client, file, filename, text, isTTS); } - public static async Task SendFileAsync(IChannel channel, DiscordRestClient client, + public static async Task SendFileAsync(IChannel channel, DiscordClient client, Stream stream, string filename, string text, bool isTTS) { var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; @@ -131,7 +131,7 @@ namespace Discord.Rest return RestUserMessage.Create(client, model); } - public static async Task DeleteMessagesAsync(IChannel channel, DiscordRestClient client, + public static async Task DeleteMessagesAsync(IChannel channel, DiscordClient client, IEnumerable messages) { var args = new DeleteMessagesParams(messages.Select(x => x.Id).ToArray()); @@ -139,31 +139,31 @@ namespace Discord.Rest } //Permission Overwrites - public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, DiscordRestClient client, + public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, DiscordClient client, IUser user, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams("member", perms.AllowValue, perms.DenyValue); await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, user.Id, args).ConfigureAwait(false); } - public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, DiscordRestClient client, + public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, DiscordClient client, IRole role, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams("role", perms.AllowValue, perms.DenyValue); await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, role.Id, args).ConfigureAwait(false); } - public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, DiscordRestClient client, + public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, DiscordClient client, IUser user) { await client.ApiClient.DeleteChannelPermissionAsync(channel.Id, user.Id).ConfigureAwait(false); } - public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, DiscordRestClient client, + public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, DiscordClient client, IRole role) { await client.ApiClient.DeleteChannelPermissionAsync(channel.Id, role.Id).ConfigureAwait(false); } //Users - public static async Task GetUserAsync(IGuildChannel channel, DiscordRestClient client, + public static async Task GetUserAsync(IGuildChannel channel, DiscordClient client, ulong id) { var model = await client.ApiClient.GetGuildMemberAsync(channel.GuildId, id); @@ -175,7 +175,7 @@ namespace Discord.Rest return user; } - public static IAsyncEnumerable> GetUsersAsync(IGuildChannel channel, DiscordRestClient client, + public static IAsyncEnumerable> GetUsersAsync(IGuildChannel channel, DiscordClient client, ulong? froUserId = null, uint? limit = DiscordConfig.MaxUsersPerBatch) { return new PagedAsyncEnumerable( @@ -203,7 +203,7 @@ namespace Discord.Rest } //Typing - public static IDisposable EnterTypingState(IChannel channel, DiscordRestClient client) + public static IDisposable EnterTypingState(IChannel channel, DiscordClient client) { throw new NotImplementedException(); //TODO: Impl } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs new file mode 100644 index 000000000..ada4008ec --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public abstract class RestChannel : RestEntity, IChannel, IUpdateable + { + internal RestChannel(DiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestChannel Create(DiscordClient discord, Model model) + { + switch (model.Type) + { + case ChannelType.Text: + return RestTextChannel.Create(discord, model); + case ChannelType.Voice: + return RestVoiceChannel.Create(discord, model); + case ChannelType.DM: + return RestDMChannel.Create(discord, model); + case ChannelType.Group: + return RestGroupChannel.Create(discord, model); + default: + throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); + } + } + internal abstract void Update(Model model); + + 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(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 796ec23cb..973a001a8 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -10,28 +10,31 @@ using Model = Discord.API.Channel; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestDMChannel : RestEntity, IDMChannel, IUpdateable + public class RestDMChannel : RestChannel, IDMChannel, IUpdateable { - public RestUser Recipient { get; } + public RestUser CurrentUser { get; private set; } + public RestUser Recipient { get; private set; } - public IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); + public IReadOnlyCollection Users => ImmutableArray.Create(CurrentUser, Recipient); - internal RestDMChannel(DiscordRestClient discord, ulong id) + internal RestDMChannel(DiscordClient discord, ulong id, ulong recipientId) : base(discord, id) { + Recipient = new RestUser(Discord, recipientId); + CurrentUser = new RestUser(Discord, discord.CurrentUser.Id); } - internal static RestDMChannel Create(DiscordRestClient discord, Model model) + internal new static RestDMChannel Create(DiscordClient discord, Model model) { - var entity = new RestDMChannel(discord, model.Id); + var entity = new RestDMChannel(discord, model.Id, model.Recipients.Value[0].Id); entity.Update(model); return entity; } - internal void Update(Model model) + internal override void Update(Model model) { Recipient.Update(model.Recipients.Value[0]); } - public async Task UpdateAsync() + public override async Task UpdateAsync() => Update(await ChannelHelper.GetAsync(this, Discord)); public Task CloseAsync() => ChannelHelper.DeleteAsync(this, Discord); @@ -41,7 +44,7 @@ namespace Discord.Rest if (id == Recipient.Id) return Recipient; else if (id == Discord.CurrentUser.Id) - return Discord.CurrentUser; + return CurrentUser; else return null; } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index b95cfce14..00be0ca11 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -10,7 +10,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestGroupChannel : RestEntity, IGroupChannel, IUpdateable + public class RestGroupChannel : RestChannel, IGroupChannel, IUpdateable { private string _iconId; private ImmutableDictionary _users; @@ -21,17 +21,17 @@ namespace Discord.Rest public IReadOnlyCollection Recipients => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); - internal RestGroupChannel(DiscordRestClient discord, ulong id) + internal RestGroupChannel(DiscordClient discord, ulong id) : base(discord, id) { } - internal static RestGroupChannel Create(DiscordRestClient discord, Model model) + internal new static RestGroupChannel Create(DiscordClient discord, Model model) { var entity = new RestGroupChannel(discord, model.Id); entity.Update(model); return entity; } - internal void Update(Model model) + internal override void Update(Model model) { if (model.Name.IsSpecified) Name = model.Name.Value; @@ -49,7 +49,7 @@ namespace Discord.Rest _users = users.ToImmutable(); } - public async Task UpdateAsync() + public override async Task UpdateAsync() => Update(await ChannelHelper.GetAsync(this, Discord)); public Task LeaveAsync() => ChannelHelper.DeleteAsync(this, Discord); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 62422efe1..4ec79496d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -10,7 +10,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public abstract class RestGuildChannel : RestEntity, IGuildChannel, IUpdateable + public abstract class RestGuildChannel : RestChannel, IGuildChannel, IUpdateable { private ImmutableArray _overwrites; @@ -21,12 +21,12 @@ namespace Discord.Rest public string Name { get; private set; } public int Position { get; private set; } - internal RestGuildChannel(DiscordRestClient discord, ulong id, ulong guildId) + internal RestGuildChannel(DiscordClient discord, ulong id, ulong guildId) : base(discord, id) { GuildId = guildId; } - internal static RestGuildChannel Create(DiscordRestClient discord, Model model) + internal new static RestGuildChannel Create(DiscordClient discord, Model model) { switch (model.Type) { @@ -38,7 +38,7 @@ namespace Discord.Rest throw new InvalidOperationException("Unknown guild channel type"); } } - internal virtual void Update(Model model) + internal override void Update(Model model) { Name = model.Name.Value; Position = model.Position.Value; @@ -50,7 +50,7 @@ namespace Discord.Rest _overwrites = newOverwrites.ToImmutable(); } - public async Task UpdateAsync() + public override async Task UpdateAsync() => Update(await ChannelHelper.GetAsync(this, Discord)); public Task ModifyAsync(Action func) => ChannelHelper.ModifyAsync(this, Discord, func); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 65877868c..69917f862 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -16,11 +16,11 @@ namespace Discord.Rest public string Mention => MentionUtils.MentionChannel(Id); - internal RestTextChannel(DiscordRestClient discord, ulong id, ulong guildId) + internal RestTextChannel(DiscordClient discord, ulong id, ulong guildId) : base(discord, id, guildId) { } - internal new static RestTextChannel Create(DiscordRestClient discord, Model model) + internal new static RestTextChannel Create(DiscordClient discord, Model model) { var entity = new RestTextChannel(discord, model.Id, model.GuildId.Value); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index a3187ff12..2b23eaad7 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -16,11 +16,11 @@ namespace Discord.Rest public int Bitrate { get; private set; } public int UserLimit { get; private set; } - internal RestVoiceChannel(DiscordRestClient discord, ulong id, ulong guildId) + internal RestVoiceChannel(DiscordClient discord, ulong id, ulong guildId) : base(discord, id, guildId) { } - internal new static RestVoiceChannel Create(DiscordRestClient discord, Model model) + internal new static RestVoiceChannel Create(DiscordClient discord, Model model) { var entity = new RestVoiceChannel(discord, model.Id, model.GuildId.Value); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 02fb76868..e88c38504 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -13,7 +13,7 @@ namespace Discord.Rest internal static class GuildHelper { //General - public static async Task ModifyAsync(IGuild guild, DiscordRestClient client, + public static async Task ModifyAsync(IGuild guild, DiscordClient client, Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -28,7 +28,7 @@ namespace Discord.Rest return await client.ApiClient.ModifyGuildAsync(guild.Id, args).ConfigureAwait(false); } - public static async Task ModifyEmbedAsync(IGuild guild, DiscordRestClient client, + public static async Task ModifyEmbedAsync(IGuild guild, DiscordClient client, Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -37,46 +37,46 @@ namespace Discord.Rest func(args); return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, args).ConfigureAwait(false); } - public static async Task ModifyChannelsAsync(IGuild guild, DiscordRestClient client, + public static async Task ModifyChannelsAsync(IGuild guild, DiscordClient client, IEnumerable args) { await client.ApiClient.ModifyGuildChannelsAsync(guild.Id, args).ConfigureAwait(false); } - public static async Task> ModifyRolesAsync(IGuild guild, DiscordRestClient client, + public static async Task> ModifyRolesAsync(IGuild guild, DiscordClient client, IEnumerable args) { return await client.ApiClient.ModifyGuildRolesAsync(guild.Id, args).ConfigureAwait(false); } - public static async Task LeaveAsync(IGuild guild, DiscordRestClient client) + public static async Task LeaveAsync(IGuild guild, DiscordClient client) { await client.ApiClient.LeaveGuildAsync(guild.Id).ConfigureAwait(false); } - public static async Task DeleteAsync(IGuild guild, DiscordRestClient client) + public static async Task DeleteAsync(IGuild guild, DiscordClient client) { await client.ApiClient.DeleteGuildAsync(guild.Id).ConfigureAwait(false); } //Bans - public static async Task> GetBansAsync(IGuild guild, DiscordRestClient client) + public static async Task> GetBansAsync(IGuild guild, DiscordClient client) { var models = await client.ApiClient.GetGuildBansAsync(guild.Id); return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); } - public static async Task AddBanAsync(IGuild guild, DiscordRestClient client, + public static async Task AddBanAsync(IGuild guild, DiscordClient client, ulong userId, int pruneDays) { var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays }; await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args); } - public static async Task RemoveBanAsync(IGuild guild, DiscordRestClient client, + public static async Task RemoveBanAsync(IGuild guild, DiscordClient client, ulong userId) { await client.ApiClient.RemoveGuildBanAsync(guild.Id, userId); } //Channels - public static async Task GetChannelAsync(IGuild guild, DiscordRestClient client, + public static async Task GetChannelAsync(IGuild guild, DiscordClient client, ulong id) { var model = await client.ApiClient.GetChannelAsync(guild.Id, id).ConfigureAwait(false); @@ -84,12 +84,12 @@ namespace Discord.Rest return RestGuildChannel.Create(client, model); return null; } - public static async Task> GetChannelsAsync(IGuild guild, DiscordRestClient client) + public static async Task> GetChannelsAsync(IGuild guild, DiscordClient client) { var models = await client.ApiClient.GetGuildChannelsAsync(guild.Id).ConfigureAwait(false); return models.Select(x => RestGuildChannel.Create(client, x)).ToImmutableArray(); } - public static async Task CreateTextChannelAsync(IGuild guild, DiscordRestClient client, + public static async Task CreateTextChannelAsync(IGuild guild, DiscordClient client, string name) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -98,7 +98,7 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args).ConfigureAwait(false); return RestTextChannel.Create(client, model); } - public static async Task CreateVoiceChannelAsync(IGuild guild, DiscordRestClient client, + public static async Task CreateVoiceChannelAsync(IGuild guild, DiscordClient client, string name) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -109,12 +109,12 @@ namespace Discord.Rest } //Integrations - public static async Task> GetIntegrationsAsync(IGuild guild, DiscordRestClient client) + public static async Task> GetIntegrationsAsync(IGuild guild, DiscordClient client) { var models = await client.ApiClient.GetGuildIntegrationsAsync(guild.Id).ConfigureAwait(false); return models.Select(x => RestGuildIntegration.Create(client, x)).ToImmutableArray(); } - public static async Task CreateIntegrationAsync(IGuild guild, DiscordRestClient client, + public static async Task CreateIntegrationAsync(IGuild guild, DiscordClient client, ulong id, string type) { var args = new CreateGuildIntegrationParams(id, type); @@ -123,14 +123,14 @@ namespace Discord.Rest } //Invites - public static async Task> GetInvitesAsync(IGuild guild, DiscordRestClient client) + public static async Task> GetInvitesAsync(IGuild guild, DiscordClient client) { var models = await client.ApiClient.GetGuildInvitesAsync(guild.Id).ConfigureAwait(false); return models.Select(x => RestInviteMetadata.Create(client, x)).ToImmutableArray(); } //Roles - public static async Task CreateRoleAsync(IGuild guild, DiscordRestClient client, + public static async Task CreateRoleAsync(IGuild guild, DiscordClient client, string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -150,7 +150,7 @@ namespace Discord.Rest } //Users - public static async Task GetUserAsync(IGuild guild, DiscordRestClient client, + public static async Task GetUserAsync(IGuild guild, DiscordClient client, ulong id) { var model = await client.ApiClient.GetGuildMemberAsync(guild.Id, id).ConfigureAwait(false); @@ -158,17 +158,17 @@ namespace Discord.Rest return RestGuildUser.Create(client, model); return null; } - public static async Task GetCurrentUserAsync(IGuild guild, DiscordRestClient client) + public static async Task GetCurrentUserAsync(IGuild guild, DiscordClient client) { return await GetUserAsync(guild, client, client.CurrentUser.Id).ConfigureAwait(false); } - public static async Task> GetUsersAsync(IGuild guild, DiscordRestClient client) + public static async Task> GetUsersAsync(IGuild guild, DiscordClient client) { var args = new GetGuildMembersParams(); var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args).ConfigureAwait(false); return models.Select(x => RestGuildUser.Create(client, x)).ToImmutableArray(); } - public static async Task PruneUsersAsync(IGuild guild, DiscordRestClient client, + public static async Task PruneUsersAsync(IGuild guild, DiscordClient client, int days = 30, bool simulate = false) { var args = new GuildPruneParams(days); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs index 8e2b65576..0d919bf0f 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs @@ -14,7 +14,7 @@ namespace Discord.Rest User = user; Reason = reason; } - internal static RestBan Create(DiscordRestClient client, Model model) + internal static RestBan Create(DiscordClient client, Model model) { return new RestBan(RestUser.Create(client, model.User), model.Reason); } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 4735debf9..48ddfa607 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -39,11 +39,11 @@ namespace Discord.Rest public IReadOnlyCollection Emojis => _emojis; public IReadOnlyCollection Features => _features; - internal RestGuild(DiscordRestClient client, ulong id) + internal RestGuild(DiscordClient client, ulong id) : base(client, id) { } - internal static RestGuild Create(DiscordRestClient discord, Model model) + internal static RestGuild Create(DiscordClient discord, Model model) { var entity = new RestGuild(discord, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs index bbbe57a0d..bb40a4da4 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs @@ -25,11 +25,11 @@ namespace Discord.Rest public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); - internal RestGuildIntegration(DiscordRestClient discord, ulong id) + internal RestGuildIntegration(DiscordClient discord, ulong id) : base(discord, id) { } - internal static RestGuildIntegration Create(DiscordRestClient discord, Model model) + internal static RestGuildIntegration Create(DiscordClient discord, Model model) { var entity = new RestGuildIntegration(discord, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs index e1f51b4a9..fab07ee33 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -15,11 +15,11 @@ namespace Discord.Rest public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); - internal RestUserGuild(DiscordRestClient discord, ulong id) + internal RestUserGuild(DiscordClient discord, ulong id) : base(discord, id) { } - internal static RestUserGuild Create(DiscordRestClient discord, Model model) + internal static RestUserGuild Create(DiscordClient discord, Model model) { var entity = new RestUserGuild(discord, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs index b6e4c2a26..465b2e185 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs @@ -13,11 +13,11 @@ namespace Discord public string SampleHostname { get; private set; } public int SamplePort { get; private set; } - internal RestVoiceRegion(DiscordRestClient client, string id) + internal RestVoiceRegion(DiscordClient client, string id) : base(client, id) { } - internal static RestVoiceRegion Create(DiscordRestClient client, Model model) + internal static RestVoiceRegion Create(DiscordClient client, Model model) { var entity = new RestVoiceRegion(client, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs b/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs index 40bbe3632..3ede5d042 100644 --- a/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs +++ b/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs @@ -5,15 +5,15 @@ namespace Discord.Rest { internal static class InviteHelper { - public static async Task GetAsync(IInvite invite, DiscordRestClient client) + public static async Task GetAsync(IInvite invite, DiscordClient client) { return await client.ApiClient.GetInviteAsync(invite.Code).ConfigureAwait(false); } - public static async Task AcceptAsync(IInvite invite, DiscordRestClient client) + public static async Task AcceptAsync(IInvite invite, DiscordClient client) { await client.ApiClient.AcceptInviteAsync(invite.Code).ConfigureAwait(false); } - public static async Task DeleteAsync(IInvite invite, DiscordRestClient client) + public static async Task DeleteAsync(IInvite invite, DiscordClient client) { await client.ApiClient.DeleteInviteAsync(invite.Code).ConfigureAwait(false); } diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 84af7e029..3cf11b4c6 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -16,11 +16,11 @@ namespace Discord.Rest public string Code => Id; public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; - internal RestInvite(DiscordRestClient discord, string id) + internal RestInvite(DiscordClient discord, string id) : base(discord, id) { } - internal static RestInvite Create(DiscordRestClient discord, Model model) + internal static RestInvite Create(DiscordClient discord, Model model) { var entity = new RestInvite(discord, model.Code); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index 138fd6f66..25ce2fb24 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -18,11 +18,11 @@ namespace Discord.Rest public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); - internal RestInviteMetadata(DiscordRestClient discord, string id) + internal RestInviteMetadata(DiscordClient discord, string id) : base(discord, id) { } - internal static RestInviteMetadata Create(DiscordRestClient discord, Model model) + internal static RestInviteMetadata Create(DiscordClient discord, Model model) { var entity = new RestInviteMetadata(discord, model.Code); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 54f0982b1..f1f0e8461 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -6,26 +6,26 @@ namespace Discord.Rest { internal static class MessageHelper { - public static async Task GetAsync(IMessage msg, DiscordRestClient client) + public static async Task GetAsync(IMessage msg, DiscordClient client) { await client.ApiClient.GetChannelMessageAsync(msg.ChannelId, msg.Id); } - public static async Task ModifyAsync(IMessage msg, DiscordRestClient client, Action func) + public static async Task ModifyAsync(IMessage msg, DiscordClient client, Action func) { var args = new ModifyMessageParams(); func(args); await client.ApiClient.ModifyMessageAsync(msg.ChannelId, msg.Id, args); } - public static async Task DeleteAsync(IMessage msg, DiscordRestClient client) + public static async Task DeleteAsync(IMessage msg, DiscordClient client) { await client.ApiClient.DeleteMessageAsync(msg.ChannelId, msg.Id); } - public static async Task PinAsync(IMessage msg, DiscordRestClient client) + public static async Task PinAsync(IMessage msg, DiscordClient client) { await client.ApiClient.AddPinAsync(msg.ChannelId, msg.Id); } - public static async Task UnpinAsync(IMessage msg, DiscordRestClient client) + public static async Task UnpinAsync(IMessage msg, DiscordClient client) { await client.ApiClient.RemovePinAsync(msg.ChannelId, msg.Id); } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestAttachment.cs b/src/Discord.Net.Rest/Entities/Messages/RestAttachment.cs index c22bf4504..3d9492f2e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestAttachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestAttachment.cs @@ -2,6 +2,7 @@ namespace Discord { + //TODO: Rename to Attachment? public class RestAttachment : IAttachment { public ulong Id { get; } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 58ab4c80b..33ce9c8f0 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -29,12 +29,12 @@ namespace Discord.Rest public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RestMessage(DiscordRestClient discord, ulong id, ulong channelId) + internal RestMessage(DiscordClient discord, ulong id, ulong channelId) : base(discord, id) { ChannelId = channelId; } - internal static RestMessage Create(DiscordRestClient discord, Model model) + internal static RestMessage Create(DiscordClient discord, Model model) { if (model.Type == MessageType.Default) return RestUserMessage.Create(discord, model); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs index 3b805696f..837fd158f 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -8,11 +8,11 @@ namespace Discord.Rest { public MessageType Type { get; private set; } - internal RestSystemMessage(DiscordRestClient discord, ulong id, ulong channelId) + internal RestSystemMessage(DiscordClient discord, ulong id, ulong channelId) : base(discord, id, channelId) { } - internal new static RestSystemMessage Create(DiscordRestClient discord, Model model) + internal new static RestSystemMessage Create(DiscordClient discord, Model model) { var entity = new RestSystemMessage(discord, model.Id, model.ChannelId); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 4db6cc372..b81fbf17f 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -30,11 +30,11 @@ namespace Discord.Rest public override IReadOnlyCollection MentionedRoles => _mentionedRoles; public override IReadOnlyCollection MentionedUsers => _mentionedUsers; - internal RestUserMessage(DiscordRestClient discord, ulong id, ulong channelId) + internal RestUserMessage(DiscordClient discord, ulong id, ulong channelId) : base(discord, id, channelId) { } - internal new static RestUserMessage Create(DiscordRestClient discord, Model model) + internal new static RestUserMessage Create(DiscordClient discord, Model model) { var entity = new RestUserMessage(discord, model.Id, model.ChannelId); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index 3131cc96d..035e6c212 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -17,11 +17,11 @@ namespace Discord.Rest public string IconUrl => API.CDN.GetApplicationIconUrl(Id, _iconId); - internal RestApplication(DiscordRestClient discord, ulong id) + internal RestApplication(DiscordClient discord, ulong id) : base(discord, id) { } - internal static RestApplication Create(DiscordRestClient discord, Model model) + internal static RestApplication Create(DiscordClient discord, Model model) { var entity = new RestApplication(discord, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/RestEntity.cs b/src/Discord.Net.Rest/Entities/RestEntity.cs index 496dfe9c6..d5bad8147 100644 --- a/src/Discord.Net.Rest/Entities/RestEntity.cs +++ b/src/Discord.Net.Rest/Entities/RestEntity.cs @@ -5,10 +5,10 @@ namespace Discord.Rest public abstract class RestEntity : IEntity where T : IEquatable { + public DiscordClient Discord { get; } public T Id { get; } - public DiscordRestClient Discord { get; } - public RestEntity(DiscordRestClient discord, T id) + internal RestEntity(DiscordClient discord, T id) { Discord = discord; Id = id; diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index ccac5f3ad..b0f3255f4 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -21,11 +21,11 @@ namespace Discord.Rest public bool IsEveryone => Id == Guild.Id; public string Mention => MentionUtils.MentionRole(Id); - internal RestRole(DiscordRestClient discord, ulong id) + internal RestRole(DiscordClient discord, ulong id) : base(discord, id) { } - internal static RestRole Create(DiscordRestClient discord, Model model) + internal static RestRole Create(DiscordClient discord, Model model) { var entity = new RestRole(discord, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index 864e81630..dc66e9288 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -7,11 +7,11 @@ namespace Discord.Rest internal static class RoleHelper { //General - public static async Task DeleteAsync(IRole role, DiscordRestClient client) + public static async Task DeleteAsync(IRole role, DiscordClient client) { await client.ApiClient.DeleteGuildRoleAsync(role.Guild.Id, role.Id).ConfigureAwait(false); } - public static async Task ModifyAsync(IRole role, DiscordRestClient client, + public static async Task ModifyAsync(IRole role, DiscordClient client, Action func) { var args = new ModifyGuildRoleParams(); diff --git a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs index a8ea34550..a9ef2907d 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs @@ -6,11 +6,11 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestGroupUser : RestUser, IGroupUser { - internal RestGroupUser(DiscordRestClient discord, ulong id) + internal RestGroupUser(DiscordClient discord, ulong id) : base(discord, id) { } - internal new static RestGroupUser Create(DiscordRestClient discord, Model model) + internal new static RestGroupUser Create(DiscordClient discord, Model model) { var entity = new RestGroupUser(discord, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index c2e25f735..489672e31 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -21,11 +21,11 @@ namespace Discord.Rest public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - internal RestGuildUser(DiscordRestClient discord, ulong id) + internal RestGuildUser(DiscordClient discord, ulong id) : base(discord, id) { } - internal static RestGuildUser Create(DiscordRestClient discord, Model model) + internal static RestGuildUser Create(DiscordClient discord, Model model) { var entity = new RestGuildUser(discord, model.User.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs index 389a2d57d..115db9ac5 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs @@ -13,11 +13,11 @@ namespace Discord.Rest public bool IsVerified { get; private set; } public bool IsMfaEnabled { get; private set; } - internal RestSelfUser(DiscordRestClient discord, ulong id) + internal RestSelfUser(DiscordClient discord, ulong id) : base(discord, id) { } - internal new static RestSelfUser Create(DiscordRestClient discord, Model model) + internal new static RestSelfUser Create(DiscordClient discord, Model model) { var entity = new RestSelfUser(discord, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 05e4aa3b2..492a18bd4 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -18,11 +18,11 @@ namespace Discord.Rest public virtual Game? Game => null; public virtual UserStatus Status => UserStatus.Unknown; - internal RestUser(DiscordRestClient discord, ulong id) + internal RestUser(DiscordClient discord, ulong id) : base(discord, id) { } - internal static RestUser Create(DiscordRestClient discord, Model model) + internal static RestUser Create(DiscordClient discord, Model model) { var entity = new RestUser(discord, model.Id); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 08a59f2c0..da8e57fad 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -8,22 +8,22 @@ namespace Discord.Rest { internal static class UserHelper { - public static async Task GetAsync(IUser user, DiscordRestClient client) + public static async Task GetAsync(IUser user, DiscordClient client) { return await client.ApiClient.GetUserAsync(user.Id); } - public static async Task GetAsync(ISelfUser user, DiscordRestClient client) + public static async Task GetAsync(ISelfUser user, DiscordClient client) { var model = await client.ApiClient.GetMyUserAsync(); if (model.Id != user.Id) throw new InvalidOperationException("Unable to update this object using a different token."); return model; } - public static async Task GetAsync(IGuildUser user, DiscordRestClient client) + public static async Task GetAsync(IGuildUser user, DiscordClient client) { return await client.ApiClient.GetGuildMemberAsync(user.GuildId, user.Id); } - public static async Task ModifyAsync(ISelfUser user, DiscordRestClient client, Action func) + public static async Task ModifyAsync(ISelfUser user, DiscordClient client, Action func) { if (user.Id != client.CurrentUser.Id) throw new InvalidOperationException("Unable to modify this object using a different token."); @@ -32,19 +32,19 @@ namespace Discord.Rest func(args); await client.ApiClient.ModifySelfAsync(args); } - public static async Task ModifyAsync(IGuildUser user, DiscordRestClient client, Action func) + public static async Task ModifyAsync(IGuildUser user, DiscordClient client, Action func) { var args = new ModifyGuildMemberParams(); func(args); await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, args); } - public static async Task KickAsync(IGuildUser user, DiscordRestClient client) + public static async Task KickAsync(IGuildUser user, DiscordClient client) { await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id); } - public static async Task CreateDMChannelAsync(IUser user, DiscordRestClient client) + public static async Task CreateDMChannelAsync(IUser user, DiscordClient client) { var args = new CreateDMChannelParams(user.Id); return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args)); diff --git a/src/Discord.Net.Rest/project.json b/src/Discord.Net.Rest/project.json index 58cb60473..ac02cf639 100644 --- a/src/Discord.Net.Rest/project.json +++ b/src/Discord.Net.Rest/project.json @@ -1,12 +1,6 @@ { "version": "1.0.0-beta2-*", - "buildOptions": { - "compile": { - "include": [ "../Discord.Net.Utils/**.cs" ] - } - }, - "configurations": { "Release": { "buildOptions": { diff --git a/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs b/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs index b128f13b0..666d92105 100644 --- a/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs +++ b/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs @@ -3,6 +3,7 @@ using Discord.API.Rpc; using Discord.Net.Queue; using Discord.Net.Rest; using Discord.Net.WebSockets; +using Discord.Rpc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -66,8 +67,8 @@ namespace Discord.API public ConnectionState ConnectionState { get; private set; } - public DiscordRpcApiClient(string clientId, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) - : base(restClientProvider, serializer, requestQueue) + public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) + : base(restClientProvider, userAgent, serializer, requestQueue) { _connectionLock = new SemaphoreSlim(1, 1); _clientId = clientId; diff --git a/src/Discord.Net.Rpc/AssemblyInfo.cs b/src/Discord.Net.Rpc/AssemblyInfo.cs new file mode 100644 index 000000000..8767c83af --- /dev/null +++ b/src/Discord.Net.Rpc/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Test")] \ No newline at end of file diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.cs b/src/Discord.Net.Rpc/DiscordRpcClient.cs index f916f42bf..8cbda4155 100644 --- a/src/Discord.Net.Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcClient.cs @@ -11,9 +11,9 @@ using System.Threading.Tasks; namespace Discord.Rpc { - public partial class DiscordRpcClient : DiscordRestClient + public partial class DiscordRpcClient : DiscordClient { - private readonly ILogger _rpcLogger; + private readonly Logger _rpcLogger; private readonly JsonSerializer _serializer; private TaskCompletionSource _connectTask; @@ -58,18 +58,7 @@ namespace Discord.Rpc }; } private static API.DiscordRpcApiClient CreateApiClient(DiscordRpcConfig config) - => new API.DiscordRpcApiClient(config.ClientId, config.Origin, config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); - - internal override void Dispose(bool disposing) - { - if (!_isDisposed) - ApiClient.Dispose(); - } - - protected override Task ValidateTokenAsync(TokenType tokenType, string token) - { - return Task.CompletedTask; //Validation is done in DiscordRpcAPIClient - } + => new API.DiscordRpcApiClient(config.ClientId, DiscordRestConfig.UserAgent, config.Origin, config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); /// public Task ConnectAsync() => ConnectAsync(false); @@ -371,20 +360,20 @@ namespace Discord.Rpc //Messages case "MESSAGE_CREATE": { - await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + /*await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); var msg = new RpcMessage(this, data.Message); - await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); + await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false);*/ } break; case "MESSAGE_UPDATE": { - await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + /*await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); var msg = new RpcMessage(this, data.Message); - await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); + await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false);*/ } break; case "MESSAGE_DELETE": diff --git a/src/Discord.Net.Rpc/Entities/IRemoteUserGuild.cs b/src/Discord.Net.Rpc/Entities/IRemoteUserGuild.cs deleted file mode 100644 index 5f92edb9b..000000000 --- a/src/Discord.Net.Rpc/Entities/IRemoteUserGuild.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord.Rpc -{ - /*public interface IRemoteUserGuild : ISnowflakeEntity - { - /// Gets the name of this guild. - string Name { get; } - }*/ -} diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs new file mode 100644 index 000000000..f98067051 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs @@ -0,0 +1,10 @@ +namespace Discord.Rpc +{ + /*internal class RpcMessage : RpcEntity, IMessage + { + internal RpcMessage(DiscordRpcClient discord, API.Message model) + : base(dicsord, model.Id) + { + } + }*/ +} diff --git a/src/Discord.Net.Rpc/Entities/RpcEntity.cs b/src/Discord.Net.Rpc/Entities/RpcEntity.cs new file mode 100644 index 000000000..9fecd77ff --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/RpcEntity.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.Rpc +{ + public abstract class RpcEntity : IEntity + where T : IEquatable + { + public DiscordRpcClient Discord { get; } + public T Id { get; } + + internal RpcEntity(DiscordRpcClient discord, T id) + { + Discord = discord; + Id = id; + } + + IDiscordClient IEntity.Discord => Discord; + } +} diff --git a/src/Discord.Net.Rpc/Entities/RemoteUserGuild.cs b/src/Discord.Net.Rpc/Entities/RpcGuild.cs similarity index 78% rename from src/Discord.Net.Rpc/Entities/RemoteUserGuild.cs rename to src/Discord.Net.Rpc/Entities/RpcGuild.cs index 98bb1c027..c42afb975 100644 --- a/src/Discord.Net.Rpc/Entities/RemoteUserGuild.cs +++ b/src/Discord.Net.Rpc/Entities/RpcGuild.cs @@ -4,7 +4,7 @@ using Model = Discord.API.Rpc.RpcUserGuild; namespace Discord.Rpc { - /*internal class RemoteUserGuild : IRemoteUserGuild, ISnowflakeEntity + /*internal class RemoteUserGuild : RpcEntity, IRemoteUserGuild, ISnowflakeEntity { public ulong Id { get; } public DiscordRestClient Discord { get; } @@ -12,7 +12,7 @@ namespace Discord.Rpc public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); - public RemoteUserGuild(DiscordRestClient discord, Model model) + internal RemoteUserGuild(DiscordRestClient discord, Model model) { Id = model.Id; Discord = discord; diff --git a/src/Discord.Net.Rpc/Entities/RpcMessage.cs b/src/Discord.Net.Rpc/Entities/RpcMessage.cs deleted file mode 100644 index 86d67bfa4..000000000 --- a/src/Discord.Net.Rpc/Entities/RpcMessage.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Discord.Rest; - -namespace Discord.Rpc -{ - internal class RpcMessage : Message - { - public override DiscordRestClient Discord { get; } - - public RpcMessage(DiscordRpcClient discord, API.Message model) - : base(null, model.Author.IsSpecified ? new User(model.Author.Value) : null, model) - { - Discord = discord; - } - } -} diff --git a/src/Discord.Net.Rpc/project.json b/src/Discord.Net.Rpc/project.json index 9a7de9a07..0d2860743 100644 --- a/src/Discord.Net.Rpc/project.json +++ b/src/Discord.Net.Rpc/project.json @@ -1,19 +1,35 @@ { "version": "1.0.0-beta2-*", - "buildOptions": { - "compile": { - "include": [ "../Discord.Net.Utils/**.cs" ] + "configurations": { + "Release": { + "buildOptions": { + "define": [ "RELEASE" ], + "nowarn": [ "CS1573", "CS1591" ], + "optimize": true, + "warningsAsErrors": true, + "xmlDoc": true + } } }, "dependencies": { + "Discord.Net.Core": { + "target": "project" + }, + "Discord.Net.Rest": { + "target": "project" + }, "NETStandard.Library": "1.6.0" }, "frameworks": { - "netstandard1.6": { - "imports": "dnxcore50" + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] } } } diff --git a/src/Discord.Net.Utils/Discord.Net.Utils.projitems b/src/Discord.Net.Utils/Discord.Net.Utils.projitems deleted file mode 100644 index 2a493f22a..000000000 --- a/src/Discord.Net.Utils/Discord.Net.Utils.projitems +++ /dev/null @@ -1,26 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 2b75119c-9893-4aaa-8d38-6176eeb09060 - - - Discord - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Discord.Net.Utils/Discord.Net.Utils.shproj b/src/Discord.Net.Utils/Discord.Net.Utils.shproj deleted file mode 100644 index e3beed6a1..000000000 --- a/src/Discord.Net.Utils/Discord.Net.Utils.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 2b75119c-9893-4aaa-8d38-6176eeb09060 - 14.0 - - - - - - - - diff --git a/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs index 38124de63..39c929ab6 100644 --- a/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs @@ -4,6 +4,7 @@ using Discord.API.Rest; using Discord.Net.Queue; using Discord.Net.Rest; using Discord.Net.WebSockets; +using Discord.WebSocket; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -31,8 +32,8 @@ namespace Discord.API public ConnectionState ConnectionState { get; private set; } - public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) - : base(restClientProvider, serializer, requestQueue) + public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) + : base(restClientProvider, userAgent, serializer, requestQueue) { _gatewayClient = webSocketProvider(); //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) diff --git a/src/Discord.Net.WebSocket/AssemblyInfo.cs b/src/Discord.Net.WebSocket/AssemblyInfo.cs new file mode 100644 index 000000000..8767c83af --- /dev/null +++ b/src/Discord.Net.WebSocket/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Test")] \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 2c4511edf..202c65da9 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -5,6 +5,7 @@ using Discord.WebSocket; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; +using System.IO; using System.Linq; using System.Net; using System.Text; @@ -34,10 +35,7 @@ namespace Discord.Audio } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); - private readonly ILogger _audioLogger; -#if BENCHMARK - private readonly ILogger _benchmarkLogger; -#endif + private readonly Logger _audioLogger; internal readonly SemaphoreSlim _connectionLock; private readonly JsonSerializer _serializer; @@ -63,9 +61,6 @@ namespace Discord.Audio Guild = guild; _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); -#if BENCHMARK - _benchmarkLogger = logManager.CreateLogger("Benchmark"); -#endif _connectionLock = new SemaphoreSlim(1, 1); @@ -181,11 +176,11 @@ namespace Discord.Audio ApiClient.SendAsync(data, count).ConfigureAwait(false); } - public RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) + public Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) { return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); } - public OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, + public Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) { return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, bitrate, application, bufferSize); @@ -193,11 +188,6 @@ namespace Discord.Audio private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { -#if BENCHMARK - Stopwatch stopwatch = Stopwatch.StartNew(); - try - { -#endif try { switch (opCode) @@ -262,15 +252,6 @@ namespace Discord.Audio await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); return; } -#if BENCHMARK - } - finally - { - stopwatch.Stop(); - double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - await _benchmarkLogger.DebugAsync($"{millis} ms").ConfigureAwait(false); - } -#endif } private async Task ProcessPacketAsync(byte[] packet) { diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index c059955a8..3a650eeaf 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -1,6 +1,6 @@ namespace Discord.Audio { - public class OpusDecodeStream : RTPReadStream + internal class OpusDecodeStream : RTPReadStream { private readonly byte[] _buffer; private readonly OpusDecoder _decoder; diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index ef773ca56..69d8b3d81 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -1,6 +1,6 @@ namespace Discord.Audio { - public class OpusEncodeStream : RTPWriteStream + internal class OpusEncodeStream : RTPWriteStream { public int SampleRate = 48000; public int Channels = 2; diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 4bf7f5e1b..cfc804abe 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -4,7 +4,7 @@ using System.IO; namespace Discord.Audio { - public class RTPReadStream : Stream + internal class RTPReadStream : Stream { private readonly BlockingCollection _queuedData; //TODO: Replace with max-length ring buffer private readonly AudioClient _audioClient; diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index d547f021a..db755c877 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -3,7 +3,7 @@ using System.IO; namespace Discord.Audio { - public class RTPWriteStream : Stream + internal class RTPWriteStream : Stream { private readonly AudioClient _audioClient; private readonly byte[] _nonce, _secretKey; diff --git a/src/Discord.Net.WebSocket/DataStore.cs b/src/Discord.Net.WebSocket/DataStore.cs index 9be13b9f0..d2b93c671 100644 --- a/src/Discord.Net.WebSocket/DataStore.cs +++ b/src/Discord.Net.WebSocket/DataStore.cs @@ -12,37 +12,37 @@ namespace Discord.WebSocket private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth - private readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _channels; private readonly ConcurrentDictionary _dmChannels; private readonly ConcurrentDictionary _guilds; private readonly ConcurrentDictionary _users; private readonly ConcurrentHashSet _groupChannels; - internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); + internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); internal IReadOnlyCollection GroupChannels => _groupChannels.Select(x => GetChannel(x) as SocketGroupChannel).ToReadOnlyCollection(_groupChannels); internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); - internal IReadOnlyCollection PrivateChannels => - _dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat( - _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) + internal IReadOnlyCollection PrivateChannels => + _dmChannels.Select(x => x.Value as IPrivateChannel).Concat( + _groupChannels.Select(x => GetChannel(x) as IPrivateChannel)) .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); public DataStore(int guildCount, int dmChannelCount) { double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; - _channels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); + _channels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); _dmChannels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); _guilds = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); _groupChannels = new ConcurrentHashSet(CollectionConcurrencyLevel, (int)(10 * CollectionMultiplier)); } - internal ISocketChannel GetChannel(ulong id) + internal SocketChannel GetChannel(ulong id) { - ISocketChannel channel; + SocketChannel channel; if (_channels.TryGetValue(id, out channel)) return channel; return null; @@ -54,7 +54,7 @@ namespace Discord.WebSocket return channel; return null; } - internal void AddChannel(ISocketChannel channel) + internal void AddChannel(SocketChannel channel) { _channels[channel.Id] = channel; @@ -68,9 +68,9 @@ namespace Discord.WebSocket _groupChannels.TryAdd(groupChannel.Id); } } - internal ISocketChannel RemoveChannel(ulong id) + internal SocketChannel RemoveChannel(ulong id) { - ISocketChannel channel; + SocketChannel channel; if (_channels.TryRemove(id, out channel)) { var dmChannel = channel as SocketDMChannel; diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj index da99d6e1b..45e13b5ce 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj @@ -4,18 +4,16 @@ 14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - 22ab6c66-536c-4ac2-bbdb-a8bc4eb6b14d - Discord.Net.WebSocket + Discord.WebSocket .\obj .\bin\ v4.6.1 - 2.0 - + \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 00b53843d..6f93f7fa6 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,5 @@ -using Discord.API.Gateway; +using Discord.API; +using Discord.API.Gateway; using Discord.Audio; using Discord.Logging; using Discord.Net.Converters; @@ -11,24 +12,22 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Discord.WebSocket { - public partial class DiscordSocketClient : DiscordRestClient, IDiscordClient + public partial class DiscordSocketClient : DiscordClient, IDiscordClient { private readonly ConcurrentQueue _largeGuilds; - private readonly ILogger _gatewayLogger; -#if BENCHMARK - private readonly ILogger _benchmarkLogger; -#endif + private readonly Logger _gatewayLogger; private readonly JsonSerializer _serializer; private string _sessionId; private int _lastSeq; - private ImmutableDictionary _voiceRegions; + private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; private CancellationTokenSource _cancelToken, _reconnectCancelToken; private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; @@ -54,16 +53,17 @@ namespace Discord.WebSocket internal int ConnectionTimeout { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } - public new API.DiscordSocketApiClient ApiClient => base.ApiClient as API.DiscordSocketApiClient; - internal SocketSelfUser CurrentUser => _currentUser as SocketSelfUser; + public new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + public new SocketSelfUser CurrentUser => base.CurrentUser as SocketSelfUser; + public IReadOnlyCollection PrivateChannels => DataStore.PrivateChannels; internal IReadOnlyCollection Guilds => DataStore.Guilds; - internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); /// Creates a new REST/WebSocket discord client. public DiscordSocketClient() : this(new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket discord client. - public DiscordSocketClient(DiscordSocketConfig config) - : base(config, CreateApiClient(config)) + public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config)) { } + private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client) + : base(config, client) { ShardId = config.ShardId; TotalShards = config.TotalShards; @@ -72,14 +72,10 @@ namespace Discord.WebSocket AudioMode = config.AudioMode; WebSocketProvider = config.WebSocketProvider; ConnectionTimeout = config.ConnectionTimeout; - DataStore = new DataStore(0, 0); + _nextAudioId = 1; - _gatewayLogger = LogManager.CreateLogger("Gateway"); -#if BENCHMARK - _benchmarkLogger = _log.CreateLogger("Benchmark"); -#endif _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => @@ -107,25 +103,25 @@ namespace Discord.WebSocket GuildUnavailable += async g => await _gatewayLogger.VerboseAsync($"Disconnected from {g.Name}").ConfigureAwait(false); LatencyUpdated += async (old, val) => await _gatewayLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); - _voiceRegions = ImmutableDictionary.Create(); + _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) - => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); - + => new API.DiscordSocketApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, config.WebSocketProvider, requestQueue: new RequestQueue()); + protected override async Task OnLoginAsync(TokenType tokenType, string token) { var voiceRegions = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); - _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); + _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); } protected override async Task OnLogoutAsync() { if (ConnectionState != ConnectionState.Disconnected) await DisconnectInternalAsync(null, false).ConfigureAwait(false); - _voiceRegions = ImmutableDictionary.Create(); + _voiceRegions = ImmutableDictionary.Create(); } - + /// public async Task ConnectAsync(bool waitForGuilds = true) { @@ -319,127 +315,55 @@ namespace Discord.WebSocket } /// - public override Task GetVoiceRegionAsync(string id) - { - VoiceRegion region; - if (_voiceRegions.TryGetValue(id, out region)) - return Task.FromResult(region); - return Task.FromResult(null); - } + public Task GetApplicationInfoAsync() + => ClientHelper.GetApplicationInfoAsync(this); + /// - public override Task GetGuildAsync(ulong id) - { - return Task.FromResult(DataStore.GetGuild(id)); - } - public override Task GetGuildEmbedAsync(ulong id) - { - var guild = DataStore.GetGuild(id); - if (guild != null) - return Task.FromResult(new GuildEmbed(guild.IsEmbeddable, guild.EmbedChannelId)); - else - return Task.FromResult(null); - } - public override Task> GetGuildSummariesAsync() - { - return Task.FromResult>(Guilds); - } - public override Task> GetGuildsAsync() + public SocketGuild GetGuild(ulong id) { - return Task.FromResult>(Guilds); + return DataStore.GetGuild(id); } - internal SocketGuild AddGuild(ExtendedGuild model, DataStore dataStore) - { - var guild = new SocketGuild(this, model, dataStore); - dataStore.AddGuild(guild); - if (model.Large) - _largeGuilds.Enqueue(model.Id); - return guild; - } - internal SocketGuild RemoveGuild(ulong id) - { - var guild = DataStore.RemoveGuild(id); - if (guild != null) - { - foreach (var channel in guild.Channels) - DataStore.RemoveChannel(id); - foreach (var user in guild.Members) - user.User.RemoveRef(this); - } - return guild; - } - /// - public override Task GetChannelAsync(ulong id) - { - return Task.FromResult(DataStore.GetChannel(id)); - } - public override Task> GetPrivateChannelsAsync() - { - return Task.FromResult>(DataStore.PrivateChannels); - } - internal ISocketChannel AddPrivateChannel(API.Channel model, DataStore dataStore) - { - switch (model.Type) - { - case ChannelType.DM: - { - var recipients = model.Recipients.Value; - var user = GetOrAddUser(recipients[0], dataStore); - var channel = new SocketDMChannel(this, new SocketDMUser(user), model); - dataStore.AddChannel(channel); - return channel; - } - case ChannelType.Group: - { - var channel = new SocketGroupChannel(this, model); - channel.UpdateUsers(model.Recipients.Value, dataStore); - dataStore.AddChannel(channel); - return channel; - } - default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } - } - internal ISocketChannel RemovePrivateChannel(ulong id) - { - var channel = DataStore.RemoveChannel(id) as ISocketPrivateChannel; - if (channel != null) - { - foreach (var recipient in channel.Recipients) - recipient.User.RemoveRef(this); - } - return channel; - } + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); /// - public override Task GetUserAsync(ulong id) + public IChannel GetChannel(ulong id) { - return Task.FromResult(DataStore.GetUser(id)); + return DataStore.GetChannel(id); } + /// - public override Task GetUserAsync(string username, string discriminator) - { - return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); - } + public Task> GetConnectionsAsync() + => ClientHelper.GetConnectionsAsync(this); + + /// + public Task GetInviteAsync(string inviteId) + => ClientHelper.GetInviteAsync(this, inviteId); + /// - public override Task GetCurrentUserAsync() + public IUser GetUser(ulong id) { - return Task.FromResult(_currentUser); + return DataStore.GetUser(id); } - internal SocketGlobalUser GetOrAddUser(API.User model, DataStore dataStore) + /// + public IUser GetUser(string username, string discriminator) { - var user = dataStore.GetOrAddUser(model.Id, _ => new SocketGlobalUser(model)); - user.AddRef(); - return user; + return DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); } - internal SocketGlobalUser RemoveUser(ulong id) + + /// + public RestVoiceRegion GetVoiceRegion(string id) { - return DataStore.RemoveUser(id); + RestVoiceRegion region; + if (_voiceRegions.TryGetValue(id, out region)) + return region; + return null; } /// Downloads the users list for all large guilds. - public Task DownloadAllUsersAsync() + /*public Task DownloadAllUsersAsync() => DownloadUsersAsync(DataStore.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) @@ -490,20 +414,10 @@ namespace Discord.WebSocket else await Task.WhenAll(batchTasks).ConfigureAwait(false); } - } + }*/ - public override Task> GetVoiceRegionsAsync() - { - return Task.FromResult>(_voiceRegions.ToReadOnlyCollection()); - } - private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) { -#if BENCHMARK - Stopwatch stopwatch = Stopwatch.StartNew(); - try - { -#endif if (seq != null) _lastSeq = seq.Value; try @@ -516,7 +430,7 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); _heartbeatTime = 0; - _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token, _clientLogger); + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token, LogManager.ClientLogger); } break; case GatewayOpCode.Heartbeat: @@ -574,9 +488,9 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); var dataStore = new DataStore(data.Guilds.Length, data.PrivateChannels.Length); - var currentUser = new SocketSelfUser(this, data.User); + var currentUser = SocketSelfUser.Create(this, 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); @@ -586,10 +500,10 @@ namespace Discord.WebSocket await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); } for (int i = 0; i < data.PrivateChannels.Length; i++) - AddPrivateChannel(data.PrivateChannels[i], dataStore); + AddPrivateChannel(data.PrivateChannels[i], dataStore);*/ _sessionId = data.SessionId; - _currentUser = currentUser; + base.CurrentUser = currentUser; _unavailableGuilds = unavailableGuilds; DataStore = dataStore; } @@ -603,7 +517,7 @@ namespace Discord.WebSocket await SyncGuildsAsync().ConfigureAwait(false); _lastGuildAvailableTime = Environment.TickCount; - _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token, _clientLogger); + _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token, LogManager.ClientLogger); await _readyEvent.InvokeAsync().ConfigureAwait(false); @@ -611,7 +525,7 @@ namespace Discord.WebSocket await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); } break; - case "RESUMED": + /*case "RESUMED": { await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); @@ -1366,7 +1280,7 @@ namespace Discord.WebSocket } else { - before = new Presence(null, UserStatus.Offline); + before = new SocketPresence(null, UserStatus.Offline); user = guild.AddOrUpdateUser(data, DataStore); } @@ -1430,7 +1344,7 @@ namespace Discord.WebSocket if (data.GuildId.HasValue) { ISocketUser user; - VoiceState before, after; + SocketVoiceState before, after; if (data.GuildId != null) { var guild = DataStore.GetGuild(data.GuildId.Value); @@ -1444,7 +1358,7 @@ namespace Discord.WebSocket if (data.ChannelId != null) { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); + before = guild.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); after = guild.AddOrUpdateVoiceState(data, DataStore); if (data.UserId == _currentUser.Id) { @@ -1453,8 +1367,8 @@ namespace Discord.WebSocket } else { - before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); - after = new VoiceState(null, data); + before = guild.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); + after = new SocketVoiceState(null, data); } user = guild.GetUser(data.UserId); @@ -1472,13 +1386,13 @@ namespace Discord.WebSocket { if (data.ChannelId != null) { - before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); after = groupChannel.AddOrUpdateVoiceState(data, DataStore); } else { - before = groupChannel.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); - after = new VoiceState(null, data); + before = groupChannel.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); + after = new SocketVoiceState(null, data); } user = groupChannel.GetUser(data.UserId); } @@ -1518,7 +1432,7 @@ namespace Discord.WebSocket } } - return; + return;*/ //Ignored (User only) case "CHANNEL_PINS_ACK": @@ -1550,18 +1464,9 @@ namespace Discord.WebSocket await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); return; } -#if BENCHMARK - } - finally - { - stopwatch.Stop(); - double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - await _benchmarkLogger.DebugAsync($"{millis} ms").ConfigureAwait(false); - } -#endif } - private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken, ILogger logger) + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken, Logger logger) { try { @@ -1601,7 +1506,7 @@ namespace Discord.WebSocket await logger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); } } - private async Task WaitForGuildsAsync(CancellationToken cancelToken, ILogger logger) + private async Task WaitForGuildsAsync(CancellationToken cancelToken, Logger logger) { //Wait for GUILD_AVAILABLEs try @@ -1626,5 +1531,42 @@ namespace Discord.WebSocket if (guildIds.Length > 0) await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); } + + //IDiscordClient + DiscordRestApiClient IDiscordClient.ApiClient => ApiClient; + + Task IDiscordClient.ConnectAsync() + => ConnectAsync(); + + async Task IDiscordClient.GetApplicationInfoAsync() + => await GetApplicationInfoAsync().ConfigureAwait(false); + + Task IDiscordClient.GetChannelAsync(ulong id) + => Task.FromResult(GetChannel(id)); + Task> IDiscordClient.GetPrivateChannelsAsync() + => Task.FromResult>(PrivateChannels); + + async Task> IDiscordClient.GetConnectionsAsync() + => await GetConnectionsAsync(); + + async Task IDiscordClient.GetInviteAsync(string inviteId) + => await GetInviteAsync(inviteId); + + Task IDiscordClient.GetGuildAsync(ulong id) + => Task.FromResult(GetGuild(id)); + Task> IDiscordClient.GetGuildsAsync() + => Task.FromResult>(Guilds); + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) + => await CreateGuildAsync(name, region, jpegIcon); + + Task IDiscordClient.GetUserAsync(ulong id) + => Task.FromResult(GetUser(id)); + Task IDiscordClient.GetUserAsync(string username, string discriminator) + => Task.FromResult(GetUser(username, discriminator)); + + Task> IDiscordClient.GetVoiceRegionsAsync() + => Task.FromResult>(_voiceRegions.ToReadOnlyCollection()); + Task IDiscordClient.GetVoiceRegionAsync(string id) + => Task.FromResult(GetVoiceRegion(id)); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs new file mode 100644 index 000000000..c9f38caec --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public abstract class SocketChannel : SocketEntity, IChannel + { + internal SocketChannel(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + internal static SocketChannel Create(DiscordSocketClient discord, 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); + case ChannelType.Group: + return SocketGroupChannel.Create(discord, model); + default: + throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); + } + } + + //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(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index c19508856..6b87fbae2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -1,77 +1,138 @@ -using System.Collections.Generic; +using Discord.Rest; +using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; using System.Threading.Tasks; using MessageModel = Discord.API.Message; using Model = Discord.API.Channel; namespace Discord.WebSocket { - internal class DMChannel : IDMChannel, ISocketChannel, ISocketMessageChannel, ISocketPrivateChannel + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketDMChannel : SocketChannel, IDMChannel { - private readonly MessageManager _messages; + private readonly MessageCache _messages; - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketDMUser Recipient => base.Recipient as SocketDMUser; - public IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); - IReadOnlyCollection ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + public SocketUser Recipient { get; private set; } - public SocketDMChannel(DiscordSocketClient discord, SocketDMUser recipient, Model model) - : base(discord, recipient, model) + public IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); + + internal SocketDMChannel(DiscordSocketClient discord, ulong id, ulong recipientId) + : base(discord, id) { + Recipient = new SocketUser(Discord, recipientId); if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord, this); - else - _messages = new MessageManager(Discord, this); } + internal new static SocketDMChannel Create(DiscordSocketClient discord, Model model) + { + var entity = new SocketDMChannel(discord, model.Id, model.Recipients.Value[0].Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Recipient.Update(model.Recipients.Value[0]); + } + + public Task CloseAsync() + => ChannelHelper.DeleteAsync(this, Discord); - public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); - public override Task> GetUsersAsync() => Task.FromResult>(Users); - public ISocketUser GetUser(ulong id) + public SocketUser GetUser(ulong id) { - var currentUser = Discord.CurrentUser; if (id == Recipient.Id) return Recipient; - else if (id == currentUser.Id) - return currentUser; + else if (id == Discord.CurrentUser.Id) + return Discord.CurrentUser as SocketSelfUser; else return null; } - public override async Task GetMessageAsync(ulong id) - { - return await _messages.DownloadAsync(id).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(int limit) - { - return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - { - return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); - } - public ISocketMessage CreateMessage(ISocketUser author, MessageModel model) + public SocketMessage GetMessage(ulong id) + => _messages?.Get(id); + public async Task GetMessageAsync(ulong id, bool allowDownload = true) { - return _messages.Create(author, model); + IMessage msg = _messages?.Get(id); + if (msg == null && allowDownload) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id); + return msg; } - public ISocketMessage AddMessage(ISocketUser author, MessageModel model) + 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> GetPinnedMessagesAsync() + => ChannelHelper.GetPinnedMessagesAsync(this, Discord); + + public Task SendMessageAsync(string text, bool isTTS) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS); + public Task SendFileAsync(string filePath, string text, bool isTTS) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS); + + public Task DeleteMessagesAsync(IEnumerable messages) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages); + + public IDisposable EnterTypingState() + => ChannelHelper.EnterTypingState(this, Discord); + + internal SocketMessage AddMessage(SocketUser author, MessageModel model) { - var msg = _messages.Create(author, model); + var msg = SocketMessage.Create(Discord, author, model); _messages.Add(msg); return msg; } - public ISocketMessage GetMessage(ulong id) - { - return _messages.Get(id); - } - public ISocketMessage RemoveMessage(ulong id) + internal SocketMessage RemoveMessage(ulong id) { return _messages.Remove(id); } public SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); - ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id); - ISocketChannel ISocketChannel.Clone() => Clone(); + public override string ToString() => $"@{Recipient}"; + private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; + + //IDMChannel + IUser IDMChannel.Recipient => 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.GetPinnedMessagesAsync() + => await GetPinnedMessagesAsync().ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) + => await SendFileAsync(filePath, text, isTTS); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS) + => await SendFileAsync(stream, filename, text, isTTS); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS) + => await SendMessageAsync(text, isTTS); + IDisposable IMessageChannel.EnterTypingState() + => EnterTypingState(); + + //IChannel + IReadOnlyCollection IChannel.CachedUsers => Users; + + IUser IChannel.GetCachedUser(ulong id) + => GetUser(id); + Task IChannel.GetUserAsync(ulong id) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IChannel.GetUsersAsync() + => ImmutableArray.Create>().ToAsyncEnumerable(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index cbe6d9c8f..7dc81fbb6 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -1,7 +1,10 @@ using Discord.Rest; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using MessageModel = Discord.API.Message; @@ -11,134 +14,124 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket { - internal class SocketGroupChannel : IGroupChannel, ISocketChannel, ISocketMessageChannel, ISocketPrivateChannel + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketGroupChannel : SocketChannel, IGroupChannel { - internal override bool IsAttached => true; + private readonly MessageCache _messages; - private readonly MessageManager _messages; - private ConcurrentDictionary _voiceStates; + private string _iconId; + private ConcurrentDictionary _users; + private ConcurrentDictionary _voiceStates; - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public IReadOnlyCollection Users - => _users.Select(x => x.Value as ISocketUser).Concat(ImmutableArray.Create(Discord.CurrentUser)).ToReadOnlyCollection(() => _users.Count + 1); - public new IReadOnlyCollection Recipients => _users.Select(x => x.Value as ISocketUser).ToReadOnlyCollection(_users); + public string Name { get; private set; } - public SocketGroupChannel(DiscordSocketClient discord, Model model) - : base(discord, model) + public IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + public IReadOnlyCollection Recipients + => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); + + internal SocketGroupChannel(DiscordSocketClient discord, ulong id) + : base(discord, id) { if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord, this); - else - _messages = new MessageManager(Discord, this); - _voiceStates = new ConcurrentDictionary(1, 5); + _voiceStates = new ConcurrentDictionary(1, 5); + _users = new ConcurrentDictionary(1, 5); } - public override void Update(Model model) + internal new static SocketGroupChannel Create(DiscordSocketClient discord, Model model) { - if (source == UpdateSource.Rest && IsAttached) return; - - base.Update(model, source); + var entity = new SocketGroupChannel(discord, model.Id); + entity.Update(model); + return entity; } + internal void Update(Model model) + { + if (model.Name.IsSpecified) + Name = model.Name.Value; + if (model.Icon.IsSpecified) + _iconId = model.Icon.Value; - internal void UpdateUsers(UserModel[] models, DataStore dataStore) + if (model.Recipients.IsSpecified) + UpdateUsers(model.Recipients.Value); + } + internal virtual void UpdateUsers(API.User[] models) { - var users = new ConcurrentDictionary(1, models.Length); + var users = new ConcurrentDictionary(1, (int)(models.Length * 1.05)); for (int i = 0; i < models.Length; i++) - { - var globalUser = Discord.GetOrAddUser(models[i], dataStore); - users[models[i].Id] = new SocketGroupUser(this, globalUser); - } + users[models[i].Id] = SocketGroupUser.Create(Discord, models[i]); _users = users; } - internal override void UpdateUsers(UserModel[] models) - => UpdateUsers(models, source, Discord.DataStore); - public SocketGroupUser AddUser(UserModel model, DataStore dataStore) - { - GroupUser user; - if (_users.TryGetValue(model.Id, out user)) - return user as SocketGroupUser; - else - { - var globalUser = Discord.GetOrAddUser(model, dataStore); - var privateUser = new SocketGroupUser(this, globalUser); - _users[privateUser.Id] = privateUser; - return privateUser; - } - } - public ISocketUser GetUser(ulong id) + public async Task UpdateAsync() + => Update(await ChannelHelper.GetAsync(this, Discord)); + public Task LeaveAsync() + => ChannelHelper.DeleteAsync(this, Discord); + + public SocketGroupUser GetUser(ulong id) { - GroupUser user; + SocketGroupUser user; if (_users.TryGetValue(id, out user)) - return user as SocketGroupUser; - if (id == Discord.CurrentUser.Id) - return Discord.CurrentUser; - return null; - } - public SocketGroupUser RemoveUser(ulong id) - { - GroupUser user; - if (_users.TryRemove(id, out user)) - return user as SocketGroupUser; + return user; return null; } - public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary voiceStates = null) - { - var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; - var voiceState = new VoiceState(voiceChannel, model); - (voiceStates ?? _voiceStates)[model.UserId] = voiceState; - return voiceState; - } - public VoiceState? GetVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) - return voiceState; - return null; - } - public VoiceState? RemoveVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) - return voiceState; - return null; - } + 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> GetPinnedMessagesAsync() + => ChannelHelper.GetPinnedMessagesAsync(this, Discord); - public override async Task GetMessageAsync(ulong id) - { - return await _messages.DownloadAsync(id).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(int limit) - { - return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - { - return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); - } - public ISocketMessage CreateMessage(ISocketUser author, MessageModel model) - { - return _messages.Create(author, model); - } - public ISocketMessage AddMessage(ISocketUser author, MessageModel model) - { - var msg = _messages.Create(author, model); - _messages.Add(msg); - return msg; - } - public ISocketMessage GetMessage(ulong id) - { - return _messages.Get(id); - } - public ISocketMessage RemoveMessage(ulong id) - { - return _messages.Remove(id); - } + public Task SendMessageAsync(string text, bool isTTS) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS); + public Task SendFileAsync(string filePath, string text, bool isTTS) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS); + + public Task DeleteMessagesAsync(IEnumerable messages) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages); + + public IDisposable EnterTypingState() + => ChannelHelper.EnterTypingState(this, Discord); + + //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.GetPinnedMessagesAsync() + => await GetPinnedMessagesAsync(); + + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) + => await SendFileAsync(filePath, text, isTTS); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS) + => await SendFileAsync(stream, filename, text, isTTS); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS) + => await SendMessageAsync(text, isTTS); + IDisposable IMessageChannel.EnterTypingState() + => EnterTypingState(); - public SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; + //IChannel + IReadOnlyCollection IChannel.CachedUsers => Users; - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); - ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id); - ISocketChannel ISocketChannel.Clone() => Clone(); + IUser IChannel.GetCachedUser(ulong id) + => GetUser(id); + Task IChannel.GetUserAsync(ulong id) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IChannel.GetUsersAsync() + => 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 613a79064..8821ff616 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -7,89 +8,59 @@ using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; -namespace Discord.Rest +namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal abstract class SocketGuildChannel : ISnowflakeEntity, IGuildChannel + public abstract class SocketGuildChannel : SocketChannel, IGuildChannel { - private List _overwrites; //TODO: Is maintaining a list here too expensive? Is this threadsafe? + private ImmutableArray _overwrites; - public string Name { get; private set; } - public int Position { get; private set; } + public IReadOnlyCollection PermissionOverwrites => _overwrites; - public Guild Guild { get; private set; } + public ulong GuildId { get; } - public override DiscordRestClient Discord => Guild.Discord; + public string Name { get; private set; } + public int Position { get; private set; } - public GuildChannel(Guild guild, Model model) - : base(model.Id) + internal SocketGuildChannel(DiscordSocketClient discord, ulong id, ulong guildId) + : base(discord, id) { - Guild = guild; - - Update(model); + GuildId = guildId; } - public virtual void Update(Model model) + internal new static SocketGuildChannel Create(DiscordSocketClient discord, Model model) + { + switch (model.Type) + { + case ChannelType.Text: + return SocketTextChannel.Create(discord, model); + case ChannelType.Voice: + return SocketVoiceChannel.Create(discord, model); + default: + throw new InvalidOperationException("Unknown guild channel type"); + } + } + internal virtual void Update(Model model) { - if (source == UpdateSource.Rest && IsAttached) return; - Name = model.Name.Value; Position = model.Position.Value; var overwrites = model.PermissionOverwrites.Value; - var newOverwrites = new List(overwrites.Length); + var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); for (int i = 0; i < overwrites.Length; i++) newOverwrites.Add(new Overwrite(overwrites[i])); - _overwrites = newOverwrites; + _overwrites = newOverwrites.ToImmutable(); } public async Task UpdateAsync() - { - if (IsAttached) throw new NotSupportedException(); - - var model = await Discord.ApiClient.GetChannelAsync(Id).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildChannelParams(); - func(args); - - if (!args._name.IsSpecified) - args._name = Name; - - var model = await Discord.ApiClient.ModifyGuildChannelAsync(Id, args).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task DeleteAsync() - { - await Discord.ApiClient.DeleteChannelAsync(Id).ConfigureAwait(false); - } - - public abstract Task GetUserAsync(ulong id); - public abstract Task> GetUsersAsync(); - - public async Task> GetInvitesAsync() - { - var models = await Discord.ApiClient.GetChannelInvitesAsync(Id).ConfigureAwait(false); - return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); - } - public async Task CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary) - { - var args = new CreateChannelInviteParams - { - MaxAge = maxAge ?? 0, - MaxUses = maxUses ?? 0, - Temporary = isTemporary - }; - var model = await Discord.ApiClient.CreateChannelInviteAsync(Id, args).ConfigureAwait(false); - return new InviteMetadata(Discord, model); - } + => Update(await ChannelHelper.GetAsync(this, Discord)); + public Task ModifyAsync(Action func) + => ChannelHelper.ModifyAsync(this, Discord, func); + public Task DeleteAsync() + => ChannelHelper.DeleteAsync(this, Discord); public OverwritePermissions? GetPermissionOverwrite(IUser user) { - for (int i = 0; i < _overwrites.Count; i++) + for (int i = 0; i < _overwrites.Length; i++) { if (_overwrites[i].TargetId == user.Id) return _overwrites[i].Permissions; @@ -98,60 +69,91 @@ namespace Discord.Rest } public OverwritePermissions? GetPermissionOverwrite(IRole role) { - for (int i = 0; i < _overwrites.Count; i++) + for (int i = 0; i < _overwrites.Length; i++) { if (_overwrites[i].TargetId == role.Id) return _overwrites[i].Permissions; } return null; } - public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms) { - var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue, Type = "member" }; - await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, user.Id, args).ConfigureAwait(false); - _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User })); + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms).ConfigureAwait(false); + _overwrites = _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User })); } public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms) { - var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue, Type = "role" }; - await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, role.Id, args).ConfigureAwait(false); + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms).ConfigureAwait(false); _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role })); } public async Task RemovePermissionOverwriteAsync(IUser user) { - await Discord.ApiClient.DeleteChannelPermissionAsync(Id, user.Id).ConfigureAwait(false); + await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user).ConfigureAwait(false); - for (int i = 0; i < _overwrites.Count; i++) + for (int i = 0; i < _overwrites.Length; i++) { if (_overwrites[i].TargetId == user.Id) { - _overwrites.RemoveAt(i); + _overwrites = _overwrites.RemoveAt(i); return; } } } public async Task RemovePermissionOverwriteAsync(IRole role) { - await Discord.ApiClient.DeleteChannelPermissionAsync(Id, role.Id).ConfigureAwait(false); + await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role).ConfigureAwait(false); - for (int i = 0; i < _overwrites.Count; i++) + for (int i = 0; i < _overwrites.Length; i++) { if (_overwrites[i].TargetId == role.Id) { - _overwrites.RemoveAt(i); + _overwrites = _overwrites.RemoveAt(i); return; } } } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id})"; - - IGuild IGuildChannel.Guild => Guild; - IReadOnlyCollection IGuildChannel.PermissionOverwrites => _overwrites.AsReadOnly(); - async Task IChannel.GetUserAsync(ulong id) => await GetUserAsync(id).ConfigureAwait(false); - async Task> IChannel.GetUsersAsync() => await GetUsersAsync().ConfigureAwait(false); + public async Task> GetInvitesAsync() + => await ChannelHelper.GetInvitesAsync(this, Discord); + public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary); + + //IGuildChannel + async Task> IGuildChannel.GetInvitesAsync() + => await GetInvitesAsync(); + async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary) + => await CreateInviteAsync(maxAge, maxUses, isTemporary); + + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) + => GetPermissionOverwrite(role); + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) + => GetPermissionOverwrite(user); + async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions) + => await AddPermissionOverwriteAsync(role, permissions); + async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions) + => await AddPermissionOverwriteAsync(user, permissions); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role) + => 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; + + //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? } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 72f5717c3..39a392ef2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -1,6 +1,10 @@ -using Discord.Rest; +using Discord.API.Rest; +using Discord.Rest; +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using MessageModel = Discord.API.Message; @@ -8,81 +12,105 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { - internal class SocketTextChannel : TextChannel, ISocketGuildChannel, ISocketMessageChannel + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketTextChannel : SocketGuildChannel, ITextChannel { - internal override bool IsAttached => true; + private readonly MessageCache _messages; - private readonly MessageManager _messages; + public string Topic { get; private set; } - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketGuild Guild => base.Guild as SocketGuild; + public string Mention => MentionUtils.MentionChannel(Id); - public IReadOnlyCollection Members - => Guild.Members.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); - - public SocketTextChannel(SocketGuild guild, Model model) - : base(guild, model) + internal SocketTextChannel(DiscordSocketClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) { if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord, this); - else - _messages = new MessageManager(Discord, this); - } - - public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); - public override Task> GetUsersAsync() => Task.FromResult>(Members); - public SocketGuildUser GetUser(ulong id, bool skipCheck = false) - { - var user = Guild.GetUser(id); - if (skipCheck) return user; - - if (user != null) - { - ulong perms = Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue); - if (Permissions.GetValue(perms, ChannelPermission.ReadMessages)) - return user; - } - return null; - } - - public override async Task GetMessageAsync(ulong id) - { - return await _messages.DownloadAsync(id).ConfigureAwait(false); } - public override async Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) + internal new static SocketTextChannel Create(DiscordSocketClient discord, Model model) { - return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); + var entity = new SocketTextChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; } - public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + internal override void Update(Model model) { - return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); - } + base.Update(model); - public ISocketMessage CreateMessage(ISocketUser author, MessageModel model) - { - return _messages.Create(author, model); + Topic = model.Topic.Value; } - public ISocketMessage AddMessage(ISocketUser author, MessageModel model) + + 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); + public Task> GetPinnedMessagesAsync() + => ChannelHelper.GetPinnedMessagesAsync(this, Discord); + + public Task SendMessageAsync(string text, bool isTTS) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS); + public Task SendFileAsync(string filePath, string text, bool isTTS) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS); + + public Task DeleteMessagesAsync(IEnumerable messages) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages); + + public IDisposable EnterTypingState() + => ChannelHelper.EnterTypingState(this, Discord); + + internal SocketMessage AddMessage(SocketUser author, MessageModel model) { - var msg = _messages.Create(author, model); + var msg = SocketMessage.Create(Discord, author, model); _messages.Add(msg); return msg; } - public ISocketMessage GetMessage(ulong id) - { - return _messages.Get(id); - } - public ISocketMessage RemoveMessage(ulong id) + internal SocketMessage RemoveMessage(ulong id) { return _messages.Remove(id); } - public SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; + public override string ToString() => Name; + private string DebuggerDisplay => $"@{Name} ({Id}, Text)"; + + //IGuildChannel + async Task IGuildChannel.GetUserAsync(ulong id) + => await GetUserAsync(id); + IAsyncEnumerable> IGuildChannel.GetUsersAsync() + => GetUsersAsync(); - IReadOnlyCollection ISocketMessageChannel.Users => Members; + //IMessageChannel + IReadOnlyCollection IMessageChannel.CachedMessages => ImmutableArray.Create(); + IMessage IMessageChannel.GetCachedMessage(ulong id) => null; - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); - ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id, skipCheck); - ISocketChannel ISocketChannel.Clone() => Clone(); + 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.GetPinnedMessagesAsync() + => await GetPinnedMessagesAsync().ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) + => await SendFileAsync(filePath, text, isTTS); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS) + => await SendFileAsync(stream, filename, text, isTTS); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS) + => await SendMessageAsync(text, isTTS); + IDisposable IMessageChannel.EnterTypingState() + => EnterTypingState(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 4f6438254..808f07e58 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -1,54 +1,50 @@ -using Discord.Audio; +using Discord.API.Rest; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; namespace Discord.WebSocket { - internal class SocketVoiceChannel : VoiceChannel, ISocketGuildChannel + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel { - internal override bool IsAttached => true; + public int Bitrate { get; private set; } + public int UserLimit { get; private set; } - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketGuild Guild => base.Guild as SocketGuild; - - public IReadOnlyCollection Members - => Guild.VoiceStates.Where(x => x.Value.VoiceChannel.Id == Id).Select(x => Guild.GetUser(x.Key)).ToImmutableArray(); - - public SocketVoiceChannel(SocketGuild guild, Model model) - : base(guild, model) + internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) { } - - public override Task GetUserAsync(ulong id) - => Task.FromResult(GetUser(id)); - public override Task> GetUsersAsync() - => Task.FromResult(Members); - public IGuildUser GetUser(ulong id) + internal new static SocketVoiceChannel Create(DiscordSocketClient discord, Model model) { - var user = Guild.GetUser(id); - if (user != null && user.VoiceChannel.Id == Id) - return user; - return null; + var entity = new SocketVoiceChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; } - - public override async Task ConnectAsync() + internal override void Update(Model model) { - var audioMode = Discord.AudioMode; - if (audioMode == AudioMode.Disabled) - throw new InvalidOperationException($"Audio is not enabled on this client, {nameof(DiscordSocketConfig.AudioMode)} in {nameof(DiscordSocketConfig)} must be set."); - - return await Guild.ConnectAudioAsync(Id, - (audioMode & AudioMode.Incoming) == 0, - (audioMode & AudioMode.Outgoing) == 0).ConfigureAwait(false); + base.Update(model); + + Bitrate = model.Bitrate.Value; + UserLimit = model.UserLimit.Value; } - public SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; + public Task ModifyAsync(Action func) + => ChannelHelper.ModifyAsync(this, Discord, func); + + //IVoiceChannel + Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } - ISocketChannel ISocketChannel.Clone() => Clone(); + //IGuildChannel + Task IGuildChannel.GetUserAsync(ulong id) + => Task.FromResult(null); + IAsyncEnumerable> IGuildChannel.GetUsersAsync() + => ImmutableArray.Create>().ToAsyncEnumerable(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 1870a703c..956596ace 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1,4 +1,5 @@ -using Discord.Audio; +using Discord.API.Rest; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Concurrent; @@ -19,402 +20,204 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket { - internal class SocketGuild : Guild, IGuild, IUserGuild + public class SocketGuild : SocketEntity, IGuild { - internal override bool IsAttached => true; - - private readonly SemaphoreSlim _audioLock; - private TaskCompletionSource _syncPromise, _downloaderPromise; - private TaskCompletionSource _audioConnectPromise; - private ConcurrentHashSet _channels; - private ConcurrentDictionary _members; - private ConcurrentDictionary _voiceStates; + private ImmutableDictionary _roles; + private ImmutableArray _emojis; + private ImmutableArray _features; internal bool _available; - public bool Available => _available && Discord.ConnectionState == ConnectionState.Connected; - public int MemberCount { get; set; } - public int DownloadedMemberCount { get; private set; } - public AudioClient AudioClient { get; private set; } - - public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; - public bool IsSynced => _syncPromise.Task.IsCompleted; - public Task SyncPromise => _syncPromise.Task; - public Task DownloaderPromise => _downloaderPromise.Task; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public SocketGuildUser CurrentUser => GetUser(Discord.CurrentUser.Id); - public IReadOnlyCollection Channels - { - get - { - var channels = _channels; - var store = Discord.DataStore; - return channels.Select(x => store.GetChannel(x) as ISocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); - } - } - public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); - public IEnumerable> VoiceStates => _voiceStates; - - public SocketGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) - { - _audioLock = new SemaphoreSlim(1, 1); - _syncPromise = new TaskCompletionSource(); - _downloaderPromise = new TaskCompletionSource(); - Update(model, dataStore); - } - - public void Update(ExtendedModel model, DataStore dataStore) - { - if (source == UpdateSource.Rest && IsAttached) return; - - _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; - } - - base.Update(model as Model, source); - - var channels = new ConcurrentHashSet(1, (int)(model.Channels.Length * 1.05)); - { - for (int i = 0; i < model.Channels.Length; i++) - AddChannel(model.Channels[i], dataStore, channels); - } - _channels = channels; - - var members = new ConcurrentDictionary(1, (int)(model.Presences.Length * 1.05)); - { - DownloadedMemberCount = 0; - for (int i = 0; i < model.Members.Length; i++) - AddOrUpdateUser(model.Members[i], dataStore, members); - if (Discord.ApiClient.AuthTokenType != TokenType.User) - { - var _ = _syncPromise.TrySetResultAsync(true); - if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true); - } - - for (int i = 0; i < model.Presences.Length; i++) - AddOrUpdateUser(model.Presences[i], dataStore, members); - } - _members = members; - MemberCount = model.MemberCount; - - var voiceStates = new ConcurrentDictionary(1, (int)(model.VoiceStates.Length * 1.05)); - { - for (int i = 0; i < model.VoiceStates.Length; i++) - AddOrUpdateVoiceState(model.VoiceStates[i], dataStore, voiceStates); - } - _voiceStates = voiceStates; - } - public void Update(GuildSyncModel model, DataStore dataStore) - { - if (source == UpdateSource.Rest && IsAttached) return; - - var members = new ConcurrentDictionary(1, (int)(model.Presences.Length * 1.05)); - { - DownloadedMemberCount = 0; - for (int i = 0; i < model.Members.Length; i++) - AddOrUpdateUser(model.Members[i], dataStore, members); - var _ = _syncPromise.TrySetResultAsync(true); - if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true); - - for (int i = 0; i < model.Presences.Length; i++) - AddOrUpdateUser(model.Presences[i], dataStore, members); + public string Name { get; private set; } + public int AFKTimeout { get; private set; } + public bool IsEmbeddable { get; private set; } + public VerificationLevel VerificationLevel { get; private set; } + public MfaLevel MfaLevel { get; private set; } + public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } + + public ulong? AFKChannelId { get; private set; } + public ulong? EmbedChannelId { get; private set; } + public ulong OwnerId { get; private set; } + public string VoiceRegionId { get; private set; } + public string IconId { get; private set; } + public string SplashId { get; private set; } + + 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 RestRole EveryoneRole => GetRole(Id); + public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + public IReadOnlyCollection Emojis => _emojis; + public IReadOnlyCollection Features => _features; + + internal SocketGuild(DiscordSocketClient client, ulong id) + : base(client, id) + { + } + internal static SocketGuild Create(DiscordSocketClient discord, Model model) + { + var entity = new SocketGuild(discord, model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + AFKChannelId = model.AFKChannelId; + EmbedChannelId = model.EmbedChannelId; + AFKTimeout = model.AFKTimeout; + IsEmbeddable = model.EmbedEnabled; + IconId = model.Icon; + Name = model.Name; + OwnerId = model.OwnerId; + VoiceRegionId = model.Region; + SplashId = model.Splash; + VerificationLevel = model.VerificationLevel; + MfaLevel = model.MfaLevel; + DefaultMessageNotifications = model.DefaultMessageNotifications; + + if (model.Emojis != null) + { + 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(); } - _members = members; - } - - public void Update(EmojiUpdateModel model) - { - if (source == UpdateSource.Rest && IsAttached) return; - - var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); - for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(new Emoji(model.Emojis[i])); - Emojis = emojis.ToImmutableArray(); - } - - public override Task GetChannelAsync(ulong id) => Task.FromResult(GetChannel(id)); - public override Task> GetChannelsAsync() => Task.FromResult>(Channels); - public ISocketGuildChannel AddChannel(ChannelModel model, DataStore dataStore, ConcurrentHashSet channels = null) - { - var channel = ToChannel(model); - (channels ?? _channels).TryAdd(model.Id); - dataStore.AddChannel(channel); - return channel; - } - public ISocketGuildChannel GetChannel(ulong id) - { - return Discord.DataStore.GetChannel(id) as ISocketGuildChannel; - } - public ISocketGuildChannel RemoveChannel(ulong id) - { - _channels.TryRemove(id); - return Discord.DataStore.RemoveChannel(id) as ISocketGuildChannel; - } - - public Role AddRole(RoleModel model, ConcurrentDictionary roles = null) - { - var role = new Role(this, model); - (roles ?? _roles)[model.Id] = role; - return role; - } - public Role RemoveRole(ulong id) - { - Role role; - if (_roles.TryRemove(id, out role)) - return role; - return null; - } - - public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); - public override Task GetCurrentUserAsync() - => Task.FromResult(CurrentUser); - public override Task> GetUsersAsync() - => Task.FromResult>(Members); - public SocketGuildUser AddOrUpdateUser(MemberModel model, DataStore dataStore, ConcurrentDictionary members = null) - { - members = members ?? _members; - - SocketGuildUser member; - if (members.TryGetValue(model.User.Id, out member)) - member.Update(model, UpdateSource.WebSocket); else - { - var user = Discord.GetOrAddUser(model.User, dataStore); - member = new SocketGuildUser(this, user, model); - members[user.Id] = member; - DownloadedMemberCount++; - } - return member; - } - public SocketGuildUser AddOrUpdateUser(PresenceModel model, DataStore dataStore, ConcurrentDictionary members = null) - { - members = members ?? _members; + _emojis = ImmutableArray.Create(); - SocketGuildUser member; - if (members.TryGetValue(model.User.Id, out member)) - member.Update(model, UpdateSource.WebSocket); + if (model.Features != null) + _features = model.Features.ToImmutableArray(); else - { - var user = Discord.GetOrAddUser(model.User, dataStore); - member = new SocketGuildUser(this, user, model); - members[user.Id] = member; - DownloadedMemberCount++; - } - return member; - } - public SocketGuildUser GetUser(ulong id) - { - SocketGuildUser member; - if (_members.TryGetValue(id, out member)) - return member; - return null; - } - public SocketGuildUser RemoveUser(ulong id) - { - SocketGuildUser member; - if (_members.TryRemove(id, out member)) - { - DownloadedMemberCount--; - return member; - } - member.User.RemoveRef(Discord); + _features = ImmutableArray.Create(); + + var roles = ImmutableDictionary.CreateBuilder(); + if (model.Roles != null) + { + throw new NotImplementedException(); + } + _roles = roles.ToImmutable(); + } + + //General + public async Task UpdateAsync() + => Update(await Discord.ApiClient.GetGuildAsync(Id)); + public Task DeleteAsync() + => GuildHelper.DeleteAsync(this, Discord); + + public Task ModifyAsync(Action func) + => GuildHelper.ModifyAsync(this, Discord, func); + public Task ModifyEmbedAsync(Action func) + => GuildHelper.ModifyEmbedAsync(this, Discord, func); + public Task ModifyChannelsAsync(IEnumerable args) + => GuildHelper.ModifyChannelsAsync(this, Discord, args); + public Task ModifyRolesAsync(IEnumerable args) + => GuildHelper.ModifyRolesAsync(this, Discord, args); + + public Task LeaveAsync() + => GuildHelper.LeaveAsync(this, Discord); + + //Bans + public Task> GetBansAsync() + => GuildHelper.GetBansAsync(this, Discord); + + public Task AddBanAsync(IUser user, int pruneDays = 0) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays); + public Task AddBanAsync(ulong userId, int pruneDays = 0) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays); + + public Task RemoveBanAsync(IUser user) + => GuildHelper.RemoveBanAsync(this, Discord, user.Id); + public Task RemoveBanAsync(ulong userId) + => GuildHelper.RemoveBanAsync(this, Discord, userId); + + //Channels + public Task> GetChannelsAsync() + => GuildHelper.GetChannelsAsync(this, Discord); + public Task GetChannelAsync(ulong id) + => GuildHelper.GetChannelAsync(this, Discord, id); + public Task CreateTextChannelAsync(string name) + => GuildHelper.CreateTextChannelAsync(this, Discord, name); + public Task CreateVoiceChannelAsync(string name) + => GuildHelper.CreateVoiceChannelAsync(this, Discord, name); + + //Integrations + public Task> GetIntegrationsAsync() + => GuildHelper.GetIntegrationsAsync(this, Discord); + public Task CreateIntegrationAsync(ulong id, string type) + => GuildHelper.CreateIntegrationAsync(this, Discord, id, type); + + //Invites + public Task> GetInvitesAsync() + => GuildHelper.GetInvitesAsync(this, Discord); + + //Roles + public RestRole GetRole(ulong id) + { + RestRole value; + if (_roles.TryGetValue(id, out value)) + return value; return null; } - public override async Task DownloadUsersAsync() - { - await Discord.DownloadUsersAsync(new [] { this }); - } - public void CompleteDownloadMembers() - { - _downloaderPromise.TrySetResultAsync(true); - } - public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary voiceStates = null) + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false) { - var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; - var voiceState = new VoiceState(voiceChannel, model); - (voiceStates ?? _voiceStates)[model.UserId] = voiceState; - return voiceState; - } - public VoiceState? GetVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) - return voiceState; - return null; - } - public VoiceState? RemoveVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) - return voiceState; - return null; - } - - public async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute) - { - try - { - TaskCompletionSource promise; - - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectAudioInternalAsync().ConfigureAwait(false); - promise = new TaskCompletionSource(); - _audioConnectPromise = promise; - await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); - } - finally - { - _audioLock.Release(); - } - - var timeoutTask = Task.Delay(15000); - if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask) - throw new TimeoutException(); - return await promise.Task.ConfigureAwait(false); - } - catch (Exception) - { - await DisconnectAudioInternalAsync().ConfigureAwait(false); - throw; - } - } - 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); - } - } - } - public async Task FinishConnectAudio(int id, string url, string token) - { - var voiceState = GetVoiceState(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(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, 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(); - } - } - public async Task FinishJoinAudioChannel() - { - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - if (AudioClient != null) - await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); - } - finally - { - _audioLock.Release(); - } - } - - public SocketGuild Clone() => MemberwiseClone() as SocketGuild; - - new internal ISocketGuildChannel ToChannel(ChannelModel model) - { - switch (model.Type) - { - case ChannelType.Text: - return new SocketTextChannel(this, model); - case ChannelType.Voice: - return new SocketVoiceChannel(this, model); - default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } + var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted); + _roles = _roles.Add(role.Id, role); + return role; } - bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id; - GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; - IAudioClient IGuild.AudioClient => AudioClient; + //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 Task PruneUsersAsync(int days = 30, bool simulate = false) + => GuildHelper.PruneUsersAsync(this, Discord, days, simulate); + + //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.CreateTextChannelAsync(string name) + => await CreateTextChannelAsync(name); + async Task IGuild.CreateVoiceChannelAsync(string name) + => await CreateVoiceChannelAsync(name); + + async Task> IGuild.GetIntegrationsAsync() + => await GetIntegrationsAsync(); + async Task IGuild.CreateIntegrationAsync(ulong id, string type) + => await CreateIntegrationAsync(id, type); + + async Task> IGuild.GetInvitesAsync() + => await GetInvitesAsync(); + + IRole IGuild.GetRole(ulong id) + => GetRole(id); + + 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.DownloadUsersAsync() { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildIntegration.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildIntegration.cs deleted file mode 100644 index bd65a3cc8..000000000 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildIntegration.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Integration; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class GuildIntegration : IEntity, IGuildIntegration - { - private long _syncedAtTicks; - - public string Name { get; private set; } - public string Type { get; private set; } - public bool IsEnabled { get; private set; } - public bool IsSyncing { get; private set; } - public ulong ExpireBehavior { get; private set; } - public ulong ExpireGracePeriod { get; private set; } - - public Guild Guild { get; private set; } - public Role Role { get; private set; } - public User User { get; private set; } - public IntegrationAccount Account { get; private set; } - - public override DiscordRestClient Discord => Guild.Discord; - public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); - - public GuildIntegration(Guild guild, Model model) - : base(model.Id) - { - Guild = guild; - Update(model); - } - - public void Update(Model model) - { - Name = model.Name; - Type = model.Type; - IsEnabled = model.Enabled; - IsSyncing = model.Syncing; - ExpireBehavior = model.ExpireBehavior; - ExpireGracePeriod = model.ExpireGracePeriod; - _syncedAtTicks = model.SyncedAt.UtcTicks; - - Role = Guild.GetRole(model.RoleId); - User = new User(model.User); - } - - public async Task DeleteAsync() - { - await Discord.ApiClient.DeleteGuildIntegrationAsync(Guild.Id, Id).ConfigureAwait(false); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildIntegrationParams(); - func(args); - var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(Guild.Id, Id, args).ConfigureAwait(false); - - Update(model, UpdateSource.Rest); - } - public async Task SyncAsync() - { - await Discord.ApiClient.SyncGuildIntegrationAsync(Guild.Id, Id).ConfigureAwait(false); - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; - - IGuild IGuildIntegration.Guild => Guild; - IUser IGuildIntegration.User => User; - IRole IGuildIntegration.Role => Role; - } -} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/ISocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/ISocketMessage.cs deleted file mode 100644 index c4f112b77..000000000 --- a/src/Discord.Net.WebSocket/Entities/Messages/ISocketMessage.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Model = Discord.API.Message; - -namespace Discord.WebSocket -{ - internal interface ISocketMessage : IMessage - { - DiscordSocketClient Discord { get; } - new ISocketMessageChannel Channel { get; } - - void Update(Model model); - ISocketMessage Clone(); - } -} diff --git a/src/Discord.Net.WebSocket/Entities/Utilities/MessageCache.cs b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs similarity index 59% rename from src/Discord.Net.WebSocket/Entities/Utilities/MessageCache.cs rename to src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs index 7fafebba5..d283f46b0 100644 --- a/src/Discord.Net.WebSocket/Entities/Utilities/MessageCache.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -1,4 +1,6 @@ -using System; +using Discord.Rest; +using Discord.WebSocket; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -7,53 +9,52 @@ using System.Threading.Tasks; namespace Discord.WebSocket { - internal class MessageCache : MessageManager + internal class MessageCache { - private readonly ConcurrentDictionary _messages; + private readonly ConcurrentDictionary _messages; private readonly ConcurrentQueue _orderedMessages; private readonly int _size; - public override IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); + public IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); - public MessageCache(DiscordSocketClient discord, ISocketMessageChannel channel) - : base(discord, channel) + public MessageCache(DiscordSocketClient discord, IChannel channel) { _size = discord.MessageCacheSize; - _messages = new ConcurrentDictionary(1, (int)(_size * 1.05)); + _messages = new ConcurrentDictionary(1, (int)(_size * 1.05)); _orderedMessages = new ConcurrentQueue(); } - public override void Add(ISocketMessage message) + public void Add(SocketMessage message) { if (_messages.TryAdd(message.Id, message)) { _orderedMessages.Enqueue(message.Id); ulong msgId; - ISocketMessage msg; + SocketMessage msg; while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId)) _messages.TryRemove(msgId, out msg); } } - public override ISocketMessage Remove(ulong id) + public SocketMessage Remove(ulong id) { - ISocketMessage msg; + SocketMessage msg; _messages.TryRemove(id, out msg); return msg; } - public override ISocketMessage Get(ulong id) + public SocketMessage Get(ulong id) { - ISocketMessage result; + SocketMessage result; if (_messages.TryGetValue(id, out result)) return result; return null; } - public override IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return ImmutableArray.Empty; + if (limit == 0) return ImmutableArray.Empty; IEnumerable cachedMessageIds; if (fromMessageId == null) @@ -67,7 +68,7 @@ namespace Discord.WebSocket .Take(limit) .Select(x => { - ISocketMessage msg; + SocketMessage msg; if (_messages.TryGetValue(x, out msg)) return msg; return null; @@ -75,13 +76,5 @@ namespace Discord.WebSocket .Where(x => x != null) .ToImmutableArray(); } - - public override async Task DownloadAsync(ulong id) - { - var msg = Get(id); - if (msg != null) - return msg; - return await base.DownloadAsync(id).ConfigureAwait(false); - } } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs new file mode 100644 index 000000000..46271c9e6 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -0,0 +1,64 @@ +using System; +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 + { + private long _timestampTicks; + + public ulong ChannelId { get; } + public SocketUser Author { get; } + + public string Content { get; private set; } + + public virtual bool IsTTS => false; + public virtual bool IsPinned => false; + public virtual DateTimeOffset? EditedTimestamp => null; + + public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedChannelIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + + internal SocketMessage(DiscordSocketClient discord, ulong id, ulong channelId, SocketUser author) + : base(discord, id) + { + ChannelId = channelId; + Author = author; + } + internal static SocketMessage Create(DiscordSocketClient discord, SocketUser author, Model model) + { + if (model.Type == MessageType.Default) + return SocketUserMessage.Create(discord, author, model); + else + return SocketSystemMessage.Create(discord, author, model); + } + internal virtual void Update(Model model) + { + if (model.Timestamp.IsSpecified) + _timestampTicks = model.Timestamp.Value.UtcTicks; + + if (model.Content.IsSpecified) + Content = model.Content.Value; + } + + public async Task UpdateAsync() + { + var model = await Discord.ApiClient.GetChannelMessageAsync(ChannelId, Id).ConfigureAwait(false); + Update(model); + } + + //IMessage + IUser IMessage.Author => Author; + MessageType IMessage.Type => MessageType.Default; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index bcd95ddf2..56a8bafaa 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -3,18 +3,25 @@ using Model = Discord.API.Message; namespace Discord.WebSocket { - internal class SocketSystemMessage : SystemMessage, ISocketMessage + internal class SocketSystemMessage : SocketMessage, ISystemMessage { - internal override bool IsAttached => true; + public MessageType Type { get; private set; } - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new ISocketMessageChannel Channel => base.Channel as ISocketMessageChannel; - - public SocketSystemMessage(ISocketMessageChannel channel, IUser author, Model model) - : base(channel, author, model) + internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ulong channelId, SocketUser author) + : base(discord, id, channelId, author) + { + } + internal new static SocketSystemMessage Create(DiscordSocketClient discord, SocketUser author, Model model) { + var entity = new SocketSystemMessage(discord, model.Id, model.ChannelId, author); + entity.Update(model); + return entity; } + internal override void Update(Model model) + { + base.Update(model); - public ISocketMessage Clone() => MemberwiseClone() as ISocketMessage; + Type = model.Type; + } } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 31ef2082a..1f60ac8f2 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -1,20 +1,133 @@ -using Discord.Rest; +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; using Model = Discord.API.Message; namespace Discord.WebSocket { - internal class SocketUserMessage : UserMessage, ISocketMessage + internal class SocketUserMessage : SocketMessage, IUserMessage { - internal override bool IsAttached => true; + private bool _isMentioningEveryone, _isTTS, _isPinned; + private long? _editedTimestampTicks; + private ImmutableArray _attachments; + private ImmutableArray _embeds; + private ImmutableArray _mentionedChannelIds; + private ImmutableArray _mentionedRoles; + private ImmutableArray _mentionedUsers; - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new ISocketMessageChannel Channel => base.Channel as ISocketMessageChannel; + public override bool IsTTS => _isTTS; + public override bool IsPinned => _isPinned; + public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); - public SocketUserMessage(ISocketMessageChannel channel, IUser author, Model model) - : base(channel, author, model) + public override IReadOnlyCollection Attachments => _attachments; + public override IReadOnlyCollection Embeds => _embeds; + public override IReadOnlyCollection MentionedChannelIds => _mentionedChannelIds; + public override IReadOnlyCollection MentionedRoles => _mentionedRoles; + public override IReadOnlyCollection MentionedUsers => _mentionedUsers; + + internal SocketUserMessage(DiscordSocketClient discord, ulong id, ulong channelId, SocketUser author) + : base(discord, id, channelId, author) + { + } + internal new static SocketUserMessage Create(DiscordSocketClient discord, SocketUser author, Model model) + { + var entity = new SocketUserMessage(discord, model.Id, model.ChannelId, author); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) { + base.Update(model); + + if (model.IsTextToSpeech.IsSpecified) + _isTTS = model.IsTextToSpeech.Value; + if (model.Pinned.IsSpecified) + _isPinned = model.Pinned.Value; + if (model.EditedTimestamp.IsSpecified) + _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; + if (model.MentionEveryone.IsSpecified) + _isMentioningEveryone = model.MentionEveryone.Value; + + if (model.Attachments.IsSpecified) + { + var value = model.Attachments.Value; + if (value.Length > 0) + { + var attachments = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + attachments.Add(RestAttachment.Create(value[i])); + _attachments = attachments.ToImmutable(); + } + else + _attachments = ImmutableArray.Create(); + } + + if (model.Embeds.IsSpecified) + { + var value = model.Embeds.Value; + if (value.Length > 0) + { + var embeds = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + embeds.Add(RestEmbed.Create(value[i])); + _embeds = embeds.ToImmutable(); + } + else + _embeds = ImmutableArray.Create(); + } + + ImmutableArray mentions = ImmutableArray.Create(); + if (model.Mentions.IsSpecified) + { + var value = model.Mentions.Value; + if (value.Length > 0) + { + var newMentions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + newMentions.Add(RestUser.Create(Discord, value[i])); + mentions = newMentions.ToImmutable(); + } + } + + if (model.Content.IsSpecified) + { + var text = model.Content.Value; + + _mentionedUsers = MentionsHelper.GetUserMentions(text, null, mentions); + _mentionedChannelIds = MentionsHelper.GetChannelMentions(text, null); + _mentionedRoles = MentionsHelper.GetRoleMentions(text, null); + model.Content = text; + } } - public ISocketMessage Clone() => MemberwiseClone() as ISocketMessage; + public Task ModifyAsync(Action func) + => MessageHelper.ModifyAsync(this, Discord, func); + public Task DeleteAsync() + => MessageHelper.DeleteAsync(this, Discord); + + public Task PinAsync() + => MessageHelper.PinAsync(this, Discord); + public Task UnpinAsync() + => MessageHelper.UnpinAsync(this, Discord); + + public string Resolve(UserMentionHandling userHandling = UserMentionHandling.Name, ChannelMentionHandling channelHandling = ChannelMentionHandling.Name, + RoleMentionHandling roleHandling = RoleMentionHandling.Name, EveryoneMentionHandling everyoneHandling = EveryoneMentionHandling.Ignore) + => Resolve(Content, userHandling, channelHandling, roleHandling, everyoneHandling); + public string Resolve(int startIndex, int length, UserMentionHandling userHandling = UserMentionHandling.Name, ChannelMentionHandling channelHandling = ChannelMentionHandling.Name, + RoleMentionHandling roleHandling = RoleMentionHandling.Name, EveryoneMentionHandling everyoneHandling = EveryoneMentionHandling.Ignore) + => Resolve(Content.Substring(startIndex, length), userHandling, channelHandling, roleHandling, everyoneHandling); + public string Resolve(string text, UserMentionHandling userHandling, ChannelMentionHandling channelHandling, + RoleMentionHandling roleHandling, EveryoneMentionHandling everyoneHandling) + { + text = MentionsHelper.ResolveUserMentions(text, null, MentionedUsers, userHandling); + text = MentionsHelper.ResolveChannelMentions(text, null, channelHandling); + text = MentionsHelper.ResolveRoleMentions(text, MentionedRoles, roleHandling); + text = MentionsHelper.ResolveEveryoneMentions(text, everyoneHandling); + return text; + } } } diff --git a/src/Discord.Net.WebSocket/Entities/SocketEntity.cs b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs new file mode 100644 index 000000000..072e414f8 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.WebSocket +{ + public abstract class SocketEntity : IEntity + where T : IEquatable + { + public DiscordSocketClient Discord { get; } + public T Id { get; } + + internal SocketEntity(DiscordSocketClient discord, T id) + { + Discord = discord; + Id = id; + } + + IDiscordClient IEntity.Discord => Discord; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/ISocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/ISocketUser.cs deleted file mode 100644 index bf152eae2..000000000 --- a/src/Discord.Net.WebSocket/Entities/Users/ISocketUser.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.WebSocket -{ - internal interface ISocketUser : IUser, IEntity - { - SocketGlobalUser User { get; } - - ISocketUser Clone(); - } -} diff --git a/src/Discord.Net.WebSocket/Entities/Users/Presence.cs b/src/Discord.Net.WebSocket/Entities/Users/Presence.cs deleted file mode 100644 index dad784870..000000000 --- a/src/Discord.Net.WebSocket/Entities/Users/Presence.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Discord.WebSocket -{ - //TODO: C#7 Candidate for record type - internal struct Presence : IPresence - { - public Game Game { get; } - public UserStatus Status { get; } - - public Presence(Game game, UserStatus status) - { - Game = game; - Status = status; - } - - public Presence Clone() => this; - } -} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketDMUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketDMUser.cs deleted file mode 100644 index d68a395f6..000000000 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketDMUser.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Diagnostics; -using PresenceModel = Discord.API.Presence; - -namespace Discord.WebSocket -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class SocketDMUser : ISocketUser - { - internal bool IsAttached => true; - bool IEntity.IsAttached => IsAttached; - - public SocketGlobalUser User { get; } - - public DiscordSocketClient Discord => User.Discord; - - public Game Game => Presence.Game; - public UserStatus Status => Presence.Status; - public Presence Presence => User.Presence; //{ get; private set; } - - public ulong Id => User.Id; - public string AvatarUrl => User.AvatarUrl; - public DateTimeOffset CreatedAt => User.CreatedAt; - public string Discriminator => User.Discriminator; - public ushort DiscriminatorValue => User.DiscriminatorValue; - public bool IsBot => User.IsBot; - public string Mention => MentionUtils.Mention(this); - public string Username => User.Username; - - public SocketDMUser(SocketGlobalUser user) - { - User = user; - } - - public void Update(PresenceModel model) - { - User.Update(model, source); - } - - public SocketDMUser Clone() => MemberwiseClone() as SocketDMUser; - ISocketUser ISocketUser.Clone() => Clone(); - - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; - } -} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index f08a2656e..39340745c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,61 +1,10 @@ -using Discord.Rest; -using System; -using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; - -namespace Discord.WebSocket +namespace Discord.WebSocket { - internal class SocketGlobalUser : User, ISocketUser + internal class SocketGlobalUser : SocketUser { - internal override bool IsAttached => true; - private readonly object _lockObj = new object(); - - private ushort _references; - - public Presence Presence { get; private set; } - - public new DiscordSocketClient Discord { get { throw new NotSupportedException(); } } - SocketGlobalUser ISocketUser.User => this; - - public SocketGlobalUser(Model model) - : base(model) + internal SocketGlobalUser(DiscordSocketClient discord, ulong id) + : base(discord, id) { } - - public void AddRef() - { - checked - { - lock (_lockObj) - _references++; - } - } - public void RemoveRef(DiscordSocketClient discord) - { - lock (_lockObj) - { - if (--_references == 0) - discord.RemoveUser(Id); - } - } - - public override void Update(Model model) - { - lock (_lockObj) - base.Update(model, source); - } - public void Update(PresenceModel model) - { - //Race conditions are okay here. Multiple shards racing already cant guarantee presence in order. - - //lock (_lockObj) - //{ - var game = model.Game != null ? new Game(model.Game) : null; - Presence = new Presence(game, model.Status); - //} - } - - public SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; - ISocketUser ISocketUser.Clone() => Clone(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index f19dc6b9d..2c2bfe74a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -1,36 +1,30 @@ using Discord.Rest; using System.Diagnostics; +using Model = Discord.API.User; namespace Discord.WebSocket { [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class SocketGroupUser : GroupUser, ISocketUser + public class SocketGroupUser : SocketUser, IGroupUser { - internal override bool IsAttached => true; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketGroupChannel Channel => base.Channel as SocketGroupChannel; - public new SocketGlobalUser User => base.User as SocketGlobalUser; - public Presence Presence => User.Presence; //{ get; private set; } - - public override Game Game => Presence.Game; - public override UserStatus Status => Presence.Status; - - public VoiceState? VoiceState => Channel.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 SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser user) - : base(channel, user) + internal SocketGroupUser(DiscordSocketClient discord, ulong id) + : base(discord, id) { } + internal new static SocketGroupUser Create(DiscordSocketClient discord, Model model) + { + var entity = new SocketGroupUser(discord, model.Id); + entity.Update(model); + return entity; + } - public SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; - ISocketUser ISocketUser.Clone() => Clone(); - - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; + //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; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 3b38eb213..33aea831c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -1,53 +1,74 @@ -using Discord.Rest; +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; using Model = Discord.API.GuildMember; using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { - internal class SocketGuildUser : GuildUser, ISocketUser, IVoiceState + internal class SocketGuildUser : SocketUser, IGuildUser { - internal override bool IsAttached => true; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketGuild Guild => base.Guild as SocketGuild; - public new SocketGlobalUser User => base.User as SocketGlobalUser; - public Presence Presence => User.Presence; //{ get; private set; } - - public override Game Game => Presence.Game; - public override UserStatus Status => Presence.Status; - - public VoiceState? 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 SocketGuildUser(SocketGuild guild, SocketGlobalUser user, Model model) - : base(guild, user, model) + private long? _joinedAtTicks; + private ImmutableArray _roleIds; + + public string Nickname { get; private set; } + public ulong GuildId { get; private set; } + + public IReadOnlyCollection RoleIds => _roleIds; + + public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); + + internal SocketGuildUser(DiscordSocketClient discord, ulong id) + : base(discord, id) { - //Presence = new Presence(null, UserStatus.Offline); } - public SocketGuildUser(SocketGuild guild, SocketGlobalUser user, PresenceModel model) - : base(guild, user, model) + internal static SocketGuildUser Create(DiscordSocketClient discord, Model model) { + var entity = new SocketGuildUser(discord, model.User.Id); + entity.Update(model); + return entity; } - - public override void Update(PresenceModel model) + internal void Update(Model model) { - base.Update(model, source); + _joinedAtTicks = model.JoinedAt.UtcTicks; + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; + UpdateRoles(model.Roles); + } + private void UpdateRoles(ulong[] roleIds) + { + var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); + roles.Add(GuildId); + for (int i = 0; i < roleIds.Length; i++) + roles.Add(roleIds[i]); + _roleIds = roles.ToImmutable(); + } - var game = model.Game != null ? new Game(model.Game) : null; - //Presence = new Presence(game, model.Status); + public override async Task UpdateAsync() + => Update(await UserHelper.GetAsync(this, Discord)); + public Task ModifyAsync(Action func) + => UserHelper.ModifyAsync(this, Discord, func); + public Task KickAsync() + => UserHelper.KickAsync(this, Discord); - User.Update(model, source); + public ChannelPermissions GetPermissions(IGuildChannel channel) + { + throw new NotImplementedException(); //TODO: Impl } - IVoiceChannel IVoiceState.VoiceChannel => VoiceState?.VoiceChannel; + //IGuildUser + IReadOnlyCollection IGuildUser.RoleIds => RoleIds; - public SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; - ISocketUser ISocketUser.Clone() => Clone(); + //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; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs new file mode 100644 index 000000000..01fd85345 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -0,0 +1,23 @@ +using Model = Discord.API.Presence; + +namespace Discord.WebSocket +{ + //TODO: C#7 Candidate for record type + internal struct SocketPresence : IPresence + { + public Game? Game { get; } + public UserStatus Status { get; } + + internal SocketPresence(Game? game, UserStatus status) + { + Game = game; + Status = status; + } + internal SocketPresence Create(Model model) + { + return new SocketPresence(model.Game != null ? Discord.Game.Create(model.Game) : (Game?)null, model.Status); + } + + public SocketPresence Clone() => this; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index e0acfbfc1..e67899de9 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -6,42 +6,39 @@ using Model = Discord.API.User; namespace Discord.WebSocket { - internal class SocketSelfUser : SelfUser, ISocketUser, ISelfUser + public class SocketSelfUser : SocketUser, ISelfUser { - internal override bool IsAttached => true; + public string Email { get; private set; } + public bool IsVerified { get; private set; } + public bool IsMfaEnabled { get; private set; } - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - SocketGlobalUser ISocketUser.User { get { throw new NotSupportedException(); } } - - public SocketSelfUser(DiscordSocketClient discord, Model model) - : base(discord, model) + internal SocketSelfUser(DiscordSocketClient discord, ulong id) + : base(discord, id) { } - - public async Task ModifyStatusAsync(Action func) + internal new static SocketSelfUser Create(DiscordSocketClient discord, Model model) { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyPresenceParams(); - func(args); - - var game = args._game.GetValueOrDefault(_game); - var status = args._status.GetValueOrDefault(_status); - - long idleSince = _idleSince; - if (status == UserStatus.Idle && _status != UserStatus.Idle) - idleSince = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - var apiGame = game != null ? new API.Game { Name = game.Name, StreamType = game.StreamType, StreamUrl = game.StreamUrl } : null; - - await Discord.ApiClient.SendStatusUpdateAsync(status == UserStatus.Idle ? _idleSince : (long?)null, apiGame).ConfigureAwait(false); - - //Save values - _idleSince = idleSince; - _game = game; - _status = status; + var entity = new SocketSelfUser(discord, model.Id); + entity.Update(model); + return entity; } + internal override void Update(Model model) + { + base.Update(model); + + if (model.Email.IsSpecified) + Email = model.Email.Value; + if (model.Verified.IsSpecified) + IsVerified = model.Verified.Value; + 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); - public SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; - ISocketUser ISocketUser.Clone() => Clone(); + Task ISelfUser.ModifyStatusAsync(Action func) { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs new file mode 100644 index 000000000..87691f427 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -0,0 +1,50 @@ +using Discord.Rest; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + public 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 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 => UserStatus.Unknown; + + 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) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Discriminator.IsSpecified) + DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + if (model.Bot.IsSpecified) + IsBot = model.Bot.Value; + if (model.Username.IsSpecified) + Username = model.Username.Value; + } + + public virtual async Task UpdateAsync() + => Update(await UserHelper.GetAsync(this, Discord)); + + public Task CreateDMChannelAsync() + => UserHelper.CreateDMChannelAsync(this, Discord); + + IDMChannel IUser.GetCachedDMChannel() => null; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/VoiceState.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs similarity index 74% rename from src/Discord.Net.WebSocket/Entities/Users/VoiceState.cs rename to src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs index 123c7ee98..9813fb039 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/VoiceState.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -4,7 +4,7 @@ using Model = Discord.API.VoiceState; namespace Discord.WebSocket { //TODO: C#7 Candidate for record type - internal struct VoiceState : IVoiceState + public struct SocketVoiceState : IVoiceState { [Flags] private enum Flags : byte @@ -28,9 +28,7 @@ namespace Discord.WebSocket public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; - public VoiceState(SocketVoiceChannel voiceChannel, Model model) - : this(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress) { } - public VoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed) + internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed) { VoiceChannel = voiceChannel; VoiceSessionId = sessionId; @@ -44,8 +42,12 @@ namespace Discord.WebSocket voiceStates |= Flags.Suppressed; _voiceStates = voiceStates; } + internal static SocketVoiceState Create(SocketVoiceChannel voiceChannel, Model model) + { + return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); + } - public VoiceState Clone() => this; + public SocketVoiceState Clone() => this; IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } diff --git a/src/Discord.Net.WebSocket/Entities/Utilities/MessageManager.cs b/src/Discord.Net.WebSocket/Entities/Utilities/MessageManager.cs deleted file mode 100644 index 713c5b635..000000000 --- a/src/Discord.Net.WebSocket/Entities/Utilities/MessageManager.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Discord.API.Rest; -using Discord.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Message; - -namespace Discord.WebSocket -{ - internal class MessageManager - { - private readonly DiscordSocketClient _discord; - private readonly ISocketMessageChannel _channel; - - public virtual IReadOnlyCollection Messages - => ImmutableArray.Create(); - - public MessageManager(DiscordSocketClient discord, ISocketMessageChannel channel) - { - _discord = discord; - _channel = channel; - } - - public virtual void Add(ISocketMessage message) { } - public virtual ISocketMessage Remove(ulong id) => null; - public virtual ISocketMessage Get(ulong id) => null; - - public virtual IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => ImmutableArray.Create(); - - public virtual async Task DownloadAsync(ulong id) - { - var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); - if (model != null) - return Create(new User(model.Author.Value), model); - return null; - } - public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) - { - //TODO: Test heavily, especially the ordering of messages - if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return ImmutableArray.Empty; - - var cachedMessages = GetMany(fromId, dir, limit); - if (cachedMessages.Count == limit) - return cachedMessages; - else if (cachedMessages.Count > limit) - return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); - else - { - var args = new GetChannelMessagesParams - { - Limit = limit - cachedMessages.Count, - RelativeDirection = dir - }; - if (cachedMessages.Count == 0) - { - if (fromId != null) - args.RelativeMessageId = fromId.Value; - } - else - args.RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; - var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); - - var guild = (_channel as ISocketGuildChannel)?.Guild; - return cachedMessages.Concat(downloadedMessages.Select(x => - { - IUser user = _channel.GetUser(x.Author.Value.Id, true); - if (user == null) - { - var newUser = new User(x.Author.Value); - if (guild != null) - user = new GuildUser(guild, newUser); - else - user = newUser; - } - return Create(user, x); - })).ToImmutableArray(); - } - } - - public ISocketMessage Create(IUser author, Model model) - { - if (model.Type == MessageType.Default) - return new SocketUserMessage(_channel, author, model); - else - return new SocketSystemMessage(_channel, author, model); - } - } -} diff --git a/src/Discord.Net.WebSocket/project.json b/src/Discord.Net.WebSocket/project.json index 9a7de9a07..7abff080a 100644 --- a/src/Discord.Net.WebSocket/project.json +++ b/src/Discord.Net.WebSocket/project.json @@ -2,18 +2,38 @@ "version": "1.0.0-beta2-*", "buildOptions": { - "compile": { - "include": [ "../Discord.Net.Utils/**.cs" ] + "allowUnsafe": true + }, + + "configurations": { + "Release": { + "buildOptions": { + "define": [ "RELEASE" ], + "nowarn": [ "CS1573", "CS1591" ], + "optimize": true, + "warningsAsErrors": true, + "xmlDoc": true + } } }, "dependencies": { + "Discord.Net.Core": { + "target": "project" + }, + "Discord.Net.Rest": { + "target": "project" + }, "NETStandard.Library": "1.6.0" }, "frameworks": { - "netstandard1.6": { - "imports": "dnxcore50" + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] } } } diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 370fde847..6bd67ae75 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -1,6 +1,6 @@ { "version": "1.0.0-beta2-*", - "description": "An unofficial .Net API wrapper for the Discord service.", + "description": "An aynchronous API wrapper for Discord using .NET. This package includes all of the optional Discord.Net components", "authors": [ "RogueException" ], "packOptions": { @@ -13,38 +13,22 @@ } }, - "buildOptions": { - "allowUnsafe": true, - "warningsAsErrors": false, - "xmlDoc": true - }, - - "configurations": { - "Release": { - "buildOptions": { - "define": [ "RELEASE" ], - "nowarn": [ "CS1573", "CS1591" ], - "optimize": true - } - } - }, - "dependencies": { - "Microsoft.Win32.Primitives": "4.0.1", - "Newtonsoft.Json": "8.0.3", - "System.Collections.Concurrent": "4.0.12", - "System.Collections.Immutable": "1.2.0", - "System.IO.Compression": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.Net.Http": "4.1.0", - "System.Net.NameResolution": "4.0.0", - "System.Net.Sockets": "4.1.0", - "System.Net.WebSockets.Client": "4.0.0", - "System.Reflection.Extensions": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.0.0", - "System.Runtime.Serialization.Primitives": "4.1.1", - "System.Text.RegularExpressions": "4.1.0" + "Discord.Net.Core": { + "target": "project" + }, + "Discord.Net.Rest": { + "target": "project" + }, + "Discord.Net.WebSocket": { + "target": "project" + } + //"Discord.Net.Rpc": { + // "target": "project" + //}, + //"Discord.Net.Commands": { + // "target": "project" + //} }, "frameworks": { @@ -56,4 +40,4 @@ ] } } -} +} \ No newline at end of file