diff --git a/Discord.Net.targets b/Discord.Net.targets index 6dc4bb140..95eccd790 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 1.0.1 + 2.0.0-alpha RogueException discord;discordapp diff --git a/LICENSE b/LICENSE index ebd70cd5a..3f78126e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 RogueException +Copyright (c) 2015-2017 Discord.Net Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/guides/commands/samples/typereader.cs b/docs/guides/commands/samples/typereader.cs index b21e6c15a..d2864a4c7 100644 --- a/docs/guides/commands/samples/typereader.cs +++ b/docs/guides/commands/samples/typereader.cs @@ -4,12 +4,12 @@ using Discord.Commands; public class BooleanTypeReader : TypeReader { - public override Task Read(CommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { bool result; if (bool.TryParse(input, out result)) return Task.FromResult(TypeReaderResult.FromSuccess(result)); - - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input could not be parsed as a boolean.")) + + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input could not be parsed as a boolean.")); } -} \ No newline at end of file +} diff --git a/docs/guides/getting_started/samples/intro/client.cs b/docs/guides/getting_started/samples/intro/client.cs index ea7c91932..a73082052 100644 --- a/docs/guides/getting_started/samples/intro/client.cs +++ b/docs/guides/getting_started/samples/intro/client.cs @@ -1,16 +1,17 @@ // Program.cs using Discord.WebSocket; // ... +private DiscordSocketClient _client; public async Task MainAsync() { - var client = new DiscordSocketClient(); + _client = new DiscordSocketClient(); - client.Log += Log; + _client.Log += Log; string token = "abcdefg..."; // Remember to keep this private! - await client.LoginAsync(TokenType.Bot, token); - await client.StartAsync(); + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); // Block this task until the program is closed. await Task.Delay(-1); -} \ No newline at end of file +} diff --git a/docs/guides/getting_started/samples/intro/complete.cs b/docs/guides/getting_started/samples/intro/complete.cs index b59b6b4d9..23b59ce6f 100644 --- a/docs/guides/getting_started/samples/intro/complete.cs +++ b/docs/guides/getting_started/samples/intro/complete.cs @@ -7,19 +7,21 @@ namespace MyBot { public class Program { + private DiscordSocketClient _client; + public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); public async Task MainAsync() { - var client = new DiscordSocketClient(); + _client = new DiscordSocketClient(); - client.Log += Log; - client.MessageReceived += MessageReceived; + _client.Log += Log; + _client.MessageReceived += MessageReceived; string token = "abcdefg..."; // Remember to keep this private! - await client.LoginAsync(TokenType.Bot, token); - await client.StartAsync(); + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); // Block this task until the program is closed. await Task.Delay(-1); @@ -39,4 +41,4 @@ namespace MyBot return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index 706d0a38d..00ce7a6c9 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -39,6 +39,9 @@ class Program // add the `using` at the top, and uncomment this line: //WebSocketProvider = WS4NetProvider.Instance }); + // Subscribe the logging handler to both the client and the CommandService. + _client.Log += Logger; + _commands.Log += Logger; } // Example of a logging handler. This can be re-used by addons @@ -77,9 +80,6 @@ class Program private async Task MainAsync() { - // Subscribe the logging handler. - _client.Log += Logger; - // Centralize the logic for commands into a seperate method. await InitCommands(); diff --git a/src/Discord.Net.Commands/Attributes/NameAttribute.cs b/src/Discord.Net.Commands/Attributes/NameAttribute.cs index 6881d1e2e..0a5156fee 100644 --- a/src/Discord.Net.Commands/Attributes/NameAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/NameAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Commands { // Override public name of command/module - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] public class NameAttribute : Attribute { public string Text { get; } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 0f865e864..b2cd3811c 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -1,13 +1,12 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { /// /// This attribute requires that the bot has a specified permission in the channel a command is invoked in. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireBotPermissionAttribute : PreconditionAttribute { public GuildPermission? GuildPermission { get; } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index 94235b1ae..b3cf25365 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -11,7 +11,7 @@ namespace Discord.Commands { public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { - if (context.Channel.IsNsfw) + if (context.Channel is ITextChannel text && text.IsNsfw) return Task.FromResult(PreconditionResult.FromSuccess()); else return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel.")); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index b7729b0c8..f5e3a9fc5 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 6fae719ee..5a3a1f25a 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -254,6 +254,9 @@ namespace Discord.Commands case ParameterPreconditionAttribute precon: builder.AddPrecondition(precon); break; + case NameAttribute name: + builder.Name = name.Text; + break; case RemainderAttribute _: if (position != count - 1) throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 6ea2abcf3..cf2b93277 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -17,6 +16,9 @@ namespace Discord.Commands public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + public event Func CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _commandExecutedEvent = new AsyncEvent>(); + private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary> _typeReaders; @@ -57,7 +59,10 @@ namespace Discord.Commands _defaultTypeReaders = new ConcurrentDictionary(); foreach (var type in PrimitiveParsers.SupportedTypes) + { _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); + _defaultTypeReaders[typeof(Nullable<>).MakeGenericType(type)] = NullableTypeReader.Create(type, _defaultTypeReaders[type]); + } _defaultTypeReaders[typeof(string)] = new PrimitiveTypeReader((string x, out string y) => { y = x; return true; }, 0); @@ -190,17 +195,35 @@ namespace Discord.Commands return true; } - //Type Readers + //Type Readers + /// + /// Adds a custom to this for the supplied object type. + /// If is a , a will also be added. + /// + /// The object type to be read by the . + /// An instance of the to be added. public void AddTypeReader(TypeReader reader) - { - var readers = _typeReaders.GetOrAdd(typeof(T), x => new ConcurrentDictionary()); - readers[reader.GetType()] = reader; - } + => AddTypeReader(typeof(T), reader); + /// + /// Adds a custom to this for the supplied object type. + /// If is a , a for the value type will also be added. + /// + /// A instance for the type to be read. + /// An instance of the to be added. public void AddTypeReader(Type type, TypeReader reader) { var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); readers[reader.GetType()] = reader; + + if (type.GetTypeInfo().IsValueType) + AddNullableTypeReader(type, reader); } + internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) + { + var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary()); + var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); + readers[nullableReader.GetType()] = nullableReader; + } internal IDictionary GetTypeReaders(Type type) { if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index ebef80baf..c94be525f 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -188,17 +188,22 @@ namespace Discord.Commands if (task is Task resultTask) { var result = await resultTask.ConfigureAwait(false); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); if (result is RuntimeResult execResult) return execResult; } else if (task is Task execTask) { - return await execTask.ConfigureAwait(false); + var result = await execTask.ConfigureAwait(false); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + return result; } else await task.ConfigureAwait(false); - return ExecuteResult.FromSuccess(); + var executeResult = ExecuteResult.FromSuccess(); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, executeResult).ConfigureAwait(false); + return executeResult; } catch (Exception ex) { diff --git a/src/Discord.Net.Commands/Readers/NullableTypeReader.cs b/src/Discord.Net.Commands/Readers/NullableTypeReader.cs new file mode 100644 index 000000000..07976fb69 --- /dev/null +++ b/src/Discord.Net.Commands/Readers/NullableTypeReader.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal static class NullableTypeReader + { + public static TypeReader Create(Type type, TypeReader reader) + { + var constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First(); + return (TypeReader)constructor.Invoke(new object[] { reader }); + } + } + + internal class NullableTypeReader : TypeReader + where T : struct + { + private readonly TypeReader _baseTypeReader; + + public NullableTypeReader(TypeReader baseTypeReader) + { + _baseTypeReader = baseTypeReader; + } + + public override async Task Read(ICommandContext context, string input, IServiceProvider services) + { + if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase)) + return TypeReaderResult.FromSuccess(new T?()); + return await _baseTypeReader.Read(context, input, services); ; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index fbb979951..ea930e112 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -7,10 +7,7 @@ namespace Discord { /// Gets the name of this channel. string Name { get; } - - /// Checks if the channel is NSFW. - bool IsNsfw { get; } - + /// Gets a collection of all users in this channel. IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 038faf6bc..b2b7e491f 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -5,6 +5,9 @@ namespace Discord { public interface ITextChannel : IMessageChannel, IMentionable, IGuildChannel { + /// Checks if the channel is NSFW. + bool IsNsfw { get; } + /// Gets the current topic for this text channel. string Topic { get; } diff --git a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs index 2461a09f2..b7b568133 100644 --- a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -7,5 +7,9 @@ /// What the topic of the channel should be set to. /// public Optional Topic { get; set; } + /// + /// Should this channel be flagged as NSFW? + /// + public Optional IsNsfw { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index ebec03e0b..1b406ef7f 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -60,6 +60,14 @@ /// public Optional AfkChannelId { get; set; } /// + /// The ITextChannel where System messages should be sent. + /// + public Optional SystemChannel { get; set; } + /// + /// The ID of the ITextChannel where System messages should be sent. + /// + public Optional SystemChannelId { get; set; } + /// /// The owner of this guild. /// public Optional Owner { get; set; } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 7874f5fd1..3ded9e038 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -36,6 +36,8 @@ namespace Discord ulong DefaultChannelId { get; } /// Gets the id of the embed channel for this guild if set, or null if not. ulong? EmbedChannelId { get; } + /// Gets the id of the channel where randomized welcome messages are sent, or null if not. + ulong? SystemChannelId { get; } /// Gets the id of the user that created this guild. ulong OwnerId { get; } /// Gets the id of the region hosting this guild's voice channels. @@ -84,6 +86,7 @@ namespace Discord Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Creates a new text channel. diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index 61f908394..52df187f8 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -22,7 +22,8 @@ namespace Discord Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); /// Removes all reactions from this message. Task RemoveAllReactionsAsync(RequestOptions options = null); - Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); + /// Gets all users that reacted to a message with a given emote + Task> GetReactionUsersAsync(IEmote emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); /// Transforms this message's text into a human readable form by resolving its tags. string Resolve( diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index 0861ed33e..eac25391e 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -1,9 +1,13 @@ using System.Threading.Tasks; +using System.IO; namespace Discord { public static class UserExtensions { + /// + /// Sends a message to the user via DM. + /// public static async Task SendMessageAsync(this IUser user, string text, bool isTTS = false, @@ -12,5 +16,33 @@ namespace Discord { return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); } + + /// + /// Sends a file to the user via DM. + /// + public static async Task SendFileAsync(this IUser user, + Stream stream, + string filename, + string text = null, + bool isTTS = false, + RequestOptions options = null + ) + { + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + } + +#if FILESYSTEM + /// + /// Sends a file to the user via DM. + /// + public static async Task SendFileAsync(this IUser user, + string filePath, + string text = null, + bool isTTS = false, + RequestOptions options = null) + { + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + } +#endif } } diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 705a15249..bec8de9dc 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -192,5 +192,13 @@ namespace Discord throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); } } + public static void NotEveryoneRole(ulong[] roles, ulong guildId, string name) + { + for (var i = 0; i < roles.Length; i++) + { + if (roles[i] == guildId) + throw new ArgumentException($"The everyone role cannot be assigned to a user", name); + } + } } } diff --git a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj index 0115d91c0..78987e739 100644 --- a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj +++ b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj @@ -4,12 +4,12 @@ Discord.Net.Providers.WS4Net Discord.Providers.WS4Net An optional WebSocket client provider for Discord.Net using WebSocket4Net - net45 + netstandard1.3 - + - \ No newline at end of file + diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index 56a24a1f4..608ddcf66 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -29,6 +29,8 @@ namespace Discord.API public Optional Topic { get; set; } [JsonProperty("last_pin_timestamp")] public Optional LastPinTimestamp { get; set; } + [JsonProperty("nsfw")] + public Optional Nsfw { get; set; } //VoiceChannel [JsonProperty("bitrate")] diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs index b69ba1293..0ca1bc236 100644 --- a/src/Discord.Net.Rest/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -25,6 +25,8 @@ namespace Discord.API public bool EmbedEnabled { get; set; } [JsonProperty("embed_channel_id")] public ulong? EmbedChannelId { get; set; } + [JsonProperty("system_channel_id")] + public ulong? SystemChannelId { get; set; } [JsonProperty("verification_level")] public VerificationLevel VerificationLevel { get; set; } [JsonProperty("voice_states")] diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs index 2c7d84087..8de10f534 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs @@ -18,6 +18,8 @@ namespace Discord.API.Rest public Optional DefaultMessageNotifications { get; set; } [JsonProperty("afk_timeout")] public Optional AfkTimeout { get; set; } + [JsonProperty("system_channel_id")] + public Optional SystemChannelId { get; set; } [JsonProperty("icon")] public Optional Icon { get; set; } [JsonProperty("splash")] diff --git a/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs index 311336ec3..9cabc67c1 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs @@ -8,5 +8,7 @@ namespace Discord.API.Rest { [JsonProperty("topic")] public Optional Topic { get; set; } + [JsonProperty("nsfw")] + public Optional IsNsfw { get; set; } } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 321f2482b..47a946f20 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -42,7 +42,7 @@ namespace Discord.Rest ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => { if (info == null) - await _restLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); else await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); }; diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 8bc800a7d..2f05d5d36 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -120,6 +120,9 @@ namespace Discord.Rest string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) { var args = new CreateGuildParams(name, region.Id); + if (jpegIcon != null) + args.Icon = new API.Image(jpegIcon); + var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); return RestGuild.Create(client, model); } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 1fac66ec5..a6c42782a 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -392,6 +392,7 @@ namespace Discord.API Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotEqual(roleId, 0, nameof(roleId)); + Preconditions.NotEqual(roleId, guildId, nameof(roleId), "The Everyone role cannot be added to a user."); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); @@ -402,6 +403,7 @@ namespace Discord.API Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotEqual(roleId, 0, nameof(roleId)); + Preconditions.NotEqual(roleId, guildId, nameof(roleId), "The Everyone role cannot be removed from a user."); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); @@ -803,7 +805,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={args.Reason}"; + string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={Uri.EscapeDataString(args.Reason)}"; await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); } public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) @@ -988,7 +990,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={reason}"; + reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={Uri.EscapeDataString(reason)}"; await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false); } public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) @@ -1000,6 +1002,8 @@ namespace Discord.API bool isCurrentUser = userId == CurrentUserId; + if (args.RoleIds.IsSpecified) + Preconditions.NotEveryoneRole(args.RoleIds.Value, guildId, nameof(args.RoleIds)); if (isCurrentUser && args.Nickname.IsSpecified) { var nickArgs = new Rest.ModifyCurrentUserNickParams(args.Nickname.Value ?? ""); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 6b7dca3a9..d61b5d14a 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -41,7 +41,8 @@ namespace Discord.Rest { Name = args.Name, Position = args.Position, - Topic = args.Topic + Topic = args.Topic, + IsNsfw = args.IsNsfw }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } @@ -290,8 +291,8 @@ namespace Discord.Rest return author; } - public static bool IsNsfw(IChannel channel) => - IsNsfw(channel.Name); + public static bool IsNsfw(IChannel channel) + => IsNsfw(channel.Name); public static bool IsNsfw(string channelName) => channelName == "nsfw" || channelName.StartsWith("nsfw-"); } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 7291b591e..342e57717 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -6,7 +6,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { - public abstract class RestChannel : RestEntity, IChannel, IUpdateable + public class RestChannel : RestEntity, IChannel, IUpdateable { public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -25,7 +25,7 @@ namespace Discord.Rest case ChannelType.Group: return CreatePrivate(discord, model) as RestChannel; default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); + return new RestChannel(discord, model.Id); } } internal static IRestPrivateChannel CreatePrivate(BaseDiscordClient discord, Model model) @@ -40,13 +40,12 @@ namespace Discord.Rest throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); } } - internal abstract void Update(Model model); + internal virtual void Update(Model model) { } - public abstract Task UpdateAsync(RequestOptions options = null); + public virtual Task UpdateAsync(RequestOptions options = null) => Task.Delay(0); //IChannel string IChannel.Name => null; - bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overriden diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 114c886c4..07832a3a9 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -7,7 +7,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { - public abstract class RestGuildChannel : RestChannel, IGuildChannel, IUpdateable + public class RestGuildChannel : RestChannel, IGuildChannel, IUpdateable { private ImmutableArray _overwrites; @@ -33,7 +33,8 @@ namespace Discord.Rest case ChannelType.Voice: return RestVoiceChannel.Create(discord, guild, model); default: - throw new InvalidOperationException("Unknown guild channel type"); + // TODO: Channel categories + return new RestGuildChannel(discord, guild, model.Id); } } internal override void Update(Model model) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index d7405fb4a..8a096302b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -15,6 +15,9 @@ namespace Discord.Rest public string Mention => MentionUtils.MentionChannel(Id); + private bool _nsfw; + public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); + internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) { @@ -30,6 +33,7 @@ namespace Discord.Rest base.Update(model); Topic = model.Topic.Value; + _nsfw = model.Nsfw.GetValueOrDefault(); } public async Task ModifyAsync(Action func, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 775f2ea82..7043c8c76 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -99,7 +99,6 @@ namespace Discord.Rest //IChannel string IChannel.Name { get { throw new NotSupportedException(); } } - bool IChannel.IsNsfw { get { throw new NotSupportedException(); } } IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { throw new NotSupportedException(); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 5cfb1e566..2fa29928c 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -26,6 +26,7 @@ namespace Discord.Rest { AfkChannelId = args.AfkChannelId, AfkTimeout = args.AfkTimeout, + SystemChannelId = args.SystemChannelId, DefaultMessageNotifications = args.DefaultMessageNotifications, Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create(), Name = args.Name, @@ -39,6 +40,11 @@ namespace Discord.Rest else if (args.AfkChannelId.IsSpecified) apiArgs.AfkChannelId = args.AfkChannelId.Value; + if (args.SystemChannel.IsSpecified) + apiArgs.SystemChannelId = args.SystemChannel.Value.Id; + else if (args.SystemChannelId.IsSpecified) + apiArgs.SystemChannelId = args.SystemChannelId.Value; + if (args.Owner.IsSpecified) apiArgs.OwnerId = args.Owner.Value.Id; else if (args.OwnerId.IsSpecified) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 11971a5c1..aee305951 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -26,6 +26,7 @@ namespace Discord.Rest public ulong? AFKChannelId { get; private set; } public ulong? EmbedChannelId { get; private set; } + public ulong? SystemChannelId { get; private set; } public ulong OwnerId { get; private set; } public string VoiceRegionId { get; private set; } public string IconId { get; private set; } @@ -33,6 +34,8 @@ namespace Discord.Rest internal bool Available { get; private set; } public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + [Obsolete("DefaultChannelId is deprecated, use GetDefaultChannelAsync")] public ulong DefaultChannelId => Id; public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); @@ -56,6 +59,7 @@ namespace Discord.Rest { AFKChannelId = model.AFKChannelId; EmbedChannelId = model.EmbedChannelId; + SystemChannelId = model.SystemChannelId; AFKTimeout = model.AFKTimeout; IsEmbeddable = model.EmbedEnabled; IconId = model.Icon; @@ -185,8 +189,12 @@ namespace Discord.Rest } public async Task GetDefaultChannelAsync(RequestOptions options = null) { - var channel = await GuildHelper.GetChannelAsync(this, Discord, DefaultChannelId, options).ConfigureAwait(false); - return channel as RestTextChannel; + var channels = await GetTextChannelsAsync(options).ConfigureAwait(false); + var user = await GetCurrentUserAsync(options).ConfigureAwait(false); + return channels + .Where(c => user.GetPermissions(c).ReadMessages) + .OrderBy(c => c.Position) + .FirstOrDefault(); } public async Task GetEmbedChannelAsync(RequestOptions options = null) { @@ -195,6 +203,16 @@ namespace Discord.Rest return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); return null; } + public async Task GetSystemChannelAsync(RequestOptions options = null) + { + var systemId = SystemChannelId; + if (systemId.HasValue) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, systemId.Value, options).ConfigureAwait(false); + return channel as RestTextChannel; + } + return null; + } public Task CreateTextChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) @@ -314,6 +332,13 @@ namespace Discord.Rest else return null; } + async Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetSystemChannelAsync(options).ConfigureAwait(false); + else + return null; + } async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index 7b0285891..f5663cea3 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -171,24 +171,16 @@ namespace Discord return this; } - public EmbedBuilder AddField(string name, object value) + public EmbedBuilder AddField(string name, object value, bool inline = false) { var field = new EmbedFieldBuilder() - .WithIsInline(false) - .WithName(name) - .WithValue(value); - AddField(field); - return this; - } - public EmbedBuilder AddInlineField(string name, object value) - { - var field = new EmbedFieldBuilder() - .WithIsInline(true) + .WithIsInline(inline) .WithName(name) .WithValue(value); AddField(field); return this; } + public EmbedBuilder AddField(EmbedFieldBuilder field) { if (Fields.Count >= MaxFieldCount) @@ -206,17 +198,6 @@ namespace Discord this.AddField(field); return this; } - public EmbedBuilder AddField(string title, string text, bool inline = false) - { - var field = new EmbedFieldBuilder - { - Name = title, - Value = text, - IsInline = inline - }; - _fields.Add(field); - return this; - } public Embed Build() { @@ -234,7 +215,6 @@ namespace Discord return _embed; } - public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); } public class EmbedFieldBuilder @@ -249,7 +229,7 @@ namespace Discord get => _field.Name; set { - if (string.IsNullOrEmpty(value)) throw new ArgumentException($"Field name must not be null or empty.", nameof(Name)); + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", nameof(Name)); if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); _field.Name = value; } diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index ccb683d1f..47bb6f926 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -43,11 +43,12 @@ namespace Discord.Rest await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options); } - public static async Task> GetReactionUsersAsync(IMessage msg, string emoji, + public static async Task> GetReactionUsersAsync(IMessage msg, IEmote emote, Action func, BaseDiscordClient client, RequestOptions options) { var args = new GetReactionUsersParams(); func(args); + string emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name); return (await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false)).Select(u => RestUser.Create(client, u)).ToImmutableArray(); } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index c79c67b38..e5eed874e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -136,10 +136,9 @@ namespace Discord.Rest => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); - - public Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); - + public Task> GetReactionUsersAsync(IEmote emote, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); + public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index 9807b8357..486f41b9e 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -19,7 +19,7 @@ namespace Discord.Rest public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; - public string Mention => MentionUtils.MentionRole(Id); + public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 2fce5f619..e571f8f73 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -46,6 +46,7 @@ namespace Discord.Rest } internal void Update(Model model) { + base.Update(model.User); if (model.JoinedAt.IsSpecified) _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) diff --git a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs index f4d591d7e..a5a440d8b 100644 --- a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs @@ -1,5 +1,6 @@ -using Newtonsoft.Json; -using System; +using System; +using System.IO; +using Newtonsoft.Json; using Model = Discord.API.Image; namespace Discord.Net.Converters @@ -23,10 +24,24 @@ namespace Discord.Net.Converters if (image.Stream != null) { - byte[] bytes = new byte[image.Stream.Length - image.Stream.Position]; - image.Stream.Read(bytes, 0, bytes.Length); + byte[] bytes; + int length; + if (image.Stream.CanSeek) + { + bytes = new byte[image.Stream.Length - image.Stream.Position]; + length = image.Stream.Read(bytes, 0, bytes.Length); + } + else + { + var cloneStream = new MemoryStream(); + image.Stream.CopyTo(cloneStream); + bytes = new byte[cloneStream.Length]; + cloneStream.Position = 0; + cloneStream.Read(bytes, 0, bytes.Length); + length = (int)cloneStream.Length; + } - string base64 = Convert.ToBase64String(bytes); + string base64 = Convert.ToBase64String(bytes, 0, length); writer.WriteValue($"data:image/jpeg;base64,{base64}"); } else if (image.Hash != null) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs index d26c593ba..0c22a03f7 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs @@ -8,7 +8,6 @@ namespace Discord.Rpc public class RpcChannel : RpcEntity { public string Name { get; private set; } - public bool IsNsfw => ChannelHelper.IsNsfw(Name); public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index 72b45e466..9de2968db 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -16,6 +16,8 @@ namespace Discord.Rpc public IReadOnlyCollection CachedMessages { get; private set; } public string Mention => MentionUtils.MentionChannel(Id); + // TODO: Check if RPC includes the 'nsfw' field on Channel models + public bool IsNsfw => ChannelHelper.IsNsfw(this); internal RpcTextChannel(DiscordRpcClient discord, ulong id, ulong guildId) : base(discord, id, guildId) diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index 91a8d7b31..bc175160d 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -108,9 +108,8 @@ namespace Discord.Rpc => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); - - public Task> GetReactionUsersAsync(string emoji, int limit, ulong? afterUserId, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId.HasValue ? afterUserId.Value : Optional.Create(); }, Discord, options); + public Task> GetReactionUsersAsync(IEmote emote, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); public Task PinAsync(RequestOptions options) => MessageHelper.PinAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 3da530633..09b10aac9 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -675,7 +675,12 @@ namespace Discord.WebSocket } } else + { + channel = State.GetChannel(data.Id); + if (channel != null) + return; //Discord may send duplicate CHANNEL_CREATEs for DMs channel = AddPrivateChannel(data, State) as SocketChannel; + } if (channel != null) await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index 42c4156f3..502e61d15 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -41,7 +41,6 @@ namespace Discord.WebSocket //IChannel string IChannel.Name => null; - bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 0e7cfde82..1fe9a741f 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -10,7 +10,7 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public abstract class SocketGuildChannel : SocketChannel, IGuildChannel + public class SocketGuildChannel : SocketChannel, IGuildChannel { private ImmutableArray _overwrites; @@ -19,7 +19,7 @@ namespace Discord.WebSocket public int Position { get; private set; } public IReadOnlyCollection PermissionOverwrites => _overwrites; - public new abstract IReadOnlyCollection Users { get; } + public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id) @@ -35,7 +35,8 @@ namespace Discord.WebSocket case ChannelType.Voice: return SocketVoiceChannel.Create(guild, state, model); default: - throw new InvalidOperationException("Unknown guild channel type"); + // TODO: Proper implementation for channel categories + return new SocketGuildChannel(guild.Discord, model.Id, guild); } } internal override void Update(ClientState state, Model model) @@ -49,7 +50,7 @@ namespace Discord.WebSocket newOverwrites.Add(overwrites[i].ToEntity()); _overwrites = newOverwrites.ToImmutable(); } - + public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); public Task DeleteAsync(RequestOptions options = null) @@ -115,7 +116,7 @@ namespace Discord.WebSocket public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - public new abstract SocketGuildUser GetUser(ulong id); + public new virtual SocketGuildUser GetUser(ulong id) => null; public override string ToString() => Name; internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; @@ -145,7 +146,7 @@ namespace Discord.WebSocket => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); - + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index c22523e00..07ec630d3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -16,6 +16,9 @@ namespace Discord.WebSocket private readonly MessageCache _messages; public string Topic { get; private set; } + + private bool _nsfw; + public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); public string Mention => MentionUtils.MentionChannel(Id); public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); @@ -41,6 +44,7 @@ namespace Discord.WebSocket base.Update(state, model); Topic = model.Topic.Value; + _nsfw = model.Nsfw.GetValueOrDefault(); } public Task ModifyAsync(Action func, RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index aae18be36..b47ca84e8 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -47,6 +47,7 @@ namespace Discord.WebSocket internal ulong? AFKChannelId { get; private set; } internal ulong? EmbedChannelId { get; private set; } + internal ulong? SystemChannelId { get; private set; } public ulong OwnerId { get; private set; } public SocketGuildUser Owner => GetUser(OwnerId); public string VoiceRegionId { get; private set; } @@ -54,7 +55,6 @@ namespace Discord.WebSocket public string SplashId { get; private set; } public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - public SocketTextChannel DefaultChannel => GetTextChannel(Id); public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); public bool HasAllMembers => MemberCount == DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; @@ -62,6 +62,10 @@ namespace Discord.WebSocket public Task SyncPromise => _syncPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task; public IAudioClient AudioClient => _audioClient; + public SocketTextChannel DefaultChannel => TextChannels + .Where(c => CurrentUser.GetPermissions(c).ReadMessages) + .OrderBy(c => c.Position) + .FirstOrDefault(); public SocketVoiceChannel AFKChannel { get @@ -78,6 +82,14 @@ namespace Discord.WebSocket return id.HasValue ? GetChannel(id.Value) : null; } } + public SocketTextChannel SystemChannel + { + get + { + var id = SystemChannelId; + return id.HasValue ? GetTextChannel(id.Value) : null; + } + } public IReadOnlyCollection TextChannels => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); public IReadOnlyCollection VoiceChannels @@ -157,8 +169,6 @@ namespace Discord.WebSocket { if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) member.Update(state, model.Presences[i], true); - else - Debug.Assert(false); } } _members = members; @@ -190,6 +200,7 @@ namespace Discord.WebSocket { AFKChannelId = model.AFKChannelId; EmbedChannelId = model.EmbedChannelId; + SystemChannelId = model.SystemChannelId; AFKTimeout = model.AFKTimeout; IsEmbeddable = model.EmbedEnabled; IconId = model.Icon; @@ -242,8 +253,6 @@ namespace Discord.WebSocket { if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) member.Update(state, model.Presences[i], true); - else - Debug.Assert(false); } } _members = members; @@ -606,8 +615,9 @@ namespace Discord.WebSocket ulong? IGuild.AFKChannelId => AFKChannelId; IAudioClient IGuild.AudioClient => null; bool IGuild.Available => true; - ulong IGuild.DefaultChannelId => Id; + ulong IGuild.DefaultChannelId => DefaultChannel?.Id ?? 0; ulong? IGuild.EmbedChannelId => EmbedChannelId; + ulong? IGuild.SystemChannelId => SystemChannelId; IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Roles => Roles; @@ -632,6 +642,8 @@ namespace Discord.WebSocket => Task.FromResult(DefaultChannel); Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(EmbedChannel); + Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(SystemChannel); async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 40588e55a..b240645e5 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -130,9 +130,8 @@ namespace Discord.WebSocket => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); - - public Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId.HasValue ? afterUserId.Value : Optional.Create(); }, Discord, options); + public Task> GetReactionUsersAsync(IEmote emote, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 7d24d8e1c..c366258cc 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -23,7 +23,7 @@ namespace Discord.WebSocket public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; - public string Mention => MentionUtils.MentionRole(Id); + public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); public IEnumerable Members => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 9695099ee..3d8307da4 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -36,7 +36,7 @@ namespace Discord.Webhook ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => { if (info == null) - await _restLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); else await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); }; diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 864083599..309532615 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,9 +2,9 @@ Discord.Net - 1.0.1$suffix$ + 2.0.0-alpha$suffix$ Discord.Net - RogueException + Discord.Net Contributors RogueException An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp @@ -13,29 +13,29 @@ false - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - \ No newline at end of file + diff --git a/test/Discord.Net.Tests/Tests.Channels.cs b/test/Discord.Net.Tests/Tests.Channels.cs index d81d28f3e..b528ca5fb 100644 --- a/test/Discord.Net.Tests/Tests.Channels.cs +++ b/test/Discord.Net.Tests/Tests.Channels.cs @@ -64,7 +64,7 @@ namespace Discord var text5 = textChannels.Where(x => x.Name == "text5").FirstOrDefault(); Assert.NotNull(text1); - Assert.True(text1.Id == guild.DefaultChannelId); + //Assert.True(text1.Id == guild.DefaultChannelId); Assert.Equal(text1.Position, 1); Assert.Equal(text1.Topic, "Topic1"); diff --git a/test/Discord.Net.Tests/Tests.Colors.cs b/test/Discord.Net.Tests/Tests.Colors.cs new file mode 100644 index 000000000..591778972 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Colors.cs @@ -0,0 +1,86 @@ +using System; +using Xunit; + +namespace Discord +{ + public class ColorTests + { + [Fact] + public void Color_New() + { + Assert.Equal(0u, new Color().RawValue); + Assert.Equal(uint.MinValue, new Color(uint.MinValue).RawValue); + Assert.Equal(uint.MaxValue, new Color(uint.MaxValue).RawValue); + } + public void Color_Default() + { + Assert.Equal(0u, Color.Default.RawValue); + Assert.Equal(0, Color.Default.R); + Assert.Equal(0, Color.Default.G); + Assert.Equal(0, Color.Default.B); + } + [Fact] + public void Color_FromRgb_Byte() + { + Assert.Equal(0xFF0000u, new Color((byte)255, (byte)0, (byte)0).RawValue); + Assert.Equal(0x00FF00u, new Color((byte)0, (byte)255, (byte)0).RawValue); + Assert.Equal(0x0000FFu, new Color((byte)0, (byte)0, (byte)255).RawValue); + Assert.Equal(0xFFFFFFu, new Color((byte)255, (byte)255, (byte)255).RawValue); + } + [Fact] + public void Color_FromRgb_Int() + { + Assert.Equal(0xFF0000u, new Color(255, 0, 0).RawValue); + Assert.Equal(0x00FF00u, new Color(0, 255, 0).RawValue); + Assert.Equal(0x0000FFu, new Color(0, 0, 255).RawValue); + Assert.Equal(0xFFFFFFu, new Color(255, 255, 255).RawValue); + } + [Fact] + public void Color_FromRgb_Int_OutOfRange() + { + Assert.Throws("r", () => new Color(-1024, 0, 0)); + Assert.Throws("r", () => new Color(1024, 0, 0)); + Assert.Throws("g", () => new Color(0, -1024, 0)); + Assert.Throws("g", () => new Color(0, 1024, 0)); + Assert.Throws("b", () => new Color(0, 0, -1024)); + Assert.Throws("b", () => new Color(0, 0, 1024)); + Assert.Throws(() => new Color(-1024, -1024, -1024)); + Assert.Throws(() => new Color(1024, 1024, 1024)); + } + [Fact] + public void Color_FromRgb_Float() + { + Assert.Equal(0xFF0000u, new Color(1.0f, 0, 0).RawValue); + Assert.Equal(0x00FF00u, new Color(0, 1.0f, 0).RawValue); + Assert.Equal(0x0000FFu, new Color(0, 0, 1.0f).RawValue); + Assert.Equal(0xFFFFFFu, new Color(1.0f, 1.0f, 1.0f).RawValue); + } + [Fact] + public void Color_FromRgb_Float_OutOfRange() + { + Assert.Throws("r", () => new Color(-2.0f, 0, 0)); + Assert.Throws("r", () => new Color(2.0f, 0, 0)); + Assert.Throws("g", () => new Color(0, -2.0f, 0)); + Assert.Throws("g", () => new Color(0, 2.0f, 0)); + Assert.Throws("b", () => new Color(0, 0, -2.0f)); + Assert.Throws("b", () => new Color(0, 0, 2.0f)); + Assert.Throws(() => new Color(-2.0f, -2.0f, -2.0f)); + Assert.Throws(() => new Color(2.0f, 2.0f, 2.0f)); + } + [Fact] + public void Color_Red() + { + Assert.Equal(0xAF, new Color(0xAF1390).R); + } + [Fact] + public void Color_Green() + { + Assert.Equal(0x13, new Color(0xAF1390).G); + } + [Fact] + public void Color_Blue() + { + Assert.Equal(0x90, new Color(0xAF1390).B); + } + } +} diff --git a/test/Discord.Net.Tests/Tests.Migrations.cs b/test/Discord.Net.Tests/Tests.Migrations.cs index e786329cd..23e55a737 100644 --- a/test/Discord.Net.Tests/Tests.Migrations.cs +++ b/test/Discord.Net.Tests/Tests.Migrations.cs @@ -57,7 +57,7 @@ namespace Discord foreach (var channel in textChannels) { - if (channel.Id != guild.DefaultChannelId) + //if (channel.Id != guild.DefaultChannelId) await channel.DeleteAsync(); } foreach (var channel in voiceChannels)