Browse Source

Merge remote-tracking branch 'source/dev' into BaseSocketClient

pull/773/head
Alex Gravely 8 years ago
parent
commit
333d5f4bdb
58 changed files with 411 additions and 137 deletions
  1. +1
    -1
      Discord.Net.targets
  2. +1
    -1
      LICENSE
  3. +4
    -4
      docs/guides/commands/samples/typereader.cs
  4. +6
    -5
      docs/guides/getting_started/samples/intro/client.cs
  5. +8
    -6
      docs/guides/getting_started/samples/intro/complete.cs
  6. +3
    -3
      docs/guides/getting_started/samples/intro/structure.cs
  7. +1
    -1
      src/Discord.Net.Commands/Attributes/NameAttribute.cs
  8. +1
    -2
      src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs
  9. +1
    -1
      src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs
  10. +0
    -1
      src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  11. +3
    -0
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  12. +29
    -6
      src/Discord.Net.Commands/CommandService.cs
  13. +7
    -2
      src/Discord.Net.Commands/Info/CommandInfo.cs
  14. +34
    -0
      src/Discord.Net.Commands/Readers/NullableTypeReader.cs
  15. +1
    -4
      src/Discord.Net.Core/Entities/Channels/IChannel.cs
  16. +3
    -0
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  17. +4
    -0
      src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs
  18. +8
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs
  19. +3
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  20. +2
    -1
      src/Discord.Net.Core/Entities/Messages/IUserMessage.cs
  21. +32
    -0
      src/Discord.Net.Core/Extensions/UserExtensions.cs
  22. +8
    -0
      src/Discord.Net.Core/Utils/Preconditions.cs
  23. +3
    -3
      src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj
  24. +2
    -0
      src/Discord.Net.Rest/API/Common/Channel.cs
  25. +2
    -0
      src/Discord.Net.Rest/API/Common/Guild.cs
  26. +2
    -0
      src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs
  27. +2
    -0
      src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs
  28. +1
    -1
      src/Discord.Net.Rest/BaseDiscordClient.cs
  29. +3
    -0
      src/Discord.Net.Rest/ClientHelper.cs
  30. +6
    -2
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  31. +4
    -3
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  32. +4
    -5
      src/Discord.Net.Rest/Entities/Channels/RestChannel.cs
  33. +3
    -2
      src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs
  34. +4
    -0
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  35. +0
    -1
      src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs
  36. +6
    -0
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  37. +27
    -2
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  38. +4
    -24
      src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs
  39. +2
    -1
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  40. +3
    -4
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  41. +1
    -1
      src/Discord.Net.Rest/Entities/Roles/RestRole.cs
  42. +1
    -0
      src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs
  43. +20
    -5
      src/Discord.Net.Rest/Net/Converters/ImageConverter.cs
  44. +0
    -1
      src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs
  45. +2
    -0
      src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs
  46. +2
    -3
      src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs
  47. +5
    -0
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  48. +0
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs
  49. +7
    -6
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  50. +4
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  51. +18
    -6
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  52. +2
    -3
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  53. +1
    -1
      src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs
  54. +1
    -1
      src/Discord.Net.Webhook/DiscordWebhookClient.cs
  55. +21
    -21
      src/Discord.Net/Discord.Net.nuspec
  56. +1
    -1
      test/Discord.Net.Tests/Tests.Channels.cs
  57. +86
    -0
      test/Discord.Net.Tests/Tests.Colors.cs
  58. +1
    -1
      test/Discord.Net.Tests/Tests.Migrations.cs

+ 1
- 1
Discord.Net.targets View File

@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>1.0.1</VersionPrefix>
<VersionPrefix>2.0.0-alpha</VersionPrefix>
<VersionSuffix></VersionSuffix>
<Authors>RogueException</Authors>
<PackageTags>discord;discordapp</PackageTags>


+ 1
- 1
LICENSE View File

@@ -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


+ 4
- 4
docs/guides/commands/samples/typereader.cs View File

@@ -4,12 +4,12 @@ using Discord.Commands;

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;
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."));
}
}
}

+ 6
- 5
docs/guides/getting_started/samples/intro/client.cs View File

@@ -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);
}
}

+ 8
- 6
docs/guides/getting_started/samples/intro/complete.cs View File

@@ -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;
}
}
}
}

+ 3
- 3
docs/guides/getting_started/samples/intro/structure.cs View File

@@ -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();



+ 1
- 1
src/Discord.Net.Commands/Attributes/NameAttribute.cs View File

@@ -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; }


+ 1
- 2
src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs View File

@@ -1,13 +1,12 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
/// <summary>
/// This attribute requires that the bot has a specified permission in the channel a command is invoked in.
/// </summary>
[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; }


+ 1
- 1
src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs View File

@@ -11,7 +11,7 @@ namespace Discord.Commands
{
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());
else
return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel."));


+ 0
- 1
src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs View File

@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{


+ 3
- 0
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -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}");


+ 29
- 6
src/Discord.Net.Commands/CommandService.cs View File

@@ -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<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
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 ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders;
@@ -57,7 +59,10 @@ namespace Discord.Commands

_defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader>();
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>((string x, out string y) => { y = x; return true; }, 0);
@@ -190,17 +195,35 @@ namespace Discord.Commands
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)
{
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)
{
var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>());
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)
{
if (_typeReaders.TryGetValue(type, out var definedTypeReaders))


+ 7
- 2
src/Discord.Net.Commands/Info/CommandInfo.cs View File

@@ -188,17 +188,22 @@ namespace Discord.Commands
if (task is Task<IResult> 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<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
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)
{


+ 34
- 0
src/Discord.Net.Commands/Readers/NullableTypeReader.cs View File

@@ -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); ;
}
}
}

+ 1
- 4
src/Discord.Net.Core/Entities/Channels/IChannel.cs View File

@@ -7,10 +7,7 @@ namespace Discord
{
/// <summary> Gets the name of this channel. </summary>
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>
IAsyncEnumerable<IReadOnlyCollection<IUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);


+ 3
- 0
src/Discord.Net.Core/Entities/Channels/ITextChannel.cs View File

@@ -5,6 +5,9 @@ namespace Discord
{
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>
string Topic { get; }



+ 4
- 0
src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs View File

@@ -7,5 +7,9 @@
/// What the topic of the channel should be set to.
/// </summary>
public Optional<string> Topic { get; set; }
/// <summary>
/// Should this channel be flagged as NSFW?
/// </summary>
public Optional<bool> IsNsfw { get; set; }
}
}

+ 8
- 0
src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs View File

@@ -60,6 +60,14 @@
/// </summary>
public Optional<ulong?> AfkChannelId { get; set; }
/// <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.
/// </summary>
public Optional<IUser> Owner { get; set; }


+ 3
- 0
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -36,6 +36,8 @@ namespace Discord
ulong DefaultChannelId { get; }
/// <summary> Gets the id of the embed channel for this guild if set, or null if not. </summary>
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>
ulong OwnerId { get; }
/// <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<IVoiceChannel> GetVoiceChannelAsync(ulong id, 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<IGuildChannel> GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> Creates a new text channel. </summary>


+ 2
- 1
src/Discord.Net.Core/Entities/Messages/IUserMessage.cs View File

@@ -22,7 +22,8 @@ namespace Discord
Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null);
/// <summary> Removes all reactions from this message. </summary>
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>
string Resolve(


+ 32
- 0
src/Discord.Net.Core/Extensions/UserExtensions.cs View File

@@ -1,9 +1,13 @@
using System.Threading.Tasks;
using System.IO;

namespace Discord
{
public static class UserExtensions
{
/// <summary>
/// Sends a message to the user via DM.
/// </summary>
public static async Task<IUserMessage> 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);
}

/// <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
}
}

+ 8
- 0
src/Discord.Net.Core/Utils/Preconditions.cs View File

@@ -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);
}
}
}
}

+ 3
- 3
src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj View File

@@ -4,12 +4,12 @@
<AssemblyName>Discord.Net.Providers.WS4Net</AssemblyName>
<RootNamespace>Discord.Providers.WS4Net</RootNamespace>
<Description>An optional WebSocket client provider for Discord.Net using WebSocket4Net</Description>
<TargetFramework>net45</TargetFramework>
<TargetFramework>netstandard1.3</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WebSocket4Net" Version="0.14.1" />
<PackageReference Include="WebSocket4Net" Version="0.15.0" />
</ItemGroup>
</Project>
</Project>

+ 2
- 0
src/Discord.Net.Rest/API/Common/Channel.cs View File

@@ -29,6 +29,8 @@ namespace Discord.API
public Optional<string> Topic { get; set; }
[JsonProperty("last_pin_timestamp")]
public Optional<DateTimeOffset?> LastPinTimestamp { get; set; }
[JsonProperty("nsfw")]
public Optional<bool> Nsfw { get; set; }

//VoiceChannel
[JsonProperty("bitrate")]


+ 2
- 0
src/Discord.Net.Rest/API/Common/Guild.cs View File

@@ -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")]


+ 2
- 0
src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs View File

@@ -18,6 +18,8 @@ namespace Discord.API.Rest
public Optional<DefaultMessageNotifications> DefaultMessageNotifications { get; set; }
[JsonProperty("afk_timeout")]
public Optional<int> AfkTimeout { get; set; }
[JsonProperty("system_channel_id")]
public Optional<ulong?> SystemChannelId { get; set; }
[JsonProperty("icon")]
public Optional<Image?> Icon { get; set; }
[JsonProperty("splash")]


+ 2
- 0
src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs View File

@@ -8,5 +8,7 @@ namespace Discord.API.Rest
{
[JsonProperty("topic")]
public Optional<string> Topic { get; set; }
[JsonProperty("nsfw")]
public Optional<bool> IsNsfw { get; set; }
}
}

+ 1
- 1
src/Discord.Net.Rest/BaseDiscordClient.cs View File

@@ -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);
};


+ 3
- 0
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -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);
}


+ 6
- 2
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -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 ?? "");


+ 4
- 3
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -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-");
}


