Browse Source

Add XML docs

pull/988/head
Still Hsu 7 years ago
parent
commit
5092358401
No known key found for this signature in database GPG Key ID: 8601A145FDA95209
89 changed files with 920 additions and 183 deletions
  1. +1
    -1
      docs/guides/getting_started/samples/first-bot/structure.cs
  2. +1
    -1
      experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs
  3. +2
    -2
      experiment/Discord.Net.Rpc/DiscordRpcConfig.cs
  4. +1
    -0
      src/Discord.Net.Commands/Builders/CommandBuilder.cs
  5. +9
    -9
      src/Discord.Net.Commands/CommandService.cs
  6. +1
    -1
      src/Discord.Net.Commands/Info/CommandInfo.cs
  7. +1
    -0
      src/Discord.Net.Commands/Map/CommandMapNode.cs
  8. +2
    -0
      src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs
  9. +1
    -1
      src/Discord.Net.Core/Audio/IAudioClient.cs
  10. +2
    -2
      src/Discord.Net.Core/CDN.cs
  11. +1
    -1
      src/Discord.Net.Core/Commands/ICommandContext.cs
  12. +1
    -1
      src/Discord.Net.Core/Entities/Activities/Game.cs
  13. +1
    -1
      src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs
  14. +2
    -2
      src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs
  15. +154
    -18
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  16. +1
    -1
      src/Discord.Net.Core/Entities/IUpdateable.cs
  17. +2
    -2
      src/Discord.Net.Core/Entities/Image.cs
  18. +1
    -1
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  19. +1
    -1
      src/Discord.Net.Core/Entities/Messages/EmbedField.cs
  20. +1
    -1
      src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs
  21. +1
    -1
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  22. +13
    -12
      src/Discord.Net.Core/Entities/Messages/MessageProperties.cs
  23. +2
    -1
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  24. +1
    -1
      src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs
  25. +1
    -1
      src/Discord.Net.Core/Entities/Roles/RoleProperties.cs
  26. +5
    -5
      src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs
  27. +2
    -2
      src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs
  28. +13
    -7
      src/Discord.Net.Core/Utils/ConcurrentHashSet.cs
  29. +1
    -0
      src/Discord.Net.Core/Utils/Optional.cs
  30. +2
    -2
      src/Discord.Net.Core/Utils/Preconditions.cs
  31. +2
    -0
      src/Discord.Net.Rest/ClientHelper.cs
  32. +16
    -3
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  33. +16
    -0
      src/Discord.Net.Rest/DiscordRestClient.cs
  34. +29
    -9
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  35. +1
    -1
      src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs
  36. +1
    -1
      src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs
  37. +1
    -1
      src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs
  38. +8
    -1
      src/Discord.Net.Rest/Entities/Channels/RestChannel.cs
  39. +18
    -2
      src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs
  40. +13
    -0
      src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs
  41. +12
    -5
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  42. +3
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestBan.cs
  43. +82
    -4
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  44. +2
    -2
      src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs
  45. +13
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs
  46. +7
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs
  47. +3
    -1
      src/Discord.Net.Rest/Entities/Invites/RestInvite.cs
  48. +3
    -1
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  49. +5
    -0
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  50. +21
    -4
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  51. +8
    -0
      src/Discord.Net.Rest/Entities/RestApplication.cs
  52. +14
    -1
      src/Discord.Net.Rest/Entities/Roles/RestRole.cs
  53. +12
    -1
      src/Discord.Net.Rest/Entities/Users/RestConnection.cs
  54. +9
    -0
      src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs
  55. +9
    -1
      src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs
  56. +14
    -0
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  57. +14
    -1
      src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs
  58. +3
    -1
      src/Discord.Net.Rest/Net/DefaultRestClient.cs
  59. +1
    -0
      src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs
  60. +1
    -1
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  61. +2
    -2
      src/Discord.Net.Rest/Net/RateLimitInfo.cs
  62. +6
    -3
      src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs
  63. +4
    -1
      src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs
  64. +7
    -3
      src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs
  65. +1
    -1
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  66. +137
    -5
      src/Discord.Net.WebSocket/BaseSocketClient.cs
  67. +3
    -3
      src/Discord.Net.WebSocket/ClientState.cs
  68. +3
    -1
      src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs
  69. +6
    -3
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  70. +37
    -7
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  71. +1
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs
  72. +5
    -4
      src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs
  73. +0
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  74. +14
    -2
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  75. +21
    -1
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  76. +4
    -1
      src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs
  77. +2
    -1
      src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs
  78. +24
    -4
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  79. +14
    -1
      src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs
  80. +1
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  81. +3
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  82. +12
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs
  83. +3
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs
  84. +12
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  85. +17
    -3
      src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs
  86. +6
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs
  87. +2
    -2
      src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs
  88. +2
    -1
      src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs
  89. +3
    -2
      src/Discord.Net.Webhook/WebhookClientHelper.cs

+ 1
- 1
docs/guides/getting_started/samples/first-bot/structure.cs View File

@@ -36,7 +36,7 @@ class Program
// you must set the MessageCacheSize. You may adjust the number as needed.
//MessageCacheSize = 50,

// If your platform doesn't have native websockets,
// If your platform doesn't have native WebSockets,
// add Discord.Net.Providers.WS4Net from NuGet,
// add the `using` at the top, and uncomment this line:
//WebSocketProvider = WS4NetProvider.Instance


+ 1
- 1
experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs View File

@@ -192,7 +192,7 @@ namespace Discord.API
internal override async Task DisconnectInternalAsync()
{
if (_webSocketClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");
throw new NotSupportedException("This client is not configured with WebSocket support.");

if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;


+ 2
- 2
experiment/Discord.Net.Rpc/DiscordRpcConfig.cs View File

@@ -14,7 +14,7 @@ namespace Discord.Rpc
/// <summary> Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. </summary>
public int ConnectionTimeout { get; set; } = 30000;

/// <summary> Gets or sets the provider used to generate new websocket connections. </summary>
/// <summary> Gets or sets the provider used to generate new WebSocket connections. </summary>
public WebSocketProvider WebSocketProvider { get; set; }

public DiscordRpcConfig()
@@ -24,7 +24,7 @@ namespace Discord.Rpc
#else
WebSocketProvider = () =>
{
throw new InvalidOperationException("The default websocket provider is not supported on this platform.\n" +
throw new InvalidOperationException("The default WebSocket provider is not supported on this platform.\n" +
"You must specify a WebSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+.");
};
#endif


+ 1
- 0
src/Discord.Net.Commands/Builders/CommandBuilder.cs View File

@@ -117,6 +117,7 @@ namespace Discord.Commands.Builders
return this;
}

/// <exception cref="InvalidOperationException">Only the last parameter in a command may have the Remainder or Multiple flag.</exception>
internal CommandInfo Build(ModuleInfo info, CommandService service)
{
//Default name to primary alias


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

@@ -40,17 +40,17 @@ namespace Discord.Commands
internal readonly LogManager _logManager;

/// <summary>
/// Represents all modules loaded within <see cref="CommandService" /> .
/// Represents all modules loaded within <see cref="CommandService" />.
/// </summary>
public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x);

/// <summary>
/// Represents all commands loaded within <see cref="CommandService" /> .
/// Represents all commands loaded within <see cref="CommandService" />.
/// </summary>
public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands);

/// <summary>
/// Represents all <see cref="TypeReader" /> loaded within <see cref="CommandService" /> .
/// Represents all <see cref="TypeReader" /> loaded within <see cref="CommandService" />.
/// </summary>
public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value);

@@ -122,12 +122,12 @@ namespace Discord.Commands
}

/// <summary>
/// Add a command module from a <see cref="Type" /> .
/// Add a command module from a <see cref="Type" />.
/// </summary>
/// <typeparam name="T">The type of module.</typeparam>
/// <param name="services">
/// The <see cref="IServiceProvider" /> for your dependency injection solution, if using one - otherwise, pass
/// <see langword="null" /> .
/// <see langword="null" />.
/// </param>
/// <returns>
/// A built module.
@@ -135,12 +135,12 @@ namespace Discord.Commands
public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services);

/// <summary>
/// Adds a command module from a <see cref="Type" /> .
/// Adds a command module from a <see cref="Type" />.
/// </summary>
/// <param name="type">The type of module.</param>
/// <param name="services">
/// The <see cref="IServiceProvider" /> for your dependency injection solution, if using one - otherwise, pass
/// <see langword="null" /> .
/// <see langword="null" />.
/// </param>
/// <returns>
/// A built module.
@@ -174,12 +174,12 @@ namespace Discord.Commands
}
}
/// <summary>
/// Add command modules from an <see cref="Assembly" /> .
/// Add command modules from an <see cref="Assembly" />.
/// </summary>
/// <param name="assembly">The <see cref="Assembly" /> containing command modules.</param>
/// <param name="services">
/// An <see cref="IServiceProvider" /> for your dependency injection solution, if using one - otherwise, pass
/// <see langword="null" /> .
/// <see langword="null" />.
/// </param>
/// <returns>
/// A collection of built modules.


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

@@ -16,7 +16,7 @@ namespace Discord.Commands
/// </summary>
/// <remarks>
/// This object contains the information of a command. This can include the module of the command, various
/// descriptions regarding the command, and its <see cref="RunMode" /> .
/// descriptions regarding the command, and its <see cref="RunMode" />.
/// </remarks>
[DebuggerDisplay("{Name,nq}")]
public class CommandInfo


+ 1
- 0
src/Discord.Net.Commands/Map/CommandMapNode.cs View File

@@ -23,6 +23,7 @@ namespace Discord.Commands
_commands = ImmutableArray.Create<CommandInfo>();
}

