| @@ -3,8 +3,8 @@ | |||||
| public async Task JoinChannel(IVoiceChannel channel = null) | public async Task JoinChannel(IVoiceChannel channel = null) | ||||
| { | { | ||||
| // Get the audio channel | // 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. | // For the next step with transmitting audio, you would want to pass this Audio Client in to a service. | ||||
| var audioClient = await channel.ConnectAsync(); | var audioClient = await channel.ConnectAsync(); | ||||
| @@ -0,0 +1,11 @@ | |||||
| using System; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| /// <summary> | |||||
| /// Instructs the command system to treat command paramters of this type | |||||
| /// as a collection of named arguments matching to its properties. | |||||
| /// </summary> | |||||
| [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | |||||
| public sealed class NamedArgumentTypeAttribute : Attribute { } | |||||
| } | |||||
| @@ -1,5 +1,4 @@ | |||||
| using System; | using System; | ||||
| using System.Reflection; | using System.Reflection; | ||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| @@ -27,8 +26,8 @@ namespace Discord.Commands | |||||
| /// => ReplyAsync(time); | /// => ReplyAsync(time); | ||||
| /// </code> | /// </code> | ||||
| /// </example> | /// </example> | ||||
| [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(); | private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); | ||||
| @@ -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); | var readers = service.GetTypeReaders(paramType); | ||||
| TypeReader reader = null; | TypeReader reader = null; | ||||
| @@ -56,11 +56,36 @@ namespace Discord.Commands.Builders | |||||
| private TypeReader GetReader(Type type) | private TypeReader GetReader(Type type) | ||||
| { | { | ||||
| var readers = Command.Module.Service.GetTypeReaders(type); | |||||
| var commands = Command.Module.Service; | |||||
| if (type.GetTypeInfo().GetCustomAttribute<NamedArgumentTypeAttribute>() != 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) | if (readers != null) | ||||
| return readers.FirstOrDefault().Value; | return readers.FirstOrDefault().Value; | ||||
| else | else | ||||
| return Command.Module.Service.GetDefaultTypeReader(type); | |||||
| return commands.GetDefaultTypeReader(type); | |||||
| } | } | ||||
| public ParameterBuilder WithSummary(string summary) | public ParameterBuilder WithSummary(string summary) | ||||
| @@ -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<T> : TypeReader | |||||
| where T : class, new() | |||||
| { | |||||
| private static readonly IReadOnlyDictionary<string, PropertyInfo> _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<TypeReaderResult> 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<object> 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<OverrideTypeReaderAttribute>(); | |||||
| 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<IEnumerable>)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<object> 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<IEnumerable> ReadMultiple<TObj>(TypeReader reader, ICommandContext context, IEnumerable<string> args, IServiceProvider services) | |||||
| { | |||||
| var objs = new List<TObj>(); | |||||
| 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<T>) | |||||
| .GetTypeInfo() | |||||
| .DeclaredMethods | |||||
| .Single(m => m.IsPrivate && m.IsStatic && m.Name == nameof(ReadMultiple)); | |||||
| private enum ReadState | |||||
| { | |||||
| LookingForParameter, | |||||
| InParameter, | |||||
| LookingForArgument, | |||||
| InArgument, | |||||
| InQuotedArgument, | |||||
| End | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -535,6 +535,18 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| Task<IRole> CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); | Task<IRole> CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); | ||||
| /// <summary> | |||||
| /// Adds a user to this guild. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// 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. | |||||
| /// </remarks> | |||||
| /// <param name="id">The snowflake identifier of the user.</param> | |||||
| /// <param name="accessToken">The OAuth2 access token for the user, requested with the guilds.join scope.</param> | |||||
| /// <param name="func">The delegate containing the properties to be applied to the user upon being added to the guild.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns>A guild user associated with the specified <paramref name="id" />; <c>null</c> if the user is already in the guild.</returns> | |||||
| Task<IGuildUser> AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a collection of all users in this guild. | /// Gets a collection of all users in this guild. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -0,0 +1,62 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> | |||||
| /// Properties that are used to add a new <see cref="IGuildUser"/> to the guild with the following parameters. | |||||
| /// </summary> | |||||
| /// <seealso cref="IGuild.AddGuildUserAsync" /> | |||||
| public class AddGuildUserProperties | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets or sets the user's nickname. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// To clear the user's nickname, this value can be set to <c>null</c> or | |||||
| /// <see cref="string.Empty"/>. | |||||
| /// </remarks> | |||||
| public Optional<string> Nickname { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets whether the user should be muted in a voice channel. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// If this value is set to <c>true</c>, no user will be able to hear this user speak in the guild. | |||||
| /// </remarks> | |||||
| public Optional<bool> Mute { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets whether the user should be deafened in a voice channel. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// If this value is set to <c>true</c>, this user will not be able to hear anyone speak in the guild. | |||||
| /// </remarks> | |||||
| public Optional<bool> Deaf { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the roles the user should have. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// <para> | |||||
| /// To add a role to a user: | |||||
| /// <see cref="IGuildUser.AddRolesAsync(IEnumerable{IRole},RequestOptions)" /> | |||||
| /// </para> | |||||
| /// <para> | |||||
| /// To remove a role from a user: | |||||
| /// <see cref="IGuildUser.RemoveRolesAsync(IEnumerable{IRole},RequestOptions)" /> | |||||
| /// </para> | |||||
| /// </remarks> | |||||
| public Optional<IEnumerable<IRole>> Roles { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the roles the user should have. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// <para> | |||||
| /// To add a role to a user: | |||||
| /// <see cref="IGuildUser.AddRolesAsync(IEnumerable{IRole},RequestOptions)" /> | |||||
| /// </para> | |||||
| /// <para> | |||||
| /// To remove a role from a user: | |||||
| /// <see cref="IGuildUser.RemoveRolesAsync(IEnumerable{IRole},RequestOptions)" /> | |||||
| /// </para> | |||||
| /// </remarks> | |||||
| public Optional<IEnumerable<ulong>> RoleIds { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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<string> Nickname { get; set; } | |||||
| [JsonProperty("roles")] | |||||
| public Optional<ulong[]> RoleIds { get; set; } | |||||
| [JsonProperty("mute")] | |||||
| public Optional<bool> IsMuted { get; set; } | |||||
| [JsonProperty("deaf")] | |||||
| public Optional<bool> IsDeafened { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -1,12 +1,17 @@ | |||||
| #pragma warning disable CS1591 | |||||
| #pragma warning disable CS1591 | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.IO; | using System.IO; | ||||
| using System.Text; | |||||
| using Discord.Net.Converters; | |||||
| using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
| { | { | ||||
| internal class UploadWebhookFileParams | internal class UploadWebhookFileParams | ||||
| { | { | ||||
| private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||||
| public Stream File { get; } | public Stream File { get; } | ||||
| public Optional<string> Filename { get; set; } | public Optional<string> Filename { get; set; } | ||||
| @@ -27,18 +32,27 @@ namespace Discord.API.Rest | |||||
| var d = new Dictionary<string, object>(); | var d = new Dictionary<string, object>(); | ||||
| d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); | d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); | ||||
| var payload = new Dictionary<string, object>(); | |||||
| if (Content.IsSpecified) | if (Content.IsSpecified) | ||||
| d["content"] = Content.Value; | |||||
| payload["content"] = Content.Value; | |||||
| if (IsTTS.IsSpecified) | if (IsTTS.IsSpecified) | ||||
| d["tts"] = IsTTS.Value.ToString(); | |||||
| payload["tts"] = IsTTS.Value.ToString(); | |||||
| if (Nonce.IsSpecified) | if (Nonce.IsSpecified) | ||||
| d["nonce"] = Nonce.Value; | |||||
| payload["nonce"] = Nonce.Value; | |||||
| if (Username.IsSpecified) | if (Username.IsSpecified) | ||||
| d["username"] = Username.Value; | |||||
| payload["username"] = Username.Value; | |||||
| if (AvatarUrl.IsSpecified) | if (AvatarUrl.IsSpecified) | ||||
| d["avatar_url"] = AvatarUrl.Value; | |||||
| payload["avatar_url"] = AvatarUrl.Value; | |||||
| if (Embeds.IsSpecified) | 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; | return d; | ||||
| } | } | ||||
| } | } | ||||
| @@ -994,6 +994,25 @@ namespace Discord.API | |||||
| } | } | ||||
| //Guild Members | //Guild Members | ||||
| public async Task<GuildMember> 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<GuildMember>("PUT", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options); | |||||
| } | |||||
| public async Task<GuildMember> GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) | public async Task<GuildMember> GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) | ||||
| { | { | ||||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | Preconditions.NotEqual(guildId, 0, nameof(guildId)); | ||||
| @@ -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<ulong>, 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<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null) | |||||
| => ChannelHelper.GetMessageAsync(this, Discord, id, options); | |||||
| public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||||
| => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); | |||||
| public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||||
| => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); | |||||
| public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||||
| => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); | |||||
| public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null) | |||||
| => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); | |||||
| public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
| => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | |||||
| public Task<RestUserMessage> 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<RestUserMessage> 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); | |||||
| /// <inheritdoc /> | |||||
| public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); | |||||
| /// <inheritdoc /> | |||||
| public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); | |||||
| /// <inheritdoc /> | |||||
| public Task TriggerTypingAsync(RequestOptions options = null) | |||||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||||
| /// <inheritdoc /> | |||||
| public IDisposable EnterTypingState(RequestOptions options = null) | |||||
| => ChannelHelper.EnterTypingState(this, Discord, options); | |||||
| private string DebuggerDisplay => $"({Id}, Text)"; | |||||
| //IMessageChannel | |||||
| async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) | |||||
| { | |||||
| if (mode == CacheMode.AllowDownload) | |||||
| return await GetMessageAsync(id, options).ConfigureAwait(false); | |||||
| else | |||||
| return null; | |||||
| } | |||||
| IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) | |||||
| { | |||||
| if (mode == CacheMode.AllowDownload) | |||||
| return GetMessagesAsync(limit, options); | |||||
| else | |||||
| return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>(); | |||||
| } | |||||
| IAsyncEnumerable<IReadOnlyCollection<IMessage>> 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<IReadOnlyCollection<IMessage>>(); | |||||
| } | |||||
| IAsyncEnumerable<IReadOnlyCollection<IMessage>> 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<IReadOnlyCollection<IMessage>>(); | |||||
| } | |||||
| async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | |||||
| => await GetPinnedMessagesAsync(options).ConfigureAwait(false); | |||||
| async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
| => await SendFileAsync(filePath, text, isTTS, embed, options); | |||||
| async Task<IUserMessage> 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<IUserMessage> 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<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => | |||||
| throw new NotSupportedException(); | |||||
| Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => | |||||
| throw new NotSupportedException(); | |||||
| } | |||||
| } | |||||
| @@ -257,6 +257,34 @@ namespace Discord.Rest | |||||
| } | } | ||||
| //Users | //Users | ||||
| public static async Task<RestGuildUser> AddGuildUserAsync(IGuild guild, BaseDiscordClient client, ulong userId, string accessToken, | |||||
| Action<AddGuildUserProperties> 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<ulong[]>() | |||||
| }; | |||||
| 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<RestGuildUser> GetUserAsync(IGuild guild, BaseDiscordClient client, | public static async Task<RestGuildUser> GetUserAsync(IGuild guild, BaseDiscordClient client, | ||||
| ulong id, RequestOptions options) | ulong id, RequestOptions options) | ||||
| { | { | ||||
| @@ -537,6 +537,10 @@ namespace Discord.Rest | |||||
| public IAsyncEnumerable<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(RequestOptions options = null) | public IAsyncEnumerable<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(RequestOptions options = null) | ||||
| => GuildHelper.GetUsersAsync(this, Discord, null, null, options); | => GuildHelper.GetUsersAsync(this, Discord, null, null, options); | ||||
| /// <inheritdoc /> | |||||
| public Task<RestGuildUser> AddGuildUserAsync(ulong id, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null) | |||||
| => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a user from this guild. | /// Gets a user from this guild. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -800,6 +804,10 @@ namespace Discord.Rest | |||||
| async Task<IRole> IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) | async Task<IRole> IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) | ||||
| => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); | => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | |||||
| async Task<IGuildUser> IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func, RequestOptions options) | |||||
| => await AddGuildUserAsync(userId, accessToken, func, options); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | async Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
| { | { | ||||
| @@ -1,7 +1,7 @@ | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using Model = Discord.API.GuildEmbed; | using Model = Discord.API.GuildEmbed; | ||||
| namespace Discord | |||||
| namespace Discord.Rest | |||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public struct RestGuildEmbed | public struct RestGuildEmbed | ||||
| @@ -2,7 +2,7 @@ using Discord.Rest; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using Model = Discord.API.VoiceRegion; | using Model = Discord.API.VoiceRegion; | ||||
| namespace Discord | |||||
| namespace Discord.Rest | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Represents a REST-based voice region. | /// Represents a REST-based voice region. | ||||
| @@ -3,7 +3,7 @@ using System.Collections.Immutable; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using Model = Discord.API.Connection; | using Model = Discord.API.Connection; | ||||
| namespace Discord | |||||
| namespace Discord.Rest | |||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class RestConnection : IConnection | public class RestConnection : IConnection | ||||
| @@ -669,6 +669,10 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| //Users | //Users | ||||
| /// <inheritdoc /> | |||||
| public Task<RestGuildUser> AddGuildUserAsync(ulong id, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null) | |||||
| => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a user from this guild. | /// Gets a user from this guild. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -1096,6 +1100,10 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) | Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) | ||||
| => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Users); | => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Users); | ||||
| /// <inheritdoc /> | |||||
| async Task<IGuildUser> IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func, RequestOptions options) | |||||
| => await AddGuildUserAsync(userId, accessToken, func, options); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
| => Task.FromResult<IGuildUser>(GetUser(id)); | => Task.FromResult<IGuildUser>(GetUser(id)); | ||||
| @@ -3,6 +3,7 @@ | |||||
| <OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
| <RootNamespace>Discord</RootNamespace> | <RootNamespace>Discord</RootNamespace> | ||||
| <TargetFramework>netcoreapp1.1</TargetFramework> | <TargetFramework>netcoreapp1.1</TargetFramework> | ||||
| <DebugType>portable</DebugType> | |||||
| <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> | <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> | ||||
| </PropertyGroup> | </PropertyGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| @@ -23,8 +24,8 @@ | |||||
| <PackageReference Include="Akavache" Version="5.0.0" /> | <PackageReference Include="Akavache" Version="5.0.0" /> | ||||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" /> | <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" /> | ||||
| <PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> | <PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> | ||||
| <PackageReference Include="xunit" Version="2.3.1" /> | |||||
| <PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" /> | |||||
| <PackageReference Include="xunit.runner.reporters" Version="2.3.1" /> | |||||
| <PackageReference Include="xunit" Version="2.4.0" /> | |||||
| <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> | |||||
| <PackageReference Include="xunit.runner.reporters" Version="2.4.0" /> | |||||
| </ItemGroup> | </ItemGroup> | ||||
| </Project> | </Project> | ||||
| @@ -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<TestModule>(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<TestModule>(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<TestModule>(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<TestModule>(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<int> ManyInts { get; set; } | |||||
| } | |||||
| public sealed class CustomTypeReader : TypeReader | |||||
| { | |||||
| public override Task<TypeReaderResult> 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); | |||||
| } | |||||
| } | |||||