+ 4
- 5
src/Discord.Net.Rest/Entities/Channels/RestChannel.cs View File

@@ -6,7 +6,7 @@ using Model = Discord.API.Channel;

namespace Discord.Rest
{
public abstract class RestChannel : RestEntity<ulong>, IChannel, IUpdateable
public class RestChannel : RestEntity<ulong>, 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<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(null); //Overriden


+ 3
- 2
src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs View File

@@ -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<Overwrite> _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)


+ 4
- 0
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -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<TextChannelProperties> func, RequestOptions options = null)


+ 0
- 1
src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs View File

@@ -99,7 +99,6 @@ namespace Discord.Rest

//IChannel
string IChannel.Name { get { throw new NotSupportedException(); } }
bool IChannel.IsNsfw { get { throw new NotSupportedException(); } }
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
{
throw new NotSupportedException();


+ 6
- 0
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -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<ImageModel?>(),
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)


+ 27
- 2
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -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<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)
{
@@ -195,6 +203,16 @@ namespace Discord.Rest
return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false);
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)
=> GuildHelper.CreateTextChannelAsync(this, Discord, name, options);
public Task<RestVoiceChannel> CreateVoiceChannelAsync(string name, RequestOptions options = null)
@@ -314,6 +332,13 @@ namespace Discord.Rest
else
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)
=> await CreateTextChannelAsync(name, options).ConfigureAwait(false);
async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, RequestOptions options)


+ 4
- 24
src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs View File

@@ -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;
}


+ 2
- 1
src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs View File

@@ -43,11 +43,12 @@ namespace Discord.Rest
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)
{
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();
}



+ 3
- 4
src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs View File

@@ -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<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)
=> MessageHelper.PinAsync(this, Discord, options);


+ 1
- 1
src/Discord.Net.Rest/Entities/Roles/RestRole.cs View File

@@ -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)


+ 1
- 0
src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs View File

@@ -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)


+ 20
- 5
src/Discord.Net.Rest/Net/Converters/ImageConverter.cs View File

@@ -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)


+ 0
- 1
src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs View File

@@ -8,7 +8,6 @@ namespace Discord.Rpc
public class RpcChannel : RpcEntity<ulong>
{
public string Name { get; private set; }
public bool IsNsfw => ChannelHelper.IsNsfw(Name);

public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);



+ 2
- 0
src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs View File

@@ -16,6 +16,8 @@ namespace Discord.Rpc
public IReadOnlyCollection<RpcMessage> 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)


+ 2
- 3
src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs View File

@@ -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<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)
=> MessageHelper.PinAsync(this, Discord, options);


+ 5
- 0
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -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);


+ 0
- 1
src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs View File

@@ -41,7 +41,6 @@ namespace Discord.WebSocket

//IChannel
string IChannel.Name => null;
bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this);

Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(null); //Overridden


+ 7
- 6
src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs View File

@@ -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<Overwrite> _overwrites;

@@ -19,7 +19,7 @@ namespace Discord.WebSocket
public int Position { get; private set; }

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)
: 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<GuildChannelProperties> 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<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);

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<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>(Users).ToAsyncEnumerable();
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)


+ 4
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs View File

@@ -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<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>();
@@ -41,6 +44,7 @@ namespace Discord.WebSocket
base.Update(state, model);

Topic = model.Topic.Value;
_nsfw = model.Nsfw.GetValueOrDefault();
}

public Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null)


+ 18
- 6
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -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<SocketTextChannel> TextChannels
=> Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray();
public IReadOnlyCollection<SocketVoiceChannel> 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<IRole> IGuild.Roles => Roles;

@@ -632,6 +642,8 @@ namespace Discord.WebSocket
=> Task.FromResult<ITextChannel>(DefaultChannel);
Task<IGuildChannel> IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options)
=> 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)
=> await CreateTextChannelAsync(name, options).ConfigureAwait(false);
async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, RequestOptions options)


+ 2
- 3
src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs View File

@@ -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<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)
=> MessageHelper.PinAsync(this, Discord, options);


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs View File

@@ -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<SocketGuildUser> Members
=> Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id));



+ 1
- 1
src/Discord.Net.Webhook/DiscordWebhookClient.cs View File

@@ -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);
};


+ 21
- 21
src/Discord.Net/Discord.Net.nuspec View File

@@ -2,9 +2,9 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Discord.Net</id>
<version>1.0.1$suffix$</version>
<version>2.0.0-alpha$suffix$</version>
<title>Discord.Net</title>
<authors>RogueException</authors>
<authors>Discord.Net Contributors</authors>
<owners>RogueException</owners>
<description>An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components.</description>
<tags>discord;discordapp</tags>
@@ -13,29 +13,29 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<dependencies>
<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 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 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>
</dependencies>
</metadata>
</package>
</package>

+ 1
- 1
test/Discord.Net.Tests/Tests.Channels.cs View File

@@ -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");



+ 86
- 0
test/Discord.Net.Tests/Tests.Colors.cs View File

@@ -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);
}
}
}

+ 1
- 1
test/Discord.Net.Tests/Tests.Migrations.cs View File

@@ -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)


Loading…
Cancel
Save