| @@ -1,6 +1,6 @@ | |||||
| <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <VersionPrefix>1.0.1</VersionPrefix> | |||||
| <VersionPrefix>2.0.0-alpha</VersionPrefix> | |||||
| <VersionSuffix></VersionSuffix> | <VersionSuffix></VersionSuffix> | ||||
| <Authors>RogueException</Authors> | <Authors>RogueException</Authors> | ||||
| <PackageTags>discord;discordapp</PackageTags> | <PackageTags>discord;discordapp</PackageTags> | ||||
| @@ -1,6 +1,6 @@ | |||||
| The MIT License (MIT) | 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 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -4,12 +4,12 @@ using Discord.Commands; | |||||
| public class BooleanTypeReader : TypeReader | public class BooleanTypeReader : TypeReader | ||||
| { | { | ||||
| public override Task<TypeReaderResult> Read(CommandContext context, string input) | |||||
| public override Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services) | |||||
| { | { | ||||
| bool result; | bool result; | ||||
| if (bool.TryParse(input, out result)) | if (bool.TryParse(input, out result)) | ||||
| return Task.FromResult(TypeReaderResult.FromSuccess(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.")); | |||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -1,16 +1,17 @@ | |||||
| // Program.cs | // Program.cs | ||||
| using Discord.WebSocket; | using Discord.WebSocket; | ||||
| // ... | // ... | ||||
| private DiscordSocketClient _client; | |||||
| public async Task MainAsync() | 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! | 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. | // Block this task until the program is closed. | ||||
| await Task.Delay(-1); | await Task.Delay(-1); | ||||
| } | |||||
| } | |||||
| @@ -7,19 +7,21 @@ namespace MyBot | |||||
| { | { | ||||
| public class Program | public class Program | ||||
| { | { | ||||
| private DiscordSocketClient _client; | |||||
| public static void Main(string[] args) | public static void Main(string[] args) | ||||
| => new Program().MainAsync().GetAwaiter().GetResult(); | => new Program().MainAsync().GetAwaiter().GetResult(); | ||||
| public async Task MainAsync() | 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! | 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. | // Block this task until the program is closed. | ||||
| await Task.Delay(-1); | await Task.Delay(-1); | ||||
| @@ -39,4 +41,4 @@ namespace MyBot | |||||
| return Task.CompletedTask; | return Task.CompletedTask; | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -39,6 +39,9 @@ class Program | |||||
| // add the `using` at the top, and uncomment this line: | // add the `using` at the top, and uncomment this line: | ||||
| //WebSocketProvider = WS4NetProvider.Instance | //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 | // Example of a logging handler. This can be re-used by addons | ||||
| @@ -77,9 +80,6 @@ class Program | |||||
| private async Task MainAsync() | private async Task MainAsync() | ||||
| { | { | ||||
| // Subscribe the logging handler. | |||||
| _client.Log += Logger; | |||||
| // Centralize the logic for commands into a seperate method. | // Centralize the logic for commands into a seperate method. | ||||
| await InitCommands(); | await InitCommands(); | ||||
| @@ -3,7 +3,7 @@ using System; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| // Override public name of command/module | // Override public name of command/module | ||||
| [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] | |||||
| [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] | |||||
| public class NameAttribute : Attribute | public class NameAttribute : Attribute | ||||
| { | { | ||||
| public string Text { get; } | public string Text { get; } | ||||
| @@ -1,13 +1,12 @@ | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.DependencyInjection; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// This attribute requires that the bot has a specified permission in the channel a command is invoked in. | /// This attribute requires that the bot has a specified permission in the channel a command is invoked in. | ||||
| /// </summary> | /// </summary> | ||||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||||
| public class RequireBotPermissionAttribute : PreconditionAttribute | public class RequireBotPermissionAttribute : PreconditionAttribute | ||||
| { | { | ||||
| public GuildPermission? GuildPermission { get; } | public GuildPermission? GuildPermission { get; } | ||||
| @@ -11,7 +11,7 @@ namespace Discord.Commands | |||||
| { | { | ||||
| public override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) | public override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) | ||||
| { | { | ||||
| if (context.Channel.IsNsfw) | |||||
| if (context.Channel is ITextChannel text && text.IsNsfw) | |||||
| return Task.FromResult(PreconditionResult.FromSuccess()); | return Task.FromResult(PreconditionResult.FromSuccess()); | ||||
| else | else | ||||
| return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel.")); | return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel.")); | ||||
| @@ -1,6 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.DependencyInjection; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| @@ -254,6 +254,9 @@ namespace Discord.Commands | |||||
| case ParameterPreconditionAttribute precon: | case ParameterPreconditionAttribute precon: | ||||
| builder.AddPrecondition(precon); | builder.AddPrecondition(precon); | ||||
| break; | break; | ||||
| case NameAttribute name: | |||||
| builder.Name = name.Text; | |||||
| break; | |||||
| case RemainderAttribute _: | case RemainderAttribute _: | ||||
| if (position != count - 1) | 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}"); | throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); | ||||
| @@ -8,7 +8,6 @@ using System.Linq; | |||||
| using System.Reflection; | using System.Reflection; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.DependencyInjection; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| @@ -17,6 +16,9 @@ namespace Discord.Commands | |||||
| public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | ||||
| internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | ||||
| public event Func<CommandInfo, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } | |||||
| internal readonly AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>>(); | |||||
| private readonly SemaphoreSlim _moduleLock; | private readonly SemaphoreSlim _moduleLock; | ||||
| private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | ||||
| private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders; | private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders; | ||||
| @@ -57,7 +59,10 @@ namespace Discord.Commands | |||||
| _defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader>(); | _defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader>(); | ||||
| foreach (var type in PrimitiveParsers.SupportedTypes) | foreach (var type in PrimitiveParsers.SupportedTypes) | ||||
| { | |||||
| _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); | _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); | ||||
| _defaultTypeReaders[typeof(Nullable<>).MakeGenericType(type)] = NullableTypeReader.Create(type, _defaultTypeReaders[type]); | |||||
| } | |||||
| _defaultTypeReaders[typeof(string)] = | _defaultTypeReaders[typeof(string)] = | ||||
| new PrimitiveTypeReader<string>((string x, out string y) => { y = x; return true; }, 0); | new PrimitiveTypeReader<string>((string x, out string y) => { y = x; return true; }, 0); | ||||
| @@ -190,17 +195,35 @@ namespace Discord.Commands | |||||
| return true; | return true; | ||||
| } | } | ||||
| //Type Readers | |||||
| //Type Readers | |||||
| /// <summary> | |||||
| /// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. | |||||
| /// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added. | |||||
| /// </summary> | |||||
| /// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam> | |||||
| /// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> | |||||
| public void AddTypeReader<T>(TypeReader reader) | public void AddTypeReader<T>(TypeReader reader) | ||||
| { | |||||
| var readers = _typeReaders.GetOrAdd(typeof(T), x => new ConcurrentDictionary<Type, TypeReader>()); | |||||
| readers[reader.GetType()] = reader; | |||||
| } | |||||
| => AddTypeReader(typeof(T), reader); | |||||
| /// <summary> | |||||
| /// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. | |||||
| /// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added. | |||||
| /// </summary> | |||||
| /// <param name="type">A <see cref="Type"/> instance for the type to be read.</param> | |||||
| /// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> | |||||
| public void AddTypeReader(Type type, TypeReader reader) | public void AddTypeReader(Type type, TypeReader reader) | ||||
| { | { | ||||
| var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>()); | var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>()); | ||||
| readers[reader.GetType()] = reader; | 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<Type, TypeReader>()); | |||||
| var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); | |||||
| readers[nullableReader.GetType()] = nullableReader; | |||||
| } | |||||
| internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | ||||
| { | { | ||||
| if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) | if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) | ||||
| @@ -188,17 +188,22 @@ namespace Discord.Commands | |||||
| if (task is Task<IResult> resultTask) | if (task is Task<IResult> resultTask) | ||||
| { | { | ||||
| var result = await resultTask.ConfigureAwait(false); | var result = await resultTask.ConfigureAwait(false); | ||||
| await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); | |||||
| if (result is RuntimeResult execResult) | if (result is RuntimeResult execResult) | ||||
| return execResult; | return execResult; | ||||
| } | } | ||||
| else if (task is Task<ExecuteResult> execTask) | else if (task is Task<ExecuteResult> 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 | else | ||||
| await task.ConfigureAwait(false); | 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) | catch (Exception ex) | ||||
| { | { | ||||
| @@ -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<T> : TypeReader | |||||
| where T : struct | |||||
| { | |||||
| private readonly TypeReader _baseTypeReader; | |||||
| public NullableTypeReader(TypeReader baseTypeReader) | |||||
| { | |||||
| _baseTypeReader = baseTypeReader; | |||||
| } | |||||
| public override async Task<TypeReaderResult> 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); ; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -7,10 +7,7 @@ namespace Discord | |||||
| { | { | ||||
| /// <summary> Gets the name of this channel. </summary> | /// <summary> Gets the name of this channel. </summary> | ||||
| string Name { get; } | string Name { get; } | ||||
| /// <summary> Checks if the channel is NSFW. </summary> | |||||
| bool IsNsfw { get; } | |||||
| /// <summary> Gets a collection of all users in this channel. </summary> | /// <summary> Gets a collection of all users in this channel. </summary> | ||||
| IAsyncEnumerable<IReadOnlyCollection<IUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | IAsyncEnumerable<IReadOnlyCollection<IUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | ||||
| @@ -5,6 +5,9 @@ namespace Discord | |||||
| { | { | ||||
| public interface ITextChannel : IMessageChannel, IMentionable, IGuildChannel | public interface ITextChannel : IMessageChannel, IMentionable, IGuildChannel | ||||
| { | { | ||||
| /// <summary> Checks if the channel is NSFW. </summary> | |||||
| bool IsNsfw { get; } | |||||
| /// <summary> Gets the current topic for this text channel. </summary> | /// <summary> Gets the current topic for this text channel. </summary> | ||||
| string Topic { get; } | string Topic { get; } | ||||
| @@ -7,5 +7,9 @@ | |||||
| /// What the topic of the channel should be set to. | /// What the topic of the channel should be set to. | ||||
| /// </summary> | /// </summary> | ||||
| public Optional<string> Topic { get; set; } | public Optional<string> Topic { get; set; } | ||||
| /// <summary> | |||||
| /// Should this channel be flagged as NSFW? | |||||
| /// </summary> | |||||
| public Optional<bool> IsNsfw { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -60,6 +60,14 @@ | |||||
| /// </summary> | /// </summary> | ||||
| public Optional<ulong?> AfkChannelId { get; set; } | public Optional<ulong?> AfkChannelId { get; set; } | ||||
| /// <summary> | /// <summary> | ||||
| /// The ITextChannel where System messages should be sent. | |||||
| /// </summary> | |||||
| public Optional<ITextChannel> SystemChannel { get; set; } | |||||
| /// <summary> | |||||
| /// The ID of the ITextChannel where System messages should be sent. | |||||
| /// </summary> | |||||
| public Optional<ulong?> SystemChannelId { get; set; } | |||||
| /// <summary> | |||||
| /// The owner of this guild. | /// The owner of this guild. | ||||
| /// </summary> | /// </summary> | ||||
| public Optional<IUser> Owner { get; set; } | public Optional<IUser> Owner { get; set; } | ||||
| @@ -36,6 +36,8 @@ namespace Discord | |||||
| ulong DefaultChannelId { get; } | ulong DefaultChannelId { get; } | ||||
| /// <summary> Gets the id of the embed channel for this guild if set, or null if not. </summary> | /// <summary> Gets the id of the embed channel for this guild if set, or null if not. </summary> | ||||
| ulong? EmbedChannelId { get; } | ulong? EmbedChannelId { get; } | ||||
| /// <summary> Gets the id of the channel where randomized welcome messages are sent, or null if not. </summary> | |||||
| ulong? SystemChannelId { get; } | |||||
| /// <summary> Gets the id of the user that created this guild. </summary> | /// <summary> Gets the id of the user that created this guild. </summary> | ||||
| ulong OwnerId { get; } | ulong OwnerId { get; } | ||||
| /// <summary> Gets the id of the region hosting this guild's voice channels. </summary> | /// <summary> Gets the id of the region hosting this guild's voice channels. </summary> | ||||
| @@ -84,6 +86,7 @@ namespace Discord | |||||
| Task<IReadOnlyCollection<IVoiceChannel>> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | Task<IReadOnlyCollection<IVoiceChannel>> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | ||||
| Task<IVoiceChannel> GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | Task<IVoiceChannel> GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | ||||
| Task<IVoiceChannel> GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | Task<IVoiceChannel> GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | ||||
| Task<ITextChannel> GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | |||||
| Task<ITextChannel> GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | Task<ITextChannel> GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | ||||
| Task<IGuildChannel> GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | Task<IGuildChannel> GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | ||||
| /// <summary> Creates a new text channel. </summary> | /// <summary> Creates a new text channel. </summary> | ||||
| @@ -22,7 +22,8 @@ namespace Discord | |||||
| Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); | Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); | ||||
| /// <summary> Removes all reactions from this message. </summary> | /// <summary> Removes all reactions from this message. </summary> | ||||
| Task RemoveAllReactionsAsync(RequestOptions options = null); | Task RemoveAllReactionsAsync(RequestOptions options = null); | ||||
| Task<IReadOnlyCollection<IUser>> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); | |||||
| /// <summary> Gets all users that reacted to a message with a given emote </summary> | |||||
| Task<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); | |||||
| /// <summary> Transforms this message's text into a human readable form by resolving its tags. </summary> | /// <summary> Transforms this message's text into a human readable form by resolving its tags. </summary> | ||||
| string Resolve( | string Resolve( | ||||
| @@ -1,9 +1,13 @@ | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using System.IO; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public static class UserExtensions | public static class UserExtensions | ||||
| { | { | ||||
| /// <summary> | |||||
| /// Sends a message to the user via DM. | |||||
| /// </summary> | |||||
| public static async Task<IUserMessage> SendMessageAsync(this IUser user, | public static async Task<IUserMessage> SendMessageAsync(this IUser user, | ||||
| string text, | string text, | ||||
| bool isTTS = false, | bool isTTS = false, | ||||
| @@ -12,5 +16,33 @@ namespace Discord | |||||
| { | { | ||||
| return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sends a file to the user via DM. | |||||
| /// </summary> | |||||
| public static async Task<IUserMessage> 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 | |||||
| /// <summary> | |||||
| /// Sends a file to the user via DM. | |||||
| /// </summary> | |||||
| public static async Task<IUserMessage> 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 | |||||
| } | } | ||||
| } | } | ||||
| @@ -192,5 +192,13 @@ namespace Discord | |||||
| throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); | 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); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -4,12 +4,12 @@ | |||||
| <AssemblyName>Discord.Net.Providers.WS4Net</AssemblyName> | <AssemblyName>Discord.Net.Providers.WS4Net</AssemblyName> | ||||
| <RootNamespace>Discord.Providers.WS4Net</RootNamespace> | <RootNamespace>Discord.Providers.WS4Net</RootNamespace> | ||||
| <Description>An optional WebSocket client provider for Discord.Net using WebSocket4Net</Description> | <Description>An optional WebSocket client provider for Discord.Net using WebSocket4Net</Description> | ||||
| <TargetFramework>net45</TargetFramework> | |||||
| <TargetFramework>netstandard1.3</TargetFramework> | |||||
| </PropertyGroup> | </PropertyGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| <PackageReference Include="WebSocket4Net" Version="0.14.1" /> | |||||
| <PackageReference Include="WebSocket4Net" Version="0.15.0" /> | |||||
| </ItemGroup> | </ItemGroup> | ||||
| </Project> | |||||
| </Project> | |||||
| @@ -29,6 +29,8 @@ namespace Discord.API | |||||
| public Optional<string> Topic { get; set; } | public Optional<string> Topic { get; set; } | ||||
| [JsonProperty("last_pin_timestamp")] | [JsonProperty("last_pin_timestamp")] | ||||
| public Optional<DateTimeOffset?> LastPinTimestamp { get; set; } | public Optional<DateTimeOffset?> LastPinTimestamp { get; set; } | ||||
| [JsonProperty("nsfw")] | |||||
| public Optional<bool> Nsfw { get; set; } | |||||
| //VoiceChannel | //VoiceChannel | ||||
| [JsonProperty("bitrate")] | [JsonProperty("bitrate")] | ||||
| @@ -25,6 +25,8 @@ namespace Discord.API | |||||
| public bool EmbedEnabled { get; set; } | public bool EmbedEnabled { get; set; } | ||||
| [JsonProperty("embed_channel_id")] | [JsonProperty("embed_channel_id")] | ||||
| public ulong? EmbedChannelId { get; set; } | public ulong? EmbedChannelId { get; set; } | ||||
| [JsonProperty("system_channel_id")] | |||||
| public ulong? SystemChannelId { get; set; } | |||||
| [JsonProperty("verification_level")] | [JsonProperty("verification_level")] | ||||
| public VerificationLevel VerificationLevel { get; set; } | public VerificationLevel VerificationLevel { get; set; } | ||||
| [JsonProperty("voice_states")] | [JsonProperty("voice_states")] | ||||
| @@ -18,6 +18,8 @@ namespace Discord.API.Rest | |||||
| public Optional<DefaultMessageNotifications> DefaultMessageNotifications { get; set; } | public Optional<DefaultMessageNotifications> DefaultMessageNotifications { get; set; } | ||||
| [JsonProperty("afk_timeout")] | [JsonProperty("afk_timeout")] | ||||
| public Optional<int> AfkTimeout { get; set; } | public Optional<int> AfkTimeout { get; set; } | ||||
| [JsonProperty("system_channel_id")] | |||||
| public Optional<ulong?> SystemChannelId { get; set; } | |||||
| [JsonProperty("icon")] | [JsonProperty("icon")] | ||||
| public Optional<Image?> Icon { get; set; } | public Optional<Image?> Icon { get; set; } | ||||
| [JsonProperty("splash")] | [JsonProperty("splash")] | ||||
| @@ -8,5 +8,7 @@ namespace Discord.API.Rest | |||||
| { | { | ||||
| [JsonProperty("topic")] | [JsonProperty("topic")] | ||||
| public Optional<string> Topic { get; set; } | public Optional<string> Topic { get; set; } | ||||
| [JsonProperty("nsfw")] | |||||
| public Optional<bool> IsNsfw { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -42,7 +42,7 @@ namespace Discord.Rest | |||||
| ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => | ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => | ||||
| { | { | ||||
| if (info == null) | 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 | else | ||||
| await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); | await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); | ||||
| }; | }; | ||||
| @@ -120,6 +120,9 @@ namespace Discord.Rest | |||||
| string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) | string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) | ||||
| { | { | ||||
| var args = new CreateGuildParams(name, region.Id); | 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); | var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); | ||||
| return RestGuild.Create(client, model); | return RestGuild.Create(client, model); | ||||
| } | } | ||||
| @@ -392,6 +392,7 @@ namespace Discord.API | |||||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | Preconditions.NotEqual(guildId, 0, nameof(guildId)); | ||||
| Preconditions.NotEqual(userId, 0, nameof(userId)); | Preconditions.NotEqual(userId, 0, nameof(userId)); | ||||
| Preconditions.NotEqual(roleId, 0, nameof(roleId)); | 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); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | var ids = new BucketIds(guildId: guildId); | ||||
| @@ -402,6 +403,7 @@ namespace Discord.API | |||||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | Preconditions.NotEqual(guildId, 0, nameof(guildId)); | ||||
| Preconditions.NotEqual(userId, 0, nameof(userId)); | Preconditions.NotEqual(userId, 0, nameof(userId)); | ||||
| Preconditions.NotEqual(roleId, 0, nameof(roleId)); | 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); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | var ids = new BucketIds(guildId: guildId); | ||||
| @@ -803,7 +805,7 @@ namespace Discord.API | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | 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); | 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) | public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) | ||||
| @@ -988,7 +990,7 @@ namespace Discord.API | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | 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); | 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) | 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; | bool isCurrentUser = userId == CurrentUserId; | ||||
| if (args.RoleIds.IsSpecified) | |||||
| Preconditions.NotEveryoneRole(args.RoleIds.Value, guildId, nameof(args.RoleIds)); | |||||
| if (isCurrentUser && args.Nickname.IsSpecified) | if (isCurrentUser && args.Nickname.IsSpecified) | ||||
| { | { | ||||
| var nickArgs = new Rest.ModifyCurrentUserNickParams(args.Nickname.Value ?? ""); | var nickArgs = new Rest.ModifyCurrentUserNickParams(args.Nickname.Value ?? ""); | ||||
| @@ -41,7 +41,8 @@ namespace Discord.Rest | |||||
| { | { | ||||
| Name = args.Name, | Name = args.Name, | ||||
| Position = args.Position, | Position = args.Position, | ||||
| Topic = args.Topic | |||||
| Topic = args.Topic, | |||||
| IsNsfw = args.IsNsfw | |||||
| }; | }; | ||||
| return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); | return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -290,8 +291,8 @@ namespace Discord.Rest | |||||
| return author; | 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) => | public static bool IsNsfw(string channelName) => | ||||
| channelName == "nsfw" || channelName.StartsWith("nsfw-"); | channelName == "nsfw" || channelName.StartsWith("nsfw-"); | ||||
| } | } | ||||
| @@ -6,7 +6,7 @@ using Model = Discord.API.Channel; | |||||
| namespace Discord.Rest | namespace Discord.Rest | ||||
| { | { | ||||
| public abstract class RestChannel : RestEntity<ulong>, IChannel, IUpdateable | |||||
| public class RestChannel : RestEntity<ulong>, IChannel, IUpdateable | |||||
| { | { | ||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| @@ -25,7 +25,7 @@ namespace Discord.Rest | |||||
| case ChannelType.Group: | case ChannelType.Group: | ||||
| return CreatePrivate(discord, model) as RestChannel; | return CreatePrivate(discord, model) as RestChannel; | ||||
| default: | default: | ||||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||||
| return new RestChannel(discord, model.Id); | |||||
| } | } | ||||
| } | } | ||||
| internal static IRestPrivateChannel CreatePrivate(BaseDiscordClient discord, Model model) | internal static IRestPrivateChannel CreatePrivate(BaseDiscordClient discord, Model model) | ||||
| @@ -40,13 +40,12 @@ namespace Discord.Rest | |||||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | 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 | //IChannel | ||||
| string IChannel.Name => null; | string IChannel.Name => null; | ||||
| bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this); | |||||
| Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
| => Task.FromResult<IUser>(null); //Overriden | => Task.FromResult<IUser>(null); //Overriden | ||||
| @@ -7,7 +7,7 @@ using Model = Discord.API.Channel; | |||||
| namespace Discord.Rest | namespace Discord.Rest | ||||
| { | { | ||||
| public abstract class RestGuildChannel : RestChannel, IGuildChannel, IUpdateable | |||||
| public class RestGuildChannel : RestChannel, IGuildChannel, IUpdateable | |||||
| { | { | ||||
| private ImmutableArray<Overwrite> _overwrites; | private ImmutableArray<Overwrite> _overwrites; | ||||
| @@ -33,7 +33,8 @@ namespace Discord.Rest | |||||
| case ChannelType.Voice: | case ChannelType.Voice: | ||||
| return RestVoiceChannel.Create(discord, guild, model); | return RestVoiceChannel.Create(discord, guild, model); | ||||
| default: | default: | ||||
| throw new InvalidOperationException("Unknown guild channel type"); | |||||
| // TODO: Channel categories | |||||
| return new RestGuildChannel(discord, guild, model.Id); | |||||
| } | } | ||||
| } | } | ||||
| internal override void Update(Model model) | internal override void Update(Model model) | ||||
| @@ -15,6 +15,9 @@ namespace Discord.Rest | |||||
| public string Mention => MentionUtils.MentionChannel(Id); | public string Mention => MentionUtils.MentionChannel(Id); | ||||
| private bool _nsfw; | |||||
| public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); | |||||
| internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) | internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) | ||||
| : base(discord, guild, id) | : base(discord, guild, id) | ||||
| { | { | ||||
| @@ -30,6 +33,7 @@ namespace Discord.Rest | |||||
| base.Update(model); | base.Update(model); | ||||
| Topic = model.Topic.Value; | Topic = model.Topic.Value; | ||||
| _nsfw = model.Nsfw.GetValueOrDefault(); | |||||
| } | } | ||||
| public async Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null) | public async Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null) | ||||
| @@ -99,7 +99,6 @@ namespace Discord.Rest | |||||
| //IChannel | //IChannel | ||||
| string IChannel.Name { get { throw new NotSupportedException(); } } | string IChannel.Name { get { throw new NotSupportedException(); } } | ||||
| bool IChannel.IsNsfw { get { throw new NotSupportedException(); } } | |||||
| IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) | IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) | ||||
| { | { | ||||
| throw new NotSupportedException(); | throw new NotSupportedException(); | ||||
| @@ -26,6 +26,7 @@ namespace Discord.Rest | |||||
| { | { | ||||
| AfkChannelId = args.AfkChannelId, | AfkChannelId = args.AfkChannelId, | ||||
| AfkTimeout = args.AfkTimeout, | AfkTimeout = args.AfkTimeout, | ||||
| SystemChannelId = args.SystemChannelId, | |||||
| DefaultMessageNotifications = args.DefaultMessageNotifications, | DefaultMessageNotifications = args.DefaultMessageNotifications, | ||||
| Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create<ImageModel?>(), | Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create<ImageModel?>(), | ||||
| Name = args.Name, | Name = args.Name, | ||||
| @@ -39,6 +40,11 @@ namespace Discord.Rest | |||||
| else if (args.AfkChannelId.IsSpecified) | else if (args.AfkChannelId.IsSpecified) | ||||
| apiArgs.AfkChannelId = args.AfkChannelId.Value; | 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) | if (args.Owner.IsSpecified) | ||||
| apiArgs.OwnerId = args.Owner.Value.Id; | apiArgs.OwnerId = args.Owner.Value.Id; | ||||
| else if (args.OwnerId.IsSpecified) | else if (args.OwnerId.IsSpecified) | ||||
| @@ -26,6 +26,7 @@ namespace Discord.Rest | |||||
| public ulong? AFKChannelId { get; private set; } | public ulong? AFKChannelId { get; private set; } | ||||
| public ulong? EmbedChannelId { get; private set; } | public ulong? EmbedChannelId { get; private set; } | ||||
| public ulong? SystemChannelId { get; private set; } | |||||
| public ulong OwnerId { get; private set; } | public ulong OwnerId { get; private set; } | ||||
| public string VoiceRegionId { get; private set; } | public string VoiceRegionId { get; private set; } | ||||
| public string IconId { get; private set; } | public string IconId { get; private set; } | ||||
| @@ -33,6 +34,8 @@ namespace Discord.Rest | |||||
| internal bool Available { get; private set; } | internal bool Available { get; private set; } | ||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| [Obsolete("DefaultChannelId is deprecated, use GetDefaultChannelAsync")] | |||||
| public ulong DefaultChannelId => Id; | public ulong DefaultChannelId => Id; | ||||
| public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); | public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); | ||||
| public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); | public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); | ||||
| @@ -56,6 +59,7 @@ namespace Discord.Rest | |||||
| { | { | ||||
| AFKChannelId = model.AFKChannelId; | AFKChannelId = model.AFKChannelId; | ||||
| EmbedChannelId = model.EmbedChannelId; | EmbedChannelId = model.EmbedChannelId; | ||||
| SystemChannelId = model.SystemChannelId; | |||||
| AFKTimeout = model.AFKTimeout; | AFKTimeout = model.AFKTimeout; | ||||
| IsEmbeddable = model.EmbedEnabled; | IsEmbeddable = model.EmbedEnabled; | ||||
| IconId = model.Icon; | IconId = model.Icon; | ||||
| @@ -185,8 +189,12 @@ namespace Discord.Rest | |||||
| } | } | ||||
| public async Task<RestTextChannel> GetDefaultChannelAsync(RequestOptions options = null) | public async Task<RestTextChannel> 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<RestGuildChannel> GetEmbedChannelAsync(RequestOptions options = null) | public async Task<RestGuildChannel> GetEmbedChannelAsync(RequestOptions options = null) | ||||
| { | { | ||||
| @@ -195,6 +203,16 @@ namespace Discord.Rest | |||||
| return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); | return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); | ||||
| return null; | return null; | ||||
| } | } | ||||
| public async Task<RestTextChannel> 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<RestTextChannel> CreateTextChannelAsync(string name, RequestOptions options = null) | public Task<RestTextChannel> CreateTextChannelAsync(string name, RequestOptions options = null) | ||||
| => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); | => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); | ||||
| public Task<RestVoiceChannel> CreateVoiceChannelAsync(string name, RequestOptions options = null) | public Task<RestVoiceChannel> CreateVoiceChannelAsync(string name, RequestOptions options = null) | ||||
| @@ -314,6 +332,13 @@ namespace Discord.Rest | |||||
| else | else | ||||
| return null; | return null; | ||||
| } | } | ||||
| async Task<ITextChannel> IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) | |||||
| { | |||||
| if (mode == CacheMode.AllowDownload) | |||||
| return await GetSystemChannelAsync(options).ConfigureAwait(false); | |||||
| else | |||||
| return null; | |||||
| } | |||||
| async Task<ITextChannel> IGuild.CreateTextChannelAsync(string name, RequestOptions options) | async Task<ITextChannel> IGuild.CreateTextChannelAsync(string name, RequestOptions options) | ||||
| => await CreateTextChannelAsync(name, options).ConfigureAwait(false); | => await CreateTextChannelAsync(name, options).ConfigureAwait(false); | ||||
| async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) | async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) | ||||
| @@ -171,24 +171,16 @@ namespace Discord | |||||
| return this; | return this; | ||||
| } | } | ||||
| public EmbedBuilder AddField(string name, object value) | |||||
| public EmbedBuilder AddField(string name, object value, bool inline = false) | |||||
| { | { | ||||
| var field = new EmbedFieldBuilder() | 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) | .WithName(name) | ||||
| .WithValue(value); | .WithValue(value); | ||||
| AddField(field); | AddField(field); | ||||
| return this; | return this; | ||||
| } | } | ||||
| public EmbedBuilder AddField(EmbedFieldBuilder field) | public EmbedBuilder AddField(EmbedFieldBuilder field) | ||||
| { | { | ||||
| if (Fields.Count >= MaxFieldCount) | if (Fields.Count >= MaxFieldCount) | ||||
| @@ -206,17 +198,6 @@ namespace Discord | |||||
| this.AddField(field); | this.AddField(field); | ||||
| return this; | 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() | public Embed Build() | ||||
| { | { | ||||
| @@ -234,7 +215,6 @@ namespace Discord | |||||
| return _embed; | return _embed; | ||||
| } | } | ||||
| public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); | |||||
| } | } | ||||
| public class EmbedFieldBuilder | public class EmbedFieldBuilder | ||||
| @@ -249,7 +229,7 @@ namespace Discord | |||||
| get => _field.Name; | get => _field.Name; | ||||
| set | 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)); | if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); | ||||
| _field.Name = value; | _field.Name = value; | ||||
| } | } | ||||
| @@ -43,11 +43,12 @@ namespace Discord.Rest | |||||
| await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options); | await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options); | ||||
| } | } | ||||
| public static async Task<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IMessage msg, string emoji, | |||||
| public static async Task<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IMessage msg, IEmote emote, | |||||
| Action<GetReactionUsersParams> func, BaseDiscordClient client, RequestOptions options) | Action<GetReactionUsersParams> func, BaseDiscordClient client, RequestOptions options) | ||||
| { | { | ||||
| var args = new GetReactionUsersParams(); | var args = new GetReactionUsersParams(); | ||||
| func(args); | 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(); | return (await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false)).Select(u => RestUser.Create(client, u)).ToImmutableArray(); | ||||
| } | } | ||||
| @@ -136,10 +136,9 @@ namespace Discord.Rest | |||||
| => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); | => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); | ||||
| public Task RemoveAllReactionsAsync(RequestOptions options = null) | public Task RemoveAllReactionsAsync(RequestOptions options = null) | ||||
| => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); | => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); | ||||
| public Task<IReadOnlyCollection<IUser>> 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<ulong>(); }, Discord, options); | |||||
| public Task<IReadOnlyCollection<IUser>> 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<ulong>(); }, Discord, options); | |||||
| public Task PinAsync(RequestOptions options = null) | public Task PinAsync(RequestOptions options = null) | ||||
| => MessageHelper.PinAsync(this, Discord, options); | => MessageHelper.PinAsync(this, Discord, options); | ||||
| @@ -19,7 +19,7 @@ namespace Discord.Rest | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| public bool IsEveryone => Id == Guild.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) | internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id) | ||||
| : base(discord, id) | : base(discord, id) | ||||
| @@ -46,6 +46,7 @@ namespace Discord.Rest | |||||
| } | } | ||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| base.Update(model.User); | |||||
| if (model.JoinedAt.IsSpecified) | if (model.JoinedAt.IsSpecified) | ||||
| _joinedAtTicks = model.JoinedAt.Value.UtcTicks; | _joinedAtTicks = model.JoinedAt.Value.UtcTicks; | ||||
| if (model.Nick.IsSpecified) | if (model.Nick.IsSpecified) | ||||
| @@ -1,5 +1,6 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| using System; | |||||
| using System.IO; | |||||
| using Newtonsoft.Json; | |||||
| using Model = Discord.API.Image; | using Model = Discord.API.Image; | ||||
| namespace Discord.Net.Converters | namespace Discord.Net.Converters | ||||
| @@ -23,10 +24,24 @@ namespace Discord.Net.Converters | |||||
| if (image.Stream != null) | 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}"); | writer.WriteValue($"data:image/jpeg;base64,{base64}"); | ||||
| } | } | ||||
| else if (image.Hash != null) | else if (image.Hash != null) | ||||
| @@ -8,7 +8,6 @@ namespace Discord.Rpc | |||||
| public class RpcChannel : RpcEntity<ulong> | public class RpcChannel : RpcEntity<ulong> | ||||
| { | { | ||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| public bool IsNsfw => ChannelHelper.IsNsfw(Name); | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| @@ -16,6 +16,8 @@ namespace Discord.Rpc | |||||
| public IReadOnlyCollection<RpcMessage> CachedMessages { get; private set; } | public IReadOnlyCollection<RpcMessage> CachedMessages { get; private set; } | ||||
| public string Mention => MentionUtils.MentionChannel(Id); | 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) | internal RpcTextChannel(DiscordRpcClient discord, ulong id, ulong guildId) | ||||
| : base(discord, id, guildId) | : base(discord, id, guildId) | ||||
| @@ -108,9 +108,8 @@ namespace Discord.Rpc | |||||
| => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); | => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); | ||||
| public Task RemoveAllReactionsAsync(RequestOptions options = null) | public Task RemoveAllReactionsAsync(RequestOptions options = null) | ||||
| => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); | => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); | ||||
| public Task<IReadOnlyCollection<IUser>> 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<ulong>(); }, Discord, options); | |||||
| public Task<IReadOnlyCollection<IUser>> 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<ulong>(); }, Discord, options); | |||||
| public Task PinAsync(RequestOptions options) | public Task PinAsync(RequestOptions options) | ||||
| => MessageHelper.PinAsync(this, Discord, options); | => MessageHelper.PinAsync(this, Discord, options); | ||||
| @@ -675,7 +675,12 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| else | else | ||||
| { | |||||
| channel = State.GetChannel(data.Id); | |||||
| if (channel != null) | |||||
| return; //Discord may send duplicate CHANNEL_CREATEs for DMs | |||||
| channel = AddPrivateChannel(data, State) as SocketChannel; | channel = AddPrivateChannel(data, State) as SocketChannel; | ||||
| } | |||||
| if (channel != null) | if (channel != null) | ||||
| await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); | await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); | ||||
| @@ -41,7 +41,6 @@ namespace Discord.WebSocket | |||||
| //IChannel | //IChannel | ||||
| string IChannel.Name => null; | string IChannel.Name => null; | ||||
| bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this); | |||||
| Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
| => Task.FromResult<IUser>(null); //Overridden | => Task.FromResult<IUser>(null); //Overridden | ||||
| @@ -10,7 +10,7 @@ using Model = Discord.API.Channel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public abstract class SocketGuildChannel : SocketChannel, IGuildChannel | |||||
| public class SocketGuildChannel : SocketChannel, IGuildChannel | |||||
| { | { | ||||
| private ImmutableArray<Overwrite> _overwrites; | private ImmutableArray<Overwrite> _overwrites; | ||||
| @@ -19,7 +19,7 @@ namespace Discord.WebSocket | |||||
| public int Position { get; private set; } | public int Position { get; private set; } | ||||
| public IReadOnlyCollection<Overwrite> PermissionOverwrites => _overwrites; | public IReadOnlyCollection<Overwrite> PermissionOverwrites => _overwrites; | ||||
| public new abstract IReadOnlyCollection<SocketGuildUser> Users { get; } | |||||
| public new virtual IReadOnlyCollection<SocketGuildUser> Users => ImmutableArray.Create<SocketGuildUser>(); | |||||
| internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | ||||
| : base(discord, id) | : base(discord, id) | ||||
| @@ -35,7 +35,8 @@ namespace Discord.WebSocket | |||||
| case ChannelType.Voice: | case ChannelType.Voice: | ||||
| return SocketVoiceChannel.Create(guild, state, model); | return SocketVoiceChannel.Create(guild, state, model); | ||||
| default: | 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) | internal override void Update(ClientState state, Model model) | ||||
| @@ -49,7 +50,7 @@ namespace Discord.WebSocket | |||||
| newOverwrites.Add(overwrites[i].ToEntity()); | newOverwrites.Add(overwrites[i].ToEntity()); | ||||
| _overwrites = newOverwrites.ToImmutable(); | _overwrites = newOverwrites.ToImmutable(); | ||||
| } | } | ||||
| public Task ModifyAsync(Action<GuildChannelProperties> func, RequestOptions options = null) | public Task ModifyAsync(Action<GuildChannelProperties> func, RequestOptions options = null) | ||||
| => ChannelHelper.ModifyAsync(this, Discord, func, options); | => ChannelHelper.ModifyAsync(this, Discord, func, options); | ||||
| public Task DeleteAsync(RequestOptions options = null) | public Task DeleteAsync(RequestOptions options = null) | ||||
| @@ -115,7 +116,7 @@ namespace Discord.WebSocket | |||||
| public async Task<RestInviteMetadata> CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public async Task<RestInviteMetadata> 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); | => 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; | public override string ToString() => Name; | ||||
| internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; | internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; | ||||
| @@ -145,7 +146,7 @@ namespace Discord.WebSocket | |||||
| => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); | => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); | ||||
| async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) | async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) | ||||
| => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); | => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); | ||||
| IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) | IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) | ||||
| => ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>(Users).ToAsyncEnumerable(); | => ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>(Users).ToAsyncEnumerable(); | ||||
| Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
| @@ -16,6 +16,9 @@ namespace Discord.WebSocket | |||||
| private readonly MessageCache _messages; | private readonly MessageCache _messages; | ||||
| public string Topic { get; private set; } | public string Topic { get; private set; } | ||||
| private bool _nsfw; | |||||
| public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); | |||||
| public string Mention => MentionUtils.MentionChannel(Id); | public string Mention => MentionUtils.MentionChannel(Id); | ||||
| public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>(); | public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>(); | ||||
| @@ -41,6 +44,7 @@ namespace Discord.WebSocket | |||||
| base.Update(state, model); | base.Update(state, model); | ||||
| Topic = model.Topic.Value; | Topic = model.Topic.Value; | ||||
| _nsfw = model.Nsfw.GetValueOrDefault(); | |||||
| } | } | ||||
| public Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null) | public Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null) | ||||
| @@ -47,6 +47,7 @@ namespace Discord.WebSocket | |||||
| internal ulong? AFKChannelId { get; private set; } | internal ulong? AFKChannelId { get; private set; } | ||||
| internal ulong? EmbedChannelId { get; private set; } | internal ulong? EmbedChannelId { get; private set; } | ||||
| internal ulong? SystemChannelId { get; private set; } | |||||
| public ulong OwnerId { get; private set; } | public ulong OwnerId { get; private set; } | ||||
| public SocketGuildUser Owner => GetUser(OwnerId); | public SocketGuildUser Owner => GetUser(OwnerId); | ||||
| public string VoiceRegionId { get; private set; } | public string VoiceRegionId { get; private set; } | ||||
| @@ -54,7 +55,6 @@ namespace Discord.WebSocket | |||||
| public string SplashId { get; private set; } | public string SplashId { get; private set; } | ||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| public SocketTextChannel DefaultChannel => GetTextChannel(Id); | |||||
| public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); | public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); | ||||
| public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); | public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); | ||||
| public bool HasAllMembers => MemberCount == DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; | public bool HasAllMembers => MemberCount == DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; | ||||
| @@ -62,6 +62,10 @@ namespace Discord.WebSocket | |||||
| public Task SyncPromise => _syncPromise.Task; | public Task SyncPromise => _syncPromise.Task; | ||||
| public Task DownloaderPromise => _downloaderPromise.Task; | public Task DownloaderPromise => _downloaderPromise.Task; | ||||
| public IAudioClient AudioClient => _audioClient; | public IAudioClient AudioClient => _audioClient; | ||||
| public SocketTextChannel DefaultChannel => TextChannels | |||||
| .Where(c => CurrentUser.GetPermissions(c).ReadMessages) | |||||
| .OrderBy(c => c.Position) | |||||
| .FirstOrDefault(); | |||||
| public SocketVoiceChannel AFKChannel | public SocketVoiceChannel AFKChannel | ||||
| { | { | ||||
| get | get | ||||
| @@ -78,6 +82,14 @@ namespace Discord.WebSocket | |||||
| return id.HasValue ? GetChannel(id.Value) : null; | return id.HasValue ? GetChannel(id.Value) : null; | ||||
| } | } | ||||
| } | } | ||||
| public SocketTextChannel SystemChannel | |||||
| { | |||||
| get | |||||
| { | |||||
| var id = SystemChannelId; | |||||
| return id.HasValue ? GetTextChannel(id.Value) : null; | |||||
| } | |||||
| } | |||||
| public IReadOnlyCollection<SocketTextChannel> TextChannels | public IReadOnlyCollection<SocketTextChannel> TextChannels | ||||
| => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); | => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); | ||||
| public IReadOnlyCollection<SocketVoiceChannel> VoiceChannels | public IReadOnlyCollection<SocketVoiceChannel> VoiceChannels | ||||
| @@ -157,8 +169,6 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) | if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) | ||||
| member.Update(state, model.Presences[i], true); | member.Update(state, model.Presences[i], true); | ||||
| else | |||||
| Debug.Assert(false); | |||||
| } | } | ||||
| } | } | ||||
| _members = members; | _members = members; | ||||
| @@ -190,6 +200,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| AFKChannelId = model.AFKChannelId; | AFKChannelId = model.AFKChannelId; | ||||
| EmbedChannelId = model.EmbedChannelId; | EmbedChannelId = model.EmbedChannelId; | ||||
| SystemChannelId = model.SystemChannelId; | |||||
| AFKTimeout = model.AFKTimeout; | AFKTimeout = model.AFKTimeout; | ||||
| IsEmbeddable = model.EmbedEnabled; | IsEmbeddable = model.EmbedEnabled; | ||||
| IconId = model.Icon; | IconId = model.Icon; | ||||
| @@ -242,8 +253,6 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) | if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) | ||||
| member.Update(state, model.Presences[i], true); | member.Update(state, model.Presences[i], true); | ||||
| else | |||||
| Debug.Assert(false); | |||||
| } | } | ||||
| } | } | ||||
| _members = members; | _members = members; | ||||
| @@ -606,8 +615,9 @@ namespace Discord.WebSocket | |||||
| ulong? IGuild.AFKChannelId => AFKChannelId; | ulong? IGuild.AFKChannelId => AFKChannelId; | ||||
| IAudioClient IGuild.AudioClient => null; | IAudioClient IGuild.AudioClient => null; | ||||
| bool IGuild.Available => true; | bool IGuild.Available => true; | ||||
| ulong IGuild.DefaultChannelId => Id; | |||||
| ulong IGuild.DefaultChannelId => DefaultChannel?.Id ?? 0; | |||||
| ulong? IGuild.EmbedChannelId => EmbedChannelId; | ulong? IGuild.EmbedChannelId => EmbedChannelId; | ||||
| ulong? IGuild.SystemChannelId => SystemChannelId; | |||||
| IRole IGuild.EveryoneRole => EveryoneRole; | IRole IGuild.EveryoneRole => EveryoneRole; | ||||
| IReadOnlyCollection<IRole> IGuild.Roles => Roles; | IReadOnlyCollection<IRole> IGuild.Roles => Roles; | ||||
| @@ -632,6 +642,8 @@ namespace Discord.WebSocket | |||||
| => Task.FromResult<ITextChannel>(DefaultChannel); | => Task.FromResult<ITextChannel>(DefaultChannel); | ||||
| Task<IGuildChannel> IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) | Task<IGuildChannel> IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) | ||||
| => Task.FromResult<IGuildChannel>(EmbedChannel); | => Task.FromResult<IGuildChannel>(EmbedChannel); | ||||
| Task<ITextChannel> IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) | |||||
| => Task.FromResult<ITextChannel>(SystemChannel); | |||||
| async Task<ITextChannel> IGuild.CreateTextChannelAsync(string name, RequestOptions options) | async Task<ITextChannel> IGuild.CreateTextChannelAsync(string name, RequestOptions options) | ||||
| => await CreateTextChannelAsync(name, options).ConfigureAwait(false); | => await CreateTextChannelAsync(name, options).ConfigureAwait(false); | ||||
| async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) | async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) | ||||
| @@ -130,9 +130,8 @@ namespace Discord.WebSocket | |||||
| => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); | => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); | ||||
| public Task RemoveAllReactionsAsync(RequestOptions options = null) | public Task RemoveAllReactionsAsync(RequestOptions options = null) | ||||
| => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); | => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); | ||||
| public Task<IReadOnlyCollection<IUser>> 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<ulong>(); }, Discord, options); | |||||
| public Task<IReadOnlyCollection<IUser>> 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<ulong>(); }, Discord, options); | |||||
| public Task PinAsync(RequestOptions options = null) | public Task PinAsync(RequestOptions options = null) | ||||
| => MessageHelper.PinAsync(this, Discord, options); | => MessageHelper.PinAsync(this, Discord, options); | ||||
| @@ -23,7 +23,7 @@ namespace Discord.WebSocket | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| public bool IsEveryone => Id == Guild.Id; | public bool IsEveryone => Id == Guild.Id; | ||||
| public string Mention => MentionUtils.MentionRole(Id); | |||||
| public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); | |||||
| public IEnumerable<SocketGuildUser> Members | public IEnumerable<SocketGuildUser> Members | ||||
| => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); | => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); | ||||
| @@ -36,7 +36,7 @@ namespace Discord.Webhook | |||||
| ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => | ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => | ||||
| { | { | ||||
| if (info == null) | 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 | else | ||||
| await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); | await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); | ||||
| }; | }; | ||||
| @@ -2,9 +2,9 @@ | |||||
| <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> | <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> | ||||
| <metadata> | <metadata> | ||||
| <id>Discord.Net</id> | <id>Discord.Net</id> | ||||
| <version>1.0.1$suffix$</version> | |||||
| <version>2.0.0-alpha$suffix$</version> | |||||
| <title>Discord.Net</title> | <title>Discord.Net</title> | ||||
| <authors>RogueException</authors> | |||||
| <authors>Discord.Net Contributors</authors> | |||||
| <owners>RogueException</owners> | <owners>RogueException</owners> | ||||
| <description>An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components.</description> | <description>An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components.</description> | ||||
| <tags>discord;discordapp</tags> | <tags>discord;discordapp</tags> | ||||
| @@ -13,29 +13,29 @@ | |||||
| <requireLicenseAcceptance>false</requireLicenseAcceptance> | <requireLicenseAcceptance>false</requireLicenseAcceptance> | ||||
| <dependencies> | <dependencies> | ||||
| <group targetFramework="net45"> | <group targetFramework="net45"> | ||||
| <dependency id="Discord.Net.Core" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Rest" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.WebSocket" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Rpc" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Commands" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Webhook" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Core" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Rest" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.WebSocket" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Rpc" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Commands" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Webhook" version="2.0.0-alpha$suffix$" /> | |||||
| </group> | </group> | ||||
| <group targetFramework="netstandard1.1"> | <group targetFramework="netstandard1.1"> | ||||
| <dependency id="Discord.Net.Core" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Rest" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.WebSocket" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Rpc" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Commands" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Webhook" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Core" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Rest" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.WebSocket" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Rpc" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Commands" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Webhook" version="2.0.0-alpha$suffix$" /> | |||||
| </group> | </group> | ||||
| <group targetFramework="netstandard1.3"> | <group targetFramework="netstandard1.3"> | ||||
| <dependency id="Discord.Net.Core" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Rest" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.WebSocket" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Rpc" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Commands" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Webhook" version="1.0.1$suffix$" /> | |||||
| <dependency id="Discord.Net.Core" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Rest" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.WebSocket" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Rpc" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Commands" version="2.0.0-alpha$suffix$" /> | |||||
| <dependency id="Discord.Net.Webhook" version="2.0.0-alpha$suffix$" /> | |||||
| </group> | </group> | ||||
| </dependencies> | </dependencies> | ||||
| </metadata> | </metadata> | ||||
| </package> | |||||
| </package> | |||||
| @@ -64,7 +64,7 @@ namespace Discord | |||||
| var text5 = textChannels.Where(x => x.Name == "text5").FirstOrDefault(); | var text5 = textChannels.Where(x => x.Name == "text5").FirstOrDefault(); | ||||
| Assert.NotNull(text1); | Assert.NotNull(text1); | ||||
| Assert.True(text1.Id == guild.DefaultChannelId); | |||||
| //Assert.True(text1.Id == guild.DefaultChannelId); | |||||
| Assert.Equal(text1.Position, 1); | Assert.Equal(text1.Position, 1); | ||||
| Assert.Equal(text1.Topic, "Topic1"); | Assert.Equal(text1.Topic, "Topic1"); | ||||
| @@ -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<ArgumentOutOfRangeException>("r", () => new Color(-1024, 0, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("r", () => new Color(1024, 0, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("g", () => new Color(0, -1024, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("g", () => new Color(0, 1024, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("b", () => new Color(0, 0, -1024)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("b", () => new Color(0, 0, 1024)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>(() => new Color(-1024, -1024, -1024)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>("r", () => new Color(-2.0f, 0, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("r", () => new Color(2.0f, 0, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("g", () => new Color(0, -2.0f, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("g", () => new Color(0, 2.0f, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("b", () => new Color(0, 0, -2.0f)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("b", () => new Color(0, 0, 2.0f)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>(() => new Color(-2.0f, -2.0f, -2.0f)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>(() => 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -57,7 +57,7 @@ namespace Discord | |||||
| foreach (var channel in textChannels) | foreach (var channel in textChannels) | ||||
| { | { | ||||
| if (channel.Id != guild.DefaultChannelId) | |||||
| //if (channel.Id != guild.DefaultChannelId) | |||||
| await channel.DeleteAsync(); | await channel.DeleteAsync(); | ||||
| } | } | ||||
| foreach (var channel in voiceChannels) | foreach (var channel in voiceChannels) | ||||