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