/// <exception cref="InvalidOperationException">Cannot add commands to the root node.</exception>
public void AddCommand(CommandService service, string text, int index, CommandInfo command)
{
int nextSegment = NextSegment(text, index, service._separatorChar);


+ 2
- 0
src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs View File

@@ -17,10 +17,12 @@ namespace Discord.Commands
private readonly TryParseDelegate<T> _tryParse;
private readonly float _score;

/// <exception cref="ArgumentOutOfRangeException"><typeparamref name="T"/> must be within the range [0, 1].</exception>
public PrimitiveTypeReader()
: this(PrimitiveParsers.Get<T>(), 1)
{ }

/// <exception cref="ArgumentOutOfRangeException"><paramref name="score"/> must be within the range [0, 1].</exception>
public PrimitiveTypeReader(TryParseDelegate<T> tryParse, float score)
{
if (score < 0 || score > 1)


+ 1
- 1
src/Discord.Net.Core/Audio/IAudioClient.cs View File

@@ -15,7 +15,7 @@ namespace Discord.Audio

/// <summary> Gets the current connection state of this client. </summary>
ConnectionState ConnectionState { get; }
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice websocket server. </summary>
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice WebSocket server. </summary>
int Latency { get; }
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. </summary>
int UdpLatency { get; }


+ 2
- 2
src/Discord.Net.Core/CDN.cs View File

@@ -13,7 +13,7 @@ namespace Discord
public static string GetApplicationIconUrl(ulong appId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null;
/// <summary>
/// Returns the user avatar URL based on the <paramref name="size"/> and <see cref="ImageFormat" /> .
/// Returns the user avatar URL based on the <paramref name="size"/> and <see cref="ImageFormat" />.
/// </summary>
public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format)
{
@@ -52,7 +52,7 @@ namespace Discord
=> $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}";

/// <summary>
/// Returns the rich presence asset URL based on the asset ID and <see cref="ImageFormat" /> .
/// Returns the rich presence asset URL based on the asset ID and <see cref="ImageFormat" />.
/// </summary>
public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format)
{


+ 1
- 1
src/Discord.Net.Core/Commands/ICommandContext.cs View File

@@ -1,7 +1,7 @@
namespace Discord.Commands
{
/// <summary>
/// Represents the context of a command. This may include the client, guild, channel, user, and message.
/// Represents a context of a command. This may include the client, guild, channel, user, and message.
/// </summary>
public interface ICommandContext
{


+ 1
- 1
src/Discord.Net.Core/Entities/Activities/Game.cs View File

@@ -18,7 +18,7 @@ namespace Discord
/// Creates a <see cref="Game"/> with the provided <paramref name="name"/> and <see cref="ActivityType"/>.
/// </summary>
/// <param name="name">The name of the game.</param>
/// <param name="type">The type of activity. Default is <see cref="Discord.ActivityType.Playing" /> .</param>
/// <param name="type">The type of activity. Default is <see cref="Discord.ActivityType.Playing" />.</param>
public Game(string name, ActivityType type = ActivityType.Playing)
{
Name = name;


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

@@ -1,7 +1,7 @@
namespace Discord
{
/// <summary>
/// Properties that are used to reorder an <see cref="IGuildChannel" /> .
/// Properties that are used to reorder an <see cref="IGuildChannel" />.
/// </summary>
public class ReorderChannelProperties
{


+ 2
- 2
src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs View File

@@ -8,11 +8,11 @@ namespace Discord
public class EmoteProperties
{
/// <summary>
/// Gets or sets the name of the <see cref="Emote" /> .
/// Gets or sets the name of the <see cref="Emote" />.
/// </summary>
public Optional<string> Name { get; set; }
/// <summary>
/// Gets or sets the roles that can access this <see cref="Emote" /> .
/// Gets or sets the roles that can access this <see cref="Emote" />.
/// </summary>
public Optional<IEnumerable<IRole>> Roles { get; set; }
}


+ 154
- 18
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic guild object.
/// Represents a generic guild/server.
/// </summary>
public interface IGuild : IDeletable, ISnowflakeEntity
{
@@ -23,7 +23,7 @@ namespace Discord
/// Determines if this guild is embeddable (i.e. can use widget).
/// </summary>
/// <returns>
/// Returns <see langword="true"/> if this guild can be embedded via widgets.
/// <see langword="true"/> if this guild can be embedded via widgets; otherwise <see langword="false"/>.
/// </returns>
bool IsEmbeddable { get; }
/// <summary>
@@ -91,58 +91,91 @@ namespace Discord
/// <summary>
/// Gets the <see cref="IAudioClient" /> currently associated with this guild.
/// </summary>
/// <returns>
/// <see cref="IAudioClient" /> currently associated with this guild.
/// </returns>
IAudioClient AudioClient { get; }
/// <summary>
/// Gets the built-in role containing all users in this guild.
/// </summary>
/// <returns>
/// Built-in role that represents an @everyone role in this guild.
/// </returns>
IRole EveryoneRole { get; }
/// <summary>
/// Gets a collection of all custom emotes for this guild.
/// </summary>
/// <returns>
/// A collection of all custom emotes for this guild.
/// </returns>
IReadOnlyCollection<GuildEmote> Emotes { get; }
/// <summary>
/// Gets a collection of all extra features added to this guild.
/// </summary>
/// <returns>
/// A collection of enabled features in this guild.
/// </returns>
IReadOnlyCollection<string> Features { get; }
/// <summary>
/// Gets a collection of all roles in this guild.
/// </summary>
/// <returns>
/// A collection of roles found within this guild.
/// </returns>
IReadOnlyCollection<IRole> Roles { get; }

/// <summary>
/// Modifies this guild.
/// </summary>
/// <param name="func">The properties to modify the guild with.</param>
/// <param name="func">The delegate containing the properties to modify the guild with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task ModifyAsync(Action<GuildProperties> func, RequestOptions options = null);
/// <summary>
/// Modifies this guild's embed channel.
/// </summary>
/// <param name="func">The properties to modify the guild widget with.</param>
/// <param name="func">The delegate containing the properties to modify the guild widget with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task ModifyEmbedAsync(Action<GuildEmbedProperties> func, RequestOptions options = null);
/// <summary>
/// Bulk modifies the order of channels in this guild.
/// </summary>
/// <param name="args">The properties to modify the channel positions with.</param>
/// <param name="args">The properties used to modify the channel positions with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task ReorderChannelsAsync(IEnumerable<ReorderChannelProperties> args, RequestOptions options = null);
/// <summary>
/// Bulk modifies the order of roles in this guild.
/// </summary>
/// <param name="args">The properties to modify the role positions with.</param>
/// <param name="args">The properties used to modify the role positions with.</param>
/// <param name="options">The options to be used when sending the request.</param>
Task ReorderRolesAsync(IEnumerable<ReorderRoleProperties> args, RequestOptions options = null);
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
/// <summary>
/// Leaves this guild. If you are the owner, use <see cref="IDeletable.DeleteAsync" /> instead.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task LeaveAsync(RequestOptions options = null);

/// <summary>
/// Gets a collection of all users banned on this guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing a collection of banned users with reasons.
/// </returns>
Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null);
/// <summary>
/// Bans the provided user from this guild and optionally prunes their recent messages.
@@ -154,6 +187,9 @@ namespace Discord
/// <param name="reason">The reason of the ban to be written in the audit log.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentException"><paramref name="pruneDays" /> is not between 0 to 7.</exception>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null);
/// <summary>
/// Bans the provided user ID from this guild and optionally prunes their recent messages.
@@ -165,14 +201,27 @@ namespace Discord
/// <param name="reason">The reason of the ban to be written in the audit log.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentException"><paramref name="pruneDays" /> is not between 0 to 7.</exception>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null);
/// <summary>
/// Unbans the provided user if they are currently banned.
/// </summary>
/// <param name="user">The user to be unbanned.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task RemoveBanAsync(IUser user, RequestOptions options = null);
/// <summary>
/// Unbans the provided user ID if it is currently banned.
/// </summary>
/// <param name="userId">The snowflake ID of the user to be unbanned.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task RemoveBanAsync(ulong userId, RequestOptions options = null);

/// <summary>
@@ -182,15 +231,22 @@ namespace Discord
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing a collection of generic channels found within this guild.
/// </returns>
Task<IReadOnlyCollection<IGuildChannel>> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the channel in this guild with the provided ID, or <see langword="null" /> if not found.
/// Gets the channel in this guild with the provided ID.
/// </summary>
/// <param name="id">The channel ID.</param>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the generic channel with the specified ID, or
/// <see langword="null"/> if none is found.
/// </returns>
Task<IGuildChannel> GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of all text channels in this guild.
@@ -199,6 +255,9 @@ namespace Discord
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing a collection of text channels found within this guild.
/// </returns>
Task<IReadOnlyCollection<ITextChannel>> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a text channel in this guild with the provided ID, or <see langword="null" /> if not found.
@@ -208,6 +267,10 @@ namespace Discord
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the text channel with the specified ID, or
/// <see langword="null"/> if none is found.
/// </returns>
Task<ITextChannel> GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of all voice channels in this guild.
@@ -216,6 +279,9 @@ namespace Discord
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing a collection of voice channels found within this guild.
/// </returns>
Task<IReadOnlyCollection<IVoiceChannel>> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of all category channels in this guild.
@@ -224,68 +290,97 @@ namespace Discord
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing a collection of category channels found within this guild.
/// </returns>
Task<IReadOnlyCollection<ICategoryChannel>> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the voice channel in this guild with the provided ID, or <see langword="null" /> if not found.
/// Gets the voice channel in this guild with the provided ID.
/// </summary>
/// <param name="id">The text channel ID.</param>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the voice channel with the specified ID, or
/// <see langword="null"/> if none is found.
/// </returns>
Task<IVoiceChannel> GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the voice AFK channel in this guild with the provided ID, or <see langword="null" /> if not found.
/// Gets the AFK voice channel in this guild.
/// </summary>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the AFK voice channel set within this guild, or
/// <see langword="null"/> if none is set.
/// </returns>
Task<IVoiceChannel> GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the default system text channel in this guild with the provided ID, or <see langword="null" /> if
/// none is set.
/// Gets the default system text channel in this guild with the provided ID.
/// </summary>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the system channel within this guild, or
/// <see langword="null" /> if none is set.
/// </returns>
Task<ITextChannel> GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the top viewable text channel in this guild with the provided ID, or <see langword="null" /> if not
/// found.
/// Gets the top viewable text channel in this guild with the provided ID.
/// </summary>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the first viewable text channel in this guild, or
/// <see langword="null"/> if none is found.
/// </returns>
Task<ITextChannel> GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild, or
/// <see langword="null" /> if none is set.
/// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
/// <param name="mode">
/// The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the embed channel set within the server's widget settings, or
/// <see langword="null"/> if none is set.
/// </returns>
Task<IGuildChannel> GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Creates a new text channel.
/// </summary>
/// <param name="name">The new name for the text channel.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the newly created text channel.
/// </returns>
Task<ITextChannel> CreateTextChannelAsync(string name, RequestOptions options = null);
/// <summary>
/// Creates a new voice channel.
/// </summary>
/// <param name="name">The new name for the voice channel.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the newly created voice channel.
/// </returns>
Task<IVoiceChannel> CreateVoiceChannelAsync(string name, RequestOptions options = null);
/// <summary>
/// Creates a new channel category.
/// </summary>
/// <param name="name">The new name for the category.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the newly created category channel.
/// </returns>
Task<ICategoryChannel> CreateCategoryAsync(string name, RequestOptions options = null);

Task<IReadOnlyCollection<IGuildIntegration>> GetIntegrationsAsync(RequestOptions options = null);
@@ -309,13 +404,24 @@ namespace Discord
/// <param name="color">The color of the role.</param>
/// <param name="isHoisted">Whether the role is separated from others on the sidebar.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the newly crated role.
/// </returns>
Task<IRole> CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null);

/// <summary>
/// Gets a collection of all users in this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from
/// cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing a collection of users found within this guild.
/// </returns>
/// <remarks>
/// This may return an incomplete list on the WebSocket implementation because Discord only sends offline
/// users on large guilds.
/// </remarks>
Task<IReadOnlyCollection<IGuildUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the user in this guild with the provided ID, or <see langword="null"/> if not found.
@@ -323,22 +429,34 @@ namespace Discord
/// <param name="id">The user ID.</param>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the guild user with the specified ID, otherwise <see langword="null"/>.
/// </returns>
Task<IGuildUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the current user for this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the currently logged-in user within this guild.
/// </returns>
Task<IGuildUser> GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the owner of this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the owner of this guild.
/// </returns>
Task<IGuildUser> GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Downloads all users for this guild if the current list is incomplete.
/// </summary>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task DownloadUsersAsync();
/// <summary>
/// Removes all users from this guild if they have not logged on in a provided number of
@@ -349,7 +467,7 @@ namespace Discord
/// <param name="simulate">Whether this prune action is a simulation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// The number of users removed from this guild.
/// An awaitable <see cref="Task"/> containing the number of users to be or has been removed from this guild.
/// </returns>
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null);

@@ -358,11 +476,17 @@ namespace Discord
/// </summary>
/// <param name="id">The webhook ID.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the webhook with the specified ID, otherwise <see langword="null"/>.
/// </returns>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
/// <summary>
/// Gets a collection of all webhook from this guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing a collection of webhooks found within the guild.
/// </returns>
Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null);

/// <summary>
@@ -370,6 +494,9 @@ namespace Discord
/// </summary>
/// <param name="id">The guild emote ID.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the emote found with the specified ID, or <see langword="null"/> if not found.
/// </returns>
Task<GuildEmote> GetEmoteAsync(ulong id, RequestOptions options = null);
/// <summary>
/// Creates a new <see cref="GuildEmote"/> in this guild.
@@ -378,20 +505,29 @@ namespace Discord
/// <param name="image">The image of the new emote.</param>
/// <param name="roles">The roles to limit the emote usage to.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the created emote.
/// </returns>
Task<GuildEmote> CreateEmoteAsync(string name, Image image, Optional<IEnumerable<IRole>> roles = default(Optional<IEnumerable<IRole>>), RequestOptions options = null);

/// <summary>
/// Modifies an existing <see cref="GuildEmote"/> in this guild.
/// </summary>
/// <param name="emote">The emote to be modified.</param>
/// <param name="func">The properties to modify the emote with.</param>
/// <param name="func">The delegate containing the properties to modify the emote with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the newly modified emote.
/// </returns>
Task<GuildEmote> ModifyEmoteAsync(GuildEmote emote, Action<EmoteProperties> func, RequestOptions options = null);
/// <summary>
/// Deletes an existing <see cref="GuildEmote"/> from this guild.
/// </summary>
/// <param name="emote">The emote to delete.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null);
}
}

+ 1
- 1
src/Discord.Net.Core/Entities/IUpdateable.cs View File

@@ -3,7 +3,7 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents whether the object is updatable or not.
/// Defines whether the object is updateable or not.
/// </summary>
public interface IUpdateable
{


+ 2
- 2
src/Discord.Net.Core/Entities/Image.cs View File

@@ -33,9 +33,9 @@ namespace Discord
/// <param name="path">The path to the file.</param>
/// <exception cref="ArgumentException">
/// <paramref name="path" /> is a zero-length string, contains only white space, or contains one or more invalid
/// characters as defined by <see cref="Path.GetInvalidPathChars" /> .
/// characters as defined by <see cref="Path.GetInvalidPathChars" />.
/// </exception>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null" /> .</exception>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null" />.</exception>
/// <exception cref="PathTooLongException">
/// The specified path, file name, or both exceed the system-defined maximum length. For example, on
/// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260


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

@@ -172,7 +172,7 @@ namespace Discord
}

/// <summary>
/// Sets the title of an <see cref="Embed" /> .
/// Sets the title of an <see cref="Embed" />.
/// </summary>
/// <param name="title">The title to be set.</param>
/// <returns>


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

@@ -3,7 +3,7 @@ using System.Diagnostics;
namespace Discord
{
/// <summary>
/// Represents a field for an <see cref="Embed" /> .
/// Represents a field for an <see cref="Embed" />.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public struct EmbedField


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

@@ -3,7 +3,7 @@ using System.Diagnostics;
namespace Discord
{
/// <summary>
/// A video featured in an <see cref="Embed" /> .
/// A video featured in an <see cref="Embed" />.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public struct EmbedVideo


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

@@ -4,7 +4,7 @@ using System.Collections.Generic;
namespace Discord
{
/// <summary>
/// Represents a Discord message object.
/// Represents a message object.
/// </summary>
public interface IMessage : ISnowflakeEntity, IDeletable
{


+ 13
- 12
src/Discord.Net.Core/Entities/Messages/MessageProperties.cs View File

@@ -4,20 +4,21 @@ namespace Discord
/// Properties that are used to modify an <see cref="IUserMessage" /> with the specified changes.
/// </summary>
/// <remarks>
/// The content of a message can be cleared with String.Empty; if and only if an Embed is present.
/// The content of a message can be cleared with <see cref="string.Empty"/> if and only if an <see cref="Discord.Embed"> is present.
/// </remarks>
/// <example>
/// <code lang="c#">
/// var message = await ReplyAsync("abc");
/// await message.ModifyAsync(x =&gt;
/// {
/// x.Content = "";
/// x.Embed = new EmbedBuilder()
/// .WithColor(new Color(40, 40, 120))
/// .WithAuthor(a =&gt; a.Name = "foxbot")
/// .WithTitle("Embed!")
/// .WithDescription("This is an embed.");
/// });
/// var message = await ReplyAsync("abc");
/// await message.ModifyAsync(x =&gt;
/// {
/// x.Content = "";
/// x.Embed = new EmbedBuilder()
/// .WithColor(new Color(40, 40, 120))
/// .WithAuthor(a =&gt; a.Name = "foxbot")
/// .WithTitle("Embed!")
/// .WithDescription("This is an embed.")
/// .Build();
/// });
/// </code>
/// </example>
public class MessageProperties
@@ -26,7 +27,7 @@ namespace Discord
/// Gets or sets the content of the message.
/// </summary>
/// <remarks>
/// This must be less than 2000 characters.
/// This must be less than the constant defined by <see cref="DiscordConfig.MaxMessageSize"/>.
/// </remarks>
public Optional<string> Content { get; set; }
/// <summary>


+ 2
- 1
src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs View File

@@ -19,7 +19,8 @@ namespace Discord
public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for group channels. </summary>
public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for a given channelType. </summary>
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for a given channel type. </summary>
/// <exception cref="ArgumentException">Unknown channel type.</exception>
public static ChannelPermissions All(IChannel channel)
{
switch (channel)


+ 1
- 1
src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs View File

@@ -1,7 +1,7 @@
namespace Discord
{
/// <summary>
/// Properties that are used to reorder an <see cref="IRole" /> .
/// Properties that are used to reorder an <see cref="IRole" />.
/// </summary>
public class ReorderRoleProperties
{


+ 1
- 1
src/Discord.Net.Core/Entities/Roles/RoleProperties.cs View File

@@ -23,7 +23,7 @@ namespace Discord
/// </remarks>
public Optional<string> Name { get; set; }
/// <summary>
/// Gets or sets the role's <see cref="GuildPermission" /> .
/// Gets or sets the role's <see cref="GuildPermission" />.
/// </summary>
public Optional<GuildPermissions> Permissions { get; set; }
/// <summary>


+ 5
- 5
src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs View File

@@ -7,10 +7,10 @@ namespace Discord
/// </summary>
/// <example>
/// <code lang="c#">
/// await (Context.User as IGuildUser)?.ModifyAsync(x =&gt;
/// {
/// x.Nickname = $"festive {Context.User.Username}";
/// });
/// await guildUser.ModifyAsync(x =&gt;
/// {
/// x.Nickname = $"festive {guildUser.Username}";
/// });
/// </code>
/// </example>
/// <seealso cref="T:Discord.IGuildUser" />
@@ -35,7 +35,7 @@ namespace Discord
/// </summary>
/// <remarks>
/// To clear the user's nickname, this value can be set to <see langword="null" /> or
/// <see cref="string.Empty" /> .
/// <see cref="string.Empty" />.
/// </remarks>
public Optional<string> Nickname { get; set; }
/// <summary>


+ 2
- 2
src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs View File

@@ -27,14 +27,14 @@ namespace Discord
/// Gets or sets the channel for this webhook.
/// </summary>
/// <remarks>
/// This field is not used when authenticated with <see cref="Discord.TokenType.Webhook" /> .
/// This field is not used when authenticated with <see cref="Discord.TokenType.Webhook" />.
/// </remarks>
public Optional<ITextChannel> Channel { get; set; }
/// <summary>
/// Gets or sets the channel ID for this webhook.
/// </summary>
/// <remarks>
/// This field is not used when authenticated with <see cref="Discord.TokenType.Webhook" /> .
/// This field is not used when authenticated with <see cref="Discord.TokenType.Webhook" />.
/// </remarks>
public Optional<ulong> ChannelId { get; set; }
}


+ 13
- 7
src/Discord.Net.Core/Utils/ConcurrentHashSet.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -157,12 +157,16 @@ namespace Discord
: this(collection, EqualityComparer<T>.Default) { }
public ConcurrentHashSet(IEqualityComparer<T> comparer)
: this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer) { }
/// <exception cref="ArgumentNullException"><paramref name="collection"/> is <see langword="null"/></exception>
public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
: this(comparer)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
InitializeFromCollection(collection);
}
/// <exception cref="ArgumentNullException">
/// <paramref name="collection" /> or <paramref name="comparer" /> is <see langword="null" />
/// </exception>
public ConcurrentHashSet(int concurrencyLevel, IEnumerable<T> collection, IEqualityComparer<T> comparer)
: this(concurrencyLevel, DefaultCapacity, false, comparer)
{
@@ -197,7 +201,7 @@ namespace Discord
{
foreach (var value in collection)
{
if (value == null) throw new ArgumentNullException("key");
if (value == null) throw new ArgumentNullException(nameof(value));

if (!TryAddInternal(value, _comparer.GetHashCode(value), false))
throw new ArgumentException();
@@ -206,10 +210,10 @@ namespace Discord
if (_budget == 0)
_budget = _tables._buckets.Length / _tables._locks.Length;
}
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/></exception>
public bool ContainsKey(T value)
{
if (value == null) throw new ArgumentNullException("key");
if (value == null) throw new ArgumentNullException(nameof(value));
return ContainsKeyInternal(value, _comparer.GetHashCode(value));
}
private bool ContainsKeyInternal(T value, int hashcode)
@@ -230,9 +234,10 @@ namespace Discord
return false;
}

/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/></exception>
public bool TryAdd(T value)
{
if (value == null) throw new ArgumentNullException("key");
if (value == null) throw new ArgumentNullException(nameof(value));
return TryAddInternal(value, _comparer.GetHashCode(value), true);
}
private bool TryAddInternal(T value, int hashcode, bool acquireLock)
@@ -279,9 +284,10 @@ namespace Discord
}
}

/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/></exception>
public bool TryRemove(T value)
{
if (value == null) throw new ArgumentNullException("key");
if (value == null) throw new ArgumentNullException(nameof(value));
return TryRemoveInternal(value);
}
private bool TryRemoveInternal(T value)
@@ -467,4 +473,4 @@ namespace Discord
Monitor.Exit(_tables._locks[i]);
}
}
}
}

+ 1
- 0
src/Discord.Net.Core/Utils/Optional.cs View File

@@ -11,6 +11,7 @@ namespace Discord
private readonly T _value;

/// <summary> Gets the value for this parameter. </summary>
/// <exception cref="InvalidOperationException" accessor="get">This property has no value set.</exception>
public T Value
{
get


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

@@ -183,7 +183,7 @@ namespace Discord
}

// Bulk Delete
/// <exception cref="ArgumentOutOfRangeException">Messages are younger than 2 weeks..</exception>
/// <exception cref="ArgumentOutOfRangeException">Messages are younger than 2 weeks.</exception>
public static void YoungerThanTwoWeeks(ulong[] collection, string name)
{
var minimum = SnowflakeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14)));
@@ -194,7 +194,7 @@ namespace Discord
throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old.");
}
}
/// <exception cref="ArgumentException">The everyone role cannot be assigned to a user</exception>
/// <exception cref="ArgumentException">The everyone role cannot be assigned to a user.</exception>
public static void NotEveryoneRole(ulong[] roles, ulong guildId, string name)
{
for (var i = 0; i < roles.Length; i++)


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

@@ -1,3 +1,4 @@
using System;
using Discord.API.Rest;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -24,6 +25,7 @@ namespace Discord.Rest
return RestChannel.Create(client, model);
return null;
}
/// <exception cref="InvalidOperationException">Unexpected channel type.</exception>
public static async Task<IReadOnlyCollection<IRestPrivateChannel>> GetPrivateChannelsAsync(BaseDiscordClient client, RequestOptions options)
{
var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false);


