diff --git a/docs/guides/voice/samples/joining_audio.cs b/docs/guides/voice/samples/joining_audio.cs index 3abbfb632..8803d3596 100644 --- a/docs/guides/voice/samples/joining_audio.cs +++ b/docs/guides/voice/samples/joining_audio.cs @@ -3,8 +3,8 @@ public async Task JoinChannel(IVoiceChannel channel = null) { // Get the audio channel - channel = channel ?? (msg.Author as IGuildUser)?.VoiceChannel; - if (channel == null) { await msg.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } + channel = channel ?? (Context.User as IGuildUser)?.VoiceChannel; + if (channel == null) { await Context.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } // For the next step with transmitting audio, you would want to pass this Audio Client in to a service. var audioClient = await channel.ConnectAsync(); diff --git a/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs b/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs new file mode 100644 index 000000000..a43286110 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Instructs the command system to treat command paramters of this type + /// as a collection of named arguments matching to its properties. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class NamedArgumentTypeAttribute : Attribute { } +} diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index 85f5df10e..a44dcb6e4 100644 --- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -1,5 +1,4 @@ using System; - using System.Reflection; namespace Discord.Commands @@ -27,8 +26,8 @@ namespace Discord.Commands /// => ReplyAsync(time); /// /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] - public class OverrideTypeReaderAttribute : Attribute + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class OverrideTypeReaderAttribute : Attribute { private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 3b71c87b0..aec8dcbe3 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -280,7 +280,7 @@ namespace Discord.Commands } } - private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) + internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) { var readers = service.GetTypeReaders(paramType); TypeReader reader = null; diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 8a59c247c..4ad5bfac0 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -56,11 +56,36 @@ namespace Discord.Commands.Builders private TypeReader GetReader(Type type) { - var readers = Command.Module.Service.GetTypeReaders(type); + var commands = Command.Module.Service; + if (type.GetTypeInfo().GetCustomAttribute() != null) + { + IsRemainder = true; + var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value; + if (reader == null) + { + Type readerType; + try + { + readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type }); + } + catch (ArgumentException ex) + { + throw new InvalidOperationException($"Parameter type '{type.Name}' for command '{Command.Name}' must be a class with a public parameterless constructor to use as a NamedArgumentType.", ex); + } + + reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands }); + commands.AddTypeReader(type, reader); + } + + return reader; + } + + + var readers = commands.GetTypeReaders(type); if (readers != null) return readers.FirstOrDefault().Value; else - return Command.Module.Service.GetDefaultTypeReader(type); + return commands.GetDefaultTypeReader(type); } public ParameterBuilder WithSummary(string summary) diff --git a/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs b/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs new file mode 100644 index 000000000..01559293f --- /dev/null +++ b/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal sealed class NamedArgumentTypeReader : TypeReader + where T : class, new() + { + private static readonly IReadOnlyDictionary _tProps = typeof(T).GetTypeInfo().DeclaredProperties + .Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic) + .ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); + + private readonly CommandService _commands; + + public NamedArgumentTypeReader(CommandService commands) + { + _commands = commands; + } + + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + var result = new T(); + var state = ReadState.LookingForParameter; + int beginRead = 0, currentRead = 0; + + while (state != ReadState.End) + { + try + { + var prop = Read(out var arg); + var propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false); + if (propVal != null) + prop.SetMethod.Invoke(result, new[] { propVal }); + else + return TypeReaderResult.FromError(CommandError.ParseFailed, $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'."); + } + catch (Exception ex) + { + //TODO: use the Exception overload after a rebase on latest + return TypeReaderResult.FromError(CommandError.Exception, ex.Message); + } + } + + return TypeReaderResult.FromSuccess(result); + + PropertyInfo Read(out string arg) + { + string currentParam = null; + char match = '\0'; + + for (; currentRead < input.Length; currentRead++) + { + var currentChar = input[currentRead]; + switch (state) + { + case ReadState.LookingForParameter: + if (Char.IsWhiteSpace(currentChar)) + continue; + else + { + beginRead = currentRead; + state = ReadState.InParameter; + } + break; + case ReadState.InParameter: + if (currentChar != ':') + continue; + else + { + currentParam = input.Substring(beginRead, currentRead - beginRead); + state = ReadState.LookingForArgument; + } + break; + case ReadState.LookingForArgument: + if (Char.IsWhiteSpace(currentChar)) + continue; + else + { + beginRead = currentRead; + state = (QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match)) + ? ReadState.InQuotedArgument + : ReadState.InArgument; + } + break; + case ReadState.InArgument: + if (!Char.IsWhiteSpace(currentChar)) + continue; + else + return GetPropAndValue(out arg); + case ReadState.InQuotedArgument: + if (currentChar != match) + continue; + else + return GetPropAndValue(out arg); + } + } + + if (currentParam == null) + throw new InvalidOperationException("No parameter name was read."); + + return GetPropAndValue(out arg); + + PropertyInfo GetPropAndValue(out string argv) + { + bool quoted = state == ReadState.InQuotedArgument; + state = (currentRead == (quoted ? input.Length - 1 : input.Length)) + ? ReadState.End + : ReadState.LookingForParameter; + + if (quoted) + { + argv = input.Substring(beginRead + 1, currentRead - beginRead - 1).Trim(); + currentRead++; + } + else + argv = input.Substring(beginRead, currentRead - beginRead); + + return _tProps[currentParam]; + } + } + + async Task ReadArgumentAsync(PropertyInfo prop, string arg) + { + var elemType = prop.PropertyType; + bool isCollection = false; + if (elemType.GetTypeInfo().IsGenericType && elemType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + elemType = prop.PropertyType.GenericTypeArguments[0]; + isCollection = true; + } + + var overridden = prop.GetCustomAttribute(); + var reader = (overridden != null) + ? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services) + : (_commands.GetDefaultTypeReader(elemType) + ?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value); + + if (reader != null) + { + if (isCollection) + { + var method = _readMultipleMethod.MakeGenericMethod(elemType); + var task = (Task)method.Invoke(null, new object[] { reader, context, arg.Split(','), services }); + return await task.ConfigureAwait(false); + } + else + return await ReadSingle(reader, context, arg, services).ConfigureAwait(false); + } + return null; + } + } + + private static async Task ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services) + { + var readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false); + return (readResult.IsSuccess) + ? readResult.BestMatch + : null; + } + private static async Task ReadMultiple(TypeReader reader, ICommandContext context, IEnumerable args, IServiceProvider services) + { + var objs = new List(); + foreach (var arg in args) + { + var read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false); + if (read != null) + objs.Add((TObj)read); + } + return objs.ToImmutableArray(); + } + private static readonly MethodInfo _readMultipleMethod = typeof(NamedArgumentTypeReader) + .GetTypeInfo() + .DeclaredMethods + .Single(m => m.IsPrivate && m.IsStatic && m.Name == nameof(ReadMultiple)); + + private enum ReadState + { + LookingForParameter, + InParameter, + LookingForArgument, + InArgument, + InQuotedArgument, + End + } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index c321cd2e3..a7206bd59 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -535,6 +535,18 @@ namespace Discord /// Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); + /// + /// Adds a user to this guild. + /// + /// + /// This method requires you have an OAuth2 access token for the user, requested with the guilds.join scope, and that the bot have the MANAGE_INVITES permission in the guild. + /// + /// The snowflake identifier of the user. + /// The OAuth2 access token for the user, requested with the guilds.join scope. + /// The delegate containing the properties to be applied to the user upon being added to the guild. + /// The options to be used when sending the request. + /// A guild user associated with the specified ; null if the user is already in the guild. + Task AddGuildUserAsync(ulong userId, string accessToken, Action func = null, RequestOptions options = null); /// /// Gets a collection of all users in this guild. /// diff --git a/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs new file mode 100644 index 000000000..e380d9027 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Properties that are used to add a new to the guild with the following parameters. + /// + /// + public class AddGuildUserProperties + { + /// + /// Gets or sets the user's nickname. + /// + /// + /// To clear the user's nickname, this value can be set to null or + /// . + /// + public Optional Nickname { get; set; } + /// + /// Gets or sets whether the user should be muted in a voice channel. + /// + /// + /// If this value is set to true, no user will be able to hear this user speak in the guild. + /// + public Optional Mute { get; set; } + /// + /// Gets or sets whether the user should be deafened in a voice channel. + /// + /// + /// If this value is set to true, this user will not be able to hear anyone speak in the guild. + /// + public Optional Deaf { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> Roles { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> RoleIds { get; set; } + } +} diff --git a/src/Discord.Net.Core/Net/RpcException.cs b/src/Discord.Net.Core/Net/RpcException.cs deleted file mode 100644 index 195fad73f..000000000 --- a/src/Discord.Net.Core/Net/RpcException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Discord -{ - public class RpcException : Exception - { - public int ErrorCode { get; } - public string Reason { get; } - - public RpcException(int errorCode, string reason = null) - : base($"The server sent error {errorCode}{(reason != null ? $": \"{reason}\"" : "")}") - { - ErrorCode = errorCode; - Reason = reason; - } - } -} diff --git a/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs b/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs new file mode 100644 index 000000000..ef6229edb --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class AddGuildMemberParams + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + [JsonProperty("nick")] + public Optional Nickname { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + [JsonProperty("mute")] + public Optional IsMuted { get; set; } + [JsonProperty("deaf")] + public Optional IsDeafened { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index 6d6eb29b2..479a7857a 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -1,12 +1,17 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using System.Collections.Generic; using System.IO; +using System.Text; +using Discord.Net.Converters; using Discord.Net.Rest; +using Newtonsoft.Json; namespace Discord.API.Rest { internal class UploadWebhookFileParams { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + public Stream File { get; } public Optional Filename { get; set; } @@ -27,18 +32,27 @@ namespace Discord.API.Rest var d = new Dictionary(); d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + var payload = new Dictionary(); if (Content.IsSpecified) - d["content"] = Content.Value; + payload["content"] = Content.Value; if (IsTTS.IsSpecified) - d["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value.ToString(); if (Nonce.IsSpecified) - d["nonce"] = Nonce.Value; + payload["nonce"] = Nonce.Value; if (Username.IsSpecified) - d["username"] = Username.Value; + payload["username"] = Username.Value; if (AvatarUrl.IsSpecified) - d["avatar_url"] = AvatarUrl.Value; + payload["avatar_url"] = AvatarUrl.Value; if (Embeds.IsSpecified) - d["embeds"] = Embeds.Value; + payload["embeds"] = Embeds.Value; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + return d; } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 7ed57ed6f..57d7c718a 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -994,6 +994,25 @@ namespace Discord.API } //Guild Members + public async Task AddGuildMemberAsync(ulong guildId, ulong userId, AddGuildMemberParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrWhitespace(args.AccessToken, nameof(args.AccessToken)); + + if (args.RoleIds.IsSpecified) + { + foreach (var roleId in args.RoleIds.Value) + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + } + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("PUT", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options); + } public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs deleted file mode 100644 index ce37af6b4..000000000 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class RestVirtualMessageChannel : RestEntity, IMessageChannel - { - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - public string Mention => MentionUtils.MentionChannel(Id); - - internal RestVirtualMessageChannel(BaseDiscordClient discord, ulong id) - : base(discord, id) - { - } - internal static RestVirtualMessageChannel Create(BaseDiscordClient discord, ulong id) - { - return new RestVirtualMessageChannel(discord, id); - } - - public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, options); - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); - public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); - - /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); - /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); - - /// - public Task TriggerTypingAsync(RequestOptions options = null) - => ChannelHelper.TriggerTypingAsync(this, Discord, options); - /// - public IDisposable EnterTypingState(RequestOptions options = null) - => ChannelHelper.EnterTypingState(this, Discord, options); - - private string DebuggerDisplay => $"({Id}, Text)"; - - //IMessageChannel - async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetMessageAsync(id, options).ConfigureAwait(false); - else - return null; - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(limit, options); - else - return AsyncEnumerable.Empty>(); - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(fromMessageId, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(fromMessage, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } - async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) - => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - - //IChannel - string IChannel.Name => - throw new NotSupportedException(); - - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => - throw new NotSupportedException(); - - Task IChannel.GetUserAsync(ulong id, 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 7b77dafc7..c31fa89f2 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -257,6 +257,34 @@ namespace Discord.Rest } //Users + public static async Task AddGuildUserAsync(IGuild guild, BaseDiscordClient client, ulong userId, string accessToken, + Action func, RequestOptions options) + { + var args = new AddGuildUserProperties(); + func?.Invoke(args); + + if (args.Roles.IsSpecified) + { + var ids = args.Roles.Value.Select(r => r.Id); + + if (args.RoleIds.IsSpecified) + args.RoleIds.Value.Concat(ids); + else + args.RoleIds = Optional.Create(ids); + } + var apiArgs = new AddGuildMemberParams + { + AccessToken = accessToken, + Nickname = args.Nickname, + IsDeafened = args.Deaf, + IsMuted = args.Mute, + RoleIds = args.RoleIds.IsSpecified ? args.RoleIds.Value.Distinct().ToArray() : Optional.Create() + }; + + var model = await client.ApiClient.AddGuildMemberAsync(guild.Id, userId, apiArgs, options); + + return model is null ? null : RestGuildUser.Create(client, guild, model); + } public static async Task GetUserAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 8cd81f218..bd70bf96b 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -537,6 +537,10 @@ namespace Discord.Rest public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) => GuildHelper.GetUsersAsync(this, Discord, null, null, options); + /// + public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); + /// /// Gets a user from this guild. /// @@ -800,6 +804,10 @@ namespace Discord.Rest async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + /// + async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) + => await AddGuildUserAsync(userId, accessToken, func, options); + /// async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs index 00f3fae69..41c76eb06 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using Model = Discord.API.GuildEmbed; -namespace Discord +namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct RestGuildEmbed diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs index 7a03b68d4..a363f051b 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs @@ -2,7 +2,7 @@ using Discord.Rest; using System.Diagnostics; using Model = Discord.API.VoiceRegion; -namespace Discord +namespace Discord.Rest { /// /// Represents a REST-based voice region. diff --git a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs index 0c91493d2..1afb813c0 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs @@ -3,7 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Model = Discord.API.Connection; -namespace Discord +namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestConnection : IConnection diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index ce8cd08de..412f3acff 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -669,6 +669,10 @@ namespace Discord.WebSocket } //Users + /// + public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); + /// /// Gets a user from this guild. /// @@ -1096,6 +1100,10 @@ namespace Discord.WebSocket /// Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Users); + + /// + async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) + => await AddGuildUserAsync(userId, accessToken, func, options); /// Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 60491a96f..0ee6f7e59 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -3,6 +3,7 @@ Exe Discord netcoreapp1.1 + portable $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 @@ -23,8 +24,8 @@ - - - + + + diff --git a/test/Discord.Net.Tests/Tests.TypeReaders.cs b/test/Discord.Net.Tests/Tests.TypeReaders.cs new file mode 100644 index 000000000..91514bfae --- /dev/null +++ b/test/Discord.Net.Tests/Tests.TypeReaders.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Discord.Commands; +using Xunit; + +namespace Discord +{ + public sealed class TypeReaderTests + { + [Fact] + public async Task TestNamedArgumentReader() + { + var commands = new CommandService(); + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "bar: hello foo: 42"); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: 42, actual: m.Foo); + Assert.Equal(expected: "hello", actual: m.Bar); + } + + [Fact] + public async Task TestQuotedArgumentValue() + { + var commands = new CommandService(); + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "foo: 42 bar: 《hello》"); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: 42, actual: m.Foo); + Assert.Equal(expected: "hello", actual: m.Bar); + } + + [Fact] + public async Task TestNonPatternInput() + { + var commands = new CommandService(); + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "foobar"); + Assert.False(result.IsSuccess); + Assert.Equal(expected: CommandError.Exception, actual: result.Error); + } + + [Fact] + public async Task TestMultiple() + { + var commands = new CommandService(); + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "manyints: \"1, 2, 3, 4, 5, 6, 7\""); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: new int[] { 1, 2, 3, 4, 5, 6, 7 }, actual: m.ManyInts); + } + } + + [NamedArgumentType] + public sealed class ArgumentType + { + public int Foo { get; set; } + + [OverrideTypeReader(typeof(CustomTypeReader))] + public string Bar { get; set; } + + public IEnumerable ManyInts { get; set; } + } + + public sealed class CustomTypeReader : TypeReader + { + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + => Task.FromResult(TypeReaderResult.FromSuccess(input)); + } + + public sealed class TestModule : ModuleBase + { + [Command("test")] + public Task TestCommand(ArgumentType arg) => Task.Delay(0); + } +}