+ 16
- 3
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -58,6 +58,9 @@ namespace Discord.API

SetBaseUrl(DiscordConfig.APIUrl);
}

/// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
/// <exception cref="Exception">A delegate callback throws an exception.</exception>
internal void SetBaseUrl(string baseUrl)
{
RestClient = _restClientProvider(baseUrl);
@@ -65,6 +68,7 @@ namespace Discord.API
RestClient.SetHeader("user-agent", UserAgent);
RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken));
}
/// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
internal static string GetPrefixedToken(TokenType tokenType, string token)
{
switch (tokenType)
@@ -76,7 +80,7 @@ namespace Discord.API
case TokenType.Bearer:
return $"Bearer {token}";
default:
throw new ArgumentException("Unknown OAuth token type", nameof(tokenType));
throw new ArgumentException("Unknown OAuth token type.", nameof(tokenType));
}
}
internal virtual void Dispose(bool disposing)
@@ -463,6 +467,7 @@ namespace Discord.API
endpoint = () => $"channels/{channelId}/messages?limit={limit}";
return await SendAsync<IReadOnlyCollection<Message>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
}
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public async Task<Message> CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null)
{
Preconditions.NotNull(args, nameof(args));
@@ -471,12 +476,14 @@ namespace Discord.API
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));

if (args.Content?.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(channelId: channelId);
return await SendJsonAsync<Message>("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
public async Task<Message> CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null)
{
if (AuthTokenType != TokenType.Webhook)
@@ -488,11 +495,12 @@ namespace Discord.API
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));

if (args.Content?.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
options = RequestOptions.CreateOrClone(options);
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null)
{
Preconditions.NotNull(args, nameof(args));
@@ -507,6 +515,9 @@ namespace Discord.API
var ids = new BucketIds(channelId: channelId);
return await SendMultipartAsync<Message>("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
public async Task<Message> UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null)
{
if (AuthTokenType != TokenType.Webhook)
@@ -559,6 +570,7 @@ namespace Discord.API
break;
}
}
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public async Task<Message> ModifyMessageAsync(ulong channelId, ulong messageId, Rest.ModifyMessageParams args, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
@@ -1267,6 +1279,7 @@ namespace Discord.API
}

//Helpers
/// <exception cref="InvalidOperationException">Client is not logged in.</exception>
protected void CheckState()
{
if (LoginState != LoginState.LoggedIn)


+ 16
- 0
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -22,12 +22,14 @@ namespace Discord.Rest
ApiClient.Dispose();
}

/// <inheritdoc />
internal override async Task OnLoginAsync(TokenType tokenType, string token)
{
var user = await ApiClient.GetMyUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
ApiClient.CurrentUserId = user.Id;
base.CurrentUser = RestSelfUser.Create(this, user);
}
/// <inheritdoc />
internal override Task OnLogoutAsync()
{
_applicationInfo = null;
@@ -80,9 +82,11 @@ namespace Discord.Rest
=> ClientHelper.GetWebhookAsync(this, id, options);

//IDiscordClient
/// <inheritdoc />
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> await GetApplicationInfoAsync(options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IChannel> IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -90,6 +94,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IReadOnlyCollection<IPrivateChannel>> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -97,6 +102,7 @@ namespace Discord.Rest
else
return ImmutableArray.Create<IPrivateChannel>();
}
/// <inheritdoc />
async Task<IReadOnlyCollection<IDMChannel>> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -104,6 +110,7 @@ namespace Discord.Rest
else
return ImmutableArray.Create<IDMChannel>();
}
/// <inheritdoc />
async Task<IReadOnlyCollection<IGroupChannel>> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -112,12 +119,15 @@ namespace Discord.Rest
return ImmutableArray.Create<IGroupChannel>();
}

/// <inheritdoc />
async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options)
=> await GetConnectionsAsync(options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options)
=> await GetInviteAsync(inviteId, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -125,6 +135,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IReadOnlyCollection<IGuild>> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -132,9 +143,11 @@ namespace Discord.Rest
else
return ImmutableArray.Create<IGuild>();
}
/// <inheritdoc />
async Task<IGuild> IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options)
=> await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IUser> IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -143,11 +156,14 @@ namespace Discord.Rest
return null;
}

/// <inheritdoc />
async Task<IReadOnlyCollection<IVoiceRegion>> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options)
=> await GetVoiceRegionsAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options)
=> await GetVoiceRegionAsync(id, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IWebhook> IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options)
=> await GetWebhookAsync(id, options).ConfigureAwait(false);
}


+ 29
- 9
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
using UserModel = Discord.API.User;
using WebhookModel = Discord.API.Webhook;

namespace Discord.Rest
{
@@ -77,14 +76,8 @@ namespace Discord.Rest
int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options)
{
var args = new CreateChannelInviteParams { IsTemporary = isTemporary, IsUnique = isUnique };
if (maxAge.HasValue)
args.MaxAge = maxAge.Value;
else
args.MaxAge = 0;
if (maxUses.HasValue)
args.MaxUses = maxUses.Value;
else
args.MaxUses = 0;
args.MaxAge = maxAge.GetValueOrDefault(0);
args.MaxUses = maxUses.GetValueOrDefault(0);
var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false);
return RestInviteMetadata.Create(client, null, channel, model);
}
@@ -160,6 +153,7 @@ namespace Discord.Rest
return builder.ToImmutable();
}

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendMessageAsync(IMessageChannel channel, BaseDiscordClient client,
string text, bool isTTS, Embed embed, RequestOptions options)
{
@@ -169,6 +163,30 @@ namespace Discord.Rest
}

#if FILESYSTEM
/// <exception cref="ArgumentException">
/// <paramref name="filePath" /> is a zero-length string, contains only white space, or contains one or more
/// invalid characters as defined by <see cref="System.IO.Path.InvalidPathChars" />.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="filePath" /> is <see langword="null" />.
/// </exception>
/// <exception cref="PathTooLongException">
/// The specified path, file name, or both exceed the system-defined maximum length. For example, on
/// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260
/// characters.
/// </exception>
/// <exception cref="DirectoryNotFoundException">
/// The specified path is invalid, (for example, it is on an unmapped drive).
/// </exception>
/// <exception cref="UnauthorizedAccessException">
/// <paramref name="filePath" /> specified a directory.-or- The caller does not have the required permission.
/// </exception>
/// <exception cref="FileNotFoundException">
/// The file specified in <paramref name="filePath" /> was not found.
/// </exception>
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client,
string filePath, string text, bool isTTS, Embed embed, RequestOptions options)
{
@@ -177,6 +195,7 @@ namespace Discord.Rest
return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options).ConfigureAwait(false);
}
#endif
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client,
Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options)
{
@@ -249,6 +268,7 @@ namespace Discord.Rest

return user;
}
/// <exception cref="InvalidOperationException">Resolving permissions requires the parent guild to be downloaded.</exception>
public static IAsyncEnumerable<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(IGuildChannel channel, IGuild guild, BaseDiscordClient client,
ulong? fromUserId, int? limit, RequestOptions options)
{


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

@@ -5,7 +5,7 @@ using System.Threading.Tasks;
namespace Discord.Rest
{
/// <summary>
/// Represents a REST channel that can send and receive messages.
/// Represents a REST-based channel that can send and receive messages.
/// </summary>
public interface IRestMessageChannel : IMessageChannel
{


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

@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Discord.Rest
{
/// <summary>
/// Represents a REST channel that is private to select recipients.
/// Represents a REST-based channel that is private to select recipients.
/// </summary>
public interface IRestPrivateChannel : IPrivateChannel
{


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

@@ -7,7 +7,7 @@ using Model = Discord.API.Channel;
namespace Discord.Rest
{
/// <summary>
/// Represents a REST category channel.
/// Represents a REST-based category channel.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestCategoryChannel : RestGuildChannel, ICategoryChannel


+ 8
- 1
src/Discord.Net.Rest/Entities/Channels/RestChannel.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -8,12 +8,14 @@ namespace Discord.Rest
{
public class RestChannel : RestEntity<ulong>, IChannel, IUpdateable
{
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);

internal RestChannel(BaseDiscordClient discord, ulong id)
: base(discord, id)
{
}
/// <exception cref="InvalidOperationException">Unexpected channel type.</exception>
internal static RestChannel Create(BaseDiscordClient discord, Model model)
{
switch (model.Type)
@@ -28,6 +30,7 @@ namespace Discord.Rest
return new RestChannel(discord, model.Id);
}
}
/// <exception cref="InvalidOperationException">Unexpected channel type.</exception>
internal static IRestPrivateChannel CreatePrivate(BaseDiscordClient discord, Model model)
{
switch (model.Type)
@@ -42,13 +45,17 @@ namespace Discord.Rest
}
internal virtual void Update(Model model) { }

/// <inheritdoc />
public virtual Task UpdateAsync(RequestOptions options = null) => Task.Delay(0);

//IChannel
/// <inheritdoc />
string IChannel.Name => null;

/// <inheritdoc />
Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(null); //Overridden
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> AsyncEnumerable.Empty<IReadOnlyCollection<IUser>>(); //Overridden
}


+ 18
- 2
src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs View File

@@ -10,7 +10,7 @@ using Model = Discord.API.Channel;
namespace Discord.Rest
{
/// <summary>
/// Represents a REST DM channel.
/// Represents a REST-based DM channel.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel
@@ -37,6 +37,7 @@ namespace Discord.Rest
Recipient.Update(model.Recipients.Value[0]);
}

/// <inheritdoc />
public override async Task UpdateAsync(RequestOptions options = null)
{
var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false);
@@ -93,16 +94,20 @@ namespace Discord.Rest
public override string ToString() => $"@{Recipient}";
private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)";

//IDMChannel
//IDMChannel
/// <inheritdoc />
IUser IDMChannel.Recipient => Recipient;

//IRestPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<RestUser> IRestPrivateChannel.Recipients => ImmutableArray.Create(Recipient);

//IPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create<IUser>(Recipient);

//IMessageChannel
/// <inheritdoc />
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -110,6 +115,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -117,6 +123,7 @@ namespace Discord.Rest
else
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>();
}
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -124,6 +131,7 @@ namespace Discord.Rest
else
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>();
}
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -131,25 +139,33 @@ namespace Discord.Rest
else
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>();
}
/// <inheritdoc />
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);

#if FILESYSTEM
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false);
#endif
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
/// <inheritdoc />
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)
=> EnterTypingState(options);

//IChannel
/// <inheritdoc />
string IChannel.Name => $"@{Recipient}";

/// <inheritdoc />
Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(GetUser(id));
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>(Users).ToAsyncEnumerable();
}


+ 13
- 0
src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs View File

@@ -146,6 +146,7 @@ namespace Discord.Rest
public override string ToString() => Name;

//IGuildChannel
/// <inheritdoc />
IGuild IGuildChannel.Guild
{
get
@@ -156,32 +157,44 @@ namespace Discord.Rest
}
}

/// <inheritdoc />
async Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync(RequestOptions options)
=> await GetInvitesAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options)
=> await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);

/// <inheritdoc />
OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role)
=> GetPermissionOverwrite(role);
/// <inheritdoc />
OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user)
=> GetPermissionOverwrite(user);
/// <inheritdoc />
async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options)
=> await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false);
/// <inheritdoc />
async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options)
=> await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false);
/// <inheritdoc />
async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options)
=> await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false);
/// <inheritdoc />
async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options)
=> await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false);

/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> AsyncEnumerable.Empty<IReadOnlyCollection<IGuildUser>>(); //Overridden //Overridden in Text/Voice
/// <inheritdoc />
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(null); //Overridden in Text/Voice

//IChannel
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> AsyncEnumerable.Empty<IReadOnlyCollection<IUser>>(); //Overridden in Text/Voice
/// <inheritdoc />
Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(null); //Overridden in Text/Voice
}


+ 12
- 5
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -1,4 +1,4 @@
using Discord.API.Rest;
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -14,10 +14,11 @@ namespace Discord.Rest
internal static class GuildHelper
{
//General
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public static async Task<Model> ModifyAsync(IGuild guild, BaseDiscordClient client,
Action<GuildProperties> func, RequestOptions options)
{
if (func == null) throw new NullReferenceException(nameof(func));
if (func == null) throw new ArgumentNullException(nameof(func));

var args = new GuildProperties();
func(args);
@@ -62,10 +63,11 @@ namespace Discord.Rest

return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false);
}
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public static async Task<EmbedModel> ModifyEmbedAsync(IGuild guild, BaseDiscordClient client,
Action<GuildEmbedProperties> func, RequestOptions options)
{
if (func == null) throw new NullReferenceException(nameof(func));
if (func == null) throw new ArgumentNullException(nameof(func));

var args = new GuildEmbedProperties();
func(args);
@@ -139,6 +141,7 @@ namespace Discord.Rest
var models = await client.ApiClient.GetGuildChannelsAsync(guild.Id, options).ConfigureAwait(false);
return models.Select(x => RestGuildChannel.Create(client, guild, x)).ToImmutableArray();
}
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
public static async Task<RestTextChannel> CreateTextChannelAsync(IGuild guild, BaseDiscordClient client,
string name, RequestOptions options)
{
@@ -148,6 +151,7 @@ namespace Discord.Rest
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestTextChannel.Create(client, guild, model);
}
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
public static async Task<RestVoiceChannel> CreateVoiceChannelAsync(IGuild guild, BaseDiscordClient client,
string name, RequestOptions options)
{
@@ -157,6 +161,7 @@ namespace Discord.Rest
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestVoiceChannel.Create(client, guild, model);
}
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
public static async Task<RestCategoryChannel> CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client,
string name, RequestOptions options)
{
@@ -191,6 +196,7 @@ namespace Discord.Rest
}

//Roles
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
public static async Task<RestRole> CreateRoleAsync(IGuild guild, BaseDiscordClient client,
string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options)
{
@@ -292,11 +298,12 @@ namespace Discord.Rest
Image = image.ToModel()
};
if (roles.IsSpecified)
apiargs.RoleIds = roles.Value?.Select(xr => xr.Id)?.ToArray();
apiargs.RoleIds = roles.Value?.Select(xr => xr.Id).ToArray();

var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options).ConfigureAwait(false);
return emote.ToEntity();
}
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public static async Task<GuildEmote> ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action<EmoteProperties> func,
RequestOptions options)
{
@@ -310,7 +317,7 @@ namespace Discord.Rest
Name = props.Name
};
if (props.Roles.IsSpecified)
apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id)?.ToArray();
apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id).ToArray();

var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options).ConfigureAwait(false);
return emote.ToEntity();


+ 3
- 1
src/Discord.Net.Rest/Entities/Guilds/RestBan.cs View File

@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using Model = Discord.API.Ban;

namespace Discord.Rest
@@ -7,6 +7,7 @@ namespace Discord.Rest
public class RestBan : IBan
{
public RestUser User { get; }
/// <inheritdoc />
public string Reason { get; }

internal RestBan(RestUser user, string reason)
@@ -23,6 +24,7 @@ namespace Discord.Rest
private string DebuggerDisplay => $"{User}: {Reason}";

//IBan
/// <inheritdoc />
IUser IBan.User => User;
}
}

+ 82
- 4
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -10,6 +10,9 @@ using Model = Discord.API.Guild;

namespace Discord.Rest
{
/// <summary>
/// Represents a REST-based guild/server.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestGuild : RestEntity<ulong>, IGuild, IUpdateable
{
@@ -17,32 +20,50 @@ namespace Discord.Rest
private ImmutableArray<GuildEmote> _emotes;
private ImmutableArray<string> _features;

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public int AFKTimeout { get; private set; }
/// <inheritdoc />
public bool IsEmbeddable { get; private set; }
/// <inheritdoc />
public VerificationLevel VerificationLevel { get; private set; }
/// <inheritdoc />
public MfaLevel MfaLevel { get; private set; }
/// <inheritdoc />
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; }

/// <inheritdoc />
public ulong? AFKChannelId { get; private set; }
/// <inheritdoc />
public ulong? EmbedChannelId { get; private set; }
/// <inheritdoc />
public ulong? SystemChannelId { get; private set; }
/// <inheritdoc />
public ulong OwnerId { get; private set; }
/// <inheritdoc />
public string VoiceRegionId { get; private set; }
/// <inheritdoc />
public string IconId { get; private set; }
/// <inheritdoc />
public string SplashId { get; private set; }
internal bool Available { get; private set; }

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

[Obsolete("DefaultChannelId is deprecated, use GetDefaultChannelAsync")]
public ulong DefaultChannelId => Id;
/// <inheritdoc />
public string IconUrl => CDN.GetGuildIconUrl(Id, IconId);
/// <inheritdoc />
public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId);

public RestRole EveryoneRole => GetRole(Id);
public IReadOnlyCollection<RestRole> Roles => _roles.ToReadOnlyCollection();
/// <inheritdoc />
public IReadOnlyCollection<GuildEmote> Emotes => _emotes;
/// <inheritdoc />
public IReadOnlyCollection<string> Features => _features;

internal RestGuild(BaseDiscordClient client, ulong id)
@@ -103,37 +124,48 @@ namespace Discord.Rest
}

//General
/// <inheritdoc />
public async Task UpdateAsync(RequestOptions options = null)
=> Update(await Discord.ApiClient.GetGuildAsync(Id, options).ConfigureAwait(false));
/// <inheritdoc />
public Task DeleteAsync(RequestOptions options = null)
=> GuildHelper.DeleteAsync(this, Discord, options);

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public async Task ModifyAsync(Action<GuildProperties> func, RequestOptions options = null)
{
var model = await GuildHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public async Task ModifyEmbedAsync(Action<GuildEmbedProperties> func, RequestOptions options = null)
{
var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="args" /> is <see langword="null" />.</exception>
public async Task ReorderChannelsAsync(IEnumerable<ReorderChannelProperties> args, RequestOptions options = null)
{
var arr = args.ToArray();
await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task ReorderRolesAsync(IEnumerable<ReorderRoleProperties> args, RequestOptions options = null)
{
var models = await GuildHelper.ReorderRolesAsync(this, Discord, args, options).ConfigureAwait(false);
foreach (var model in models)
{
var role = GetRole(model.Id);
if (role != null)
role.Update(model);
role?.Update(model);
}
}

/// <inheritdoc />
public Task LeaveAsync(RequestOptions options = null)
=> GuildHelper.LeaveAsync(this, Discord, options);

@@ -141,13 +173,17 @@ namespace Discord.Rest
public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null)
=> GuildHelper.GetBansAsync(this, Discord, options);

/// <inheritdoc />
public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null)
=> GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options);
/// <inheritdoc />
public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null)
=> GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options);

/// <inheritdoc />
public Task RemoveBanAsync(IUser user, RequestOptions options = null)
=> GuildHelper.RemoveBanAsync(this, Discord, user.Id, options);
/// <inheritdoc />
public Task RemoveBanAsync(ulong userId, RequestOptions options = null)
=> GuildHelper.RemoveBanAsync(this, Discord, userId, options);

@@ -261,6 +297,7 @@ namespace Discord.Rest
public Task<RestGuildUser> GetOwnerAsync(RequestOptions options = null)
=> GuildHelper.GetUserAsync(this, Discord, OwnerId, options);

/// <inheritdoc />
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);

@@ -270,28 +307,45 @@ namespace Discord.Rest
public Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
=> GuildHelper.GetWebhooksAsync(this, Discord, options);

/// <summary>
/// Returns the name of the guild.
/// </summary>
/// <returns>
/// The name of the guild.
/// </returns>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";

//Emotes
/// <inheritdoc />
public Task<GuildEmote> GetEmoteAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetEmoteAsync(this, Discord, id, options);
/// <inheritdoc />
public Task<GuildEmote> CreateEmoteAsync(string name, Image image, Optional<IEnumerable<IRole>> roles = default(Optional<IEnumerable<IRole>>), RequestOptions options = null)
=> GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options);
/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null" />.</exception>
public Task<GuildEmote> ModifyEmoteAsync(GuildEmote emote, Action<EmoteProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options);
/// <inheritdoc />
public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null)
=> GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options);

//IGuild
/// <inheritdoc />
bool IGuild.Available => Available;
/// <inheritdoc />
IAudioClient IGuild.AudioClient => null;
/// <inheritdoc />
IRole IGuild.EveryoneRole => EveryoneRole;
/// <inheritdoc />
IReadOnlyCollection<IRole> IGuild.Roles => Roles;

/// <inheritdoc />
async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options)
=> await GetBansAsync(options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildChannel>> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -299,6 +353,7 @@ namespace Discord.Rest
else
return ImmutableArray.Create<IGuildChannel>();
}
/// <inheritdoc />
async Task<IGuildChannel> IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -306,6 +361,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IReadOnlyCollection<ITextChannel>> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -313,6 +369,7 @@ namespace Discord.Rest
else
return ImmutableArray.Create<ITextChannel>();
}
/// <inheritdoc />
async Task<ITextChannel> IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -320,6 +377,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IReadOnlyCollection<IVoiceChannel>> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -327,6 +385,7 @@ namespace Discord.Rest
else
return ImmutableArray.Create<IVoiceChannel>();
}
/// <inheritdoc />
async Task<IReadOnlyCollection<ICategoryChannel>> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -334,6 +393,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IVoiceChannel> IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -341,6 +401,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IVoiceChannel> IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -348,6 +409,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<ITextChannel> IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -355,6 +417,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IGuildChannel> IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -362,6 +425,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<ITextChannel> IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -369,26 +433,35 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<ITextChannel> IGuild.CreateTextChannelAsync(string name, RequestOptions options)
=> await CreateTextChannelAsync(name, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, RequestOptions options)
=> await CreateVoiceChannelAsync(name, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<ICategoryChannel> IGuild.CreateCategoryAsync(string name, RequestOptions options)
=> await CreateCategoryChannelAsync(name, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildIntegration>> IGuild.GetIntegrationsAsync(RequestOptions options)
=> await GetIntegrationsAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IGuildIntegration> IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options)
=> await CreateIntegrationAsync(id, type, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IInviteMetadata>> IGuild.GetInvitesAsync(RequestOptions options)
=> await GetInvitesAsync(options).ConfigureAwait(false);

/// <inheritdoc />
IRole IGuild.GetRole(ulong id)
=> GetRole(id);
/// <inheritdoc />
async Task<IRole> IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options)
=> await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -396,6 +469,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IGuildUser> IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -403,6 +477,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -410,6 +485,7 @@ namespace Discord.Rest
else
return null;
}
/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -418,12 +494,14 @@ namespace Discord.Rest
return ImmutableArray.Create<IGuildUser>();
}
/// <inheritdoc />
/// <exception cref="NotSupportedException">Downloading users is not supported with a REST-based guild.</exception>
/// <exception cref="NotSupportedException">Downloading users is not supported for a REST-based guild.</exception>
Task IGuild.DownloadUsersAsync() =>
throw new NotSupportedException();

/// <inheritdoc />
async Task<IWebhook> IGuild.GetWebhookAsync(ulong id, RequestOptions options)
=> await GetWebhookAsync(id, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options)
=> await GetWebhooksAsync(options).ConfigureAwait(false);
}


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

@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using Model = Discord.API.GuildEmbed;

namespace Discord
@@ -19,7 +19,7 @@ namespace Discord
return new RestGuildEmbed(model.Enabled, model.ChannelId);
}

public override string ToString() => ChannelId?.ToString();
public override string ToString() => ChannelId?.ToString() ?? "Unknown";
private string DebuggerDisplay => $"{ChannelId} ({(IsEnabled ? "Enabled" : "Disabled")})";
}
}

+ 13
- 1
src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Integration;
@@ -10,18 +10,28 @@ namespace Discord.Rest
{
private long _syncedAtTicks;

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public string Type { get; private set; }
/// <inheritdoc />
public bool IsEnabled { get; private set; }
/// <inheritdoc />
public bool IsSyncing { get; private set; }
/// <inheritdoc />
public ulong ExpireBehavior { get; private set; }
/// <inheritdoc />
public ulong ExpireGracePeriod { get; private set; }
/// <inheritdoc />
public ulong GuildId { get; private set; }
/// <inheritdoc />
public ulong RoleId { get; private set; }
public RestUser User { get; private set; }
/// <inheritdoc />
public IntegrationAccount Account { get; private set; }
internal IGuild Guild { get; private set; }

/// <inheritdoc />
public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks);

internal RestGuildIntegration(BaseDiscordClient discord, IGuild guild, ulong id)
@@ -78,6 +88,7 @@ namespace Discord.Rest
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})";

/// <inheritdoc />
IGuild IGuildIntegration.Guild
{
get
@@ -87,6 +98,7 @@ namespace Discord.Rest
throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object.");
}
}
/// <inheritdoc />
IUser IGuildIntegration.User => User;
}
}

+ 7
- 1
src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs View File

@@ -9,12 +9,17 @@ namespace Discord.Rest
public class RestUserGuild : RestEntity<ulong>, IUserGuild
{
private string _iconId;

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public bool IsOwner { get; private set; }
/// <inheritdoc />
public GuildPermissions Permissions { get; private set; }

/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc />
public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId);

internal RestUserGuild(BaseDiscordClient discord, ulong id)
@@ -40,6 +45,7 @@ namespace Discord.Rest
{
await Discord.ApiClient.LeaveGuildAsync(Id, options).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task DeleteAsync(RequestOptions options = null)
{
await Discord.ApiClient.DeleteGuildAsync(Id, options).ConfigureAwait(false);


+ 3
- 1
src/Discord.Net.Rest/Entities/Invites/RestInvite.cs View File

@@ -59,7 +59,8 @@ namespace Discord.Rest

public override string ToString() => Url;
private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})";

/// <inheritdoc />
IGuild IInvite.Guild
{
get
@@ -71,6 +72,7 @@ namespace Discord.Rest
throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object.");
}
}
/// <inheritdoc />
IChannel IInvite.Channel
{
get


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

@@ -10,11 +10,13 @@ namespace Discord.Rest
{
internal static class MessageHelper
{
/// <exception cref="InvalidOperationException">Only the author of a message may modify the message.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<Model> ModifyAsync(IMessage msg, BaseDiscordClient client, Action<MessageProperties> func,
RequestOptions options)
{
if (msg.Author.Id != client.CurrentUser.Id)
throw new InvalidOperationException("Only the author of a message may change it.");
throw new InvalidOperationException("Only the author of a message may modify the message.");

var args = new MessageProperties();
func(args);


+ 5
- 0
src/Discord.Net.Rest/Entities/Messages/RestMessage.cs View File

@@ -35,6 +35,7 @@ namespace Discord.Rest
/// <inheritdoc />
public virtual IReadOnlyCollection<ulong> MentionedRoleIds => ImmutableArray.Create<ulong>();
public virtual IReadOnlyCollection<RestUser> MentionedUsers => ImmutableArray.Create<RestUser>();
/// <inheritdoc />
public virtual IReadOnlyCollection<ITag> Tags => ImmutableArray.Create<ITag>();

/// <inheritdoc />
@@ -75,10 +76,14 @@ namespace Discord.Rest

public override string ToString() => Content;

/// <inheritdoc />
MessageType IMessage.Type => MessageType.Default;
IUser IMessage.Author => Author;
/// <inheritdoc />
IReadOnlyCollection<IAttachment> IMessage.Attachments => Attachments;
/// <inheritdoc />
IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds;
/// <inheritdoc />
IReadOnlyCollection<ulong> IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray();
}
}

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

@@ -17,23 +17,33 @@ namespace Discord.Rest
private ImmutableArray<Embed> _embeds;
private ImmutableArray<ITag> _tags;
private ImmutableArray<RestReaction> _reactions;

/// <inheritdoc />
public override bool IsTTS => _isTTS;
/// <inheritdoc />
public override bool IsPinned => _isPinned;
/// <inheritdoc />
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks);
/// <inheritdoc />
public override IReadOnlyCollection<Attachment> Attachments => _attachments;
/// <inheritdoc />
public override IReadOnlyCollection<Embed> Embeds => _embeds;
/// <inheritdoc />
public override IReadOnlyCollection<ulong> MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags);
/// <inheritdoc />
public override IReadOnlyCollection<ulong> MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags);
/// <inheritdoc />
public override IReadOnlyCollection<RestUser> MentionedUsers => MessageHelper.FilterTagsByValue<RestUser>(TagType.UserMention, _tags);
/// <inheritdoc />
public override IReadOnlyCollection<ITag> Tags => _tags;
/// <inheritdoc />
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me });

internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source)
: base(discord, id, channel, author, source)
{
}
internal static new RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model)
internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model)
{
var entity = new RestUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model));
entity.Update(model);
@@ -124,30 +134,37 @@ namespace Discord.Rest
}
}

/// <inheritdoc />
public async Task ModifyAsync(Action<MessageProperties> func, RequestOptions options = null)
{
var model = await MessageHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
public Task AddReactionAsync(IEmote emote, RequestOptions options = null)
=> MessageHelper.AddReactionAsync(this, emote, Discord, options);
/// <inheritdoc />
public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null)
=> MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options);
/// <inheritdoc />
public Task RemoveAllReactionsAsync(RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsAsync(this, Discord, options);
/// <inheritdoc />
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);
/// <inheritdoc />
public Task PinAsync(RequestOptions options = null)
=> MessageHelper.PinAsync(this, Discord, options);
/// <inheritdoc />
public Task UnpinAsync(RequestOptions options = null)
=> MessageHelper.UnpinAsync(this, Discord, options);

public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
=> MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
/// <inheritdoc />
public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
=> MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);


+ 8
- 0
src/Discord.Net.Rest/Entities/RestApplication.cs View File

@@ -19,6 +19,7 @@ namespace Discord.Rest
public string Description { get; private set; }
/// <inheritdoc />
public string[] RPCOrigins { get; private set; }
/// <inheritdoc />
public ulong Flags { get; private set; }

/// <inheritdoc />
@@ -52,6 +53,7 @@ namespace Discord.Rest
Owner = RestUser.Create(Discord, model.Owner.Value);
}

/// <exception cref="InvalidOperationException">Unable to update this object from a different application token.</exception>
public async Task UpdateAsync()
{
var response = await Discord.ApiClient.GetMyApplicationAsync().ConfigureAwait(false);
@@ -60,6 +62,12 @@ namespace Discord.Rest
Update(response);
}

/// <summary>
/// Gets the name of the application.
/// </summary>
/// <returns>
/// Name of the application.
/// </returns>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";
}


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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Role;
@@ -9,16 +9,25 @@ namespace Discord.Rest
public class RestRole : RestEntity<ulong>, IRole
{
internal IGuild Guild { get; }
/// <inheritdoc />
public Color Color { get; private set; }
/// <inheritdoc />
public bool IsHoisted { get; private set; }
/// <inheritdoc />
public bool IsManaged { get; private set; }
/// <inheritdoc />
public bool IsMentionable { get; private set; }
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public GuildPermissions Permissions { get; private set; }
/// <inheritdoc />
public int Position { get; private set; }

/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
public bool IsEveryone => Id == Guild.Id;
/// <inheritdoc />
public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id);

internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id)
@@ -43,20 +52,24 @@ namespace Discord.Rest
Permissions = new GuildPermissions(model.Permissions);
}

/// <inheritdoc />
public async Task ModifyAsync(Action<RoleProperties> func, RequestOptions options = null)
{
var model = await RoleHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false);
Update(model);
}
/// <inheritdoc />
public Task DeleteAsync(RequestOptions options = null)
=> RoleHelper.DeleteAsync(this, Discord, options);

/// <inheritdoc />
public int CompareTo(IRole role) => RoleUtils.Compare(this, role);

public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";

//IRole
/// <inheritdoc />
IGuild IRole.Guild
{
get


+ 12
- 1
src/Discord.Net.Rest/Entities/Users/RestConnection.cs View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Model = Discord.API.Connection;
@@ -8,10 +8,15 @@ namespace Discord
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestConnection : IConnection
{
/// <inheritdoc />
public string Id { get; }
/// <inheritdoc />
public string Type { get; }
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public bool IsRevoked { get; }
/// <inheritdoc />
public IReadOnlyCollection<ulong> IntegrationIds { get; }

internal RestConnection(string id, string type, string name, bool isRevoked, IReadOnlyCollection<ulong> integrationIds)
@@ -28,6 +33,12 @@ namespace Discord
return new RestConnection(model.Id, model.Type, model.Name, model.Revoked, model.Integrations.ToImmutableArray());
}

/// <summary>
/// Gets the name of the connection.
/// </summary>
/// <returns>
/// Name of the connection.
/// </returns>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked ? ", Revoked" : "")})";
}


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

@@ -24,7 +24,9 @@ namespace Discord.Rest

/// <inheritdoc />
public ulong GuildId => Guild.Id;

/// <inheritdoc />
/// <exception cref="InvalidOperationException" accessor="get">Resolving permissions requires the parent guild to be downloaded.</exception>
public GuildPermissions GuildPermissions
{
get
@@ -112,6 +114,7 @@ namespace Discord.Rest
=> UserHelper.RemoveRolesAsync(this, Discord, roles, options);

/// <inheritdoc />
/// <exception cref="InvalidOperationException">Resolving permissions requires the parent guild to be downloaded.</exception>
public ChannelPermissions GetPermissions(IGuildChannel channel)
{
var guildPerms = GuildPermissions;
@@ -119,6 +122,7 @@ namespace Discord.Rest
}

//IGuildUser
/// <inheritdoc />
IGuild IGuildUser.Guild
{
get
@@ -130,10 +134,15 @@ namespace Discord.Rest
}

//IVoiceState
/// <inheritdoc />
bool IVoiceState.IsSelfDeafened => false;
/// <inheritdoc />
bool IVoiceState.IsSelfMuted => false;
/// <inheritdoc />
bool IVoiceState.IsSuppressed => false;
/// <inheritdoc />
IVoiceChannel IVoiceState.VoiceChannel => null;
/// <inheritdoc />
string IVoiceState.VoiceSessionId => null;
}
}

+ 9
- 1
src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.User;
@@ -8,8 +8,11 @@ namespace Discord.Rest
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestSelfUser : RestUser, ISelfUser
{
/// <inheritdoc />
public string Email { get; private set; }
/// <inheritdoc />
public bool IsVerified { get; private set; }
/// <inheritdoc />
public bool IsMfaEnabled { get; private set; }

internal RestSelfUser(BaseDiscordClient discord, ulong id)
@@ -22,6 +25,7 @@ namespace Discord.Rest
entity.Update(model);
return entity;
}
/// <inheritdoc />
internal override void Update(Model model)
{
base.Update(model);
@@ -34,6 +38,8 @@ namespace Discord.Rest
IsMfaEnabled = model.MfaEnabled.Value;
}

/// <inheritdoc />
/// <exception cref="InvalidOperationException">Unable to update this object using a different token.</exception>
public override async Task UpdateAsync(RequestOptions options = null)
{
var model = await Discord.ApiClient.GetMyUserAsync(options).ConfigureAwait(false);
@@ -42,6 +48,8 @@ namespace Discord.Rest
Update(model);
}

/// <inheritdoc />
/// <exception cref="InvalidOperationException">Unable to modify this object using a different token.</exception>
public async Task ModifyAsync(Action<SelfUserProperties> func, RequestOptions options = null)
{
if (Id != Discord.CurrentUser.Id)


+ 14
- 0
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -8,16 +8,26 @@ namespace Discord.Rest
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestUser : RestEntity<ulong>, IUser, IUpdateable
{
/// <inheritdoc />
public bool IsBot { get; private set; }
/// <inheritdoc />
public string Username { get; private set; }
/// <inheritdoc />
public ushort DiscriminatorValue { get; private set; }
/// <inheritdoc />
public string AvatarId { get; private set; }

/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc />
public string Discriminator => DiscriminatorValue.ToString("D4");
/// <inheritdoc />
public string Mention => MentionUtils.MentionUser(Id);
/// <inheritdoc />
public virtual IActivity Activity => null;
/// <inheritdoc />
public virtual UserStatus Status => UserStatus.Offline;
/// <inheritdoc />
public virtual bool IsWebhook => false;

internal RestUser(BaseDiscordClient discord, ulong id)
@@ -48,6 +58,7 @@ namespace Discord.Rest
Username = model.Username.Value;
}

/// <inheritdoc />
public virtual async Task UpdateAsync(RequestOptions options = null)
{
var model = await Discord.ApiClient.GetUserAsync(Id, options).ConfigureAwait(false);
@@ -57,9 +68,11 @@ namespace Discord.Rest
public Task<RestDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null)
=> UserHelper.CreateDMChannelAsync(this, Discord, options);

/// <inheritdoc />
public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
=> CDN.GetUserAvatarUrl(Id, AvatarId, size, format);

/// <inheritdoc />
public string GetDefaultAvatarUrl()
=> CDN.GetDefaultUserAvatarUrl(DiscriminatorValue);

@@ -67,6 +80,7 @@ namespace Discord.Rest
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})";

//IUser
/// <inheritdoc />
async Task<IDMChannel> IUser.GetOrCreateDMChannelAsync(RequestOptions options)
=> await GetOrCreateDMChannelAsync(options).ConfigureAwait(false);
}


+ 14
- 1
src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Webhook;
@@ -11,14 +11,21 @@ namespace Discord.Rest
internal IGuild Guild { get; private set; }
internal ITextChannel Channel { get; private set; }

/// <inheritdoc />
public ulong ChannelId { get; }
/// <inheritdoc />
public string Token { get; }

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public string AvatarId { get; private set; }
/// <inheritdoc />
public ulong? GuildId { get; private set; }
/// <inheritdoc />
public IUser Creator { get; private set; }

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

internal RestWebhook(BaseDiscordClient discord, IGuild guild, ulong id, string token, ulong channelId)
@@ -59,12 +66,14 @@ namespace Discord.Rest
Name = model.Name.Value;
}

/// <inheritdoc />
public async Task UpdateAsync(RequestOptions options = null)
{
var model = await Discord.ApiClient.GetWebhookAsync(Id, options).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
=> CDN.GetUserAvatarUrl(Id, AvatarId, size, format);

@@ -74,6 +83,7 @@ namespace Discord.Rest
Update(model);
}

/// <inheritdoc />
public Task DeleteAsync(RequestOptions options = null)
=> WebhookHelper.DeleteAsync(this, Discord, options);

@@ -81,10 +91,13 @@ namespace Discord.Rest
private string DebuggerDisplay => $"Webhook: {Name} ({Id})";

//IWebhook
/// <inheritdoc />
IGuild IWebhook.Guild
=> Guild ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object.");
/// <inheritdoc />
ITextChannel IWebhook.Channel
=> Channel ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object.");
/// <inheritdoc />
Task IWebhook.ModifyAsync(Action<WebhookProperties> func, RequestOptions options)
=> ModifyAsync(func, options);
}


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

@@ -82,6 +82,8 @@ namespace Discord.Net.Rest
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}

/// <exception cref="InvalidOperationException">Unsupported param type.</exception>
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
@@ -111,7 +113,7 @@ namespace Discord.Net.Rest
content.Add(new StreamContent(stream), p.Key, fileValue.Filename);
continue;
}
default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"");
default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\".");
}
}
}


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

@@ -6,6 +6,7 @@ namespace Discord.Net.Rest
{
public static readonly RestClientProvider Instance = Create();

/// <exception cref="PlatformNotSupportedException">The default RestClientProvider is not supported on this platform.</exception>
public static RestClientProvider Create(bool useProxy = false)
{
return url =>


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

@@ -126,7 +126,7 @@ namespace Discord.Net.Queue
if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0)
throw;

await Task.Delay(500);
await Task.Delay(500).ConfigureAwait(false);
continue; //Retry
}
/*catch (Exception)


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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;

namespace Discord.Net
@@ -15,7 +15,7 @@ namespace Discord.Net
internal RateLimitInfo(Dictionary<string, string> headers)
{
IsGlobal = headers.TryGetValue("X-RateLimit-Global", out string temp) &&
bool.TryParse(temp, out var isGlobal) ? isGlobal : false;
bool.TryParse(temp, out var isGlobal) && isGlobal;
Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) &&
int.TryParse(temp, out var limit) ? limit : (int?)null;
Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) &&


+ 6
- 3
src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using System.Threading.Tasks;

@@ -22,19 +22,22 @@ namespace Discord.Audio.Streams
_decoder = new OpusDecoder();
}

/// <exception cref="InvalidOperationException">Header received with no payload.</exception>
public override void WriteHeader(ushort seq, uint timestamp, bool missed)
{
if (_hasHeader)
throw new InvalidOperationException("Header received with no payload");
throw new InvalidOperationException("Header received with no payload.");
_hasHeader = true;

_nextMissed = missed;
_next.WriteHeader(seq, timestamp, missed);
}

/// <exception cref="InvalidOperationException">Received payload without an RTP header.</exception>
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{
if (!_hasHeader)
throw new InvalidOperationException("Received payload without an RTP header");
throw new InvalidOperationException("Received payload without an RTP header.");
_hasHeader = false;

if (!_nextMissed)


+ 4
- 1
src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs View File

@@ -1,4 +1,5 @@
using System.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio.Streams
@@ -20,6 +21,8 @@ namespace Discord.Audio.Streams
_nonce = new byte[24];
}

/// <exception cref="OperationCanceledException">The token has had cancellation requested.</exception>
/// <exception cref="ObjectDisposedException">The associated <see cref="T:System.Threading.CancellationTokenSource" /> has been disposed.</exception>
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{
cancelToken.ThrowIfCancellationRequested();


+ 7
- 3
src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs View File

@@ -20,21 +20,25 @@ namespace Discord.Audio.Streams
_client = (AudioClient)client;
_nonce = new byte[24];
}

/// <exception cref="InvalidOperationException">Header received with no payload.</exception>
public override void WriteHeader(ushort seq, uint timestamp, bool missed)
{
if (_hasHeader)
throw new InvalidOperationException("Header received with no payload");
throw new InvalidOperationException("Header received with no payload.");

_nextSeq = seq;
_nextTimestamp = timestamp;
_hasHeader = true;
}
/// <exception cref="InvalidOperationException">Received payload without an RTP header.</exception>
/// <exception cref="OperationCanceledException">The token has had cancellation requested.</exception>
/// <exception cref="ObjectDisposedException">The associated <see cref="T:System.Threading.CancellationTokenSource" /> has been disposed.</exception>
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{
cancelToken.ThrowIfCancellationRequested();
if (!_hasHeader)
throw new InvalidOperationException("Received payload without an RTP header");
throw new InvalidOperationException("Received payload without an RTP header.");
_hasHeader = false;

if (_client.SecretKey == null)


+ 1
- 1
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;

namespace Discord.WebSocket


+ 137
- 5
src/Discord.Net.WebSocket/BaseSocketClient.cs View File

@@ -10,42 +10,174 @@ namespace Discord.WebSocket
{
protected readonly DiscordSocketConfig BaseConfig;

/// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary>
/// <summary>
/// Gets the estimated round-trip latency, in milliseconds, to the gateway server.
/// </summary>
public abstract int Latency { get; protected set; }
public abstract UserStatus Status { get; protected set; }
/// <summary>
/// Gets the status for the logged-in user.
/// </summary>
public abstract UserStatus Status { get; protected set; }
/// <summary>
/// Gets the activity for the logged-in user.
/// </summary>
public abstract IActivity Activity { get; protected set; }

internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;

/// <summary>
/// Gets the current logged-in user.
/// </summary>
public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; }
/// <summary>
/// Gets a collection of guilds that the logged-in user is currently in.
/// </summary>
public abstract IReadOnlyCollection<SocketGuild> Guilds { get; }
/// <summary>
/// Gets a collection of private channels that are currently open for the logged-in user.
/// </summary>
public abstract IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels { get; }
/// <summary>
/// Gets a collection of available voice regions for the logged-in user.
/// </summary>
public abstract IReadOnlyCollection<RestVoiceRegion> VoiceRegions { get; }

internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client)
: base(config, client) => BaseConfig = config;
private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent);

/// <summary>
/// Gets a Discord application information for the logged-in user.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// Application information. This reflects your application information you submitted when creating a
/// Discord application via the Developer Portal.
/// </returns>
public abstract Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null);
/// <summary>
/// Gets a user who shares a mutual guild with logged-in user with the provided snowflake ID.
/// </summary>
/// <param name="id">The user snowflake ID.</param>
/// <returns>
/// A user who shares a mutual guild with the logged-in user and who is also present in the WebSocket cache;
/// or <see langword="null"/> when the user cannot be found.
/// </returns>
public abstract SocketUser GetUser(ulong id);

/// <summary>
/// Gets a user who shares a mutual guild with the logged-in user with the provided username and discriminator value combo.
/// </summary>
/// <param name="username">The name of the user.</param>
/// <param name="discriminator">The discriminator value of the user.</param>
/// <returns>
/// A user who shares a mutual guild with the logged-in user and who is also present in the WebSocket cache;
/// or <see langword="null"/> when the user cannot be found.
/// </returns>
public abstract SocketUser GetUser(string username, string discriminator);
/// <summary>
/// Gets a channel that the logged-in user is accessible to with the provided ID.
/// </summary>
/// <param name="id">The channel snowflake ID.</param>
/// <returns>
/// A generic channel object (voice, text, category, etc.); or <see langword="null" /> when the channel
/// cannot be found.
/// </returns>
public abstract SocketChannel GetChannel(ulong id);
/// <summary>
/// Gets a guild that the logged-in user is accessible to with the provided ID.
/// </summary>
/// <param name="id">The guild snowflake ID.</param>
/// <returns>
/// A guild; or <see langword="null"/> when the guild cannot be found.
/// </returns>
public abstract SocketGuild GetGuild(ulong id);
/// <summary>
/// Gets a voice region with the provided ID.
/// </summary>
/// <param name="id">The unique identifier of the voice region.</param>
/// <returns>
/// A voice region; or <see langword="null"/> if none can be found.
/// </returns>
public abstract RestVoiceRegion GetVoiceRegion(string id);
/// <inheritdoc />
public abstract Task StartAsync();
/// <inheritdoc />
public abstract Task StopAsync();
/// <summary>
/// Sets the current status of the logged-in user (e.g. Online, Do not Disturb).
/// </summary>
/// <param name="status">The new status to be set.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
public abstract Task SetStatusAsync(UserStatus status);
/// <summary>
/// Sets the game of the logged-in user.
/// </summary>
/// <param name="name">The name of the game.</param>
/// <param name="streamUrl">If streaming, the URL of the stream. Must be a valid Twitch URL.</param>
/// <param name="type">The type of the game.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing);
/// <summary>
/// Sets the <paramref name="activity"/> of the logged-in user.
/// </summary>
/// <remarks>
/// This method sets the <paramref name="activity"/> of the user. Please note that Rich Presence cannot be
/// set via this method or client. Rich Presence is strictly limited to RPC clients only. Furthermore,
/// Discord will only accept setting of name and the type of activity.
/// </remarks>
/// <param name="activity">The activty to be set.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
public abstract Task SetActivityAsync(IActivity activity);
public abstract Task DownloadUsersAsync(IEnumerable<IGuild> guilds);
/// <summary>
/// Attempts to download users into the user cache for the selected guilds.
/// </summary>
/// <param name="guilds">The guilds to download the members from.</param>
/// <returns>
/// An awaitable <see cref="Task"/>.
/// </returns>
public abstract Task DownloadUsersAsync(IEnumerable<IGuild> guilds);

/// <summary>
/// Creates a guild for the logged-in user who is in less than 10 active guilds.
/// </summary>
/// <remarks>
/// This method creates a new guild on behalf of the logged-in user. Note that due to Discord's limitation,
/// this method will only work for users that are in less than 10 guilds.
/// </remarks>
/// <param name="name">The name of the new guild.</param>
/// <param name="region">The voice region to create the guild with.</param>
/// <param name="jpegIcon">The icon of the guild.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the newly created guild.
/// </returns>
public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null)
=> ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options ?? RequestOptions.Default);
/// <summary>
/// Gets the connections that the logged-in user has set up.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing a collection of connections.
/// </returns>
public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null)
=> ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default);
/// <summary>
/// Gets an invite with the provided invite identifier.
/// </summary>
/// <param name="inviteId">The invitation identifier.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the invite information.
/// </returns>
public Task<RestInvite> GetInviteAsync(string inviteId, RequestOptions options = null)
=> ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default);


+ 3
- 3
src/Discord.Net.WebSocket/ClientState.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -72,9 +72,9 @@ namespace Discord.WebSocket
switch (channel)
{
case SocketDMChannel dmChannel:
_dmChannels.TryRemove(dmChannel.Recipient.Id, out var ignored);
_dmChannels.TryRemove(dmChannel.Recipient.Id, out var _);
break;
case SocketGroupChannel groupChannel:
case SocketGroupChannel _:
_groupChannels.TryRemove(id);
break;
}


+ 3
- 1
src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs View File

@@ -2,7 +2,9 @@ using Discord.WebSocket;

namespace Discord.Commands
{
/// <summary> The WebSocket variant of <see cref="ICommandContext"/>, which may contain the client, user, guild, channel, and message. </summary>
/// <summary>
/// Represents a WebSocket-based context of a command. This may include the client, guild, channel, user, and message.
/// </summary>
public class SocketCommandContext : ICommandContext
{
/// <summary>


+ 6
- 3
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -120,12 +120,14 @@ namespace Discord.API
}
finally { _stateLock.Release(); }
}
/// <exception cref="InvalidOperationException">The client must be logged in before connecting.</exception>
/// <exception cref="NotSupportedException">This client is not configured with WebSocket support.</exception>
internal override async Task ConnectInternalAsync()
{
if (LoginState != LoginState.LoggedIn)
throw new InvalidOperationException("You must log in before connecting.");
throw new InvalidOperationException("The client must be logged in before connecting.");
if (WebSocketClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");
throw new NotSupportedException("This client is not configured with WebSocket support.");

//Re-create streams to reset the zlib state
_compressed?.Dispose();
@@ -176,10 +178,11 @@ namespace Discord.API
}
finally { _stateLock.Release(); }
}
/// <exception cref="NotSupportedException">This client is not configured with WebSocket support.</exception>
internal override async Task DisconnectInternalAsync()
{
if (WebSocketClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");
throw new NotSupportedException("This client is not configured with WebSocket support.");

if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;


+ 37
- 7
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -46,7 +46,9 @@ namespace Discord.WebSocket
public ConnectionState ConnectionState => _connection.State;
/// <inheritdoc />
public override int Latency { get; protected set; }
/// <inheritdoc />
public override UserStatus Status { get; protected set; } = UserStatus.Online;
/// <inheritdoc />
public override IActivity Activity { get; protected set; }

//From DiscordSocketConfig
@@ -58,14 +60,17 @@ namespace Discord.WebSocket
internal WebSocketProvider WebSocketProvider { get; private set; }
internal bool AlwaysDownloadUsers { get; private set; }
internal int? HandlerTimeout { get; private set; }
internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;
/// <inheritdoc />
public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds;
/// <inheritdoc />
public override IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels => State.PrivateChannels;
public IReadOnlyCollection<SocketDMChannel> DMChannels
=> State.PrivateChannels.Select(x => x as SocketDMChannel).Where(x => x != null).ToImmutableArray();
public IReadOnlyCollection<SocketGroupChannel> GroupChannels
=> State.PrivateChannels.Select(x => x as SocketGroupChannel).Where(x => x != null).ToImmutableArray();
/// <inheritdoc />
public override IReadOnlyCollection<RestVoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection();

/// <summary> Creates a new REST/WebSocket Discord client. </summary>
@@ -128,6 +133,7 @@ namespace Discord.WebSocket
}
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost);
/// <inheritdoc />
internal override void Dispose(bool disposing)
{
if (disposing)
@@ -136,7 +142,8 @@ namespace Discord.WebSocket
ApiClient.Dispose();
}
}

/// <inheritdoc />
internal override async Task OnLoginAsync(TokenType tokenType, string token)
{
if (_parentClient == null)
@@ -147,6 +154,7 @@ namespace Discord.WebSocket
else
_voiceRegions = _parentClient._voiceRegions;
}
/// <inheritdoc />
internal override async Task OnLogoutAsync()
{
await StopAsync().ConfigureAwait(false);
@@ -154,8 +162,10 @@ namespace Discord.WebSocket
_voiceRegions = ImmutableDictionary.Create<string, RestVoiceRegion>();
}

/// <inheritdoc />
public override async Task StartAsync()
=> await _connection.StartAsync().ConfigureAwait(false);
/// <inheritdoc />
public override async Task StopAsync()
=> await _connection.StopAsync().ConfigureAwait(false);
@@ -277,7 +287,7 @@ namespace Discord.WebSocket
return null;
}

/// <summary> Downloads the users list for the provided guilds, if they don't have a complete list. </summary>
/// <inheritdoc />
public override async Task DownloadUsersAsync(IEnumerable<IGuild> guilds)
{
if (ConnectionState == ConnectionState.Connected)
@@ -316,6 +326,7 @@ namespace Discord.WebSocket
}
}

/// <inheritdoc />
public override async Task SetStatusAsync(UserStatus status)
{
Status = status;
@@ -325,6 +336,7 @@ namespace Discord.WebSocket
_statusSince = null;
await SendStatusAsync().ConfigureAwait(false);
}
/// <inheritdoc />
public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing)
{
if (!string.IsNullOrEmpty(streamUrl))
@@ -335,6 +347,7 @@ namespace Discord.WebSocket
Activity = null;
await SendStatusAsync().ConfigureAwait(false);
}
/// <inheritdoc />
public override async Task SetActivityAsync(IActivity activity)
{
Activity = activity;
@@ -351,8 +364,8 @@ namespace Discord.WebSocket

var gameModel = new GameModel();
// Discord only accepts rich presence over RPC, don't even bother building a payload
if (Activity is RichGame game)
throw new NotSupportedException("Outgoing Rich Presences are not supported");
if (Activity is RichGame)
throw new NotSupportedException("Outgoing Rich Presences are not supported via WebSocket.");

if (Activity != null)
{
@@ -479,7 +492,7 @@ namespace Discord.WebSocket
await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false);
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false);
});
var _ = _connection.CompleteAsync();
_ = _connection.CompleteAsync();
}
break;
case "RESUMED":
@@ -1173,7 +1186,7 @@ namespace Discord.WebSocket

var msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id);
bool isCached = msg != null;
var cacheable = new Cacheable<IMessage, ulong>(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id));
var cacheable = new Cacheable<IMessage, ulong>(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false));

await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false);
}
@@ -1609,6 +1622,7 @@ namespace Discord.WebSocket
return guild;
}

/// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception>
internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state)
{
var channel = SocketChannel.CreatePrivate(this, state, model);
@@ -1781,43 +1795,59 @@ namespace Discord.WebSocket
internal int GetAudioId() => _nextAudioId++;

//IDiscordClient
/// <inheritdoc />
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> await GetApplicationInfoAsync().ConfigureAwait(false);

/// <inheritdoc />
Task<IChannel> IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IChannel>(GetChannel(id));
/// <inheritdoc />
Task<IReadOnlyCollection<IPrivateChannel>> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IPrivateChannel>>(PrivateChannels);
/// <inheritdoc />
Task<IReadOnlyCollection<IDMChannel>> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IDMChannel>>(DMChannels);
/// <inheritdoc />
Task<IReadOnlyCollection<IGroupChannel>> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IGroupChannel>>(GroupChannels);

/// <inheritdoc />
async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options)
=> await GetConnectionsAsync().ConfigureAwait(false);

/// <inheritdoc />
async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options)
=> await GetInviteAsync(inviteId).ConfigureAwait(false);

/// <inheritdoc />
Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuild>(GetGuild(id));
/// <inheritdoc />
Task<IReadOnlyCollection<IGuild>> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IGuild>>(Guilds);
/// <inheritdoc />
async Task<IGuild> IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options)
=> await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false);

/// <inheritdoc />
Task<IUser> IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(GetUser(id));
/// <inheritdoc />
Task<IUser> IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options)
=> Task.FromResult<IUser>(GetUser(username, discriminator));

/// <inheritdoc />
Task<IReadOnlyCollection<IVoiceRegion>> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IVoiceRegion>>(VoiceRegions);
/// <inheritdoc />
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options)
=> Task.FromResult<IVoiceRegion>(GetVoiceRegion(id));

/// <inheritdoc />
async Task IDiscordClient.StartAsync()
=> await StartAsync().ConfigureAwait(false);
/// <inheritdoc />
async Task IDiscordClient.StopAsync()
=> await StopAsync().ConfigureAwait(false);
}


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

@@ -58,6 +58,7 @@ namespace Discord.WebSocket
internal abstract SocketUser GetUserInternal(ulong id);
internal abstract IReadOnlyCollection<SocketUser> GetUsersInternal();

private string DebuggerDisplay => $"Unknown ({Id}, Channel)";
internal SocketChannel Clone() => MemberwiseClone() as SocketChannel;

//IChannel


+ 5
- 4
src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -57,7 +57,7 @@ namespace Discord.WebSocket
else
return ImmutableArray.Create<SocketMessage>();
}
/// <exception cref="NotSupportedException">Unexpected <see cref="ISocketMessageChannel"/> type.</exception>
public static void AddMessage(ISocketMessageChannel channel, DiscordSocketClient discord,
SocketMessage msg)
{
@@ -66,9 +66,10 @@ namespace Discord.WebSocket
case SocketDMChannel dmChannel: dmChannel.AddMessage(msg); break;
case SocketGroupChannel groupChannel: groupChannel.AddMessage(msg); break;
case SocketTextChannel textChannel: textChannel.AddMessage(msg); break;
default: throw new NotSupportedException("Unexpected ISocketMessageChannel type");
default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type.");
}
}
/// <exception cref="NotSupportedException">Unexpected <see cref="ISocketMessageChannel"/> type.</exception>
public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord,
ulong id)
{
@@ -77,7 +78,7 @@ namespace Discord.WebSocket
case SocketDMChannel dmChannel: return dmChannel.RemoveMessage(id);
case SocketGroupChannel groupChannel: return groupChannel.RemoveMessage(id);
case SocketTextChannel textChannel: return textChannel.RemoveMessage(id);
default: throw new NotSupportedException("Unexpected ISocketMessageChannel type");
default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type.");
}
}
}


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

@@ -111,7 +111,6 @@ namespace Discord.WebSocket
/// <inheritdoc />
public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
/// <inheritdoc />
public IDisposable EnterTypingState(RequestOptions options = null)
=> ChannelHelper.EnterTypingState(this, Discord, options);



+ 14
- 2
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -357,9 +357,12 @@ namespace Discord.WebSocket
=> GuildHelper.DeleteAsync(this, Discord, options);

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public Task ModifyAsync(Action<GuildProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyAsync(this, Discord, func, options);

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public Task ModifyEmbedAsync(Action<GuildEmbedProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyEmbedAsync(this, Discord, func, options);
/// <inheritdoc />
@@ -431,31 +434,37 @@ namespace Discord.WebSocket
/// </returns>
public SocketVoiceChannel GetVoiceChannel(ulong id)
=> GetChannel(id) as SocketVoiceChannel;

/// <summary>
/// Creates a text channel with the provided name.
/// </summary>
/// <param name="name">The name of the new channel.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <see langword="null" />.</exception>
/// <returns>
/// The created text channel.
/// </returns>
public Task<RestTextChannel> CreateTextChannelAsync(string name, RequestOptions options = null)
=> GuildHelper.CreateTextChannelAsync(this, Discord, name, options);

/// <summary>
/// Creates a voice channel with the provided name.
/// </summary>
/// <param name="name">The name of the new channel.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <see langword="null" />.</exception>
/// <returns>
/// The created voice channel.
/// </returns>
public Task<RestVoiceChannel> CreateVoiceChannelAsync(string name, RequestOptions options = null)
=> GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options);

/// <summary>
/// Creates a category channel with the provided name.
/// </summary>
/// <param name="name">The name of the new channel.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <see langword="null" />.</exception>
/// <returns>
/// The created category channel.
/// </returns>
@@ -507,6 +516,7 @@ namespace Discord.WebSocket
return value;
return null;
}

/// <summary>
/// Creates a role.
/// </summary>
@@ -517,6 +527,7 @@ namespace Discord.WebSocket
/// <param name="color">The color of the role. Set to <see langword="null" /> to use the default color.</param>
/// <param name="isHoisted">Used to determine if users of this role are separated in the user list.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <see langword="null" />.</exception>
/// <returns>
/// The created role.
/// </returns>
@@ -645,6 +656,7 @@ namespace Discord.WebSocket
public Task<GuildEmote> CreateEmoteAsync(string name, Image image, Optional<IEnumerable<IRole>> roles = default(Optional<IEnumerable<IRole>>), RequestOptions options = null)
=> GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options);
/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public Task<GuildEmote> ModifyEmoteAsync(GuildEmote emote, Action<EmoteProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options);
/// <inheritdoc />
@@ -931,9 +943,9 @@ namespace Discord.WebSocket

/// <inheritdoc />
async Task<IWebhook> IGuild.GetWebhookAsync(ulong id, RequestOptions options)
=> await GetWebhookAsync(id, options);
=> await GetWebhookAsync(id, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options)
=> await GetWebhooksAsync(options);
=> await GetWebhooksAsync(options).ConfigureAwait(false);
}
}

+ 21
- 1
src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -8,27 +8,38 @@ using Model = Discord.API.Message;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based message.
/// </summary>
public abstract class SocketMessage : SocketEntity<ulong>, IMessage
{
private long _timestampTicks;
public SocketUser Author { get; }
public ISocketMessageChannel Channel { get; }
/// <inheritdoc />
public MessageSource Source { get; }

/// <inheritdoc />
public string Content { get; private set; }

/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc />
public virtual bool IsTTS => false;
/// <inheritdoc />
public virtual bool IsPinned => false;
/// <inheritdoc />
public virtual DateTimeOffset? EditedTimestamp => null;
public virtual IReadOnlyCollection<Attachment> Attachments => ImmutableArray.Create<Attachment>();
public virtual IReadOnlyCollection<Embed> Embeds => ImmutableArray.Create<Embed>();
public virtual IReadOnlyCollection<SocketGuildChannel> MentionedChannels => ImmutableArray.Create<SocketGuildChannel>();
public virtual IReadOnlyCollection<SocketRole> MentionedRoles => ImmutableArray.Create<SocketRole>();
public virtual IReadOnlyCollection<SocketUser> MentionedUsers => ImmutableArray.Create<SocketUser>();
/// <inheritdoc />
public virtual IReadOnlyCollection<ITag> Tags => ImmutableArray.Create<ITag>();

/// <inheritdoc />
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks);

internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source)
@@ -54,6 +65,7 @@ namespace Discord.WebSocket
Content = model.Content.Value;
}

/// <inheritdoc />
public Task DeleteAsync(RequestOptions options = null)
=> MessageHelper.DeleteAsync(this, Discord, options);

@@ -61,13 +73,21 @@ namespace Discord.WebSocket
internal SocketMessage Clone() => MemberwiseClone() as SocketMessage;

//IMessage
/// <inheritdoc />
IUser IMessage.Author => Author;
/// <inheritdoc />
IMessageChannel IMessage.Channel => Channel;
/// <inheritdoc />
MessageType IMessage.Type => MessageType.Default;
/// <inheritdoc />
IReadOnlyCollection<IAttachment> IMessage.Attachments => Attachments;
/// <inheritdoc />
IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds;
/// <inheritdoc />
IReadOnlyCollection<ulong> IMessage.MentionedChannelIds => MentionedChannels.Select(x => x.Id).ToImmutableArray();
/// <inheritdoc />
IReadOnlyCollection<ulong> IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray();
/// <inheritdoc />
IReadOnlyCollection<ulong> IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray();
}
}

+ 4
- 1
src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs View File

@@ -1,4 +1,4 @@
using Model = Discord.API.Gateway.Reaction;
using Model = Discord.API.Gateway.Reaction;

namespace Discord.WebSocket
{
@@ -9,6 +9,7 @@ namespace Discord.WebSocket
public ulong MessageId { get; }
public Optional<SocketUserMessage> Message { get; }
public ISocketMessageChannel Channel { get; }
/// <inheritdoc />
public IEmote Emote { get; }

internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional<SocketUserMessage> message, ulong userId, Optional<IUser> user, IEmote emoji)
@@ -30,6 +31,7 @@ namespace Discord.WebSocket
return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote);
}

/// <inheritdoc />
public override bool Equals(object other)
{
if (other == null) return false;
@@ -41,6 +43,7 @@ namespace Discord.WebSocket
return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote);
}

/// <inheritdoc />
public override int GetHashCode()
{
unchecked


+ 2
- 1
src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs View File

@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using Model = Discord.API.Message;

namespace Discord.WebSocket
@@ -6,6 +6,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketSystemMessage : SocketMessage, ISystemMessage
{
/// <inheritdoc />
public MessageType Type { get; private set; }

internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author)


+ 24
- 4
src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs View File

@@ -17,24 +17,34 @@ namespace Discord.WebSocket
private ImmutableArray<Attachment> _attachments;
private ImmutableArray<Embed> _embeds;
private ImmutableArray<ITag> _tags;
private List<SocketReaction> _reactions = new List<SocketReaction>();
private readonly List<SocketReaction> _reactions = new List<SocketReaction>();

/// <inheritdoc />
public override bool IsTTS => _isTTS;
/// <inheritdoc />
public override bool IsPinned => _isPinned;
/// <inheritdoc />
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks);
/// <inheritdoc />
public override IReadOnlyCollection<Attachment> Attachments => _attachments;
/// <inheritdoc />
public override IReadOnlyCollection<Embed> Embeds => _embeds;
/// <inheritdoc />
public override IReadOnlyCollection<ITag> Tags => _tags;
/// <inheritdoc />
public override IReadOnlyCollection<SocketGuildChannel> MentionedChannels => MessageHelper.FilterTagsByValue<SocketGuildChannel>(TagType.ChannelMention, _tags);
/// <inheritdoc />
public override IReadOnlyCollection<SocketRole> MentionedRoles => MessageHelper.FilterTagsByValue<SocketRole>(TagType.RoleMention, _tags);
/// <inheritdoc />
public override IReadOnlyCollection<SocketUser> MentionedUsers => MessageHelper.FilterTagsByValue<SocketUser>(TagType.UserMention, _tags);
/// <inheritdoc />
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) });

internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source)
: base(discord, id, channel, author, source)
{
}
internal static new SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model)
internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model)
{
var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model));
entity.Update(state, model);
@@ -121,30 +131,40 @@ namespace Discord.WebSocket
_reactions.Clear();
}

/// <inheritdoc />
/// <exception cref="InvalidOperationException">Only the author of a message may modify the message.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions options = null)
=> MessageHelper.ModifyAsync(this, Discord, func, options);

/// <inheritdoc />
public Task AddReactionAsync(IEmote emote, RequestOptions options = null)
=> MessageHelper.AddReactionAsync(this, emote, Discord, options);
/// <inheritdoc />
public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null)
=> MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options);
/// <inheritdoc />
public Task RemoveAllReactionsAsync(RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsAsync(this, Discord, options);
/// <inheritdoc />
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);

/// <inheritdoc />
public Task PinAsync(RequestOptions options = null)
=> MessageHelper.PinAsync(this, Discord, options);
/// <inheritdoc />
public Task UnpinAsync(RequestOptions options = null)
=> MessageHelper.UnpinAsync(this, Discord, options);

public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
=> MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
/// <inheritdoc />
public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
=> MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})";
internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage;
}


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

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -13,16 +13,25 @@ namespace Discord.WebSocket
{
public SocketGuild Guild { get; }

/// <inheritdoc />
public Color Color { get; private set; }
/// <inheritdoc />
public bool IsHoisted { get; private set; }
/// <inheritdoc />
public bool IsManaged { get; private set; }
/// <inheritdoc />
public bool IsMentionable { get; private set; }
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public GuildPermissions Permissions { get; private set; }
/// <inheritdoc />
public int Position { get; private set; }

/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
public bool IsEveryone => Id == Guild.Id;
/// <inheritdoc />
public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id);
public IEnumerable<SocketGuildUser> Members
=> Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id));
@@ -49,8 +58,10 @@ namespace Discord.WebSocket
Permissions = new GuildPermissions(model.Permissions);
}

/// <inheritdoc />
public Task ModifyAsync(Action<RoleProperties> func, RequestOptions options = null)
=> RoleHelper.ModifyAsync(this, Discord, func, options);
/// <inheritdoc />
public Task DeleteAsync(RequestOptions options = null)
=> RoleHelper.DeleteAsync(this, Discord, options);

@@ -58,9 +69,11 @@ namespace Discord.WebSocket
private string DebuggerDisplay => $"{Name} ({Id})";
internal SocketRole Clone() => MemberwiseClone() as SocketRole;

/// <inheritdoc />
public int CompareTo(IRole role) => RoleUtils.Compare(this, role);

//IRole
/// <inheritdoc />
IGuild IRole.Guild => Guild;
}
}

+ 1
- 1
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs View File

@@ -13,7 +13,7 @@ using PresenceModel = Discord.API.Presence;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket guild user.
/// Represents a WebSocket-based guild user.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGuildUser : SocketUser, IGuildUser


+ 3
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs View File

@@ -1,13 +1,14 @@
using System.Diagnostics;
using System.Diagnostics;
using Model = Discord.API.Presence;

namespace Discord.WebSocket
{
//TODO: C#7 Candidate for record type
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public struct SocketPresence : IPresence
{
/// <inheritdoc />
public UserStatus Status { get; }
/// <inheritdoc />
public IActivity Activity { get; }

internal SocketPresence(UserStatus status, IActivity activity)


+ 12
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
@@ -9,17 +9,26 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketSelfUser : SocketUser, ISelfUser
{
/// <inheritdoc />
public string Email { get; private set; }
/// <inheritdoc />
public bool IsVerified { get; private set; }
/// <inheritdoc />
public bool IsMfaEnabled { get; private set; }
internal override SocketGlobalUser GlobalUser { get; }

/// <inheritdoc />
public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } }
/// <inheritdoc />
public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } }
/// <inheritdoc />
public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } }
/// <inheritdoc />
public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } }
/// <inheritdoc />
internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } }

/// <inheritdoc />
public override bool IsWebhook => false;

internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser)
@@ -53,7 +62,8 @@ namespace Discord.WebSocket
}
return hasGlobalChanges;
}

/// <inheritdoc />
public Task ModifyAsync(Action<SelfUserProperties> func, RequestOptions options = null)
=> UserHelper.ModifyAsync(this, Discord, func, options);



+ 3
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using Model = Discord.API.User;

@@ -15,7 +15,8 @@ namespace Discord.WebSocket
public override bool IsWebhook => false;

internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } }
internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } }
internal override SocketGlobalUser GlobalUser =>
throw new NotSupportedException();

internal SocketUnknownUser(DiscordSocketClient discord, ulong id)
: base(discord, id)


+ 12
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs View File

@@ -1,11 +1,15 @@
using Discord.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.User;

namespace Discord.WebSocket
{
/// <summary> The WebSocket variant of <see cref="IUser"/>. Represents a Discord user. </summary>
/// <summary>
/// Represents a WebSocket-based user.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public abstract class SocketUser : SocketEntity<ulong>, IUser
{
/// <inheritdoc />
@@ -68,7 +72,7 @@ namespace Discord.WebSocket

/// <inheritdoc />
public async Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null)
=> GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel;
=> GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false) as IDMChannel;

/// <inheritdoc />
public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
@@ -78,6 +82,12 @@ namespace Discord.WebSocket
public string GetDefaultAvatarUrl()
=> CDN.GetDefaultUserAvatarUrl(DiscriminatorValue);

/// <summary>
/// Gets the full name of the user (e.g. Example#0001).
/// </summary>
/// <returns>
/// The full name of the user.
/// </returns>
public override string ToString() => $"{Username}#{Discriminator}";
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})";
internal SocketUser Clone() => MemberwiseClone() as SocketUser;


+ 17
- 3
src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs View File

@@ -1,10 +1,9 @@
using System;
using System;
using System.Diagnostics;
using Model = Discord.API.VoiceState;

namespace Discord.WebSocket
{
//TODO: C#7 Candidate for record type
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public struct SocketVoiceState : IVoiceState
{
@@ -22,14 +21,23 @@ namespace Discord.WebSocket
}

private readonly Flags _voiceStates;

/// <summary>
/// Gets the voice channel that the user is currently in; or <see langword="null"/> if none.
/// </summary>
public SocketVoiceChannel VoiceChannel { get; }
/// <inheritdoc />
public string VoiceSessionId { get; }

/// <inheritdoc />
public bool IsMuted => (_voiceStates & Flags.Muted) != 0;
/// <inheritdoc />
public bool IsDeafened => (_voiceStates & Flags.Deafened) != 0;
/// <inheritdoc />
public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0;
/// <inheritdoc />
public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0;
/// <inheritdoc />
public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0;

internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed)
@@ -55,6 +63,12 @@ namespace Discord.WebSocket
return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress);
}

/// <summary>
/// Gets the name of the voice channel.
/// </summary>
/// <returns>
/// The name of the voice channel.
/// </returns>
public override string ToString() => VoiceChannel?.Name ?? "Unknown";
private string DebuggerDisplay => $"{VoiceChannel?.Name ?? "Unknown"} ({_voiceStates})";
internal SocketVoiceState Clone() => this;


+ 6
- 0
src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs View File

@@ -63,26 +63,32 @@ namespace Discord.WebSocket
/// <inheritdoc />
ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue);
/// <inheritdoc />
/// <exception cref="NotSupportedException">Webhook users cannot be kicked.</exception>
Task IGuildUser.KickAsync(string reason, RequestOptions options) =>
throw new NotSupportedException("Webhook users cannot be kicked.");

/// <inheritdoc />
/// <exception cref="NotSupportedException">Webhook users cannot be modified.</exception>
Task IGuildUser.ModifyAsync(Action<GuildUserProperties> func, RequestOptions options) =>
throw new NotSupportedException("Webhook users cannot be modified.");

/// <inheritdoc />
/// <exception cref="NotSupportedException">Roles are not supported on webhook users.</exception>
Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) =>
throw new NotSupportedException("Roles are not supported on webhook users.");

/// <inheritdoc />
/// <exception cref="NotSupportedException">Roles are not supported on webhook users.</exception>
Task IGuildUser.AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options) =>
throw new NotSupportedException("Roles are not supported on webhook users.");

/// <inheritdoc />
/// <exception cref="NotSupportedException">Roles are not supported on webhook users.</exception>
Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) =>
throw new NotSupportedException("Roles are not supported on webhook users.");

/// <inheritdoc />
/// <exception cref="NotSupportedException">Roles are not supported on webhook users.</exception>
Task IGuildUser.RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options) =>
throw new NotSupportedException("Roles are not supported on webhook users.");



+ 2
- 2
src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs View File

@@ -1,4 +1,4 @@
#if DEFAULTWEBSOCKET
#if DEFAULTWEBSOCKET
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -248,4 +248,4 @@ namespace Discord.Net.WebSockets
}
}
}
#endif
#endif

+ 2
- 1
src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs View File

@@ -8,6 +8,7 @@ namespace Discord.Net.WebSockets
#if DEFAULTWEBSOCKET
public static readonly WebSocketProvider Instance = Create();

/// <exception cref="PlatformNotSupportedException">The default WebSocketProvider is not supported on this platform.</exception>
public static WebSocketProvider Create(IWebProxy proxy = null)
{
return () =>
@@ -30,4 +31,4 @@ namespace Discord.Net.WebSockets
};
#endif
}
}
}

+ 3
- 2
src/Discord.Net.Webhook/WebhookClientHelper.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -12,11 +12,12 @@ namespace Discord.Webhook
{
internal static class WebhookClientHelper
{
/// <exception cref="InvalidOperationException">Could not find a webhook with the supplied credentials.</exception>
public static async Task<RestInternalWebhook> GetWebhookAsync(DiscordWebhookClient client, ulong webhookId)
{
var model = await client.ApiClient.GetWebhookAsync(webhookId).ConfigureAwait(false);
if (model == null)
throw new InvalidOperationException("Could not find a webhook for the supplied credentials.");
throw new InvalidOperationException("Could not find a webhook with the supplied credentials.");
return RestInternalWebhook.Create(client, model);
}
public static async Task<ulong> SendMessageAsync(DiscordWebhookClient client,


Loading…
Cancel
Save